Запись времени, секвенированного в Android AudioTrack

В настоящее время я пишу код для примера секвенсора в Android. Я использую класс AudioTrack. Мне сказали, что единственный правильный способ получить точное время - это использовать время AudioTrack. Например, я знаю, что если я запишу буфер X сэмплов в AudioTrack, воспроизводимый со скоростью 44100 сэмплов в секунду, то время записи будет (1/44100)X секунд.

Затем вы используете эту информацию, чтобы узнать, какие сэмплы и когда должны быть записаны.

Я пытаюсь реализовать свою первую попытку, используя этот подход. Я использую только один образец и записываю его как непрерывные 16-е ноты в темпе 120 ударов в минуту. Но по какой-то причине он играет со скоростью 240 ударов в минуту.

Сначала я проверил свой код, чтобы получить время 16-й (наносекундной) ноты в темпе X. Он проверяется.

private void setPeriod()
{
    period=(int)((1/(((double)TEMPO)/60))*1000);
    period=(period*1000000)/4;
    Log.i("test",String.valueOf(period));
}

Затем я проверил, что мой код для получения времени воспроизведения моего буфера на частоте 44100 кГц в наносекундах и это правильно.

long bufferTime=(1000000000/SAMPLE_RATE)*buffSize;

Так что теперь я думаю, что звуковая дорожка воспроизводится с частотой, отличной от 44100. Может быть, 96000 кГц, что объясняет удвоение скорости. Но когда я создаю audioTrack, он действительно был установлен на 44100 кГц.

окончательный 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;
                        }
                    }

                }

person user3083522    schedule 29.01.2014    source источник


Ответы (1)


отредактировано: перефразировано и обновлено из-за первоначального ошибочного предположения, что системное время использовалось для синхронизации последовательного звука :)

Что касается воспроизведения аудио с удвоенной скоростью, это немного странно, так как метод «записи» AudioTrack блокируется до тех пор, пока собственный слой не поставит в очередь следующий буфер. Вы уверены, что цикл рендеринга не вызывается из двух разных sources (хотя я предполагаю, что из вашего примера вы вызываете цикл из потока).

Однако несомненно то, что существует проблема синхронизации времени, которую необходимо решить: здесь проблема заключается в расчете времени буфера, которое вы используете в своем примере:

(1000000000/SAMPLE_RATE)*buffSize;

Который будет всегда возвращать 4353741 при размере буфера 192 сэмпла и частоте дискретизации 44 100 Гц, таким образом, игнорируя любые реплики в темпе (например, это будет то же самое при 300 BPM или 40 BPM). Теперь, в вашем примере это не имеет никаких последствий для фактической синхронизации как таковой, но я хотел бы указать на это, так как мы вскоре вернемся к этому в этом тексте.

Кроме того, наносекунды — очень точная единица измерения, но для аудиоопераций будет достаточно миллисекунд. Таким образом, я продолжу иллюстрацию в миллисекундах.

Ваш расчет для периода 16-й ноты при 120 ударах в минуту действительно подтверждает правильное значение 125 мс. Ранее упомянутый расчет для периода, соответствующего каждому размеру буфера, составляет 4,3537 мс. Это означает, что вы будете повторять цикл буфера 28,7112 раз, прежде чем истечет время одной шестнадцатой ноты. Однако в вашем примере вы проверяете, прошло ли «смещение» для этой шестнадцатой ноты в КОНЦЕ цикла итерации буфера (где период для одного буфера уже добавлен к прошедшему времени!), используя:

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

ИЛИ: Эти расчеты более удобны, если думать в музыкальном контексте, как вы упомянули в своем посте. Вычислите количество сэмплов, которые присутствуют в одном такте музыки при заданной частоте дискретизации (в Гц), темпе (в 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

и Т. Д.

Затем вы записываете синхронизированные сэмплы в выходной буфер, отслеживая «позицию воспроизведения» в обратном вызове вашего буфера, т.е. каждый раз, когда вы записываете буфер, вы будете увеличивать позицию воспроизведения с длиной буфера. Возвращаясь к музыкальному контексту: если вы должны были «зациклить один такт со скоростью 120 ударов в минуту в размере 4/4», когда позиция воспроизведения превысит (( sampleRate * 60 ) / 120 * 4 = 88200 сэмплов, вы сбрасываете его на 0 для "зацикливания" с самого начала.

Итак, давайте предположим, что у вас есть два «события» звука, которые происходят в последовательности одного такта размером 4/4 со скоростью 120 ударов в минуту. Одно событие должно играть на 1-й доле такта и длится дрожь (1/8 такта), а другое - играть на 3-й доле такта и длится еще дрожание. Эти два «события» (которые вы могли бы представить в объекте-значении) будут иметь следующие свойства для первого события:

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

Это должно дать вам идеально рассчитанный подход к написанию аудио. Есть еще некоторая магия, которую вам нужно отработать, когда вы переходите к итерации, где последовательность будет зацикливаться (т.е. считывать оставшуюся необработанную длину буфера с начала семпла для бесшовного зацикливания), но я надеюсь, что это даст вам общее представление о рабочий подход.

person Igor Zinken    schedule 30.01.2014
comment
Спасибо за ваш ответ. Хотя я в замешательстве. Вы только что повторили точный подход, который я использую. Я не использую системное время, и я заявляю об этом в первом абзаце. Я слежу за временем по написанным образцам. Вы не особо разобрались в моей проблеме. Не могли бы вы перечитать, пожалуйста. Если у вас есть время. Большое спасибо. Любой вызов системного времени связан исключительно с операторами журнала, чтобы получить приблизительное представление о прохождении времени. - person user3083522; 31.01.2014
comment
Привет, я прошу прощения за свой энтузиазм, я упустил из виду точное использование времени в вашем примере кода;) Однако все еще существует проблема с логикой, используемой для расчета затраченного времени на цикл записи. Я обновил свой первоначальный ответ, чтобы решить эту проблему. - person Igor Zinken; 04.02.2014