mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2026-06-17 06:01:13 +00:00
Switch to SDL for audio output
This commit is contained in:
+64
-170
@@ -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];
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
#define SDL_MAIN_HANDLED
|
||||
#import <SDL.h>
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
@autoreleasepool {
|
||||
SDL_SetMainReady();
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user