Android-dev AudioRecord没有阻塞或线程

时间:2013-04-04 07:21:04

标签: android-audiomanager android-audiorecord

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

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

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

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

以下是我发现的内容:

初始化AudioRecord对象时,它会创建自己的内部环型缓冲区。 当调用.start()时,它开始记录到所述环形缓冲区(或者它实际上是什么类型。)

当调用.read()时,它会读取bufferSize的一半或指定的字节数(以较小者为准)然后返回。

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

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

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

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

2:必须在句点或位置计时器开始计数之前调用.read() - 换句话说,在调用.start()之后再调用.read(),并且每次监听器都是呼叫,再次呼叫.read()

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

因此,使用这些规则,我能够使回调/监听器工作,但由于某种原因,读取仍然是阻塞的,我无法弄清楚如何在完全读取值时调用监听器

如果我设置按钮视图以点击阅读,那么我可以点击它,如果快速点击,则读取块。但是,如果我等待音频缓冲区填充,那么第一次点击是即时的(读取立即返回),但是由于read()必须等待,我猜测是否会暂停快速点击。

非常感谢有关如何使Listener按预期工作的任何见解 - 以这样一种方式,当有足够的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

如果关心此错误的人登录并投票和/或评论该错误,Google可能会尽快解决。

2 个答案:

答案 0 :(得分:0)

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

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

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

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

答案 1 :(得分:0)

这已经很晚了,但我想我知道杰西在哪里犯了错误。他的读取呼叫被阻止,因为他正在请求与缓冲区大小相同的短路,但缓冲区大小以字节为单位,短路包含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可以告诉我们缓冲区包含的短路数。

我前段时间工作过,所以请告诉我它是否适合你。