我希望录制麦克风音频流,以便我可以在其上进行实时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可能会尽快解决。
答案 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可以告诉我们缓冲区包含的短路数。
我前段时间工作过,所以请告诉我它是否适合你。