Android音频开发

这篇博客深入探讨Android音频开发,从音频基础知识到具体实践,包括使用AudioRecord录制PCM音频、实现暂停恢复功能、PCM转WAV、MP3录制以及音乐可视化。涵盖了音频格式、编码、权限、动态权限和NDK编译Lame库等内容。
摘要由CSDN通过智能技术生成

这篇博客 转载自 https://www.jianshu.com/p/c0222de2faed

这里涉及到ndk的一些知识,对于.mk文件不太熟悉的同学要自己去 官网 或者搜索一些博客了解基本知识。

1. 音频基础知识

音频基础知识

采样和采样频率:
现在是数字时代,在音频处理时要先把音频的模拟信号变成数字信号,这叫A/D转换。要把音频的模拟信号变成数字信号,就需要采样。一秒钟内采样的次数称为采样频率

采样频率越高,越接近原始信号,但是也加大了运算处理的复杂度。16000Hz和44.1kHZ(1

采样位数/位宽:
数字信号是用0和1来表示的。采样位数就是采样值用多少位0和1来表示,也叫采样精度,用的位数越多就越接近真实声音。如用8位表示,采样值取值范围就是-128 ~ 127,如用16位表示,采样值取值范围就是-32768 ~ 32767。

声道(channel):
通常语音只用一个声道。而对于音乐来说,既可以是单声道(mono),也可以是双声道(即左声道右声道,叫立体声stereo),还可以是多声道,叫环绕立体声。

编解码 :
通常把音频采样过程也叫做脉冲编码调制编码,即PCM(Pulse Code Modulation)编码,采样值也叫PCM值。 如果把采样值直接保存或者发送,会占用很大的存储空间。以16kHz采样率16位采样位数单声道为例,一秒钟就有16/8*16000 = 32000字节。为了节省保存空间或者发送流量,会对PCM值压缩。

目前主要有三大技术标准组织制定压缩标准:

  1. ITU,主要制定有线语音的压缩标准(g系列),有g711/g722/g726/g729等。
  2. 3GPP,主要制定无线语音的压缩标准(amr系列等),有amr-nb/amr-wb。后来ITU吸纳了amr-wb,形成了g722.2。
  3. MPEG,主要制定音乐的压缩标准,有11172-3,13818-3/7,14496-3等。
    一些大公司或者组织也制定压缩标准,比如iLBC,OPUS。
编码过程:模拟信号->抽样->量化->编码->数字信号

压缩:
对于自然界中的音频信号,如果转换成数字信号,进行音频编码,那么只能无限接近,不可能百分百还原。所以说实际上任何信号转换成数字信号都会“有损”。但是在计算机应用中,能够达到最高保真水平的就是PCM编码。因此,PCM约定俗成了无损编码。我们而习惯性的把MP3列入有损音频编码范畴,是相对PCM编码的。强调编码的相对性的有损和无损

码率:
码率 = 采样频率 * 采样位数 * 声道个数; 例:采样频率44.1KHz,量化位数16bit,立体声(双声道),未压缩时的码率 = 44.1KHz * 16 * 2 = 1411.2Kbps = 176.4KBps,即每秒要录制的资源大小,理论上码率和质量成正比。

800 bps – 能够分辨的语音所需最低码率(需使用专用的FS-1015语音编解码器)
8 kbps —电话质量(使用语音编码)
8-500 kbps --Ogg Vorbis和MPEG1 Player1/2/3中使用的有损音频模式
500 kbps–1.4 Mbps —44.1KHz的无损音频,解码器为FLAC Audio,WavPack或Monkey's Audio
1411.2 - 2822.4 Kbps —脉冲编码调制(PCM)声音格式CD光碟的数字音频
5644.8 kbps —SACD使用的Direct Stream Digital格式

常用音频格式

WAV 格式:音质高 无损格式 体积较大
AAC(Advanced Audio Coding) 格式:相对于 mp3,AAC 格式的音质更佳,文件更小,有损压缩,一般苹果或者Android SDK4.1.2(API 16)及以上版本支持播放,性价比高
AMR 格式:压缩比比较大,但相对其他的压缩格式质量比较差,多用于人声,通话录音

AMR分类:
AMR(AMR-NB): 语音带宽范围:3003400Hz,8KHz抽样

mp3 格式:特点 使用广泛, 有损压缩,牺牲了12KHz到16KHz高音频的音质

音频开发的主要应用

  • 音频播放器
  • 录音机
  • 语音电话
  • 音视频监控应用
  • 音视频直播应用
  • 音频编辑/处理软件(ktv音效、变声, 铃声转换)
  • 蓝牙耳机/音箱

音频开发的具体内容

  • 音频采集/播放
  • 音频算法处理(去噪、静音检测、回声消除、音效处理、功放/增强、混音/分离,等等)
  • 音频的编解码和格式转换
  • 音频传输协议的开发(SIP,A2DP、AVRCP,等等)

2. 使用AudioRecord录制pcm格式音频

AudioRecord类的介绍

1. AudioRecord构造函数:

 /**
 * @param audioSource :录音源
 * 这里选择使用麦克风:MediaRecorder.AudioSource.MIC
 * @param sampleRateInHz: 采样率
 * @param channelConfig:声道数  
 * @param audioFormat: 采样位数.
 *   See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},
 *   and {@link AudioFormat#ENCODING_PCM_FLOAT}.
 * @param bufferSizeInBytes: 音频录制的缓冲区大小
 *   See {@link #getMinBufferSize(int, int, int)}  
 */
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
        int bufferSizeInBytes)

2. getMinBufferSize()

/**
* 获取AudioRecord所需的最小缓冲区大小
* @param sampleRateInHz: 采样率
* @param channelConfig:声道数  
* @param audioFormat: 采样位数.
*/
public static int getMinBufferSize (int sampleRateInHz, 
            int channelConfig, 
            int audioFormat)

3. getRecordingState()

/**
* 获取AudioRecord当前的录音状态 
*   @see AudioRecord#RECORDSTATE_STOPPED    
*   @see AudioRecord#RECORDSTATE_RECORDING
*/
public int getRecordingState()

4. startRecording()

 /**
 * 开始录制
 */
 public int startRecording()

5. stop()

 /**
 * 停止录制
 */
 public int stop()

6. read()

/**
 * 从录音设备中读取音频数据
 * @param audioData 音频数据写入的byte[]缓冲区
 * @param offsetInBytes 偏移量
 * @param sizeInBytes 读取大小
 * @return 返回负数则表示读取失败
 *      see {@link #ERROR_INVALID_OPERATION} -3 : 初始化错误
        {@link #ERROR_BAD_VALUE}  -3: 参数错误
        {@link #ERROR_DEAD_OBJECT} -6: 
        {@link #ERROR}  
 */
public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) 

实现

实现过程就是调用上面的API的方法,构造AudioRecord实例后再调用startRecording(),开始录音,并通过read()方法不断获取录音数据记录下来,生成PCM文件。涉及耗时操作,所以最好在子线程中进行。

public class RecordHelper {
   
    //0.此状态用于控制线程中的循环操作,应用volatile修饰,保持数据的一致性
    private volatile RecordState state = RecordState.IDLE;
    private AudioRecordThread audioRecordThread;
    private File tmpFile = null;

    public void start(String filePath, RecordConfig config) {
   
        if (state != RecordState.IDLE) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }
        recordFile = new File(filePath);
        String tempFilePath = getTempFilePath();
        Logger.i(TAG, "tmpPCM File: %s", tempFilePath);
        tmpFile = new File(tempFilePath);
        //1.开启录音线程并准备录音
        audioRecordThread = new AudioRecordThread();
        audioRecordThread.start();
    }

    public void stop() {
   
        if (state == RecordState.IDLE) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }

        state = RecordState.STOP;
    }

    private class AudioRecordThread extends Thread {
   
        private AudioRecord audioRecord;
        private int bufferSize;

        AudioRecordThread() {
   
            //2.根据录音参数构造AudioRecord实体对象
            bufferSize = AudioRecord.getMinBufferSize(currentConfig.getFrequency(),
                    currentConfig.getChannel(), currentConfig.getEncoding()) * RECORD_AUDIO_BUFFER_TIMES;
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getFrequency(),
                    currentConfig.getChannel(), currentConfig.getEncoding(), bufferSize);
        }

        @Override
        public void run() {
   
            super.run();
            state = RecordState.RECORDING;
            Logger.d(TAG, "开始录制");
            FileOutputStream fos = null;
            try {
   
                fos = new FileOutputStream(tmpFile);
                audioRecord.startRecording();
                byte[] byteBuffer = new byte[bufferSize];

                while (state == RecordState.RECORDING) {
   
                    //3.不断读取录音数据并保存至文件中
                    int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                    fos.write(byteBuffer, 0, end);
                    fos.flush();
                }
                //4.当执行stop()方法后state != RecordState.RECORDING,终止循环,停止录音
                audioRecord.stop();
            } catch (Exception e) {
   
                Logger.e(e, TAG, e.getMessage());
            } finally {
   
                try {
   
                    if (fos != null) {
   
                        fos.close();
                    }
                } catch (IOException e) {
   
                    Logger.e(e, TAG, e.getMessage());
                }
            }
            state = RecordState.IDLE;
            Logger.d(TAG, "录音结束");
        }
    }
}

其他

  • 这里实现了PCM音频的录制,AudioRecord
    API中只有开始和停止的方法,在实际开发中可能还需要暂停/恢复的操作,以及PCM转WAV的功能,下一篇再继续完善。
  • 需要录音及文件处理的动态权限

3. 使用AudioRecord实现录音的暂停和恢复

上一部分主要写了AudioRecord实现音频录制的开始和停止,AudioRecord并没有暂停和恢复播放功能的API,所以需要手动实现。

解决办法

思路很简单,现在可以实现音频的文件录制和停止,并生成pcm文件,那么暂停时将这次文件先保存下来,恢复播放后开始新一轮的录制,那么最后会生成多个pcm音频,再将这些pcm文件进行合并,这样就实现了暂停/恢复的功能了。

实现

  • 实现的重点在于如何控制录音的状态
public class RecordHelper {
   
    private volatile RecordState state = RecordState.IDLE;
    private AudioRecordThread audioRecordThread;

    private File recordFile = null;
    private File tmpFile = null;
    private List<File> files = new ArrayList<>();

    public void start(String filePath, RecordConfig config) {
   
        this.currentConfig = config;
        if (state != RecordState.IDLE) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }
        recordFile = new File(filePath);
        String tempFilePath = getTempFilePath();
        Logger.i(TAG, "tmpPCM File: %s", tempFilePath);
        tmpFile = new File(tempFilePath);
        audioRecordThread = new AudioRecordThread();
        audioRecordThread.start();
    }

    public void stop() {
   
        if (state == RecordState.IDLE) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }

        //若在暂停中直接停止,则直接合并文件即可
        if (state == RecordState.PAUSE) {
   
            makeFile();
            state = RecordState.IDLE;
        } else {
   
            state = RecordState.STOP;
        }
    }

    public void pause() {
   
        if (state != RecordState.RECORDING) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }
        state = RecordState.PAUSE;
    }

    public void resume() {
   
        if (state != RecordState.PAUSE) {
   
            Logger.e(TAG, "状态异常当前状态: %s", state.name());
            return;
        }
        String tempFilePath = getTempFilePath();
        Logger.i(TAG, "tmpPCM File: %s", tempFilePath);
        tmpFile = new File(tempFilePath);
        audioRecordThread = new AudioRecordThread();
        audioRecordThread.start();
    }

    private class AudioRecordThread extends Thread {
   
        private AudioRecord audioRecord;
        private int bufferSize;

        AudioRecordThread() {
   
            bufferSize = AudioRecord.getMinBufferSize(currentConfig.getFrequency(),
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值