将时间顺序写入Android AudioTrack
我目前正在为Android中的示例排序器编写一些代码。 我正在使用AudioTrack类。 我被告知,准确定时的唯一正确方法是使用AudioTrack的时间。 EG我知道,如果我以每秒44100个采样率的速度向AudioTrack写入X个样本的缓冲区,则写入时间将为(1/44100)X秒。
然后你使用这个信息来知道什么时候应该写什么样的样本。
我正尝试使用这种方法来实现我的第一次尝试。 我只使用一个样本,并以120bpm的速度将它作为连续的第16个音符书写。 但由于某种原因,它正在以240bpm的速度播放。
首先,我检查了我的代码,以得出速度为X的第16个(纳秒)音符的时间。它检查出来。
private void setPeriod()
{
period=(int)((1/(((double)TEMPO)/60))*1000);
period=(period*1000000)/4;
Log.i("test",String.valueOf(period));
}
然后,我验证了我的代码是为了让我的缓冲区以44100khz在纳秒中播放,这是正确的。
long bufferTime=(1000000000/SAMPLE_RATE)*buffSize;
所以现在我仍然认为音轨的播放速度不同于44100.也许96000khz,这将解释速度加倍。 但是当我实例化audioTrack时,它确实设置为44100khz。
final int SAMPLE_RATE设置为44100
buffSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
track = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
buffSize,
AudioTrack.MODE_STREAM);
所以我很困惑,为什么我的节奏加倍。 我运行了一个调试程序,将时间流逝的audioTrack与时间流逝的系统时间进行比较,并且似乎audiotrack确实是其播放速度的两倍。 我很困惑。
只是为了确保,这是我的循环播放。
public void run() {
// TODO Auto-generated method stub
int buffSize=192;
byte[] output = new byte[buffSize];
int pos1=0;//index for output array
int pos2=0;//index for sample array
long bufferTime=(1000000000/SAMPLE_RATE)*buffSize;
long elapsed=0;
int writes=0;
currTrigger=trigger[triggerPointer];
Log.i("test","period="+String.valueOf(period));
Log.i("test","bufferTime="+String.valueOf(bufferTime));
long time=System.nanoTime();
while(play)
{
//fill up the buffer
while(pos1<buffSize)
{
output[pos1]=0;
if(currTrigger&&pos2<sample.length)
{
output[pos1]=sample[pos2];
pos2++;
}
pos1++;
}
track.write(output, 0, buffSize);
elapsed=elapsed+bufferTime;
writes++;
//time passed is more than one 16th note
if(elapsed>=period)
{
Log.i("test",String.valueOf(writes));
Log.i("test","elapsed A.T.="+String.valueOf(elapsed)+" elapsed S.T.="+String.valueOf(System.nanoTime()-time));
time=System.nanoTime();
writes=0;
elapsed=0;
triggerPointer++;
if(triggerPointer==16)
triggerPointer=0;
currTrigger=trigger[triggerPointer];
pos2=0;
}
pos1=0;
}
}
}
编辑 :改写和更新由于最初的错误假设系统时间被用来同步顺序音频:)
至于音频以两倍的速度播放,这有点奇怪,因为AudioTrack的“写入”方法会阻塞,直到本机层将下一个缓冲区排入队列,您确定渲染循环没有从两个不同的调用中调用来源(尽管我从你的例子中假设你从一个线程中调用循环)。
然而,可以肯定的是,有一个时间同步问题需要解决:这里的问题在于计算您在示例中使用的缓冲时间:
(1000000000/SAMPLE_RATE)*buffSize;
在44100Hz的采样率下,它总是会返回4353741缓冲区大小为192个采样点,因此忽略任何速度的线索(例如,在300 BPM或40 BPM时这将是相同的)。现在,在您的示例中,不会对实际的同步本身产生任何影响,但我想指出这一点,因为我们将在本文中进一步回到它。
另外,纳秒是一个很好的精确单位,但是对于音频操作而言,毫秒就足够了。 因此,我将以毫秒为单位继续说明。
对于第120个BPM的第16个音符的计算结果确实以125 ms的正确值进行检查。 前面提到的与每个缓冲区大小对应的时间段的计算是4.3537毫秒。 这表明您将在单个十六分音符的时间通过之前迭代缓冲循环28.7112次。 然而在你的例子中,你可以通过使用下面的命令来检查这个十六分音符的“偏移量”是否已经在缓冲区迭代循环的末尾(单个缓冲区的周期已经加到了经过的时间!
elapsed>=period
这会在第一次引起漂移,因为此时“经过”将是(192 * 29迭代)5568个样本(或126.26毫秒),而不是在(192 * 28.7112迭代)5512个样本(或126毫秒) 。 这是56个样本的差异(或者在及时说话时:1.02毫秒)。 这当然不会导致样本播放比预期的更快(如您所述),但已导致播放不规则。 对于第二十六个音符(这将发生在第57.4224次迭代中,漂移将是11136-11025 = 111个样本或2.517毫秒(超过缓冲时间的一半)。因此,您必须执行此检查
while(pos1<buffSize)
循环,在那里你正在增加读指针,直到达到缓冲区的大小。 因此,您需要通过缓冲区周期PER缓冲区样本的一部分增加另一个变量。
我希望上面的例子说明了为什么我最初会提出通过样本迭代计算时间而不是经过时间(当然,样本DO表示时间,因为它们仅仅是将时间单位转换为缓冲区中的样本数量,但是您可以使用这些数字作为标记,而不是像在渲染循环中一样向计数器添加固定时间间隔)。
首先,一些便利的数学来帮助你获得这些值:
// calculate the amount of samples are necessary for storing the given length of time
// ( in milliSeconds ) at the given sample rate ( in Hz )
int millisecondsToSamples( int milliSeconds, int sampleRate )
{
return ( int ) ( milliSeconds * ( sampleRate / 1000 ));
}
或者:这些计算在您的文章中提到的音乐环境中思考时更方便。 以给定的采样率(Hz),节奏(BPM)和拍号(timeSigBeatUnit为“4”,timeSigBeatAmount为拍号中的“3”)计算单个乐曲音棒中存在的采样数量3/4 - 尽管大多数序列发生器限制为4/4我已经添加了解释逻辑的计算)。
int samplesPerBeat = ( int ) (( sampleRate * 60 ) / tempo );
int samplesPerBar = samplesPerBeat * timeSigBeatAmount;
int samplesPerSixteenth = ( int ) ( samplesPerBeat / 4 ); // 1/4 of a beat being a 16th
等等
然后,您将定时采样写入输出缓冲区的方式是跟踪缓冲区回调中的“回放位置”,即每次写入缓冲区时,都会使用缓冲区的长度递增回放位置。回到音乐上下文:如果您要“在4/4时间内循环播放120 bpm的单个条”,当播放位置超过((sampleRate * 60)/ 120 * 4 = 88200个样本时,将其重置为0从一开始就“循环”。
因此,让我们假设您有两个音频事件,它们以120 BPM的单个4/4时间条序列出现。 一个事件是在一个酒吧的第一个节拍上进行并且持续一个颤音(一个酒吧的1/8),另一个是在酒吧的第三个节拍上进行并且持续另一个颤音。 这两个“事件”(您可以在值对象中表示)对于第一个事件将具有以下属性:
int start = 0; // buffer position 0 is at the 1st beat/start of the bar
int length = 11025; // 1/8 of the full bar size
int end = 11025; // start + length
第二个事件:
int start = 44100; // 3rd beat (or half-way through the bar)
int length = 11025;
int end = 55125; // start + length
这些值对象可以有两个额外的属性,例如“sample”(可能是包含实际音频的缓冲区)和“readPointer”,它们将保存顺序器从上次读取的最后一个样本缓冲区索引。
然后在缓冲区写入循环中:
int playbackPosition = 0; // at start of bar
int maximumPlaybackPosition = 88200; // i.e. a single bar of 4/4 at 120 bpm
public void run()
{
// loop through list of "audio events" / samples
for ( CustomValueObject audioEvent : audioEventList )
{
// loop through the buffer length this cycle will write
for ( int i = 0; i < bufferSize; ++i )
{
// calculate "sequence position" from playback position and current iteration
int seqPosition = playbackPosition + i;
// sequence position within start and end range of audio event ?
if ( seqPosition >= audioEvent.start && seqPosition <= audioEvent.end )
{
// YES! write its sample content into the output buffer
output[ i ] += audioEvent.sample[ audioEvent.readPointer ];
// update the sample read pointer to the next slot (but keep in bounds)
if ( ++audioEvent.readPointer == audioEvent.length )
audioEvent.readPointer = 0;
}
}
// update playback position and keep within sequencer range for looping
if ( playbackPosition += bufferSize > maximumPosition )
playbackPosition -= maximumPosition;
}
}
这应该给你一个完美的时间写作音频的方法。 当你触及序列循环的迭代时(即从样本开始处读取剩余未处理的缓冲区长度以实现无缝循环),仍然需要制定一些法术,但是我希望这会给你一个关于工作方法。
链接地址: http://www.djcxy.com/p/5797.html