【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
相关推荐
stevenzqzq2 小时前
Android MVI 中 setState(reduce: State.() -> State) 设计说明文档
android·mvi框架
鸣弦artha2 小时前
Flutter框架跨平台鸿蒙开发——InheritedWidget基础使用-计数器案例
android·flutter·harmonyos
嵌入式-老费2 小时前
Android开发(开发板的三种操作系统)
android
凛_Lin~~3 小时前
安卓网络框架——OkHttp源码解析(基于3.14.x)
android·网络·okhttp
stevenzqzq3 小时前
android SharedFlow和Channel比较
android·channel·sharedflow
zhangphil4 小时前
Kotlin实现Glide/Coil图/视频加载框架(二)
android·kotlin
shughui4 小时前
APP、Web、H5、iOS与Android的区别及关系
android·前端·ios
千里马学框架4 小时前
敏感权限如何自动授权?pkms的permission部分常用命令汇总
android·车载系统·framework·perfetto·权限·系统开发·pkms
a2591748032-随心所记5 小时前
android14 google默认进程、apk、hal、以及service等
android