Android 原生音频录制AudioRecorder+AudioTrack

1 音频播放

支持的音频格式

  • AAC:高级音频编码(Advanced Audio Coding)编解码器(以及HEAAC(高效AAC)的两个配置文件),对应的是.m4a(audio/m4a)或.3gp(audio/3gpp)文件。AAC是一种流行的标准。Android支持这种在MPEG-4音频文件(MPEG-4就是.mp4文件)以及3GP文件(3GP就是.3gp文件,3GP基于MPEG-4格式)内部的音频格式,同时还支持新添加到AAC规范中的高效AAC(HEAAC)格式。

  • MP3:MPEG-1 Audio Layer 3(MPEG-1 音频层3),对应的是.mp3(audio/mp3)文件。Android支持MP3,这可能是使用最广泛的音频编解码器。允许Android利用在网上通过各种网站和音乐商店提供的大多数音频。

  • AMR:自适应多速率(Adaptive Multi-Rate)编解码器(包括AMR窄带AMR-NB和AMR宽带AMR-WB),对应的是.3gp(audio/3gpp)或.amr(audio/amr)文件。AMR是由3GPP(3rd Generation Partnership Project,第三代合作伙伴项目)使用的基本语音音频编解码器标准。AMR编解码器主要用于现代手机上的语音呼叫应用程序,对于语音编解码通常是有用的,但不利于处理更复杂类型的音频,如音乐。

  • Ogg:Ogg Vorbis,对应的是.ogg(application/ogg)文件。Ogg Vorbis是一种开放源代码的、无专利权的音频编解码器。

  • PCM:脉冲编码调制(Pulse Code Modulation)通常用于WAVE或WAV文件(Waveform Audio Format,波形音频格式),对应的是.wav(audio/x-wav)文件。PCM是用于计算机和其他数字音频设备上存储音频的技术。它通常是一个未压缩的音频文件,其数据表示一段音频随事件而变化的振幅。“采样率”表示存储振幅读数的频率。“位深“是表示一个单独采样所需的位数。采样率为16kHz、位深为32位的一段音频数据意味着它将以32位数据表示音频振幅,而且每秒钟包含16000个这样的数据。采样率和位深越大,则音频的数字化越准确。采样率和位深还决定了当给定长度时音频文件的大小。Android支持WAV文件中的PCM音频数据。WAV是PC上一个长期使用的标准音频格式。

总结:

AAC、MP3、AMR、Ogg这些都是表示的音频编解码器,相当于音频的编解码算法;针对不同的编解码算法可以输出不同的音频格式文件。

AAC编解码器输出.m4a或.3gp文件;MP3编解码器输出.mp3文件;AMR编解码器输出.3gp或.amr文件;PCM输出.wav文件。

2 系统数据库存储音频

2.1

MediaStore.Audio.Media.EXTERNAL_CONTENT_URI // 存储音频文件的标准音频外部存储uri

音频常用相关列:

  • MediaStore.Audio.Media.DATA // 音频文件路径

  • MediaStore.Audio.Media._ID // 音频文件id

  • MediaStore.Audio.Media.TITLE // 音频文件标题

  • MediaStore.Audio.Media.DISPLAY_NAME // 音频文件名称

  • MediaStore.Audio.Media.MIME_TYPE // 音频文件格式类型,如audio/mp3等

  • MediaStore.Audio.Media.ARTIST // 艺人名称

  • MediaStore.Audio.Media.ALBUM // 艺人专辑

  • MediaStore.Audio.Media.IS_RINGTONE // 是否为警报音频文件类型

  • MediaStore.Audio.Media.IS_ALARM // 是否为闹铃音频文件类型

  • MediaStore.Audio.Media.IS_MUSIC // 是否为音乐音频文件类型

  • MediaStore.Audio.Media.IS_NOTIFICATION // 是否为通知音频文件类型

2.2

音频文件(特别是音乐文件)可以按照唱片集、艺术家和流派(Genre)来查找,也可以直接在MediaStore中查找。

MediaStore.Audio.Albums、MediaStore.Audio.Artists、MediaStore.Audio.Genres三个类都实现了BaseColumns,以及各自实现对应的列接口AlbumColumns、ArtistsColumns、GenresColumns。

3 音频捕获(MediaRecorder)

3.1 MediaRecorder

使用MediaRecorder捕获音频步骤如下(每步调用的执行顺序不同对结果影响非常大):

  • 创建MediaRecorder实例

  • 设置音频源(MediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC))

  • 设置输出格式(MediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4))

  • 设置音频编码器(MediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB))

  • 设置输出文件(MediaRecorder.setOutputFile(filePath))

  • 准备录制(MediaRecorder.prepare())

  • 开始录制(MediaRecorder.start())

  • 重置/停止(MediaRecorder.reset()/MediaRecorder.stop())

  • 释放资源(MediaRecorder.release())

音频源

  • MediaRecorder.AudioSource.MIC:是表示通过麦克风接收捕获音频。

  • MediaRecorder.AudioSource.VOICE_CALL、VOICE_DOWNLINK、VOICE_UPLINK:但是似乎没有任何Android手机或版本能够真正从电话中录制音频。

  • MediaRecorder.AudioSource.CAMCORDER、VOICE_RECOGNITION:如果设备有一个以上的麦克风,就可以使用它们。

输出格式

  • MediaRecorder.OutputFormat.MPEG-4:这个常量指定输出的文件将是一个MPEG-4文件。它可能同时包含音频和视频轨。

  • MediaRecorder.OutputFormat.RAW_AMR:这个常量表示输出一个没有任何容器类型的原始文件。只有在捕获没有视频的音频且音频编码器是AMR_NB时才会使用这个常量。

  • MediaRecorder.OutputFormat.THREE_GPP:这个常量指定输出的文件将是一个3GPP(扩展名为.3gp)。它可能同时包含音频和视频轨。

音频编解码器

  • MediaRecorder.AudioEncoder.DEFAULT:使用默认的音频编解码器。

  • MediaRecorder.AudioEncoder.AMR_NB:这是自适应多速率窄带编解码器。这种编解码器针对语音进行了优化,因此其不适用于语音之外的其他内容。默认情况下它的采样率为8kHz,码率在4.75~12.2kbps之间,这两个数据对于录制除语音之外的其他内容而言非常低。

输出和录制

3.2 其他MediaRecorder方法

  • getMaxAmplitude:允许请求由MediaPlayer录制的音频的最大振幅。每次调用此方法时都会重置该值,因此每次调用都将返回自从上一次调用以来的最大振幅。可通过定期调用该方法实现音量表。

  • setMaxDuration:允许以毫秒为单位指定最大录制持续事件。必须在setOutputFormat方法之后和prepare方法之前调用该方法。

  • setMaxFileSize:允许以字节为单位指定录制的最大文件大小。与setMaxDuration一样,必须在setOutputFormat方法之后和prepare方法之前调用该方法。

  • setAudioChannels:允许指定将录制的音频通道数。通常是一个通道(单声道)或两个通道(双声道)。必须在prepare方法之前调用该方法。

  • setAudioEncodingBitRate:允许指定当压缩音频时编码器所使用的每秒位数(bit/s)。必须在prepare方法之前调用该方法。

  • setAudioSamplingRate:允许指定捕获和编码的音频的采样率。硬件和使用的编解码器将会决定合适的采样率。必须在prepare方法之前调用该方法。

3.3 AudioRecorder录制原始音频

使用AudioRecorder捕获音频步骤如下:

  • 创建AudioRecorder对象实例时指定音频源、录制的采样率、捕获音频通道的数量、音频格式和缓冲区大小

  • 录制音频文件输出

  • 开始录制(AudioRecorder.startRecording())

  • 停止录制(AudioRecorder.stop())

音频源

int audioSource = MediaRecorder.AudioSource.MIC;

录制的采样率

录制的采样率应以Hz为单位指定。MediaRecorder采样的音频是8kHz或8000Hz,而CD质量的音频通常是44.1kHz或44100Hz。Hz是每秒的样本数量。不同的Android手机硬件将能够以不同的采样率进行采样。一个常用的采样率是11025Hz。

int sampleRateInHz = 11025;

获音频通道的数量

  • AudioFormat.CHANNEL_CONFIGURATION_MONO:单声道

  • AudioFormat.CHANNEL_CONFIGURATION_STEREO:立体声道

  • AudioFormat.CHANNEL_CONFIGURATION_INVALID:不设置声道

  • AudioFormat.CHANNEL_CONFIGURATION_DEFAULT:默认声道

int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;

音频格式

  • AudioFormat.ENCODING_DEFAULT:默认音频格式

  • AudioFormat.ENCODING_INVALID:不设置音频格式

  • AudioFormat.ENCODING_PCM_16BIT:脉冲编码调制16位音频格式

  • AudioFormat.ENCODING_PCM_8BIT:脉冲编码调制8位音频格式

PCM实际上是原始的音频样本。可以设置每个样本的分辨率为16位或8位。16位将占用更多的空间和处理能力,但表示的音频更接近真实。

int audioFormat = AudioFormat.ENCODING_PCM_16BIT;

缓冲区大小

实际上可以查询AudioRecorder类以获得最小的缓冲区大小,查询方式是调用getMinBufferSize静态方法,同时传入采样率、通道配置以及音频格式。

int bufferSizeInBytes = AudioRecorder.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

录制音频文件输出

AudioRecorder类实际上不保存捕获的音频,因此需要手动保存捕获的音频。将音频录制到一个文件中。

开始录制

结束录制

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_start_record_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始录制音频" />

    <Button
        android:id="@+id/btn_stop_record_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="停止录制音频" />

    <Button
        android:id="@+id/btn_play_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="播放录制音频" />

    <Button
        android:id="@+id/btn_stop_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="停止播放录制视频" />

    <TextView
        android:id="@+id/tv_max_amplitude"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="10dp" />

</LinearLayout>


package com.example.media.audio;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.example.media.R;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;


public class MediaRecorderActivity extends AppCompatActivity implements MediaPlayer.OnCompletionListener {
    private static final String TAG = MediaRecorderActivity.class.getSimpleName();

    private Button mBtnStartRecordAudio;
    private Button mBtnStopRecordAudio;
    private Button mBtnStartPlayAudio;
    private Button mBtnStopPlayAudio;

    private MediaRecorder mMediaRecorder;
    private MediaPlayer mMediaPlayer;
    private String mAudioFilePath;

    private TextView mTvMaxAmplitude; // 显示最大振幅
    private RecordAmplitude mRecordAmplitude; // 异步显示最大振幅类
    private boolean mIsRecording; // 控制振幅展示标志

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_recorder);

        mBtnStartRecordAudio = findViewById(R.id.btn_start_record_audio);
        mBtnStopRecordAudio = findViewById(R.id.btn_stop_record_audio);
        mBtnStartPlayAudio = findViewById(R.id.btn_play_audio);
        mBtnStopPlayAudio = findViewById(R.id.btn_stop_audio);

        mTvMaxAmplitude = findViewById(R.id.tv_max_amplitude);
        mTvMaxAmplitude.setText("0");

        mBtnStartRecordAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MediaRecorderActivity.this, "开始录制音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(false);
                mBtnStopRecordAudio.setEnabled(true);

                // 在录制音频时显示振幅
                mIsRecording = true;
                mRecordAmplitude = new RecordAmplitude();
                mRecordAmplitude.execute();

                mMediaRecorder = new MediaRecorder();
                mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
                mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
                String outputFile = getOutputFile();
                if (outputFile != null) {
                    mAudioFilePath = outputFile;
                    mMediaRecorder.setOutputFile(outputFile);
                }
                try {
                    mMediaRecorder.prepare();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mMediaRecorder.start();
            }
        });

        mBtnStopRecordAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MediaRecorderActivity.this, "停止录制音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(true);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(true);

                // 停止振幅展示
                mIsRecording = false;
                mRecordAmplitude.cancel(true);
                mTvMaxAmplitude.setText("0");

                mMediaRecorder.stop();
                mMediaRecorder.release();
                mMediaRecorder = null;

                // 将录制的音频插入到数据库
                ContentValues contentValues = new ContentValues();
                contentValues.put(MediaStore.Audio.Media.TITLE, "this is my record audio");
                contentValues.put(MediaStore.Audio.Media.DATA, mAudioFilePath);
                contentValues.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis());
                Uri recordAudioUri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues);
                Log.v(TAG, "record audio uri = " + recordAudioUri);
            }
        });

        mBtnStartPlayAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MediaRecorderActivity.this, "开始播放音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(false);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(false);
                mBtnStopPlayAudio.setEnabled(true);

                if (mAudioFilePath != null) {
                    try {
                        mMediaPlayer = new MediaPlayer();
                        mMediaPlayer.setDataSource(mAudioFilePath);
                        mMediaPlayer.setOnCompletionListener(MediaRecorderActivity.this);
                        mMediaPlayer.prepare();
                        mMediaPlayer.start();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        mBtnStopPlayAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MediaRecorderActivity.this, "停止播放音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(true);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(true);
                mBtnStopPlayAudio.setEnabled(false);

                mMediaPlayer.stop();
                mMediaPlayer.release();
                mMediaPlayer = null;
            }
        });

        mBtnStopRecordAudio.setEnabled(false);
        mBtnStartPlayAudio.setEnabled(false);
        mBtnStopPlayAudio.setEnabled(false);
    }

    private String getOutputFile() {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return null;
        }

        String outputFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() +
                "/Android/data/com.example.media/audio/";
        File filePath = new File(outputFilePath);
        if (!filePath.exists()) {
            if (filePath.mkdirs()) {
                Log.v(TAG, "audio file path create = " + filePath.getAbsolutePath());
            }
        }
        try {
            File tempAudioFile = File.createTempFile("audio" +
                            new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(new Date()),
                    ".3gp", filePath);
            return tempAudioFile.getAbsolutePath();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        if (new File(mAudioFilePath).delete()) {
            int isDeleted = getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    MediaStore.Audio.Media.DATA + "=?", new String[]{mAudioFilePath});
            Log.v(TAG, "audio file delete = " + isDeleted); // 1表示删除 0表示未删除
        }

        mBtnStartRecordAudio.setEnabled(true);
        mBtnStopRecordAudio.setEnabled(false);
        mBtnStartPlayAudio.setEnabled(false);
        mBtnStopPlayAudio.setEnabled(false);

        mMediaPlayer.stop();
        mMediaPlayer.release();
        mMediaPlayer = null;
    }

    @SuppressLint("StaticFieldLeak")
    private class RecordAmplitude extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            while (mIsRecording) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                publishProgress(mMediaRecorder.getMaxAmplitude()); // 定期获取最大振幅
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            String amplitude = values[0].toString();
            mTvMaxAmplitude.setText(amplitude);
        }
    }
}

4 使用AudioTrack播放原始音频

AudioTrack是Android中的一个类,它允许播放原始音频样本。利用该类能够播放使用AudioRecorder捕获的音频。而它们并不能使用MediaPlayer对象来播放。

使用AudioTrack播放音频步骤如下:

  • 创建AudioTrack对象实例时指定流类型、播放的音频数据采样率、通道配置、音频格式、存储音频的缓冲区大小、模式。

  • 读取音频数据

  • 播放音频(AudioTrack.play())

流类型

可能的值定义为AudioManager类中的常量。AudioManager.STREAM_MUSIC,用于正常播放音乐的音频流。

音频数据采样率

以Hz为单位,采用11025Hz的采样率来捕获音频,因此在播放时需要指定相同的值。

通道配置

  • AudioFormat.CHANNEL_CONFIGURATION_MONO:单声道

  • AudioFormat.CHANNEL_CONFIGURATION_STEREO:立体声道

  • AudioFormat.CHANNEL_CONFIGURATION_INVALID:不设置声道

  • AudioFormat.CHANNEL_CONFIGURATION_DEFAULT:默认声道

音频格式

  • AudioFormat.ENCODING_DEFAULT:默认音频格式

  • AudioFormat.ENCODING_INVALID:不设置音频格式

  • AudioFormat.ENCODING_PCM_16BIT:脉冲编码调制16位音频格式

  • AudioFormat.ENCODING_PCM_8BIT:脉冲编码调制8位音频格式

存储音频的缓冲区大小

为了确定最小缓冲区大小,可以调用getMinBufferSize方法,同时传入采样率、通道配置和音频格式。

int frequency = 11025;
int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

int bufferSize = AudioTrack.getMinBufferSize(frequency, channelConfig, audioEncoding);

模式

  • AudioTrack.MODE_STATIC:在播放发生之前将所有的音频数据转移到AudioTrack对象。

  • AudioTrack.MODE_STREAM:在播放的同时将音频数据持续地转移到AudioTrack对象。

读取音频数据

播放音频

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_start_record_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始录制音频" />

    <Button
        android:id="@+id/btn_stop_record_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="停止录制音频" />

    <Button
        android:id="@+id/btn_play_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="播放录制音频" />

    <Button
        android:id="@+id/btn_stop_audio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="停止播放录制视频" />

    <TextView
        android:id="@+id/tv_audio_record_process"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="10dp" />

</LinearLayout>
package com.example.media.audio;

import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.example.media.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;


public class AudioRecorderActivity extends AppCompatActivity {
    private static final String TAG = AudioRecorderActivity.class.getSimpleName();

    private Button mBtnStartRecordAudio;
    private Button mBtnStopRecordAudio;
    private Button mBtnStartPlayAudio;
    private Button mBtnStopPlayAudio;

    private String mAudioFilePath;

    private boolean mIsRecording; // 控制录制音频标志
    private AudioRecordTask mAudioRecordTask; // 异步录制音频任务
    private boolean mIsPlaying; // 控制播放音频标志
    private AudioPlayTask mAudioPlayTask; // 异步播放音频任务

    private TextView mTvRecordProcess;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio_recorder);

        mTvRecordProcess = findViewById(R.id.tv_audio_record_process);

        mBtnStartRecordAudio = findViewById(R.id.btn_start_record_audio);
        mBtnStopRecordAudio = findViewById(R.id.btn_stop_record_audio);
        mBtnStartPlayAudio = findViewById(R.id.btn_play_audio);
        mBtnStopPlayAudio = findViewById(R.id.btn_stop_audio);

        mBtnStartRecordAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(AudioRecorderActivity.this, "开始录制音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(false);
                mBtnStopRecordAudio.setEnabled(true);

                mAudioRecordTask = new AudioRecordTask();
                mAudioRecordTask.execute();
            }
        });

        mBtnStopRecordAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(AudioRecorderActivity.this, "停止录制音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(true);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(true);

                mIsRecording = false;
            }
        });

        mBtnStartPlayAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(AudioRecorderActivity.this, "开始播放音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(false);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(false);
                mBtnStopPlayAudio.setEnabled(true);

                mAudioPlayTask = new AudioPlayTask();
                mAudioPlayTask.execute();
            }
        });

        mBtnStopPlayAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(AudioRecorderActivity.this, "停止播放音频", Toast.LENGTH_SHORT).show();
                mBtnStartRecordAudio.setEnabled(true);
                mBtnStopRecordAudio.setEnabled(false);
                mBtnStartPlayAudio.setEnabled(true);
                mBtnStopPlayAudio.setEnabled(false);

                mIsPlaying = false;

                if (new File(mAudioFilePath).delete()) {
                    Log.v(TAG, "audio file delete"); // 1表示删除 0表示未删除
                }
            }
        });

        mBtnStopRecordAudio.setEnabled(false);
        mBtnStartPlayAudio.setEnabled(false);
        mBtnStopPlayAudio.setEnabled(false);
    }

    private boolean createOutputFile() {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return false;
        }

        String outputFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() +
                "/Android/data/com.example.media/audio/";
        File filePath = new File(outputFilePath);
        if (!filePath.exists()) {
            if (filePath.mkdirs()) {
                Log.v(TAG, "audio file path create = " + filePath.getAbsolutePath());
            }
        }
        try {
            File tempAudioFile = File.createTempFile("audio" +
                            new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA).format(new Date()),
                    ".pcm", filePath);
            mAudioFilePath = tempAudioFile.getAbsolutePath();
            return tempAudioFile.exists();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    // 异步录制原始音频
    @SuppressLint("StaticFieldLeak")
    private class AudioRecordTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            mIsRecording = true;

            int sampleRateInHz = 11025;
            int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            int bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
            AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig,
                    audioFormat, bufferSizeInBytes);

            if (createOutputFile()) {
                DataOutputStream dos = null;
                try {
                    dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mAudioFilePath)));

                    short[] buffer = new short[bufferSizeInBytes];
                    audioRecord.startRecording();

                    int r = 0;
                    while (mIsRecording) {
                        int bufferReadResult = audioRecord.read(buffer, 0, bufferSizeInBytes);
                        for (int i = 0; i < bufferReadResult; i++) {
                            dos.writeShort(buffer[i]);
                        }

                        publishProgress(r);
                        r++;
                    }

                    audioRecord.stop();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    closeSilently(dos);
                }
            }

            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            String process = values[0].toString();
            mTvRecordProcess.setText(process);
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mBtnStartRecordAudio.setEnabled(true);
            mBtnStopRecordAudio.setEnabled(false);
            mBtnStartPlayAudio.setEnabled(true);
        }
    }

    // 异步播放原始音频
    @SuppressLint("StaticFieldLeak")
    private class AudioPlayTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            mIsPlaying = true;

            int sampleRateInHz = 11025;
            int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
            short[] buffer = new short[bufferSize / 4];

            DataInputStream dis = null;
            try {
                dis = new DataInputStream(new BufferedInputStream(new FileInputStream(mAudioFilePath)));

                AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig,
                        audioFormat, bufferSize, AudioTrack.MODE_STREAM);
                audioTrack.play();

                while (mIsPlaying && dis.available() > 0) {
                    int i = 0;
                    while (dis.available() > 0 && i < buffer.length) {
                        buffer[i] = dis.readShort();
                        i++;
                    }
                    audioTrack.write(buffer, 0, buffer.length);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                closeSilently(dis);
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mBtnStartRecordAudio.setEnabled(true);
            mBtnStopRecordAudio.setEnabled(false);
            mBtnStartPlayAudio.setEnabled(true);
            mBtnStopPlayAudio.setEnabled(false);
        }
    }

    private void closeSilently(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果您想使用AndroidAudioRecorder录制音频,并在蓝牙音箱上播放,请按照以下步骤操作: 1. 配置和准备录音器 创建`AudioRecorder`对象,设置音频源、采样率、声道数、编码格式等参数,并调用`prepare()`方法准备录音器。 2. 启动录音 调用`start()`方法开始录音。录音期间,将蓝牙音箱连接到Android设备,并将其设置为音频输出源。 3. 播放录制音频 录音完成后,您可以使用`MediaPlayer`对象加载和播放录制音频文件。在播放期间,音频将通过蓝牙音箱进行输出。 下面是一个简单的示例代码: ``` private AudioRecorder audioRecorder; private MediaPlayer mediaPlayer; private void startRecordingAndPlayback() { audioRecorder = new AudioRecorder(); audioRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); audioRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); audioRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); audioRecorder.setAudioSamplingRate(44100); audioRecorder.setAudioChannels(2); audioRecorder.setOutputFile(getFilePath()); // 设置音频文件保存路径 try { audioRecorder.prepare(); audioRecorder.start(); } catch (Exception e) { e.printStackTrace(); } mediaPlayer = new MediaPlayer(); mediaPlayer.setDataSource(getFilePath()); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.prepare(); mediaPlayer.start(); } private void stopRecordingAndPlayback() { if (mediaPlayer != null) { mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer = null; } if (audioRecorder != null) { audioRecorder.stop(); audioRecorder.release(); audioRecorder = null; } } ``` 请注意,这只是一个基本示例。您可能需要根据自己的需求进行更改和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值