Switch to SDL for audio output

This commit is contained in:
Cameron Gutman
2022-01-13 22:00:47 -06:00
parent eccf517dc3
commit 8391c766c7
94 changed files with 47719 additions and 170 deletions
+64 -170
View File
@@ -9,10 +9,11 @@
#import "Connection.h"
#import "Utils.h"
#import <AudioUnit/AudioUnit.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>
#define SDL_MAIN_HANDLED
#import <SDL.h>
#include "Limelight.h"
#include "opus_multistream.h"
@@ -37,24 +38,11 @@ static video_stats_t currentVideoStats;
static video_stats_t lastVideoStats;
static NSLock* videoStatsLock;
#define OUTPUT_BUS 0
static SDL_AudioDeviceID audioDevice;
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
static void* audioBuffer;
static int audioFrameSize;
// My iPod touch 5th Generation seems to really require 80 ms
// of buffering to deliver glitch-free playback :(
// FIXME: Maybe we can use a smaller buffer on more modern iOS versions?
#define CIRCULAR_BUFFER_DURATION 80
static int audioBufferEntries;
static int audioBufferWriteIndex;
static int audioBufferReadIndex;
static int audioBufferStride;
static int audioSamplesPerFrame;
static short* audioCircularBuffer;
#define AUDIO_QUEUE_BUFFERS 3
static AudioQueueRef audioQueue;
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
static VideoDecoderRenderer* renderer;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
@@ -169,113 +157,49 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
pts:decodeUnit->presentationTimeMs];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION originalOpusConfig, void* context, int flags)
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
{
int err;
AudioChannelLayout channelLayout = {};
OPUS_MULTISTREAM_CONFIGURATION opusConfig = *originalOpusConfig;
SDL_AudioSpec want, have;
// Initialize the circular buffer
audioBufferWriteIndex = audioBufferReadIndex = 0;
audioSamplesPerFrame = opusConfig.samplesPerFrame;
audioBufferStride = opusConfig.channelCount * opusConfig.samplesPerFrame;
audioBufferEntries = CIRCULAR_BUFFER_DURATION / (opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000));
audioCircularBuffer = malloc(audioBufferEntries * audioBufferStride * sizeof(short));
if (audioCircularBuffer == NULL) {
Log(LOG_E, @"Error allocating output queue\n");
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
return -1;
}
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = opusConfig->samplesPerFrame;
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (audioDevice == 0) {
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return -1;
}
switch (opusConfig.channelCount) {
case 2:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
break;
case 4:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic;
break;
case 6:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_1;
break;
case 8:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_7_1;
// Swap SL/SR and RL/RR to match the selected channel layout
opusConfig.mapping[4] = originalOpusConfig->mapping[6];
opusConfig.mapping[5] = originalOpusConfig->mapping[7];
opusConfig.mapping[6] = originalOpusConfig->mapping[4];
opusConfig.mapping[7] = originalOpusConfig->mapping[5];
break;
default:
// Unsupported channel layout
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig.channelCount);
abort();
}
audioConfig = *opusConfig;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
audioBuffer = SDL_malloc(audioFrameSize);
opusDecoder = opus_multistream_decoder_create(opusConfig.sampleRate,
opusConfig.channelCount,
opusConfig.streams,
opusConfig.coupledStreams,
opusConfig.mapping,
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&err);
// Configure the audio session for our app
NSError *audioSessionError = nil;
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredSampleRate:opusConfig.sampleRate error:&audioSessionError];
[audioSession setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:&audioSessionError];
[audioSession setPreferredIOBufferDuration:(opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000)) / 1000.0
error:&audioSessionError];
[audioSession setActive: YES error: &audioSessionError];
// FIXME: Calling this breaks surround audio for some reason
//[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
// Start playback
SDL_PauseAudioDevice(audioDevice, 0);
OSStatus status;
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = opusConfig.sampleRate;
audioFormat.mBitsPerChannel = 16;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
audioFormat.mChannelsPerFrame = opusConfig.channelCount;
audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel / 8);
audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame;
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
audioFormat.mReserved = 0;
status = AudioQueueNewOutput(&audioFormat, FillOutputBuffer, nil, nil, nil, 0, &audioQueue);
if (status != noErr) {
Log(LOG_E, @"Error allocating output queue: %d\n", status);
return status;
}
// We need to specify a channel layout for surround sound configurations
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
if (status != noErr) {
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
return status;
}
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig.samplesPerFrame, &audioBuffers[i]);
if (status != noErr) {
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
return status;
}
FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
}
status = AudioQueueStart(audioQueue, nil);
if (status != noErr) {
Log(LOG_E, @"Error starting queue: %d\n", status);
return status;
}
return status;
return 0;
}
void ArCleanup(void)
@@ -285,43 +209,43 @@ void ArCleanup(void)
opusDecoder = NULL;
}
// Stop before disposing to avoid massive delay inside
// AudioQueueDispose() (iOS bug?)
AudioQueueStop(audioQueue, true);
// Also frees buffers
AudioQueueDispose(audioQueue, true);
// Must be freed after the queue is stopped
if (audioCircularBuffer != NULL) {
free(audioCircularBuffer);
audioCircularBuffer = NULL;
if (audioDevice != 0) {
SDL_CloseAudioDevice(audioDevice);
audioDevice = 0;
}
// Audio session is now inactive
[[AVAudioSession sharedInstance] setActive: NO error: nil];
if (audioBuffer != NULL) {
SDL_free(audioBuffer);
audioBuffer = NULL;
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int decodeLen;
// Check if there is space for this sample in the buffer. Again, this can race
// but in the worst case, we'll not see the sample callback having consumed a sample.
if (((audioBufferWriteIndex + 1) % audioBufferEntries) == audioBufferReadIndex) {
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return;
}
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
(short*)&audioCircularBuffer[audioBufferWriteIndex * audioBufferStride], audioSamplesPerFrame, 0);
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
if (decodeLen > 0) {
// Use a full memory barrier to ensure the circular buffer is written before incrementing the index
__sync_synchronize();
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
SDL_Delay(1);
}
// This can race with the reader in the sample callback, however this is a benign
// race since we'll either read the original value of s_WriteIndex (which is safe,
// we just won't consider this sample) or the new value of s_WriteIndex
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % audioBufferEntries;
if (SDL_QueueAudio(audioDevice,
audioBuffer,
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
}
}
}
@@ -508,8 +432,7 @@ void ClConnectionStatusUpdate(int status)
_arCallbacks.init = ArInit;
_arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting;
@@ -524,35 +447,6 @@ void ClConnectionStatusUpdate(int status)
return self;
}
static void FillOutputBuffer(void *aqData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
inBuffer->mAudioDataByteSize = audioBufferStride * sizeof(short);
assert(inBuffer->mAudioDataByteSize == inBuffer->mAudioDataBytesCapacity);
// If the indexes aren't equal, we have a sample
if (audioBufferWriteIndex != audioBufferReadIndex) {
// Copy data to the audio buffer
memcpy(inBuffer->mAudioData,
&audioCircularBuffer[audioBufferReadIndex * audioBufferStride],
inBuffer->mAudioDataByteSize);
// Use a full memory barrier to ensure the circular buffer is read before incrementing the index
__sync_synchronize();
// This can race with the reader in the AudDecDecodeAndPlaySample function. This is
// not a problem because at worst, it just won't see that we've consumed this sample yet.
audioBufferReadIndex = (audioBufferReadIndex + 1) % audioBufferEntries;
}
else {
// No data, so play silence
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
}
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
-(void) main
{
[initLock lock];