news 2026/5/8 17:45:50

【Android】声控拍照例子

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Android】声控拍照例子

让AI写一个简单的声控拍照app,用来实现快速拍照,比如要逮那些乱跑的车辆,最初步的是通过音量来触发拍照,复杂一点可以加入语音内容识别,这里从最初步的来试验。

以下是完整、稳定、可直接运行的“声音触发拍照”Android App(Java + CameraX),已修复所有常见问题:

  • ✅ 使用临时文件替代/dev/null(兼容国产 ROM)
  • ✅ 完整权限处理(动态申请 + 防御检查)
  • ✅ 正确保存照片到相册(Android 10+ 兼容)
  • ✅ 麦克风释放安全处理
  • ✅ 防连拍机制

📁 项目结构

app/ ├── src/main/ │ ├── AndroidManifest.xml │ ├── res/layout/activity_main.xml │ └── java/com/example/voicetakephoto/MainActivity.java └── build.gradle (Module: app)

1️⃣AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 仅 Android 9 及以下需要 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="VoicePhoto" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

2️⃣res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

3️⃣MainActivity.java(完整版)

package com.example.voicetakephoto; import android.Manifest; import android.content.ContentValues; import android.content.pm.PackageManager; import android.media.MediaRecorder; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.camera.core.CameraSelector; import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCaptureException; import androidx.camera.core.Preview; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.camera.view.PreviewView; import androidx.core.content.ContextCompat; import com.google.common.util.concurrent.ListenableFuture; import java.io.File; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class MainActivity extends AppCompatActivity { private static final String TAG = "VoicePhoto"; private static final int VOLUME_THRESHOLD = 2500; // 根据设备调整 private PreviewView previewView; private ImageCapture imageCapture; private ExecutorService cameraExecutor; private MediaRecorder mediaRecorder; private boolean isListening = false; private boolean isCooldown = false; private final ActivityResultLauncher<String[]> requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { Boolean cameraGranted = result.getOrDefault(Manifest.permission.CAMERA, false); Boolean audioGranted = result.getOrDefault(Manifest.permission.RECORD_AUDIO, false); if (cameraGranted && audioGranted) { startCamera(); startAudioMonitoring(); } else { Toast.makeText(this, "需要相机和麦克风权限", Toast.LENGTH_SHORT).show(); finish(); } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); previewView = findViewById(R.id.previewView); cameraExecutor = Executors.newSingleThreadExecutor(); requestPermissionLauncher.launch(new String[]{ Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO }); } private void startCamera() { ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); bindPreview(cameraProvider); } catch (Exception e) { Log.e(TAG, "启动相机失败", e); } }, ContextCompat.getMainExecutor(this)); } private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) { Preview preview = new Preview.Builder().build(); preview.setSurfaceProvider(previewView.getSurfaceProvider()); imageCapture = new ImageCapture.Builder() .setTargetRotation(previewView.getDisplay().getRotation()) .build(); try { cameraProvider.unbindAll(); cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture); } catch (Exception e) { Log.e(TAG, "绑定预览失败", e); } } private void startAudioMonitoring() { if (isListening) return; // 再次检查权限(防御性) if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "请开启麦克风权限", Toast.LENGTH_SHORT).show(); return; } isListening = true; File tempFile = new File(getCacheDir(), "voice_monitor.tmp"); mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mediaRecorder.setOutputFile(tempFile.getAbsolutePath()); try { mediaRecorder.prepare(); mediaRecorder.start(); } catch (Exception e) { Log.e(TAG, "MediaRecorder 启动失败", e); isListening = false; releaseMediaRecorder(); Toast.makeText(this, "无法访问麦克风,请关闭其他录音应用后重试", Toast.LENGTH_LONG).show(); return; } // 启动监听线程 new Thread(() -> { while (isListening) { try { int amplitude = mediaRecorder.getMaxAmplitude(); if (amplitude > VOLUME_THRESHOLD && !isCooldown) { Log.d(TAG, "音量触发拍照: " + amplitude); takePhoto(); setCooldown(true); } Thread.sleep(100); } catch (Exception e) { break; } } }).start(); } private void setCooldown(boolean enable) { isCooldown = enable; if (enable) { new Handler(Looper.getMainLooper()).postDelayed(() -> isCooldown = false, 3000); } } private void takePhoto() { if (imageCapture == null) return; ImageCapture.OutputFileOptions outputFileOptions = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, generateFileName() + ".jpg"); contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/VoicePhoto"); Uri imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); if (imageUri == null) { Log.e(TAG, "无法创建 MediaStore 条目"); return; } try { OutputStream outputStream = getContentResolver().openOutputStream(imageUri); outputFileOptions = new ImageCapture.OutputFileOptions.Builder(outputStream).build(); } catch (Exception e) { Log.e(TAG, "无法打开输出流", e); return; } } else { // Android 9 及以下 String fileName = generateFileName() + ".jpg"; File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName); outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build(); } imageCapture.takePicture( outputFileOptions, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() { @Override public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) { runOnUiThread(() -> Toast.makeText(MainActivity.this, "照片已保存到相册", Toast.LENGTH_SHORT).show() ); } @Override public void onError(@NonNull ImageCaptureException error) { Log.e(TAG, "拍照失败", error); } } ); } private String generateFileName() { return "VOICE_PHOTO_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); } private void releaseMediaRecorder() { if (mediaRecorder != null) { try { mediaRecorder.stop(); } catch (Exception ignored) {} try { mediaRecorder.release(); } catch (Exception ignored) {} mediaRecorder = null; } // 删除临时文件 File temp = new File(getCacheDir(), "voice_monitor.tmp"); if (temp.exists()) temp.delete(); } @Override protected void onDestroy() { super.onDestroy(); isListening = false; releaseMediaRecorder(); if (cameraExecutor != null) { cameraExecutor.shutdown(); } } }

4️⃣build.gradle(Module: app)

plugins { id 'com.android.application' } android { namespace 'com.example.voicetakephoto' compileSdk 34 defaultConfig { applicationId "com.example.voicetakephoto" minSdk 21 targetSdk 34 versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // CameraX def camerax_version = "1.3.0" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" }

✅ 使用说明

  1. 安装运行
  2. 授权相机 + 麦克风权限
  3. 对着手机喊一声(如“茄子!”)
  4. 照片自动保存到:相册 → VoicePhoto 文件夹

🔧 调整建议

表格

需求修改位置
更灵敏(小声也能触发)降低VOLUME_THRESHOLD(如1500
更迟钝(避免误触发)提高VOLUME_THRESHOLD(如5000
拍照后等待时间修改setCooldown中的3000(毫秒)
使用前置摄像头CameraSelector.DEFAULT_BACK_CAMERA改为DEFAULT_FRONT_CAMERA
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/3 8:25:19

小白必看!AutoGen Studio保姆级教程:快速部署AI代理团队

小白必看&#xff01;AutoGen Studio保姆级教程&#xff1a;快速部署AI代理团队 1. 引言 随着大模型技术的快速发展&#xff0c;构建具备协作能力的AI代理团队已成为提升自动化任务效率的重要手段。然而&#xff0c;传统多代理系统开发对编程能力要求较高&#xff0c;限制了其…

作者头像 李华
网站建设 2026/5/2 21:06:35

shadPS4键鼠配置完全指南:用键盘鼠标畅玩PS4游戏

shadPS4键鼠配置完全指南&#xff1a;用键盘鼠标畅玩PS4游戏 【免费下载链接】shadPS4 shadPS4 是一个PlayStation 4 模拟器&#xff0c;支持 Windows、Linux 和 macOS 系统&#xff0c;用 C 编写。还提供了调试文档、键盘鼠标映射说明等&#xff0c;方便用户使用。源项目地址&…

作者头像 李华
网站建设 2026/5/1 7:13:03

ESPHome JK-BMS终极指南:打造智能电池管理系统的完整解决方案

ESPHome JK-BMS终极指南&#xff1a;打造智能电池管理系统的完整解决方案 【免费下载链接】esphome-jk-bms ESPHome component to monitor and control a Jikong Battery Management System (JK-BMS) via UART-TTL or BLE 项目地址: https://gitcode.com/gh_mirrors/es/espho…

作者头像 李华
网站建设 2026/4/25 4:15:38

ArkOS游戏掌机实战手册:从入门到精通的高效配置技巧

ArkOS游戏掌机实战手册&#xff1a;从入门到精通的高效配置技巧 【免费下载链接】arkos Another rockchip Operating System 项目地址: https://gitcode.com/gh_mirrors/ar/arkos ArkOS作为专为Rockchip芯片游戏掌机设计的开源操作系统&#xff0c;为玩家带来了完整的怀…

作者头像 李华
网站建设 2026/5/1 9:40:15

Elasticsearch下载与安全认证配置实战示例

Elasticsearch 安全部署实战&#xff1a;从下载到认证的完整避坑指南最近帮团队搭建日志分析平台&#xff0c;又和 Elasticsearch 打了一次交道。说实话&#xff0c;这玩意儿功能强大是真的&#xff0c;但默认“裸奔”的设定也真让人捏把汗——新装的 ES 实例不加任何防护就对外…

作者头像 李华
网站建设 2026/5/1 9:41:22

DeepSeek-R1支持哪些硬件?CPU兼容性测试报告

DeepSeek-R1支持哪些硬件&#xff1f;CPU兼容性测试报告 1. 背景与技术定位 随着大模型在推理、编程和数学等复杂任务中的表现日益突出&#xff0c;如何将高性能模型部署到资源受限的设备上成为工程落地的关键挑战。DeepSeek-R1 系列通过知识蒸馏技术&#xff0c;在保留原始模…

作者头像 李华