9

第一次在这里海报。我通常喜欢自己找到答案(无论是通过研究还是反复试验),但我在这里被难住了。

我正在尝试做的事情: 我正在构建一个简单的 android 音频合成器。现在,我只是实时播放正弦音,UI 中有一个滑块,可以在用户调整时改变音调的频率。

我是如何构建它的: 基本上,我有两个线程——一个工作线程和一个输出线程。每次调用它的 tick() 方法时,工作线程只需用正弦波数据填充缓冲区。一旦缓冲区被填满,它会提醒输出线程数据已准备好写入音轨。我使用两个线程的原因是因为 audiotrack.write() 阻塞,并且我希望工作线程能够尽快开始处理其数据(而不是等待音轨完成写入)。UI 上的滑块只是更改工作线程中的一个变量,因此对频率的任何更改(通过滑块)都将被工作线程的 tick() 方法读取。

什么有效: 几乎所有东西;线程沟通良好,播放中似乎没有任何间隙或点击。尽管缓冲区大小很大(感谢 android),但响应能力还可以。频率变量确实发生了变化,tick() 方法中缓冲区计算期间使用的中间值也发生了变化(由 Log.i() 验证)。

什么不起作用: 出于某种原因,我似乎无法获得可听频率的连续变化。当我调整滑块时,频率会逐步变化,通常与四度或五度一样宽。从理论上讲,我应该听到 1Hz 的微小变化,但我不是。奇怪的是,滑块的变化似乎导致正弦波在谐波系列的间隔中播放。但是,我可以验证频率变量没有捕捉到默认频率的整数倍。

我的音轨设置如下:

_buffSize = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
_audioTrackOut = new AudioTrack(AudioManager.STREAM_MUSIC, _sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, _buffSize, AudioTrack.MODE_STREAM);

正在填充工作线程的缓冲区(通过 tick()),如下所示:

public short[] tick()
{
    short[] outBuff = new short[_outBuffSize/2]; // (buffer size in Bytes) / 2
    for (int i = 0; i < _outBuffSize/2; i++) 
    {
        outBuff[i] = (short) (Short.MAX_VALUE * ((float) Math.sin(_currentAngle)));

        //Update angleIncrement, as the frequency may have changed by now
        _angleIncrement = (float) (2.0f * Math.PI) * _freq / _sampleRate;
        _currentAngle = _currentAngle + _angleIncrement;    
    }
    return outBuff;     
}

音频数据是这样写的:

_audioTrackOut.write(fromWorker, 0, fromWorker.length);

任何帮助将不胜感激。我怎样才能获得更渐进的频率变化?我非常有信心我在 tick() 中的逻辑是合理的,因为 Log.i() 验证变量 angleIncrement 和 currentAngle 正在正确更新。

谢谢!

更新:

我在这里发现了一个类似的问题:Android AudioTrack buffering questions 该解决方案提出必须能够为audioTrack 生成足够快的样本,这是有道理的。我将采样率降低到 22050Hz,并进行了一些经验测试——在最坏的情况下,我可以在大约 6 毫秒内填充我的缓冲区(通过 tick())。这绰绰有余。在 22050Hz 时,audioTrack 为我提供了 2048 个样本(或 4096 字节)的缓冲区大小。因此,每个填充的缓冲区持续约 0.0928 秒的音频,这比创建数据所需的时间(1~6 毫秒)要长得多。所以,我知道我没有任何问题足够快地生产样品。

我还应该注意到,在应用程序生命周期的前 3 秒内,它运行良好 - 滑块的平滑扫描会在音频输出中产生平滑扫描。在此之后,它开始变得非常不稳定(声音仅每 100Mhz 变化一次),之后,它完全停止响应滑块输入。

我还修复了一个错误,但我认为它没有效果。AudioTrack.getMinBufferSize() 返回允许的最小缓冲区大小(以 BYTES 为单位),我使用这个数字作为 tick() 中的缓冲区长度 - 我现在使用这个数字的一​​半(每个样本 2 个字节)。

4

1 回答 1

5

我找到了!

事实证明,这个问题与缓冲区或线程无关。

前几秒钟听起来不错,因为计算的角度相对较小。随着程序的运行和角度的增长,Math.sin(_currentAngle) 开始产生不可靠的值。

所以,我Math.sin()FloatMath.sin().

我也换了 _currentAngle = _currentAngle + _angleIncrement;

_currentAngle = ((_currentAngle + _angleIncrement) % (2.0f * (float) Math.PI));,所以角度总是 < 2*PI。

奇迹般有效!非常感谢您的帮助,执政官机器人!

于 2012-04-15T08:23:32.343 回答