Точное время окончания воспроизведения аудио-очереди

Я использую Audio Queues для воспроизведения аудио файлов. Мне нужно точное время на финише последнего буфера. Мне нужно уведомить функцию не позднее, чем через 150 мс-200 мс после воспроизведения последнего буфера...

Через метод обратного вызова я знаю, сколько буферов поставлено в очередь

Я знаю размер буфера, я знаю, сколько байтов заполнено последним буфером.

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

Когда больше нет доступных аудиоданных, Audio Queue отправляет мне последний пустой буфер, поэтому я заполняю его любыми данными:

            if (sharedCache.numberOfToTalPackets>0)
            {
                if (currentlyReadingBufferIndex==[sharedCache.baseAudioCache count]-1) {
                    inBuffer->mAudioDataByteSize = (UInt32)bytesFilled;
                    lastEnqueudBufferSize=bytesFilled;
                    err=AudioQueueEnqueueBuffer(inAQ,inBuffer,(UInt32)packetsFilled,packetDescs);
                    if (err) {
                        [self failWithErrorCode:err customError:AP_AUDIO_QUEUE_ENQUEUE_FAILED];
                    }
                    printf("if that was the last free packet description, then enqueue the buffer\n");
                    //go to the next item on keepbuffer array
                    isBufferFilled=YES;
                    [self incrementBufferUsedCount];
                    return;
                }
            }

Когда Audio Queue запрашивает больше данных через обратный вызов, и у меня больше нет данных, я начинаю обратный отсчет буферов. Если количество буферов равно нулю, что означает, что на рейсе остается только один буфер для воспроизведения, в момент воспроизведения я пытаюсь остановить аудио-очередь.

-(void)decrementBufferUsedCount
{

    if (buffersUsed>0) {
        buffersUsed--;
        printf("buffer on the queue %i\n",buffersUsed);
        if (buffersUsed==0) {
            NSLog(@"playback is finished\n");
            // end playback
            isPlayBackDone=YES;
            double sampleRate = dataFormat.mSampleRate;
            double bufferDuration = lastEnqueudBufferSize/ sampleRate;
            double estimatedTimeNeded=bufferDuration*1;
            [self performSelector:@selector(stopPlayer) withObject:nil afterDelay:estimatedTimeNeded];
        }
    }
}  

-(void)stopPlayer
{
    @synchronized(self)
    {
        state=AP_STOPPING;
    }
    err=AudioQueueStop(queue, TRUE);
    if (err) {
        [self failWithErrorCode:err customError:AP_AUDIO_QUEUE_STOP_FAILED];
    }
    else
    {
        @synchronized(self)
        {
            state=AP_STOPPED;
            NSLog(@"Stopped\n");
        }

Однако, похоже, я не могу получить точное время здесь. Выше код останавливает игрока рано.

если я сделаю следующие аудио-сокращения рано

double bufferDuration = XMAQDefaultBufSize/ sampleRate;
double estimatedTimeNeded=bufferDuration*1;

если увеличить 1 в 2 так как размер буфера большой, я получаю некоторую задержку, кажется 1,5 оптимальное значение на данный момент, но я не понимаю, почему lastEnqueudBufferSize/ sampleRate это не wotking

Детали аудио файла и буферы:

Audio file has 22050 sample rate
#define kNumberPlaybackBuffers  4
#define kAQDefaultBufSize 16384
it is a vbr file format with no bitrate information available

2 ответа

Решение

РЕДАКТИРОВАТЬ:

Я нашел более простой способ получить те же результаты (+/-10 мс). После настройки выходной очереди с помощью AudioQueueNewOutput() вы инициализируете AudioQueueTimelineRef для использования в обратном вызове вывода. (функция ticksToSeconds включена ниже в мой первый метод) не забудьте импортировать <mach/mach_time.h>

//After AudioQueueNewOutput()
AudioQueueTimelineRef timeLine;     //ivar
AudioQueueCreateTimeline(queue, self.timeLine);

Затем в выходном обратном вызове вы вызываете AudioQueueGetCurrentTime(). Предостережение: очередь должна воспроизводиться с действительными временными метками. Поэтому для очень коротких файлов вам может понадобиться метод AudioQueueProcessingTap, представленный ниже.

AudioTimeStamp timestamp;
AudioQueueGetCurrentTime(queue, self->timeLine, &timestamp, NULL);

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

Float64 samplesLeft    = self->frameCount - timestamp.mSampleTime;//samples in file - current sample
Float64 secondsLeft    = samplesLeft / self->sampleRate;          //seconds of audio to play
UInt64  ticksLeft      = secondsLeft / ticksToSeconds();          //seconds converted to machine ticks  
UInt64  machTimeFinish = timestamp.mHostTime + ticksLeft;         //machine time of first sample + ticks left 

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

UInt64 currentMachTime = mach_absolute_time();
Uint64 ticksFromNow = machTimeFinish - currentMachTime;
float secondsFromNow = ticksFromNow * ticksToSeconds();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsFromNow * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    //do the thing!!!
    printf("Giggety");
});

Если GCD dispatch_async не достаточно точен, есть способы установить точный таймер

Использование AudioQueueProcessingTap

Вы можете получить довольно низкое время отклика из AudioQueueProcessingTap. Сначала вы делаете обратный вызов, который по существу помещает себя между аудиопотоком. Тип MyObject - это то, чем является self в вашем коде (здесь используется ARC-соединение, чтобы получить self внутри функции). Проверка ioFlags сообщает вам, когда поток начинается и заканчивается. IoTimeStamp выходного обратного вызова описывает время, когда первый пример обратного вызова попадет в динамик в будущем. Так что если вы хотите получить точную информацию, вот как вы это делаете. Я добавил несколько удобных функций для преобразования времени машины в секунды.

#import <mach/mach_time.h>

double getTimeConversion(){
    double timecon;
    mach_timebase_info_data_t tinfo;
    kern_return_t kerror;
    kerror = mach_timebase_info(&tinfo);
    timecon = (double)tinfo.numer / (double)tinfo.denom;

    return  timecon;
}
double ticksToSeconds(){
    static double ticksToSeconds = 0;
    if (!ticksToSeconds) {
        ticksToSeconds = getTimeConversion() * 0.000000001;
    }
    return ticksToSeconds;
}

void processingTapCallback(
                 void *                          inClientData,
                 AudioQueueProcessingTapRef      inAQTap,
                 UInt32                          inNumberFrames,
                 AudioTimeStamp *                ioTimeStamp,
                 UInt32 *                        ioFlags,
                 UInt32 *                        outNumberFrames,
                 AudioBufferList *               ioData){

    MyObject *self = (__bridge Object *)inClientData;
    AudioQueueProcessingTapGetSourceAudio(inAQTap, inNumberFrames, ioTimeStamp, ioFlags, outNumberFrames, ioData);
    if (*ioFlags ==  kAudioQueueProcessingTap_EndOfStream) {
        Float64 sampTime;
        UInt32 frameCount;
        AudioQueueProcessingTapGetQueueTime(inAQTap, &sampTime, &frameCount);
        Float64 samplesInThisCallback = self->frameCount - sampleTime;//file sampleCount - queue current sample
        //double secondsInCallback = outNumberFrames / (double)self->sampleRate; outNumberFrames was inaccurate
        double secondsInCallback = * samplesInThisCallback / (double)self->sampleRate;
        uint64_t timeOfLastSampleLeavingSpeaker = ioTimeStamp->mHostTime + (secondsInCallback / ticksToSeconds());
        [self lastSampleDoneAt:timeOfLastSampleLeavingSpeaker];
    }
}

-(void)lastSampleDoneAt:(uint64_t)lastSampTime{
    uint64_t currentTime = mach_absolute_time();
    if (lastSampTime > currentTime) {
        double secondsFromNow = (lastSampTime - currentTime) * ticksToSeconds();
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsFromNow * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //do the thing!!!
        });
    }
    else{
        //do the thing!!!
    }
}

Вы устанавливаете это так после AudioQueueNewOutput и перед AudioQueueStart. Обратите внимание на передачу bridged self аргументу inClientData. Очередь фактически хранит себя как void* для использования в обратном вызове, когда мы соединяем его обратно с объектом target-C в обратном вызове.

AudioStreamBasicDescription format;
AudioQueueProcessingTapRef tapRef;
UInt32 maxFrames = 0;
AudioQueueProcessingTapNew(queue, processingTapCallback, (__bridge void *)self, kAudioQueueProcessingTap_PostEffects, &maxFrames, &format, &tapRef);

Вы можете получить конечное машинное время, как только файл тоже запустится. Немного чище тоже.

void processingTapCallback(
                 void *                          inClientData,
                 AudioQueueProcessingTapRef      inAQTap,
                 UInt32                          inNumberFrames,
                 AudioTimeStamp *                ioTimeStamp,
                 UInt32 *                        ioFlags,
                 UInt32 *                        outNumberFrames,
                 AudioBufferList *               ioData){

    MyObject *self = (__bridge Object *)inClientData;
    AudioQueueProcessingTapGetSourceAudio(inAQTap, inNumberFrames, ioTimeStamp, ioFlags, outNumberFrames, ioData);
    if (*ioFlags ==  kAudioQueueProcessingTap_StartOfStream) {

        uint64_t timeOfLastSampleLeavingSpeaker = ioTimeStamp->mHostTime + (self->audioDurSeconds / ticksToSeconds());
        [self lastSampleDoneAt:timeOfLastSampleLeavingSpeaker];
    }
}

Для меня это сработало очень хорошо, потому что я учел:

  • остановка очереди в обратном вызове, когда данные закончились с помощью AudioQueueStop(queue, FALSE), в то время как:
  • прослушивание фактической остановки с использованием свойства kAudioQueueProperty_IsRunning (происходит позже, чем вызывается AudioQueueStop(), фактически, когда последний буфер фактически визуализируется)

после остановки очереди Вы можете подготовиться к действию. Вам нужно выполнить окончание аудио, а при срабатывании слушателя - фактически выполнить это действие.

Я не уверен в точности времени этого события, но для моей задачи это определенно лучше, чем использование уведомления прямо из обратного вызова. Внутри AudioQueue и самого устройства вывода есть буферизация, поэтому прослушиватель IsRunning определенно дает лучшие результаты относительно того, когда AudioQueue прекращает воспроизведение.

Если вы используете AudioQueueStop в асинхронном режиме остановка происходит после того, как все буферы в очереди были воспроизведены или записаны. Смотрите док.

Вы используете его в синхронном режиме, где остановка происходит КАК МОЖНО СКОРЕЕ, и воспроизведение немедленно прекращается, независимо от ранее буферизованных аудиоданных. Вам нужно точное время, но только потому, что звук отключается. Правильно? Поэтому вместо того, чтобы идти синхронно + добавить дополнительный код синхронизации / обратного вызова, я рекомендую идти асинхронно:

err=AudioQueueStop(queue, FALSE);

Из документов:

Если вы передадите false, функция вернется немедленно, но аудио-очередь не остановится, пока ее очереди не будут воспроизведены или записаны (то есть остановка происходит асинхронно). Обратные вызовы аудио-очереди вызываются по мере необходимости, пока очередь не остановится.

введите описание изображения здесь

Другие вопросы по тегам