【问题标题】:Writing time sequenced to Android AudioTrack将时间序列写入 Android AudioTrack
【发布时间】:2014-02-20 15:38:32
【问题描述】:

我目前正在为 Android 中的示例音序器编写一些代码。我正在使用 AudioTrack 类。有人告诉我,获得准确计时的唯一正确方法是使用 AudioTrack 的计时。 EG 我知道如果我将 X 个样本的缓冲区写入以每秒 44100 个样本的速率播放的 AudioTrack,则写入时间将是 (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。

最终 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 与经过的系统时间,并且似乎音轨的播放速度确实是应有的速度的两倍。我很困惑。

只是为了确保,这是我的播放循环。

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;
                        }
                    }

                }

【问题讨论】:

    标签: android audio signal-processing audiotrack vst


    【解决方案1】:

    已编辑:由于最初错误假设系统时间用于同步序列音频,因此重新措辞和更新 :)

    至于以两倍的速度播放音频,这有点奇怪,因为 AudioTrack 的“写入”方法会阻塞,直到本机层将下一个缓冲区排入队列,您确定没有调用渲染循环来自两个不同的来源(尽管我从您的示例中假设您从线程中调用循环)。

    但是,可以肯定的是,有一个时间同步问题需要解决:这里的问题在于您在示例中使用的缓冲时间的计算:

    (1000000000/SAMPLE_RATE)*buffSize;
    

    这将总是以 44100 Hz 的采样率以 192 个样本的缓冲区大小返回 4353741,因此 忽略任何节奏提示(例如,这将是在 300 BPM 或 40 BPM 时相同),现在,在您的示例中,这对实际同步本身没有任何影响,但我想指出这一点,因为我们稍后会在本文中进一步讨论.

    此外,纳秒是一个非常精确的单位,但对于音频操作来说,毫秒就足够了。因此,我将以毫秒为单位继续说明。

    您对 120 BPM 的 16 分音符周期的计算确实以 125 毫秒的正确值检查。前面提到的每个缓冲区大小对应的周期的计算是 4.3537 毫秒。这表明您将在单个十六分音符通过之前迭代缓冲区循环 28.7112 次。但是,在您的示例中,您使用以下命令检查第十六个音符的“偏移量”是否已在缓冲区迭代循环的 END 结束(其中单个缓冲区的周期已添加到经过的时间!):

    elapsed>=period
    

    这已经在第一次导致漂移,因为此时“经过”将是(192 * 29 次迭代)5568 个样本(或 126.26 毫秒),而不是(192 * 28.7112 次迭代)5512 个样本(或126 毫秒)。这是 56 个样本的差异(或说话时间:1.02 毫秒)。这当然不会导致样本播放比预期更快(如您所说),但已经导致播放不规则。对于第二个 16 分音符(将在第 57.4224 次迭代中发生,漂移将是 11136 - 11025 = 111 个样本或 2.517 毫秒(超过缓冲时间的一半!)因此,您必须在

    while(pos1<buffSize)
    

    循环,您将在其中递增读取指针,直到达到缓冲区的大小。因此,您需要将另一个变量增加每个缓冲样本的缓冲周期的一小部分。

    我希望上面的示例说明了为什么我最初建议通过样本迭代而不是经过时间来计算时间(当然,样本确实表示时间,因为它们只是将一个时间单位转换为一个缓冲区,但您可以使用这些数字作为标记,而不是像在渲染循环中那样向计数器添加固定间隔)。

    首先,一些方便的数学可以帮助您获得这些值:

    // 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 ));
    }
    

    OR :这些计算在您在帖子中提到的音乐环境中思考时更方便。以给定的采样率(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 拍上演奏并持续一个 quaver(小节的 1/8),另一种是在小节的第 3 拍上演奏并持续另一个 quaver。对于第一个事件,这两个“事件”(您可以在值对象中表示)将具有以下属性:

    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;
        }
    }
    

    这应该给你一个完美的时间来编写音频。当您遇到序列将循环的迭代时,您仍然需要解决一些魔术(即从样本开始读取剩余的未处理缓冲区长度以进行无缝循环),但我希望这能让您对工作方法。

    【讨论】:

    • 感谢您的回复。不过我很困惑。您刚刚重申了我正在使用的确切方法。我没有使用系统时间,我在第一段中说明了这一点。我通过正在编写的样本来跟踪时间。你并没有真正解决我的问题。请你再读一遍。如果你有时间。非常感谢。任何对系统时间的调用都纯粹与 Log 语句相关,以大致了解时间流逝。
    • 您好,请原谅我的热情,我忽略了您的代码示例中时间的确切使用;)但是,用于计算每个写入周期所用时间的逻辑仍然存在问题。我已经更新了我原来的回复来解决这个问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-17
    • 2016-08-13
    • 2013-12-29
    • 2017-03-28
    相关资源
    最近更新 更多