Android音频开发(二):录制音频(WAV及MP3格式)

附GitHub源码:MultimediaExplore

首先来看录音播放效果图:

[En]

First of all, take a look at the audio recording and playback effect diagram:

Android音频开发(二):录制音频(WAV及MP3格式)

CSDN不支持本地视频上传,我就先上传了一张截图:

上面是录音:长按即可录音,支持声波动画,右滑删除等。支持录制pcm、wav、mp3格式音频。

下面是播放:点击左边扬声器icon,开始播放刚录制的本地音频文件【也支持在线音频播放】,支持播放进度,支持切换播放模式(听筒/扬声器/耳机)等。

一、音频录制权限:

无论在开发任何功能之前,都需要添加并申请相关权限,后续工作才能正常进行。录音需要的权限如下,您需要在代码中动态申请这些敏感权限,才能正常录制:

[En]

No matter before you develop any function, you have to add and apply for relevant permissions before the follow-up work can be carried out normally. The permissions required for audio recording are as follows, and you need to apply for these sensitive permissions dynamically in the code before you can record them normally:


二、录音文件的配置:

通过第一节讲述音频的基本概念,我们可以知道录音的相关配置应该在录制之前进行,这直接决定了音频质量、文件大小、音频格式等。

[En]

Through the first section talking about the basic concept of audio, we can know that the relevant configuration of recording should be carried out before recording, which directly determines the audio quality, file size, audio format and so on.

    /**
     * 录音音频的相关配置
     */
    private void initConfig() {
        recordConfig = new RecordConfig();
        //采样位宽
        recordConfig.setEncodingConfig(AudioFormat.ENCODING_PCM_16BIT);
        //录音格式
        recordConfig.setFormat(RecordConfig.RecordFormat.MP3);
        // recordConfig.setFormat(RecordConfig.RecordFormat.WAV);
        //采样频率
        recordConfig.setSampleRate(16000);
        String recordDir = String.format(Locale.getDefault(), "%s/Record/zhongyao/",
                Environment.getExternalStorageDirectory().getAbsolutePath());
        //存储目录
        recordConfig.setRecordDir(recordDir);
        AudioRecordManager.getInstance().setCurrentConfig(recordConfig);
    }

三、音频录制:

音频录制类主要有两个封装类:分别是 AudioRecorder 、 AudioRecordManager。

AudioRecorder:主要是使用系统的AudioRecord来进行录音。并把录制好的音频文件进行合并,转码等,生成我们所需的音频文件。该文件是全局单例的,保证音频录制类只有一个实例。

AudioRecordManager:对AudioRecorder的封装管理,与外界交互均通过此类来完成,包括录音的各种生命周期控制调用等。减少了外界与AudioRecorder的直接交互,已达到对录音类的更好的管理,此类也是一个全局单例类。

1、录音对象初始化:

这里主要根据之前的录音配置,生成 bufferSizeInBytes【缓冲区字节大小】,和audioRecord对象。

    /**
     * 创建默认的录音对象
     */
    public void prepareRecord() {
        // 获得缓冲区字节大小
        if (bufferSizeInBytes == 0) {
            bufferSizeInBytes = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),
                    currentConfig.getChannelConfig(), currentConfig.getEncodingConfig());
        }
        if (audioRecord == null) {
            audioRecord = new AudioRecord(AUDIO_INPUT, currentConfig.getSampleRate(),
                    currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSizeInBytes);
        }

        audioRecordStatus = AudioRecordStatus.AUDIO_RECORD_PREPARE;
    }

2、录制wav音频文件:

wav音频文件是无损的,所以音质会接近原生,但也正是因为是无损的,所以wav音频文件几乎没有压缩,相对来说会比较大。

录制wav音频得先进行录制采用,获得pcm文件,然后把pcm文件合并,最后再转成wav音频文件。

(1)开始录制pcm文件:

    private void startPcmRecorder() {
        audioRecordStatus = AudioRecordStatus.AUDIO_RECORD_START;
        notifyState();
        Logger.d(TAG, "开始录制 Pcm");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(tmpFile);
            audioRecord.startRecording();
            byte[] byteBuffer = new byte[bufferSizeInBytes];

            while (audioRecordStatus == AudioRecordStatus.AUDIO_RECORD_START) {
                int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                notifyData(byteBuffer);
                fos.write(byteBuffer, 0, end);
                fos.flush();
            }
            audioRecord.stop();
            files.add(tmpFile);
            if (audioRecordStatus == AudioRecordStatus.AUDIO_RECORD_STOP) {
                makeFile();
            } else {
                Logger.d(TAG, "取消录制...");
            }
        } catch (Exception e) {
            Logger.e(e, TAG, e.getMessage());
            notifyError("录音失败");
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (audioRecordStatus != AudioRecordStatus.AUDIO_RECORD_PAUSE) {
            audioRecordStatus = AudioRecordStatus.AUDIO_RECORD_IDLE;
            notifyState();
            Logger.d(TAG, "录音结束");
        }
    }

(2)合并生成的多个pcm文件:

    /**
     * 合并pcm文件
     */
    private void mergePcmFile() {
        boolean mergeSuccess = mergePcmFiles(resultFile, files);
        if (!mergeSuccess) {
            notifyError("合并失败");
        }
    }

(3)将合并好的pcm文件转成wav文件:

    /**
     * 添加Wav头文件
     */
    private void makeWav() {
        if (!FileUtil.isFile(resultFile) || resultFile.length() == 0) {
            return;
        }
        byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());
        WavUtils.writeHeader(resultFile, header);
    }

3、录制MP3音频文件

相比WAV音频文件而言,MP3音频文件,就更加常见,商业上使用的也比较多,就是因为MP3音频时经过压缩的,文件大小只有WAV的十二分之一,但是音质上几乎没有较大的差异性。当对音质没有极高要求的情况下,如录音文件,MP3格式是极好的选择。

(1)开始录制音频缓存:

这里有开启一个线程Mp3EncodeThread,将录音产生的字节数组byteBuffer不断的进行编解码生成MP3文件。

    private void startMp3Recorder() {
        audioRecordStatus = AudioRecordStatus.AUDIO_RECORD_START;
        notifyState();

        try {
            audioRecord.startRecording();
            short[] byteBuffer = new short[bufferSizeInBytes];

            while (audioRecordStatus == AudioRecordStatus.AUDIO_RECORD_START) {
                int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                if (mp3EncodeThread != null) {
                    mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));
                }
                notifyData(ByteUtils.toBytes(byteBuffer));
            }
            audioRecord.stop();
        } catch (Exception e) {
            Logger.e(e, TAG, e.getMessage());
            notifyError("录音失败");
        }
        if (audioRecordStatus != AudioRecordStatus.AUDIO_RECORD_PAUSE) {
            if (audioRecordStatus == AudioRecordStatus.AUDIO_RECORD_CANCEL) {
                deleteMp3Encoded();
            } else {
                stopMp3Encoded();
            }
        } else {
            Logger.d(TAG, "暂停");
        }
    }

(2)MP3音频编解码:

Android原生的音频录制,支持直接生成WAV文件,但其实是不支持直接生成MP3文件的。这里对应MP3编解码,主要用到了开源的 libmp3lame.so这个音频编解码库。以下是lame编解码方法及Mp3Encoder类:

MP3编解码方法:

    private void lameData(ChangeBuffer changeBuffer) {
        if (changeBuffer == null) {
            return;
        }
        short[] buffer = changeBuffer.getData();
        int readSize = changeBuffer.getReadSize();
        if (readSize > 0) {
            int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);
            if (encodedSize < 0) {
                Logger.e(TAG, "Lame encoded size: " + encodedSize);
            }
            try {
                os.write(mp3Buffer, 0, encodedSize);
            } catch (IOException e) {
                Logger.e(e, TAG, "Unable to write to file");
            }
        }
    }

Mp3Encoder类:


public class Mp3Encoder {

    static {
        System.loadLibrary("mp3lame");
    }

    public native static void close();

    public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);

    public native static int flush(byte[] mp3buf);

    public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);

    public static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate) {
        init(inSampleRate, outChannel, outSampleRate, outBitrate, 7);
    }
}

(3)生成 libmp3lame.so :

本项目提供的源码中有lame的jni源码,如果想生成libmp3lame.so文件,供自己的项目使用,那么需要对Mp3Encoder.c 和Mp3Encoder.h文件进行修改,然后通过ndk build 生成该so文件。

Android音频开发(二):录制音频(WAV及MP3格式)

修改的内容主要是把文件中的Mp3Encoder路径改成自己项目中的Mp3Encoder的路径,否则会找不到相关的native方法。

Android音频开发(二):录制音频(WAV及MP3格式)

另外一般情况下,cpu类型支持 armeabi-v7a 、arm64-v8a 两种即可,如果想支持其他的可在Application.mk中添加。

Android音频开发(二):录制音频(WAV及MP3格式)

上面文件修改完毕,通过ndk-build指令即可编译生成对应的 libmp3lame.so 文件。

将so不同CPU类型的文件放置 jniLibs 下,或者通过sourceSets配置的路径下,如:

    sourceSets.main {
        jni.srcDirs = []//disable automatic ndk-build call
        jniLibs.srcDirs = ['libs']
    }

即可进行MP3音频格式的录制。

四、音频录制管理【AudioRecordManager】:

通过全局单例模式的AudioRecorderManager与业务进行交互,即保证了音频录制实例的单一性,又能较好的对音频生命周期等进行较好的管理。

/**
 * Create by zhongyao on 2021/8/18
 * Description:音频录制管理类
 */
public class AudioRecordManager {

    private static final String TAG = "AudioRecordManager";

    private AudioRecordManager() {
    }

    public static AudioRecordManager getInstance() {
        return AudioRecordManagerHolder.instance;
    }

    public static class AudioRecordManagerHolder {
        public static AudioRecordManager instance = new AudioRecordManager();
    }

    public void setCurrentConfig(RecordConfig recordConfig) {
        AudioRecorder.getInstance().setCurrentConfig(recordConfig);
    }

    public RecordConfig getCurrentConfig() {
        return AudioRecorder.getInstance().getCurrentConfig();
    }

    /**
     * 录音状态监听回调
     */
    public void setRecordStateListener(RecordStateListener listener) {
        AudioRecorder.getInstance().setRecordStateListener(listener);
    }

    /**
     * 录音数据监听回调
     */
    public void setRecordDataListener(RecordDataListener listener) {
        AudioRecorder.getInstance().setRecordDataListener(listener);
    }

    /**
     * 录音可视化数据回调,傅里叶转换后的频域数据
     */
    public void setRecordFftDataListener(RecordFftDataListener recordFftDataListener) {
        AudioRecorder.getInstance().setRecordFftDataListener(recordFftDataListener);
    }

    /**
     * 录音文件转换结束回调
     */
    public void setRecordResultListener(RecordResultListener listener) {
        AudioRecorder.getInstance().setRecordResultListener(listener);
    }

    /**
     * 录音音量监听回调
     */
    public void setRecordSoundSizeListener(RecordSoundSizeListener listener) {
        AudioRecorder.getInstance().setRecordSoundSizeListener(listener);
    }

    public void setStatus(AudioRecordStatus curAudioRecordStatus) {
        switch (curAudioRecordStatus) {
            case AUDIO_RECORD_IDLE:

                break;

            case AUDIO_RECORD_PREPARE:
                AudioRecorder.getInstance().prepareRecord();
                break;

            case AUDIO_RECORD_START:
                AudioRecorder.getInstance().startRecord();
                break;

            case AUDIO_RECORD_PAUSE:
                AudioRecorder.getInstance().pauseRecord();
                break;

            case AUDIO_RECORD_STOP:
                AudioRecorder.getInstance().stopRecord();
                break;

            case AUDIO_RECORD_CANCEL:
                AudioRecorder.getInstance().cancelRecord();
                break;

            case AUDIO_RECORD_RELEASE:
                AudioRecorder.getInstance().releaseRecord();
                break;

            default:
                break;
        }
    }

    public AudioRecordStatus getStatus() {
        return AudioRecorder.getInstance().getAudioRecordStatus();
    }

    public String getAudioPath() {
        return AudioRecorder.getInstance().getAudioPath();
    }
}

Original: https://blog.csdn.net/u012440207/article/details/121719075
Author: 红日666
Title: Android音频开发(二):录制音频(WAV及MP3格式)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/527087/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球