From 28e2f13e876c154ad1badf563d2ba145c1f106e5 Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Mon, 27 Oct 2025 17:12:45 +0800 Subject: [PATCH 1/2] test: replace SDL with system audio engine to support audio playing when screen is dimmed out --- VoidLink/Input/MicHandler.swift | 5 +- VoidLink/Stream/Connection.h | 5 ++ VoidLink/Stream/Connection.m | 97 +++++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/VoidLink/Input/MicHandler.swift b/VoidLink/Input/MicHandler.swift index 57d951b4..cbc690cf 100644 --- a/VoidLink/Input/MicHandler.swift +++ b/VoidLink/Input/MicHandler.swift @@ -185,11 +185,11 @@ public class MicHandler: NSObject { private func configureSession() throws { let session = AVAudioSession.sharedInstance() - try session.setCategory(.playAndRecord, mode: .videoRecording, options: [.mixWithOthers, .defaultToSpeaker, .allowBluetooth]) + let bluetoothAudioOption = self.useBuiltinMic ? AVAudioSession.CategoryOptions.allowBluetoothA2DP : AVAudioSession.CategoryOptions.allowBluetooth + try session.setCategory(.playAndRecord, mode: .videoRecording, options: [.mixWithOthers, .defaultToSpeaker, bluetoothAudioOption]) if #available(iOS 13.0, *) { try session.setAllowHapticsAndSystemSoundsDuringRecording(true) } - try session.setActive(true) // 列出所有可用输入 if self.useBuiltinMic, let inputs = session.availableInputs { @@ -201,6 +201,7 @@ public class MicHandler: NSObject { } } } + try session.setActive(true) } private func sendOpusFrameFromDequeBuffer() { diff --git a/VoidLink/Stream/Connection.h b/VoidLink/Stream/Connection.h index f4e30315..136fdefc 100644 --- a/VoidLink/Stream/Connection.h +++ b/VoidLink/Stream/Connection.h @@ -5,6 +5,10 @@ // Created by Diego Waxemberg on 1/19/14. // Copyright (c) 2014 Moonlight Stream. All rights reserved. // +// Modified by True砖家 since 2025.9 +// Copyright © 2025 True砖家 on Bilibili. All rights reserved. +// + #import "ConnectionCallbacks.h" #import "VideoDecoderRenderer.h" @@ -23,5 +27,6 @@ -(BOOL) getVideoStats:(video_stats_t*)stats; -(NSString*) getActiveCodecName; + (void)setVolume:(float)newVolume; ++ (void)setUseSystemAudioEngine:(bool)useSysAudioEngine; @end diff --git a/VoidLink/Stream/Connection.m b/VoidLink/Stream/Connection.m index 3c067525..d4149aca 100644 --- a/VoidLink/Stream/Connection.m +++ b/VoidLink/Stream/Connection.m @@ -5,6 +5,9 @@ // Created by Diego Waxemberg on 1/19/14. // Copyright (c) 2015 Moonlight Stream. All rights reserved. // +// Modified by True砖家 since 2025.9 +// Copyright © 2025 True砖家 on Bilibili. All rights reserved. +// #import "Connection.h" #import "Plot.h" @@ -47,6 +50,12 @@ static void* audioBuffer; static float volume = 1.0; static int audioFrameSize; +static bool useSystemAudioEngine; +static AVAudioEngine *audioEngine; +static AVAudioPlayerNode *audioPlayerNode; +static AVAudioPCMBuffer *pcmBuffer; +static AVAudioFormat *audioFormat; + static VideoDecoderRenderer* renderer; static BandwidthTracker *bwTracker; @@ -276,10 +285,20 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v SDL_PauseAudioDevice(audioDevice, 0); // Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default) - - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; - + // AVAudioSessionCategoryOptionAllowBluetoothA2DP + // AVAudioSessionCategoryOptionAllowBluetooth + DataManager* dataMan = [[DataManager alloc] init]; + TemporarySettings* tempSettings = [dataMan getSettings]; + bool useBluetoothD2P = tempSettings.useBuiltinMic || !tempSettings.redirectMic; + AVAudioSessionCategoryOptions bluetoothAudioOption = useBluetoothD2P ? AVAudioSessionCategoryOptionAllowBluetoothA2DP : AVAudioSessionCategoryOptionAllowBluetooth; + AVAudioSession *session = [AVAudioSession sharedInstance]; + [session setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionDefaultToSpeaker|bluetoothAudioOption + error:nil]; + [session setActive:YES error:nil]; + AudioEngineInit(audioConfig.sampleRate, audioConfig.channelCount); + return 0; } @@ -310,6 +329,30 @@ void ArCleanup(void) volume = powf(linearVolume, exponent); } ++ (void)setUseSystemAudioEngine:(bool)useSysAudioEngine{ + useSystemAudioEngine = useSysAudioEngine; +} + +void AudioEngineInit(int sampleRate, int channelCount) { + audioEngine = [[AVAudioEngine alloc] init]; + audioPlayerNode = [[AVAudioPlayerNode alloc] init]; + + [audioEngine attachNode:audioPlayerNode]; + + audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:sampleRate + channels:channelCount + interleaved:NO]; + + [audioEngine connect:audioPlayerNode to:audioEngine.mainMixerNode format:audioFormat]; + + NSError *err = nil; + if (![audioEngine startAndReturnError:&err]) { + NSLog(@"AudioEngine start error: %@", err); + } + [audioPlayerNode play]; +} + void ArDecodeAndPlaySample(char* sampleData, int sampleLength) { int decodeLen; @@ -333,21 +376,41 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength) float* fbuf = (float*)audioBuffer; - if(volume != 1.0){ - int totalSamples = decodeLen * audioConfig.channelCount; - for (int i = 0; i < totalSamples; i++) { - fbuf[i] *= volume; + // if(useSystemAudioEngine && false) { + if(true){ + // 创建 AVAudioPCMBuffer + AVAudioFrameCount frameCount = decodeLen; + AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFormat frameCapacity:frameCount]; + buffer.frameLength = frameCount; + + // 拷贝数据到 buffer + for (int ch = 0; ch < audioConfig.channelCount; ch++) { + float *dst = buffer.floatChannelData[ch]; + for (int i = 0; i < decodeLen; i++) { + dst[i] = fbuf[i * audioConfig.channelCount + ch] * volume; // 非交错数据 + } } + // 播放 + [audioPlayerNode scheduleBuffer:buffer completionHandler:nil]; } - while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) { - [NSThread sleepForTimeInterval:0.001f]; - } - - if (SDL_QueueAudio(audioDevice, - audioBuffer, - sizeof(float) * decodeLen * audioConfig.channelCount) < 0) { - Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError()); + else{ + if(volume != 1.0){ + int totalSamples = decodeLen * audioConfig.channelCount; + for (int i = 0; i < totalSamples; i++) { + fbuf[i] *= volume; + } + } + + while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) { + [NSThread sleepForTimeInterval:0.001f]; + } + + if (SDL_QueueAudio(audioDevice, + audioBuffer, + sizeof(float) * decodeLen * audioConfig.channelCount) < 0) { + Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError()); + } } } } @@ -423,7 +486,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t // won't be able to acquire it if LiStartConnection is in // progress. LiInterruptConnection(); - + [audioPlayerNode stop]; + [audioEngine stop]; + // We dispatch this async to get out because this can be invoked // on a thread inside common and we don't want to deadlock. It also avoids // blocking on the caller's thread waiting to acquire initLock. From cd9ef74f3fbcd75ab8f28ede4dc289b86eddb850 Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Tue, 28 Oct 2025 21:54:16 +0800 Subject: [PATCH 2/2] add multi-channel audio support --- VoidLink/Stream/Connection.m | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/VoidLink/Stream/Connection.m b/VoidLink/Stream/Connection.m index d4149aca..32a6b4a5 100644 --- a/VoidLink/Stream/Connection.m +++ b/VoidLink/Stream/Connection.m @@ -334,15 +334,41 @@ void ArCleanup(void) } void AudioEngineInit(int sampleRate, int channelCount) { + audioEngine = [[AVAudioEngine alloc] init]; audioPlayerNode = [[AVAudioPlayerNode alloc] init]; [audioEngine attachNode:audioPlayerNode]; + + AVAudioChannelLayout *layout; - audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 - sampleRate:sampleRate - channels:channelCount - interleaved:NO]; + switch (channelCount) { + case 2: + default: + audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:sampleRate + channels:channelCount + interleaved:NO]; + break; + case 6: + layout = + [[AVAudioChannelLayout alloc] initWithLayoutTag:kAudioChannelLayoutTag_MPEG_5_1_A]; + audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:sampleRate + interleaved:NO + channelLayout:layout]; + break; + case 8: + layout = + [[AVAudioChannelLayout alloc] initWithLayoutTag:kAudioChannelLayoutTag_MPEG_7_1_A]; + audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32 + sampleRate:sampleRate + interleaved:NO + channelLayout:layout]; + break; + } + + if(!audioFormat) return; [audioEngine connect:audioPlayerNode to:audioEngine.mainMixerNode format:audioFormat]; @@ -350,6 +376,7 @@ void AudioEngineInit(int sampleRate, int channelCount) { if (![audioEngine startAndReturnError:&err]) { NSLog(@"AudioEngine start error: %@", err); } + [audioPlayerNode play]; }