Skip to content

Commit 7eb840b

Browse files
committed
Native CoreAudio renderer with spatial audio
* Inspired by an example app from Apple. [1] * Needs an M1 or newer Mac. * Operates in a standard passthrough mode for stereo or when you have enough real channels (HDMI). * When headphones or built-in Macbook speakers are used and a 5.1 or 7.1 stream is being sent by Sunshine, this will render the surround channels into high quality spatial audio. * Supports optional head-tracking (enable in settings). * Supports personalized HRTF if you've scanned your ears with your iPhone. This can greatly improve spatial audio for many people. Known issues: * System sound menu does not indicate spatial audio is active or that multichannel audio is playing. * If Moonlight is in Game Mode and you toggle back and forth by say, swiping out of full screen to another app, the audio may stutter a bit. [1] https://developer.apple.com/documentation/audiotoolbox/generating_spatial_audio_from_a_multichannel_audio_stream
1 parent 04a8922 commit 7eb840b

19 files changed

+2086
-13
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[submodule "moonlight-common-c/moonlight-common-c"]
22
path = moonlight-common-c/moonlight-common-c
3-
url = https://github.com/moonlight-stream/moonlight-common-c.git
3+
url = https://github.com/andygrundman/moonlight-common-c.git
44
[submodule "qmdnsengine/qmdnsengine"]
55
path = qmdnsengine/qmdnsengine
66
url = https://github.com/cgutman/qmdnsengine.git

app/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
</array>
3232
<key>GCSupportsControllerUserInteraction</key>
3333
<true/>
34+
<key>LSApplicationCategoryType</key>
35+
<string>public.app-category.games</string>
3436
<key>NSAppTransportSecurity</key>
3537
<dict>
3638
<key>NSAllowsArbitraryLoads</key>

app/app.pro

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,19 @@ macx {
160160
CONFIG += discord-rpc
161161
}
162162

163-
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
164-
165-
# For libsoundio
166-
LIBS += -framework CoreAudio -framework AudioUnit
163+
LIBS += -lobjc \
164+
-framework Accelerate \
165+
-framework AppKit \
166+
-framework AudioToolbox \
167+
-framework AudioUnit \
168+
-framework AVFoundation \
169+
-framework CoreAudio \
170+
-framework CoreVideo \
171+
-framework CoreGraphics \
172+
-framework CoreMedia \
173+
-framework Metal \
174+
-framework QuartzCore \
175+
-framework VideoToolbox
167176

168177
CONFIG += ffmpeg soundio
169178
}
@@ -392,14 +401,23 @@ win32:!winrt {
392401
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
393402
}
394403
macx {
395-
message(VideoToolbox renderer selected)
404+
message(CoreAudio + VideoToolbox renderers selected)
405+
406+
DEFINES += HAVE_COREAUDIO
396407

397408
SOURCES += \
409+
streaming/audio/renderers/coreaudio/au_spatial_renderer.mm \
410+
streaming/audio/renderers/coreaudio/coreaudio.cpp \
411+
streaming/audio/renderers/coreaudio/TPCircularBuffer.c \
398412
streaming/video/ffmpeg-renderers/vt_base.mm \
399413
streaming/video/ffmpeg-renderers/vt_avsamplelayer.mm \
400414
streaming/video/ffmpeg-renderers/vt_metal.mm
401415

402416
HEADERS += \
417+
streaming/audio/renderers/coreaudio/au_spatial_renderer.h \
418+
streaming/audio/renderers/coreaudio/coreaudio.h \
419+
streaming/audio/renderers/coreaudio/coreaudio_helpers.h \
420+
streaming/audio/renderers/coreaudio/TPCircularBuffer.h \
403421
streaming/video/ffmpeg-renderers/vt.h
404422
}
405423
soundio {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.developer.spatial-audio.profile-access</key>
8+
<true/>
9+
<key>com.apple.developer.coremotion.head-pose</key>
10+
<true/>
11+
</dict>
12+
</plist>

app/gui/SettingsView.qml

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,78 @@ Flickable {
881881
}
882882
}
883883

884+
Label {
885+
width: parent.width
886+
id: resSpatialAudioTitle
887+
text: qsTr("Spatial audio")
888+
font.pointSize: 12
889+
wrapMode: Text.Wrap
890+
visible: Qt.platform.os == "osx"
891+
}
892+
893+
Row {
894+
spacing: 5
895+
width: parent.width
896+
visible: Qt.platform.os == "osx"
897+
898+
AutoResizingComboBox {
899+
// ignore setting the index at first, and actually set it when the component is loaded
900+
Component.onCompleted: {
901+
var saved_sac = StreamingPreferences.spatialAudioConfig
902+
currentIndex = 0
903+
for (var i = 0; i < spatialAudioListModel.count; i++) {
904+
var el_audio = spatialAudioListModel.get(i).val;
905+
if (saved_sac === el_audio) {
906+
currentIndex = i
907+
break
908+
}
909+
}
910+
activated(currentIndex)
911+
}
912+
913+
id: spatialAudioComboBox
914+
enabled: StreamingPreferences.audioConfig != StreamingPreferences.AC_STEREO
915+
textRole: "text"
916+
model: ListModel {
917+
id: spatialAudioListModel
918+
ListElement {
919+
text: qsTr("Enabled")
920+
val: StreamingPreferences.SAC_AUTO
921+
}
922+
ListElement {
923+
text: qsTr("Disabled")
924+
val: StreamingPreferences.SAC_DISABLED
925+
}
926+
}
927+
928+
// ::onActivated must be used, as it only listens for when the index is changed by a human
929+
onActivated : {
930+
StreamingPreferences.spatialAudioConfig = spatialAudioListModel.get(currentIndex).val
931+
}
932+
933+
ToolTip.delay: 1000
934+
ToolTip.timeout: 5000
935+
ToolTip.visible: hovered
936+
ToolTip.text: qsTr("Spatial audio will be used when using any type of headphones, built-in Macbook speakers, and 2-channel USB devices.")
937+
}
938+
939+
CheckBox {
940+
id: spatialHeadTracking
941+
enabled: StreamingPreferences.audioConfig != StreamingPreferences.AC_STEREO && StreamingPreferences.spatialAudioConfig != StreamingPreferences.SAC_DISABLED
942+
width: parent.width
943+
text: qsTr("Enable head-tracking")
944+
font.pointSize: 12
945+
checked: StreamingPreferences.spatialHeadTracking
946+
onCheckedChanged: {
947+
StreamingPreferences.spatialHeadTracking = checked
948+
}
949+
950+
ToolTip.delay: 1000
951+
ToolTip.timeout: 5000
952+
ToolTip.visible: hovered
953+
ToolTip.text: qsTr("Requires supported Apple or Beats headphones")
954+
}
955+
}
884956

885957
CheckBox {
886958
id: audioPcCheck
@@ -1176,7 +1248,7 @@ Flickable {
11761248
ListElement {
11771249
text: qsTr("Maximized")
11781250
val: StreamingPreferences.UI_MAXIMIZED
1179-
}
1251+
}
11801252
ListElement {
11811253
text: qsTr("Fullscreen")
11821254
val: StreamingPreferences.UI_FULLSCREEN

app/settings/streamingpreferences.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
#define SER_FULLSCREEN "fullscreen"
2020
#define SER_VSYNC "vsync"
2121
#define SER_GAMEOPTS "gameopts"
22+
#define SER_HEADTRACKING "headtracking"
2223
#define SER_HOSTAUDIO "hostaudio"
2324
#define SER_MULTICONT "multicontroller"
2425
#define SER_AUDIOCFG "audiocfg"
26+
#define SER_SPATIALAUDIOCFG "spatialaudiocfg"
2527
#define SER_VIDEOCFG "videocfg"
2628
#define SER_HDR "hdr"
2729
#define SER_YUV444 "yuv444"
@@ -124,6 +126,7 @@ void StreamingPreferences::reload()
124126
unlockBitrate = settings.value(SER_UNLOCK_BITRATE, false).toBool();
125127
enableVsync = settings.value(SER_VSYNC, true).toBool();
126128
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
129+
spatialHeadTracking = settings.value(SER_HEADTRACKING, false).toBool();
127130
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
128131
multiController = settings.value(SER_MULTICONT, true).toBool();
129132
enableMdns = settings.value(SER_MDNS, true).toBool();
@@ -148,6 +151,8 @@ void StreamingPreferences::reload()
148151
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
149152
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
150153
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
154+
spatialAudioConfig = static_cast<SpatialAudioConfig>(settings.value(SER_SPATIALAUDIOCFG,
155+
static_cast<int>(SpatialAudioConfig::SAC_AUTO)).toInt());
151156
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
152157
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
153158
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
@@ -314,6 +319,7 @@ void StreamingPreferences::save()
314319
settings.setValue(SER_UNLOCK_BITRATE, unlockBitrate);
315320
settings.setValue(SER_VSYNC, enableVsync);
316321
settings.setValue(SER_GAMEOPTS, gameOptimizations);
322+
settings.setValue(SER_HEADTRACKING, spatialHeadTracking);
317323
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
318324
settings.setValue(SER_MULTICONT, multiController);
319325
settings.setValue(SER_MDNS, enableMdns);
@@ -328,6 +334,7 @@ void StreamingPreferences::save()
328334
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
329335
settings.setValue(SER_SHOWPERFOVERLAY, showPerformanceOverlay);
330336
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
337+
settings.setValue(SER_SPATIALAUDIOCFG, static_cast<int>(spatialAudioConfig));
331338
settings.setValue(SER_HDR, enableHdr);
332339
settings.setValue(SER_YUV444, enableYUV444);
333340
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));

app/settings/streamingpreferences.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class StreamingPreferences : public QObject
2626
};
2727
Q_ENUM(AudioConfig)
2828

29+
enum SpatialAudioConfig
30+
{
31+
SAC_AUTO,
32+
SAC_DISABLED
33+
};
34+
Q_ENUM(SpatialAudioConfig)
35+
2936
enum VideoCodecConfig
3037
{
3138
VCC_AUTO,
@@ -112,6 +119,7 @@ class StreamingPreferences : public QObject
112119
Q_PROPERTY(bool unlockBitrate MEMBER unlockBitrate NOTIFY unlockBitrateChanged)
113120
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
114121
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
122+
Q_PROPERTY(bool spatialHeadTracking MEMBER spatialHeadTracking NOTIFY spatialHeadTrackingChanged)
115123
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
116124
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
117125
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
@@ -125,6 +133,7 @@ class StreamingPreferences : public QObject
125133
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged)
126134
Q_PROPERTY(bool showPerformanceOverlay MEMBER showPerformanceOverlay NOTIFY showPerformanceOverlayChanged)
127135
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
136+
Q_PROPERTY(SpatialAudioConfig spatialAudioConfig MEMBER spatialAudioConfig NOTIFY spatialAudioConfigChanged)
128137
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
129138
Q_PROPERTY(bool enableHdr MEMBER enableHdr NOTIFY enableHdrChanged)
130139
Q_PROPERTY(bool enableYUV444 MEMBER enableYUV444 NOTIFY enableYUV444Changed)
@@ -151,6 +160,7 @@ class StreamingPreferences : public QObject
151160
bool unlockBitrate;
152161
bool enableVsync;
153162
bool gameOptimizations;
163+
bool spatialHeadTracking;
154164
bool playAudioOnHost;
155165
bool multiController;
156166
bool enableMdns;
@@ -171,6 +181,7 @@ class StreamingPreferences : public QObject
171181
bool keepAwake;
172182
int packetSize;
173183
AudioConfig audioConfig;
184+
SpatialAudioConfig spatialAudioConfig;
174185
VideoCodecConfig videoCodecConfig;
175186
bool enableHdr;
176187
bool enableYUV444;
@@ -187,6 +198,7 @@ class StreamingPreferences : public QObject
187198
void unlockBitrateChanged();
188199
void enableVsyncChanged();
189200
void gameOptimizationsChanged();
201+
void spatialHeadTrackingChanged();
190202
void playAudioOnHostChanged();
191203
void multiControllerChanged();
192204
void unsupportedFpsChanged();
@@ -195,6 +207,7 @@ class StreamingPreferences : public QObject
195207
void absoluteMouseModeChanged();
196208
void absoluteTouchModeChanged();
197209
void audioConfigChanged();
210+
void spatialAudioConfigChanged();
198211
void videoCodecConfigChanged();
199212
void enableHdrChanged();
200213
void enableYUV444Changed();

app/streaming/audio/audio.cpp

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
#include "renderers/slaud.h"
1010
#endif
1111

12+
#ifdef HAVE_COREAUDIO
13+
#include "renderers/coreaudio/coreaudio.h"
14+
#endif
15+
1216
#include "renderers/sdl.h"
1317

1418
#include <Limelight.h>
@@ -29,6 +33,12 @@ IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATI
2933
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
3034
return nullptr;
3135
}
36+
#ifdef HAVE_COREAUDIO
37+
else if (mlAudio == "coreaudio") {
38+
TRY_INIT_RENDERER(CoreAudioRenderer, opusConfig)
39+
return nullptr;
40+
}
41+
#endif
3242
#ifdef HAVE_SOUNDIO
3343
else if (mlAudio == "libsoundio") {
3444
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
@@ -55,6 +65,11 @@ IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATI
5565
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
5666
#endif
5767

68+
#ifdef HAVE_COREAUDIO
69+
// Native renderer for macOS/iOS/tvOS, suports spatial audio
70+
TRY_INIT_RENDERER(CoreAudioRenderer, opusConfig)
71+
#endif
72+
5873
// Default to SDL and use libsoundio as a fallback
5974
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
6075
#ifdef HAVE_SOUNDIO
@@ -261,7 +276,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
261276
s_ActiveSession->m_AudioRenderer->flipAudioStatsWindows();
262277
}
263278

264-
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredBufferSize)) {
279+
if (s_ActiveSession->m_AudioRenderer->submitAudio(desiredBufferSize)) {
280+
// keep stats on how long the audio pipline took to execute
281+
s_ActiveSession->m_AudioRenderer->statsTrackDecodeTime(startTimeUs);
282+
}
283+
else {
265284
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
266285
"Reinitializing audio renderer after failure");
267286

@@ -271,9 +290,6 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
271290
delete s_ActiveSession->m_AudioRenderer;
272291
s_ActiveSession->m_AudioRenderer = nullptr;
273292
}
274-
275-
// keep stats on how long the audio pipline took to execute
276-
s_ActiveSession->m_AudioRenderer->statsTrackDecodeTime(startTimeUs);
277293
}
278294

279295
// Only try to recreate the audio renderer every 200 samples (1 second)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright © 2024 Apple Inc.
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
5+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
6+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
7+
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
12+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
13+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
14+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
*/
16+
#pragma once
17+
18+
#include <AudioToolbox/AudioToolbox.h>
19+
20+
class AllocatedAudioBufferList
21+
{
22+
public:
23+
AllocatedAudioBufferList(UInt32 channelCount, uint16_t bufferSize)
24+
{
25+
26+
mBufferList = static_cast<AudioBufferList *>(malloc(sizeof(AudioBufferList) + (sizeof(AudioBuffer) * channelCount)));
27+
mBufferList->mNumberBuffers = channelCount;
28+
for (UInt32 c = 0; c < channelCount; ++c) {
29+
mBufferList->mBuffers[c].mNumberChannels = 1;
30+
mBufferList->mBuffers[c].mDataByteSize = bufferSize * sizeof(float);
31+
mBufferList->mBuffers[c].mData = malloc(sizeof(float) * bufferSize);
32+
}
33+
}
34+
35+
AllocatedAudioBufferList(const AllocatedAudioBufferList&) = delete;
36+
37+
AllocatedAudioBufferList& operator=(const AllocatedAudioBufferList&) = delete;
38+
39+
~AllocatedAudioBufferList()
40+
{
41+
if (mBufferList == nullptr) { return; }
42+
43+
for (UInt32 i = 0; i < mBufferList->mNumberBuffers; ++i) {
44+
free(mBufferList->mBuffers[i].mData);
45+
}
46+
free(mBufferList);
47+
mBufferList = nullptr;
48+
}
49+
50+
AudioBufferList * _Nonnull get()
51+
{
52+
return mBufferList;
53+
}
54+
55+
private:
56+
AudioBufferList * _Nonnull mBufferList = { nullptr };
57+
};

0 commit comments

Comments
 (0)