news 2026/3/1 11:35:12

【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/2/27 16:43:58

通俗解释有源蜂鸣器和无源蜂鸣器发声原理差异

有源蜂鸣器 vs 无源蜂鸣器&#xff1a;一文讲透发声原理与工程选型你有没有遇到过这样的情况&#xff1f;电路接好了&#xff0c;代码也烧录了&#xff0c;电源一上&#xff0c;结果——该响的不响&#xff0c;不该响的一直“嘀嘀嘀”……排查半天&#xff0c;最后发现&#xf…

作者头像 李华
网站建设 2026/2/27 15:13:28

OpenDataLab MinerU可扩展性分析:添加自定义任务的接口开发指南

OpenDataLab MinerU可扩展性分析&#xff1a;添加自定义任务的接口开发指南 1. 背景与技术定位 随着智能文档处理需求的快速增长&#xff0c;传统OCR工具在语义理解、图表解析和上下文推理方面逐渐显现出局限性。OpenDataLab推出的MinerU系列模型&#xff0c;特别是基于Inter…

作者头像 李华
网站建设 2026/3/1 4:15:41

MinerU智能文档服务:合同风险点自动检测

MinerU智能文档服务&#xff1a;合同风险点自动检测 1. 技术背景与问题提出 在企业法务、金融风控和供应链管理等场景中&#xff0c;合同审查是一项高频且高风险的任务。传统的人工审核方式不仅耗时长、成本高&#xff0c;还容易因疏忽遗漏关键条款或隐藏陷阱。随着大模型技术…

作者头像 李华
网站建设 2026/3/1 0:51:33

Qwen_Image_Cute_Animal_For_Kids部署教程:教育机构必备工具

Qwen_Image_Cute_Animal_For_Kids部署教程&#xff1a;教育机构必备工具 1. 技术背景与应用场景 随着人工智能在教育领域的深入应用&#xff0c;生成式AI正逐步成为教学资源创作的重要工具。尤其在幼儿教育场景中&#xff0c;生动、可爱、富有童趣的视觉素材对提升儿童学习兴…

作者头像 李华
网站建设 2026/2/27 23:49:29

Brave浏览器终极隐私保护指南:重新定义安全上网体验

Brave浏览器终极隐私保护指南&#xff1a;重新定义安全上网体验 【免费下载链接】brave-browser Brave browser for Android, iOS, Linux, macOS, Windows. 项目地址: https://gitcode.com/GitHub_Trending/br/brave-browser 在数字足迹无处不在的今天&#xff0c;你是否…

作者头像 李华
网站建设 2026/2/28 12:13:57

PojavLauncher iOS版终极指南:在iPhone上玩转Minecraft Java版

PojavLauncher iOS版终极指南&#xff1a;在iPhone上玩转Minecraft Java版 【免费下载链接】PojavLauncher_iOS A Minecraft: Java Edition Launcher for Android and iOS based on Boardwalk. This repository contains source code for iOS/iPadOS platform. 项目地址: htt…

作者头像 李华