| | |
| | | /* |
| | | Simple DirectMedia Layer |
| | | Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org> |
| | | Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org> |
| | | |
| | | This software is provided 'as-is', without any express or implied |
| | | warranty. In no event will the authors be held liable for any damages |
| | |
| | | |
| | | #include "SDL_audio.h" |
| | | #include "SDL_hints.h" |
| | | #include "SDL_timer.h" |
| | | #include "../SDL_audio_c.h" |
| | | #include "../SDL_sysaudio.h" |
| | | #include "SDL_coreaudio.h" |
| | |
| | | @autoreleasepool { |
| | | AVAudioSession *session = [AVAudioSession sharedInstance]; |
| | | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; |
| | | |
| | | /* Set category to ambient by default so that other music continues playing. */ |
| | | NSString *category = AVAudioSessionCategoryAmbient; |
| | | NSString *mode = AVAudioSessionModeDefault; |
| | | NSUInteger options = 0; |
| | | NSError *err = nil; |
| | | |
| | | if (open_playback_devices && open_capture_devices) { |
| | | category = AVAudioSessionCategoryPlayAndRecord; |
| | | #if !TARGET_OS_TV |
| | | options = AVAudioSessionCategoryOptionDefaultToSpeaker; |
| | | #endif |
| | | } else if (open_capture_devices) { |
| | | category = AVAudioSessionCategoryRecord; |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | if ([session respondsToSelector:@selector(setCategory:mode:options:error:)]) { |
| | | if (![session setCategory:category mode:mode options:options error:&err]) { |
| | | NSString *desc = err.description; |
| | | SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String); |
| | | return NO; |
| | | } |
| | | } else { |
| | | if (![session setCategory:category error:&err]) { |
| | | NSString *desc = err.description; |
| | | SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String); |
| | | return NO; |
| | | } |
| | | } |
| | | |
| | | if (open && (open_playback_devices + open_capture_devices) == 1) { |
| | |
| | | /* An interruption end notification is not guaranteed to be sent if |
| | | we were previously interrupted... resuming if needed when the app |
| | | becomes active seems to be the way to go. */ |
| | | // Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications. johna |
| | | [center addObserver:listener |
| | | selector:@selector(applicationBecameActive:) |
| | | name:UIApplicationDidBecomeActiveNotification |
| | | object:session]; |
| | | object:nil]; |
| | | |
| | | [center addObserver:listener |
| | | selector:@selector(applicationBecameActive:) |
| | | name:UIApplicationWillEnterForegroundNotification |
| | | object:session]; |
| | | object:nil]; |
| | | |
| | | this->hidden->interruption_listener = CFBridgingRetain(listener); |
| | | } else { |
| | |
| | | outputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) |
| | | { |
| | | SDL_AudioDevice *this = (SDL_AudioDevice *) inUserData; |
| | | SDL_assert(inBuffer->mAudioDataBytesCapacity == this->hidden->bufferSize); |
| | | SDL_memcpy(inBuffer->mAudioData, this->hidden->buffer, this->hidden->bufferSize); |
| | | SDL_memset(this->hidden->buffer, '\0', this->hidden->bufferSize); /* zero out in case we have to fill again without new data. */ |
| | | inBuffer->mAudioDataByteSize = this->hidden->bufferSize; |
| | | if (SDL_AtomicGet(&this->hidden->shutdown)) { |
| | | return; /* don't do anything. */ |
| | | } |
| | | |
| | | if (!SDL_AtomicGet(&this->enabled) || SDL_AtomicGet(&this->paused)) { |
| | | /* Supply silence if audio is not enabled or paused */ |
| | | SDL_memset(inBuffer->mAudioData, this->spec.silence, inBuffer->mAudioDataBytesCapacity); |
| | | } else if (this->stream ) { |
| | | UInt32 remaining = inBuffer->mAudioDataBytesCapacity; |
| | | Uint8 *ptr = (Uint8 *) inBuffer->mAudioData; |
| | | |
| | | while (remaining > 0) { |
| | | if ( SDL_AudioStreamAvailable(this->stream) == 0 ) { |
| | | /* Generate the data */ |
| | | SDL_LockMutex(this->mixer_lock); |
| | | (*this->callbackspec.callback)(this->callbackspec.userdata, |
| | | this->hidden->buffer, this->hidden->bufferSize); |
| | | SDL_UnlockMutex(this->mixer_lock); |
| | | this->hidden->bufferOffset = 0; |
| | | SDL_AudioStreamPut(this->stream, this->hidden->buffer, this->hidden->bufferSize); |
| | | } |
| | | if ( SDL_AudioStreamAvailable(this->stream) > 0 ) { |
| | | int got; |
| | | UInt32 len = SDL_AudioStreamAvailable(this->stream); |
| | | if ( len > remaining ) |
| | | len = remaining; |
| | | got = SDL_AudioStreamGet(this->stream, ptr, len); |
| | | SDL_assert((got < 0) || (got == len)); |
| | | if (got != len) { |
| | | SDL_memset(ptr, this->spec.silence, len); |
| | | } |
| | | ptr = ptr + len; |
| | | remaining -= len; |
| | | } |
| | | } |
| | | } else { |
| | | UInt32 remaining = inBuffer->mAudioDataBytesCapacity; |
| | | Uint8 *ptr = (Uint8 *) inBuffer->mAudioData; |
| | | |
| | | while (remaining > 0) { |
| | | UInt32 len; |
| | | if (this->hidden->bufferOffset >= this->hidden->bufferSize) { |
| | | /* Generate the data */ |
| | | SDL_LockMutex(this->mixer_lock); |
| | | (*this->callbackspec.callback)(this->callbackspec.userdata, |
| | | this->hidden->buffer, this->hidden->bufferSize); |
| | | SDL_UnlockMutex(this->mixer_lock); |
| | | this->hidden->bufferOffset = 0; |
| | | } |
| | | |
| | | len = this->hidden->bufferSize - this->hidden->bufferOffset; |
| | | if (len > remaining) { |
| | | len = remaining; |
| | | } |
| | | SDL_memcpy(ptr, (char *)this->hidden->buffer + |
| | | this->hidden->bufferOffset, len); |
| | | ptr = ptr + len; |
| | | remaining -= len; |
| | | this->hidden->bufferOffset += len; |
| | | } |
| | | } |
| | | |
| | | AudioQueueEnqueueBuffer(this->hidden->audioQueue, inBuffer, 0, NULL); |
| | | this->hidden->refill = SDL_TRUE; |
| | | } |
| | | |
| | | static Uint8 * |
| | | COREAUDIO_GetDeviceBuf(_THIS) |
| | | { |
| | | return this->hidden->buffer; |
| | | } |
| | | |
| | | static void |
| | | COREAUDIO_WaitDevice(_THIS) |
| | | { |
| | | while (SDL_AtomicGet(&this->enabled) && !this->hidden->refill) { |
| | | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1); |
| | | } |
| | | this->hidden->refill = SDL_FALSE; |
| | | inBuffer->mAudioDataByteSize = inBuffer->mAudioDataBytesCapacity; |
| | | } |
| | | |
| | | static void |
| | |
| | | const AudioStreamPacketDescription *inPacketDescs ) |
| | | { |
| | | SDL_AudioDevice *this = (SDL_AudioDevice *) inUserData; |
| | | if (SDL_AtomicGet(&this->enabled)) { |
| | | SDL_AudioStream *stream = this->hidden->capturestream; |
| | | if (SDL_AudioStreamPut(stream, inBuffer->mAudioData, inBuffer->mAudioDataByteSize) == -1) { |
| | | /* yikes, out of memory or something. I guess drop the buffer. Our WASAPI target kills the device in this case, though */ |
| | | |
| | | if (SDL_AtomicGet(&this->shutdown)) { |
| | | return; /* don't do anything. */ |
| | | } |
| | | |
| | | /* ignore unless we're active. */ |
| | | if (!SDL_AtomicGet(&this->paused) && SDL_AtomicGet(&this->enabled) && !SDL_AtomicGet(&this->paused)) { |
| | | const Uint8 *ptr = (const Uint8 *) inBuffer->mAudioData; |
| | | UInt32 remaining = inBuffer->mAudioDataByteSize; |
| | | while (remaining > 0) { |
| | | UInt32 len = this->hidden->bufferSize - this->hidden->bufferOffset; |
| | | if (len > remaining) { |
| | | len = remaining; |
| | | } |
| | | |
| | | SDL_memcpy((char *)this->hidden->buffer + this->hidden->bufferOffset, ptr, len); |
| | | ptr += len; |
| | | remaining -= len; |
| | | this->hidden->bufferOffset += len; |
| | | |
| | | if (this->hidden->bufferOffset >= this->hidden->bufferSize) { |
| | | SDL_LockMutex(this->mixer_lock); |
| | | (*this->callbackspec.callback)(this->callbackspec.userdata, this->hidden->buffer, this->hidden->bufferSize); |
| | | SDL_UnlockMutex(this->mixer_lock); |
| | | this->hidden->bufferOffset = 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | AudioQueueEnqueueBuffer(this->hidden->audioQueue, inBuffer, 0, NULL); |
| | | this->hidden->refill = SDL_TRUE; |
| | | } |
| | | } |
| | | |
| | | static int |
| | | COREAUDIO_CaptureFromDevice(_THIS, void *buffer, int buflen) |
| | | { |
| | | SDL_AudioStream *stream = this->hidden->capturestream; |
| | | while (SDL_AtomicGet(&this->enabled)) { |
| | | const int avail = SDL_AudioStreamAvailable(stream); |
| | | if (avail > 0) { |
| | | const int cpy = SDL_min(buflen, avail); |
| | | SDL_AudioStreamGet(stream, buffer, cpy); |
| | | return cpy; |
| | | } |
| | | |
| | | /* wait for more data, try again. */ |
| | | while (SDL_AtomicGet(&this->enabled) && !this->hidden->refill) { |
| | | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1); |
| | | } |
| | | this->hidden->refill = SDL_FALSE; |
| | | } |
| | | |
| | | return 0; /* not enabled, giving up. */ |
| | | } |
| | | |
| | | static void |
| | | COREAUDIO_FlushCapture(_THIS) |
| | | { |
| | | while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, 1) == kCFRunLoopRunHandledSource) { |
| | | /* spin. */ |
| | | } |
| | | this->hidden->refill = SDL_FALSE; |
| | | SDL_AudioStreamClear(this->hidden->capturestream); |
| | | } |
| | | |
| | | |
| | |
| | | AudioObjectRemovePropertyListener(this->hidden->deviceID, &alive_address, device_unplugged, this); |
| | | #endif |
| | | |
| | | #if !MACOSX_COREAUDIO |
| | | update_audio_session(this, SDL_FALSE); |
| | | #endif |
| | | |
| | | if (this->hidden->audioQueue) { |
| | | AudioQueueDispose(this->hidden->audioQueue, 1); |
| | | } |
| | | |
| | | if (this->hidden->capturestream) { |
| | | SDL_FreeAudioStream(this->hidden->capturestream); |
| | | } |
| | | |
| | | /* AudioQueueDispose() frees the actual buffer objects. */ |
| | | SDL_free(this->hidden->audioBuffer); |
| | | SDL_free(this->hidden->buffer); |
| | | SDL_free(this->hidden); |
| | | |
| | | if (iscapture) { |
| | | open_capture_devices--; |
| | | } else { |
| | | open_playback_devices--; |
| | | } |
| | | |
| | | #if !MACOSX_COREAUDIO |
| | | update_audio_session(this, SDL_FALSE); |
| | | #endif |
| | | |
| | | /* if callback fires again, feed silence; don't call into the app. */ |
| | | SDL_AtomicSet(&this->paused, 1); |
| | | |
| | | if (this->hidden->audioQueue) { |
| | | AudioQueueDispose(this->hidden->audioQueue, 1); |
| | | } |
| | | |
| | | if (this->hidden->thread) { |
| | | SDL_AtomicSet(&this->hidden->shutdown, 1); |
| | | SDL_WaitThread(this->hidden->thread, NULL); |
| | | } |
| | | |
| | | if (this->hidden->ready_semaphore) { |
| | | SDL_DestroySemaphore(this->hidden->ready_semaphore); |
| | | } |
| | | |
| | | /* AudioQueueDispose() frees the actual buffer objects. */ |
| | | SDL_free(this->hidden->audioBuffer); |
| | | SDL_free(this->hidden->thread_error); |
| | | SDL_free(this->hidden->buffer); |
| | | SDL_free(this->hidden); |
| | | } |
| | | |
| | | #if MACOSX_COREAUDIO |
| | |
| | | } |
| | | #endif |
| | | |
| | | |
| | | /* this all happens in the audio thread, since it needs a separate runloop. */ |
| | | static int |
| | | prepare_audioqueue(_THIS) |
| | | { |
| | |
| | | } |
| | | #endif |
| | | |
| | | /* Calculate the final parameters for this audio specification */ |
| | | SDL_CalculateAudioSpec(&this->spec); |
| | | |
| | | /* Set the channel layout for the audio queue */ |
| | | AudioChannelLayout layout; |
| | | SDL_zero(layout); |
| | | switch (this->spec.channels) { |
| | | case 1: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; |
| | | break; |
| | | case 2: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo; |
| | | break; |
| | | case 3: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_DVD_4; |
| | | break; |
| | | case 4: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic; |
| | | break; |
| | | case 5: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_0_A; |
| | | break; |
| | | case 6: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A; |
| | | break; |
| | | case 7: |
| | | /* FIXME: Need to move channel[4] (BC) to channel[6] */ |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_6_1_A; |
| | | break; |
| | | case 8: |
| | | layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_7_1_A; |
| | | break; |
| | | } |
| | | if (layout.mChannelLayoutTag != 0) { |
| | | result = AudioQueueSetProperty(this->hidden->audioQueue, kAudioQueueProperty_ChannelLayout, &layout, sizeof(layout)); |
| | | CHECK_RESULT("AudioQueueSetProperty(kAudioQueueProperty_ChannelLayout)"); |
| | | } |
| | | |
| | | /* Allocate a sample buffer */ |
| | | this->hidden->bufferSize = this->spec.size; |
| | | this->hidden->bufferOffset = iscapture ? 0 : this->hidden->bufferSize; |
| | | |
| | | this->hidden->buffer = SDL_malloc(this->hidden->bufferSize); |
| | | if (this->hidden->buffer == NULL) { |
| | | SDL_OutOfMemory(); |
| | | return 0; |
| | | } |
| | | |
| | | /* Make sure we can feed the device a minimum amount of time */ |
| | | double MINIMUM_AUDIO_BUFFER_TIME_MS = 15.0; |
| | | #if defined(__IPHONEOS__) |
| | |
| | | numAudioBuffers = ((int)SDL_ceil(MINIMUM_AUDIO_BUFFER_TIME_MS / msecs) * 2); |
| | | } |
| | | |
| | | this->hidden->numAudioBuffers = numAudioBuffers; |
| | | this->hidden->audioBuffer = SDL_calloc(1, sizeof (AudioQueueBufferRef) * numAudioBuffers); |
| | | if (this->hidden->audioBuffer == NULL) { |
| | | SDL_OutOfMemory(); |
| | |
| | | return 1; |
| | | } |
| | | |
| | | static void |
| | | COREAUDIO_ThreadInit(_THIS) |
| | | static int |
| | | audioqueue_thread(void *arg) |
| | | { |
| | | SDL_AudioDevice *this = (SDL_AudioDevice *) arg; |
| | | const int rc = prepare_audioqueue(this); |
| | | if (!rc) { |
| | | /* !!! FIXME: do this in RunAudio, and maybe block OpenDevice until ThreadInit finishes, too, to report an opening error */ |
| | | SDL_OpenedAudioDeviceDisconnected(this); /* oh well. */ |
| | | } |
| | | this->hidden->thread_error = SDL_strdup(SDL_GetError()); |
| | | SDL_SemPost(this->hidden->ready_semaphore); |
| | | return 0; |
| | | } |
| | | |
| | | static void |
| | | COREAUDIO_PrepareToClose(_THIS) |
| | | { |
| | | /* run long enough to queue some silence, so we know our actual audio |
| | | has been played */ |
| | | CFRunLoopRunInMode(kCFRunLoopDefaultMode, (((this->spec.samples * 1000) / this->spec.freq) * 2) / 1000.0f, 0); |
| | | AudioQueueStop(this->hidden->audioQueue, 1); |
| | | SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); |
| | | |
| | | /* init was successful, alert parent thread and start running... */ |
| | | SDL_SemPost(this->hidden->ready_semaphore); |
| | | while (!SDL_AtomicGet(&this->hidden->shutdown)) { |
| | | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1); |
| | | } |
| | | |
| | | if (!this->iscapture) { /* Drain off any pending playback. */ |
| | | const CFTimeInterval secs = (((this->spec.size / (SDL_AUDIO_BITSIZE(this->spec.format) / 8)) / this->spec.channels) / ((CFTimeInterval) this->spec.freq)) * 2.0; |
| | | CFRunLoopRunInMode(kCFRunLoopDefaultMode, secs, 0); |
| | | } |
| | | |
| | | return 0; |
| | | } |
| | | |
| | | static int |
| | |
| | | AVAudioSession* session = [AVAudioSession sharedInstance]; |
| | | [session setPreferredSampleRate:this->spec.freq error:nil]; |
| | | this->spec.freq = (int)session.sampleRate; |
| | | #if TARGET_OS_TV |
| | | if (iscapture) { |
| | | [session setPreferredInputNumberOfChannels:this->spec.channels error:nil]; |
| | | this->spec.channels = session.preferredInputNumberOfChannels; |
| | | } else { |
| | | [session setPreferredOutputNumberOfChannels:this->spec.channels error:nil]; |
| | | this->spec.channels = session.preferredOutputNumberOfChannels; |
| | | } |
| | | #else |
| | | /* Calling setPreferredOutputNumberOfChannels seems to break audio output on iOS */ |
| | | #endif /* TARGET_OS_TV */ |
| | | } |
| | | #endif |
| | | |
| | |
| | | |
| | | while ((!valid_datatype) && (test_format)) { |
| | | this->spec.format = test_format; |
| | | /* Just a list of valid SDL formats, so people don't pass junk here. */ |
| | | /* CoreAudio handles most of SDL's formats natively, but not U16, apparently. */ |
| | | switch (test_format) { |
| | | case AUDIO_U8: |
| | | case AUDIO_S8: |
| | | case AUDIO_U16LSB: |
| | | case AUDIO_S16LSB: |
| | | case AUDIO_U16MSB: |
| | | case AUDIO_S16MSB: |
| | | case AUDIO_S32LSB: |
| | | case AUDIO_S32MSB: |
| | |
| | | else if (SDL_AUDIO_ISSIGNED(this->spec.format)) |
| | | strdesc->mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; |
| | | break; |
| | | |
| | | default: |
| | | test_format = SDL_NextAudioFormat(); |
| | | break; |
| | | } |
| | | } |
| | | |
| | |
| | | return SDL_SetError("Unsupported audio format"); |
| | | } |
| | | |
| | | strdesc->mBytesPerFrame = strdesc->mBitsPerChannel * strdesc->mChannelsPerFrame / 8; |
| | | strdesc->mBytesPerFrame = strdesc->mChannelsPerFrame * strdesc->mBitsPerChannel / 8; |
| | | strdesc->mBytesPerPacket = strdesc->mBytesPerFrame * strdesc->mFramesPerPacket; |
| | | |
| | | #if MACOSX_COREAUDIO |
| | |
| | | } |
| | | #endif |
| | | |
| | | /* Calculate the final parameters for this audio specification */ |
| | | SDL_CalculateAudioSpec(&this->spec); |
| | | |
| | | if (iscapture) { |
| | | this->hidden->capturestream = SDL_NewAudioStream(this->spec.format, this->spec.channels, this->spec.freq, this->spec.format, this->spec.channels, this->spec.freq); |
| | | if (!this->hidden->capturestream) { |
| | | return -1; /* already set SDL_Error */ |
| | | } |
| | | } else { |
| | | this->hidden->bufferSize = this->spec.size; |
| | | this->hidden->buffer = SDL_malloc(this->hidden->bufferSize); |
| | | if (this->hidden->buffer == NULL) { |
| | | return SDL_OutOfMemory(); |
| | | } |
| | | /* This has to init in a new thread so it can get its own CFRunLoop. :/ */ |
| | | SDL_AtomicSet(&this->hidden->shutdown, 0); |
| | | this->hidden->ready_semaphore = SDL_CreateSemaphore(0); |
| | | if (!this->hidden->ready_semaphore) { |
| | | return -1; /* oh well. */ |
| | | } |
| | | |
| | | return 0; |
| | | this->hidden->thread = SDL_CreateThreadInternal(audioqueue_thread, "AudioQueue thread", 512 * 1024, this); |
| | | if (!this->hidden->thread) { |
| | | return -1; |
| | | } |
| | | |
| | | SDL_SemWait(this->hidden->ready_semaphore); |
| | | SDL_DestroySemaphore(this->hidden->ready_semaphore); |
| | | this->hidden->ready_semaphore = NULL; |
| | | |
| | | if ((this->hidden->thread != NULL) && (this->hidden->thread_error != NULL)) { |
| | | SDL_SetError("%s", this->hidden->thread_error); |
| | | return -1; |
| | | } |
| | | |
| | | return (this->hidden->thread != NULL) ? 0 : -1; |
| | | } |
| | | |
| | | static void |
| | |
| | | impl->OpenDevice = COREAUDIO_OpenDevice; |
| | | impl->CloseDevice = COREAUDIO_CloseDevice; |
| | | impl->Deinitialize = COREAUDIO_Deinitialize; |
| | | impl->ThreadInit = COREAUDIO_ThreadInit; |
| | | impl->WaitDevice = COREAUDIO_WaitDevice; |
| | | impl->GetDeviceBuf = COREAUDIO_GetDeviceBuf; |
| | | impl->PrepareToClose = COREAUDIO_PrepareToClose; |
| | | impl->CaptureFromDevice = COREAUDIO_CaptureFromDevice; |
| | | impl->FlushCapture = COREAUDIO_FlushCapture; |
| | | |
| | | #if MACOSX_COREAUDIO |
| | | impl->DetectDevices = COREAUDIO_DetectDevices; |
| | |
| | | impl->OnlyHasDefaultCaptureDevice = 1; |
| | | #endif |
| | | |
| | | impl->ProvidesOwnCallbackThread = 1; |
| | | impl->HasCaptureSupport = 1; |
| | | |
| | | return 1; /* this audio target is available. */ |