8

我希望录制麦克风音频流,以便可以对其进行实时 DSP。

我想这样做而不必使用线程并且.read()在等待新的音频数据时没有阻塞。

更新/回答:这是 Android 中的一个错误。4.2.2 仍然有问题,但 5.01 已修复!我不确定分歧在哪里,但这就是故事。

注意:请不要说“只使用线程”。线程很好,但这与它们无关,Android 开发人员打算让 AudioRecord 完全可用,而无需我指定线程,也无需我处理阻塞 read()。谢谢!

这是我发现的:

当 AudioRecord 对象初始化时,它会创建自己的内部环形缓冲区。当.start()被调用时,它开始记录到所述环形缓冲区(或任何它真正的类型。)

.read()被调用时,它读取缓冲区大小的一半或指定的字节数(以较小者为准),然后返回。

如果内部缓冲区中有足够多的音频样本,则 read() 会立即返回数据。如果还不够,则 read() 等到有,然后返回数据。

.setRecordPositionUpdateListener()可用于设置监听器, 和.setPositionNotificationPeriod().setNotificationMarkerPosition()用于分别设置通知周期和位置。

但是,除非满足某些要求,否则似乎永远不会调用 Listener:

1:Period 或 Position 必须等于 bufferSize/2 或 (bufferSize/2)-1。

2:.read()必须在 Period 或 Position 计时器开始计数之前调用 A - 换句话说,在调用.start()之后 then 还调用.read(),并且每次调用 Listener 时,.read()再次调用。

3:.read()每次必须读取至少一半的bufferSize。

所以使用这些规则我可以​​让回调/监听器工作,但由于某种原因,读取仍然阻塞,我无法弄清楚如何让监听器只在有完整读取的价值时才被调用。

如果我设置一个按钮视图来点击阅读,那么我可以点击它,如果快速点击,阅读块。但是如果我等待音频缓冲区填满,那么第一次点击是即时的(读取立即返回)但随后的快速点击被阻止,因为 read() 必须等待,我猜。

非常感谢我如何使监听器按预期工作的任何见解 - 这样当有足够的数据供 read() 立即返回时,我的监听器会被调用。

下面是我的代码的相关部分。

我的代码中有一些日志语句,它们将字符串发送到 logcat,这使我可以查看每个命令需要多长时间,这就是我知道 read() 阻塞的方式。(而且我的简单测试应用程序中的按钮在重复读取时响应也非常慢,但 CPU 没有固定。)

谢谢,~杰西

在我的 OnCreate() 中:

    bufferSize=AudioRecord.getMinBufferSize(samplerate,AudioFormat.CHANNEL_CONFIGURATION_MONO,AudioFormat.ENCODING_PCM_16BIT)*4;
        recorder = new AudioRecord (AudioSource.MIC,samplerate,AudioFormat.CHANNEL_CONFIGURATION_MONO,AudioFormat.ENCODING_PCM_16BIT,bufferSize);
        recorder.setRecordPositionUpdateListener(mRecordListener);
        recorder.setPositionNotificationPeriod(bufferSize/2);
        //recorder.setNotificationMarkerPosition(bufferSize/2);
        audioData = new short [bufferSize];

    recorder.startRecording();
            samplesread=recorder.read(audioData,0,bufferSize);//This triggers it to start doing the callback.

然后这是我的听众:

public OnRecordPositionUpdateListener mRecordListener = new OnRecordPositionUpdateListener() 
{
    public void onPeriodicNotification(AudioRecord recorder) //This one gets called every period. 
    {
        Log.d("TimeTrack", "AAA");
        samplesread=recorder.read(audioData,0,bufferSize);
        Log.d("TimeTrack", "BBB");
        //player.write(audioData, 0, samplesread);
        //Log.d("TimeTrack", "CCC");
        reads++;
    }
    @Override
    public void onMarkerReached(AudioRecord recorder) //This one gets called only once -- when the marker is reached.
    {
        Log.d("TimeTrack", "AAA");
        samplesread=recorder.read(audioData,0,bufferSize);
        Log.d("TimeTrack", "BBB");
        //player.write(audioData, 0, samplesread);
        //Log.d("TimeTrack", "CCC");
    }
};

更新:我已经在 Android 2.2.3、2.3.4 和现在的 4.0.3 上尝试过这个,并且都表现相同。另外:code.google 上有一个关于它的开放错误 - 一个条目于 2012 年由其他人开始,然后一个条目从 2013 年由我开始(我不知道第一个):

2016 年更新:啊啊啊啊,经过多年的怀疑是我还是安卓,我终于有了答案!我在 4.2.2 上尝试了上面的代码,同样的问题。我在 5.01 上尝试了上面的代码,它可以工作!!!并且不再需要最初的 .read() 调用。现在,一旦 .setPositionNotificationPeriod() 和 .StartRecording() 被调用,mRecordListener() 就会在每次有可用数据时神奇地开始被调用,因此它不再阻塞,因为直到记录了足够的数据才会调用回调. 我没有听过数据来知道它是否正确记录,但是回调正在发生,并且它没有像以前那样阻塞活动!

http://code.google.com/p/android/issues/detail?id=53996

http://code.google.com/p/android/issues/detail?id=25138

如果关心这个错误的人登录并投票和/或评论这个错误,谷歌可能会尽快解决它。

4

2 回答 2

1

很晚了,但我想我知道杰西在哪里做错了。他的读取调用被阻止,因为他正在请求大小与缓冲区大小相同的短裤,但缓冲区大小以字节为单位,而 short 包含 2 个字节。如果我们将短数组的长度与缓冲区相同,我们将读取两倍的数据。

解决方案是audioData = new short[bufferSize/2]如果缓冲区大小为 1000 字节,这样我们将请求 500 个短裤,即 1000 字节。

他也应该samplesread=recorder.read(audioData,0,bufferSize)改为samplesread=recorder.read(audioData,0,audioData.length)

更新

好的,杰西。我可以看到另一个错误可能在哪里 - positionNotificationPeriod。这个值必须足够大,这样它就不会过于频繁地调用监听器,我们需要确保当监听器被调用时,要读取的字节已经准备好被收集。如果在调用监听器时字节还没有准备好,主线程将被 recorder.read(audioData, 0, audioData.length) 调用阻塞,直到 AudioRecord 收集到请求的字节。您应该根据您设置的时间间隔计算缓冲区大小和短路数组长度 - 您希望多久调用一次侦听器。位置通知周期、缓冲区大小和短裤数组长度都必须正确调整。让我给你看一个例子:

int periodInFrames = sampleRate / 10;
int bufferSize = periodInFrames * 1 * 16 / 8;
audioData = new short [bufferSize / 2];

int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
if (bufferSize < minBufferSize) bufferSize = minBufferSize;

recorder = new AudioRecord(AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffersize);
recorder.setRecordPositionUpdateListener(mRecordListener);
recorder.setPositionNotificationPeriod(periodInFrames);
recorder.startRecording();

public OnRecordPositionUpdateListener mRecordListener = new OnRecordPositionUpdateListener() {
    public void onPeriodicNotification(AudioRecord recorder) {
        samplesread = recorder.read(audioData, 0, audioData.length);
        player.write(short2byte(audioData));
    }
};

private byte[] short2byte(short[] data) {
        int dataSize = data.length;
        byte[] bytes = new byte[dataSize * 2];

        for (int i = 0; i < dataSize; i++) {
            bytes[i * 2] = (byte) (data[i] & 0x00FF);
            bytes[(i * 2) + 1] = (byte) (data[i] >> 8);
            data[i] = 0;
        }
        return bytes;
    }

所以现在稍微解释一下。

首先,我们设置必须调用侦听器以收集音频数据的频率 (periodInFrames)。PositionNotificationPeriod 以帧表示。采样率以每秒帧数表示,因此对于 44100 采样率,我们有 44100 帧每秒。我将它除以 10,因此每 4410 帧 = 100 毫秒将调用一次侦听器 - 这是合理的时间间隔。

现在我们根据 periodInFrames 计算缓冲区大小,因此在我们收集之前不会覆盖任何数据。缓冲区大小以字节表示。我们的时间间隔是 4410 帧,每帧包含 1 个单声道字节或 2 个立体声字节,因此我们将其乘以通道数(在您的情况下为 1)。每个通道包含 1 个字节用于 ENCODING_8BIT 或 2 个字节用于 ENCODING_16BIT,因此我们将其乘以每个样本的位数(ENCODING_16BIT 为 16,ENCODING_8BIT 为 8)并除以 8。

然后我们将 audioData 长度设置为 bufferSize 的一半,这样我们就可以确保在调用侦听器时,要读取的字节已经在那里等待收集。这是因为 short 包含 2 个字节,而 bufferSize 以字节表示。

然后我们检查 bufferSize 是否足够大以成功初始化 AudioRecord 对象,如果不是,那么我们将 bufferSize 设置为它的最小大小 - 我们不需要更改时间间隔或 audioData 长度。

在我们的监听器中,我们读取数据并将其存储到短数组中。这就是我们使用 audioData.length 代替缓冲区大小的原因,因为只有 audioData.length 可以告诉我们缓冲区包含的短裤数量。

前段时间我让它工作了,所以请让我知道它是否对你有用。

于 2015-06-10T16:29:45.507 回答
0

我不确定你为什么要避免产生单独的线程,但如果是因为你不想正确处理它们的编码,你可以在每个 .read 之后在 Timer 对象上使用 .schedule,其中时间间隔是设置为填充缓冲区所需的时间(缓冲区中的样本数/sampleRate)。是的,我知道这是使用单独的线程,但给出此建议的前提是您避免使用线程的原因是为了避免必须正确编码它们。

这样,它可能阻塞线程的最长时间应该可以忽略不计。但我不知道你为什么要这么做。

如果上述原因不是您避免使用单独线程的原因,请问为什么?

另外,实时到底是什么意思?您是否打算使用 AudioTrack 播放受影响的音频?因为大多数 Android 设备的延迟非常糟糕。

于 2013-05-02T15:38:53.500 回答