Re: How does one update a view position during a core audio render callback?
Re: How does one update a view position during a core audio render callback?
- Subject: Re: How does one update a view position during a core audio render callback?
- From: Iain Holmes <email@hidden>
- Date: Sun, 08 Mar 2015 00:20:38 +0000
This is a more specific version of the question of how to communicate between the realtime thread and the UI thread.
The core audio renderer callback shouldn’t be calling any ObjC classes or methods, as these can be non-deterministic WRT to locking and may block the realtime thread, so using performSelectorOnMainThread: and NSTimer on the renderer thread is out.
The common solution is to use a lock free ring buffer: I ended up using the implementation in PortAudio: https://subversion.assembla.com/svn/portaudio/portaudio/trunk/src/common/pa_ringbuffer.c
When playback is started, the main thread creates a ring buffer and it is passed to the core audio functions so that the renderer thread can access it. The main thread then starts a timer with whatever resolution needed: for my case I set it to 20times a second, that seemed to be good enough for me. This timer checks the ring buffer to see if there was any message from the render thread, and if so, it acts upon it.
Then every time the render callback completes, it posts a message to the ring buffer and carries on rendering the audio.
Something like this (edited to only show the ring buffer related pieces):
- (void)playFromFrame:(NSUInteger)startFrame
toFrame:(NSUInteger)endFrame
{
_playbackData = malloc(sizeof(PlaybackData));
// < snip > set up anything else the renderer may need
// Set up the ringbuffer to hold 32 MessageData structs
_playbackData->RTToMainBuffer = malloc(sizeof(MessageData) * 32);
PaUtil_InitializeRingBuffer(&_playbackData->RTToMainRB, sizeof(MessageData), 32, _playbackData->RTToMainBuffer);
// Setup the audio queue and start playback
AudioQueueNewOutput(&newAsbd, AudioOutputCallback, _playbackData, NULL, NULL, 0, &_playbackQueue);
AudioQueueStart(_playbackQueue, NULL);
// Start a short callback to read from the ring buffer which will be called on the UI thread
_playbackTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self
selector:@selector(readFromRingBuffer:)
userInfo:self repeats:YES];
}
// The scheduled method is called on the main UI thread
- (void)readFromRingBuffer:(NSTimer *)timer
{
while (PaUtil_GetRingBufferReadAvailable(&_playbackData->RTToMainRB)) {
MessageData *dataPtr1, *dataPtr2;
ring_buffer_size_t sizePtr1, sizePtr2;
// Should we read more than one at a time?
if (PaUtil_GetRingBufferReadRegions(&_playbackData->RTToMainRB, 1,
(void *)&dataPtr1, &sizePtr1,
(void *)&dataPtr2, &sizePtr2) != 1) {
continue;
}
// Parse message
switch (dataPtr1->type) {
case MessageTypeEOS:
[_delegate samplePlaybackDidEnd:self];
return;
case MessageTypePosition:
[_delegate sample:self playbackPositionChanged:dataPtr1->data.position.position];
break;
default:
break;
}
PaUtil_AdvanceRingBufferReadIndex(&_playbackData->RTToMainRB, 1);
}
}
And the renderer callback, which is run on the realtime thread does something like this:
static void
AudioOutputCallback (void *userData,
AudioQueueRef queue,
AudioQueueBufferRef buffer)
{
PlaybackData *data = (PlaybackData *)userData;
// < SNIP > Do whatever you need to to to actually put the audio data in the buffers, updating framesWritten with the number
// < SNIP > Calculate the audio position somehow
MessageData *dataPtr1, *dataPtr2;
ring_buffer_size_t sizePtr1, sizePtr2;
// Check if there is space in the ring buffer for a new message.
if (PaUtil_GetRingBufferWriteRegions(&data->RTToMainRB, 1,
(void *)&dataPtr1, &sizePtr1,
(void *)&dataPtr2, &sizePtr2)) {
dataPtr1->type = MessageTypePosition;
dataPtr1->data.position.position = data->position;
PaUtil_AdvanceRingBufferWriteIndex(&data->RTToMainRB, 1);
} else {
// Maybe something has blocked the UI thread and the NSTimer hasn’t been able to fire for a while
// and some messages haven’t been read yet filling up the ring buffer
// We can drop a position counter or two, while the NSTimer callback catches up and reads some messages
}
}
Hope that’s useful.
> On 7 Mar 2015, at 18:20, Patrick J. Collins <email@hidden> wrote:
>
> Hmmm... So yeah, I tried using a timer-- and strangely, it's similar to
> when I did the performSelectorOnMainThread... The update only happens a
> handful of times and I don't get why...
>
> self.timer = [NSTimer scheduledTimerWithTimeInterval:(1.0f / 60) target:self selector:@selector(updatePlayhead) userInfo:nil repeats:YES];
>
> For a 1.6 second audio clip, the callback is fired only 5 times..
>
> ?? Why is this happening?
>
> here is the actual code for my sample player:
> https://gist.github.com/patrick99e99/72712182158ecbfcbfea
>
> Patrick J. Collins
> http://collinatorstudios.com
>
>
> On Sat, 7 Mar 2015, Dave O'Neill wrote:
>
>>
>> ---------- Forwarded message ----------
>> From: Dave O'Neill <email@hidden>
>> Date: Sat, Mar 7, 2015 at 9:44 AM
>> Subject: Re: How does one update a view position during a core audio render callback?
>> To: "Patrick J. Collins" <email@hidden>
>>
>>
>> The simplest way to do is to just set a variable to your current time in your render callback.
>> And then employ a repeating timer to check that value and update your view on the main thread.
>> @implementation MyObject{
>> float currentTime;
>> }
>>
>> OSStatus myRenderCallback{
>> MyObject *object = (MyObject *)inRefCon;
>> object->currentTime = CalculateTimeOnRenderThread(); //with no Obj-C messaging!
>> }
>>
>> -(void)myTimerCallback{
>> self.playhead.position = currentTime;
>> }
>>
>> If you want it to refresh the currentTime faster than the render callback you should get the
>> mach_absolute_time() at the start of playing and again in your timer and calculate the difference.
>>
>> @implementation MyObject{
>> UInt64 startMachTime;
>> }
>> // if you need accuracy get the mHostTime from your render callback
>> // otherwise do this
>>
>> -(void)startAudio{
>> startMachTime = mach_absolute_time();
>> }
>>
>> -(void)myTimerCallback{
>> UInt64 ticksSinceStart = mach_absolute_time() - startMachTime;
>> self.playhead.position = convertTicksToSeconds(ticksSinceStart);
>> }
>>
>> A CADisplyLink is an excellent timer for this
>>
>> On Sat, Mar 7, 2015 at 9:17 AM, Patrick J. Collins <email@hidden> wrote:
>> Hi,
>>
>> So I have plotted out a waveform, and simply want to have a vertical
>> line represent a playhead which will move across the waveform's X-axis
>> as it plays.
>>
>> I am using an NSBox as my playhead, and in my callback proc, I tried doing inside the
>> loop:
>>
>> player->sampler.playheadView.position = player->sampler.playheadView.containerWidth
>> / player->buffer.size * currentSampleIndex
>>
>> my playheadView's position setter just does this:
>>
>> -(void)setPosition:(NSUInteger)position {
>> if (position == self.position) return;
>> [self setFrameOrigin:NSMakePoint(position, 0)];
>> }
>>
>> However, this causes everything to slow down to the point that the audio plays
>> with clicks and gaps inbetween the frames..
>>
>> I changed this to do a performSelectorOnMainThread, where this operation
>> occurs, but it seems like the playhead only gets updated a handful of times
>> during playback, so it does not look good..
>>
>> What is the ideal way to get visual feedback like this during playback?
>>
>> Thanks!
>>
>> Patrick J. Collins
>> http://collinatorstudios.com
>>
>> _______________________________________________
>> Do not post admin requests to the list. They will be ignored.
>> Coreaudio-api mailing list (email@hidden)
>> Help/Unsubscribe/Update your Subscription:
>>
>> This email sent to email@hidden
>>
>>
>>
>>
>>
> _______________________________________________
> Do not post admin requests to the list. They will be ignored.
> Coreaudio-api mailing list (email@hidden)
> Help/Unsubscribe/Update your Subscription:
>
> This email sent to email@hidden
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Coreaudio-api mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden