将原始音频字节记录到 Android 中的局部变量

Posted

技术标签:

【中文标题】将原始音频字节记录到 Android 中的局部变量【英文标题】:Record Raw Audio Bytes to Local Variable in Android 【发布时间】:2021-12-27 23:02:40 【问题描述】:

我在 android 上将音频作为原始字节录制到一个变量中,该变量可由 AudioTrack 的实例播放。

也就是说,不是作为文件。

我选择了采样率为 44100 的 8 位单声道 PCM,因为该音频数据应该是未压缩的……并且是可预测的、与平台无关的格式。

别担心...我们在这里只处理小型音频 sn-ps...此外,现在人们拥有如此多的 RAM,以至于它是超高频的。

我已经尝试过下面的方法......虽然它显然有效,但相关部分 writeAudioToLocalVariable() 是来自互联网的修改后的剪切粘贴而且我对内容还不是很了解。

考虑到我们无法预测最终用户设备的硬件能力,是否有更博学的人告诉我,这是否是 writeAudioToLocalVariable() 函数中最稳定可靠的处理方式?

这是我得到的:

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.io.ByteArrayOutputStream;

public class MainActivity extends AppCompatActivity 
    private static final int NOT_USED = 999999999;
    private static final int SAMPLERATE = 44100;
    private static final int REC_CHANNELS = AudioFormat.CHANNEL_IN_MONO;
    private static final int PLAYBACK_CHANNELS = AudioFormat.CHANNEL_OUT_MONO;
    private static final int ENCODING = AudioFormat.ENCODING_PCM_8BIT;
    // !!! needs to be large enough for all devices
    private static final int MY_CHOSEN_BUFFER_SIZE = 8192;
    private AudioRecord recorder = null;
    private Thread recordingThread = null;
    boolean isRecording = false;

    byte[] recordedAudioAsBytes; // <------ this is where recorded audio ends up

    AudioTrack player;

    @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int minBufferSize = AudioRecord.getMinBufferSize(SAMPLERATE,
                REC_CHANNELS, ENCODING);
        Log.i("ABC", "FYI, min buffer size for this device is : " + minBufferSize);
    

    private void startRecording() 
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) 
            ActivityCompat.requestPermissions(this, new String[]Manifest.permission.RECORD_AUDIO, NOT_USED);
            Log.i("ABC", "permission fail, returning.");
            return;
        
        recorder = new AudioRecord(MediaRecorder.Audiosource.MIC,
                SAMPLERATE, REC_CHANNELS,
                ENCODING, MY_CHOSEN_BUFFER_SIZE);
        recorder.startRecording();
        isRecording = true;
        recordingThread = new Thread(new Runnable() 
            public void run() 
                Log.i("ABC", "the thread is running");
                writeAudioToLocalVariable();
            
        , "AudioRecorder Thread");
        recordingThread.start();
    

    private void writeAudioToLocalVariable() 
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] temporaryChunkOfBytes = new byte[MY_CHOSEN_BUFFER_SIZE];
        while (isRecording) 
            recorder.read(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE);
            try 
                System.out.println("Appending to baos : " + temporaryChunkOfBytes);
                //printBytes(temporaryChunkOfBytes);
                baos.write(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE);
             catch (Exception e) 
                Log.i("ABC", "Exception while appending bytes : " + e); // <----- this is not called and that is good.
            
        
        recordedAudioAsBytes = baos.toByteArray();
    

    private void stopRecording() 
        // stops the recording activity
        if (null != recorder) 
            isRecording = false;
            recorder.stop();
            recorder.release();
            recorder = null;
            recordingThread = null;
        
    

    public void recordButtonClicked(View v) 
        isRecording = true;
        startRecording();
    

    public void stopButtonClicked(View v) 
        isRecording = false;
        stopRecording();
    

    public void playButtonPressed(View v) 

        // this verifies that audio data exists as expected
        for (int i=0; i<recordedAudioAsBytes.length; i++) 
            Log.i("ABC", "byte[" + i + "] = " + recordedAudioAsBytes[i]);
        

        // STREAM MODE ACTUALLY WORKS!! (STATIC MODE DOES NOT WORK)
        AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLERATE, PLAYBACK_CHANNELS,
                ENCODING, MY_CHOSEN_BUFFER_SIZE, AudioTrack.MODE_STREAM);
        player.play();
        player.write(recordedAudioAsBytes, 0, recordedAudioAsBytes.length);

    


谢谢你...来自我和许多其他人在未来丰富的 RAM !

【问题讨论】:

【参考方案1】:

实现的想法是正确的,但它有一些错误。 ByteArrayOutputStream 是一个输出流,它将内存存储为以后可以抓取的字节,并根据需要处理不断增长的数组。您可以假设在最坏的情况下,这样做需要 2*sound_data_size 的存储空间(它可能需要在某个时候进行复制)。

所有循环所做的只是从音频数据中一次读取 8K 并将其写入循环中的 baos 直到完成。我确实看到了一些错误——它可以在最后一次写入时写入额外的数据(读取可以返回少于完整缓冲区)。如果流在 isRecording 完成之前关闭(读取将立即返回 -1),它将快速循环,并且在这种情况下可能会向您发送 OOM。它可能会在接近尾声时丢失数据(如果循环变量在读取数据之前变为假)。试试这个:

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] temporaryChunkOfBytes = new byte[MY_CHOSEN_BUFFER_SIZE];
    int read = 0;
    while((read = recorder.read(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE) >= 0)
        try 
            System.out.println("Appending to baos : " + temporaryChunkOfBytes);
            //printBytes(temporaryChunkOfBytes);
            baos.write(temporaryChunkOfBytes, 0, read);
         catch (Exception e) 
            Log.i("ABC", "Exception while appending bytes : " + e); // <----- this is not called and that is good.
        
    
    recordedAudioAsBytes = baos.toByteArray();

使用此版本,您不应该只让线程始终运行。开始录制时启动线程。这修复了上述所有 3 个错误 - 如果读取返回 -1,它会中断循环。如果读取量小于缓冲区,则仅写入读取量。它会一直读取直到文件关闭,直到变量设置为 false 才会处理缓冲区中未读取的数据。

您最终会遇到一些内存问题。基本上你每秒会占用 44K 的内存。如果你录制几秒钟,没问题。如果您要录制几分钟,则需要将其写入文件,然后在处理文件时流式传输。

【讨论】:

抱歉,如果你看到了这个的初始版本,我写完后发现了错误 不用担心,感谢您的帮助!欣赏你的经历。 IDE告诉我“read = recorder.read(temporaryChunkOfBytes, 0, MY_CHOSEN_BUFFER_SIZE) >= 0”是一个整数,但应该是一个布尔值......所以我编辑了你的答案,而不是试图粘贴在这里...... lmk 如果有意义的话。 我的和你的之间的一个细微差别是你的不能完全防止-1。如果 numBytesRead == -1 你不想 baos.write 好的,我明白了...我撤消了编辑,但仍需要进行某种修改。

以上是关于将原始音频字节记录到 Android 中的局部变量的主要内容,如果未能解决你的问题,请参考以下文章

从android上的wav文件中读取原始数据

使用 java [重复] 确定给定一个字节 [] 记录的音频数据的频率

即时将原始音频字节从 NAudio 转换为 wav 字节

如何将声音字节转换为可以在变量中访问的 .wav 文件?

如何在 Windows 10 上使用 C++ 将连续的原始音频数据记录到循环缓冲区中?

Android音频记录和处理