diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard
index a2b93428..909bdac8 100644
--- a/VoidLink/Base.lproj/iPad.storyboard
+++ b/VoidLink/Base.lproj/iPad.storyboard
@@ -500,10 +500,11 @@
-
+
+
@@ -2943,7 +2944,7 @@
-
+
@@ -2952,13 +2953,13 @@
-
+
-
+
-
+
diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard
index d3538fe4..28e66e55 100644
--- a/VoidLink/Base.lproj/iPhone.storyboard
+++ b/VoidLink/Base.lproj/iPhone.storyboard
@@ -1700,10 +1700,11 @@
-
+
+
@@ -2937,19 +2938,19 @@
-
+
-
+
-
+
-
+
diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h
index 4136420d..b5900842 100644
--- a/VoidLink/Database/DataManager.h
+++ b/VoidLink/Database/DataManager.h
@@ -27,11 +27,6 @@ typedef NS_ENUM(NSInteger, ControllerGyroSwitchMode) {
ControllerGyroSwitchHoldDown
};
-typedef NS_ENUM(NSInteger, AudioEngine) {
- SystemAudioEngine,
- SDLAudioEngine
-};
-
typedef NS_ENUM(NSInteger, TouchMode) {
RelativeTouch,
NativeTouch,
diff --git a/VoidLink/Input/InertialScroller.swift b/VoidLink/Input/InertialScroller.swift
index 8f037679..a0427ad7 100644
--- a/VoidLink/Input/InertialScroller.swift
+++ b/VoidLink/Input/InertialScroller.swift
@@ -2,7 +2,7 @@
// InertialScroller.swift
// VoidLink
//
-// Created by True砖家 on 2025/11/5.
+// Created by True砖家 on 2025/12/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
diff --git a/VoidLink/Input/MicHandler.swift b/VoidLink/Input/MicHandler.swift
index 8bb33ca0..4d82c57b 100644
--- a/VoidLink/Input/MicHandler.swift
+++ b/VoidLink/Input/MicHandler.swift
@@ -16,6 +16,7 @@ import Collections
@objcMembers
public class MicHandler: NSObject {
+ private var notificationTokens = [NSObjectProtocol]()
private let engine = AVAudioEngine()
private let playerNode = AVAudioPlayerNode()
private var audioSink: Any?
@@ -48,6 +49,16 @@ public class MicHandler: NSObject {
@objc public init(useBuiltinMic:Bool) {
super.init()
+
+ let token = NotificationCenter.default.addObserver(
+ forName: AVAudioSession.interruptionNotification,
+ object: nil,
+ queue: .main
+ ) { [weak self] notification in
+ self?.handleInterruption(notification)
+ }
+ notificationTokens.append(token)
+
self.useBuiltinMic = useBuiltinMic
do {
try configureSession()
@@ -140,6 +151,29 @@ public class MicHandler: NSObject {
/* ----------- Audio Session -------------*/
+ private func handleInterruption(_ notification: Notification) {
+ guard let info = notification.userInfo,
+ let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue)
+ else { return }
+
+ switch type {
+ case .began:
+ self.stopTapping(stopEngine: true)
+ case .ended:
+ do {
+ try configureEngine()
+ } catch {
+ notify(error)
+ }
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
+ self.startTapping()
+ }
+ @unknown default:
+ break
+ }
+ }
+
private func configureOpus(sampleRate: Int32, channels: Int) throws {
var err: Int32 = 0
guard let enc = opus_encoder_create(sampleRate, Int32(channels), OPUS_APPLICATION_VOIP, &err), err == OPUS_OK else {
@@ -336,6 +370,10 @@ public class MicHandler: NSObject {
}
@objc public func clean() {
+ for token in notificationTokens {
+ NotificationCenter.default.removeObserver(token)
+ }
+ notificationTokens.removeAll()
self.timer?.clean()
}
diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings
index eab2bfca..7c23756a 100644
--- a/VoidLink/Localization/Localizable.xcstrings
+++ b/VoidLink/Localization/Localizable.xcstrings
@@ -858,6 +858,35 @@
}
}
},
+ "audioConfigStackTip" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "- Stereo: Stereo mode driven by the system audio engine. Offers better audio quality and continues playback when the screen is off.\n\n- Stereo (SDL): Stereo mode driven by the SDL engine. This mode is essentially equivalent to Moonlight-iOS audio. Playback cannot be maintained after the screen is turned off.\n\n- 5.1 / 7.1 Channels: both are driven by the SDL engine. Playback cannot be maintained after the screen is turned off. Available for iOS 18+."
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "- 双声道:由系统音频引擎驱动的双声道模式。音质更好、熄屏可维持播放。\n\n- 双声道(SDL):由SDL引擎驱动的双声道模式。此模式基本上等价于moonlight-iOS音频。熄屏后无法维持播放。\n\n- 5.1 / 7.1 声道:均为SDL引擎驱动。熄屏后无法维持播放。需 iOS 18 以上支持。"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "- 雙聲道:由系統音訊引擎驅動的雙聲道模式。音質較佳,且在熄屏後仍可維持播放。\n\n- 雙聲道(SDL):由 SDL 引擎驅動的雙聲道模式。此模式基本等同於 Moonlight-iOS 的音訊。熄屏後無法維持播放。\n\n5.1/7.1 聲道:皆由 SDL 引擎驅動。熄屏後無法維持播放,需 iOS 18 以上支援。"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "- 雙聲道:由系統音訊引擎驅動的雙聲道模式。音質較佳,且在熄屏後仍可維持播放。\n\n- 雙聲道(SDL):由 SDL 引擎驅動的雙聲道模式。此模式基本等同於 Moonlight-iOS 的音訊。熄屏後無法維持播放。\n\n5.1/7.1 聲道:皆由 SDL 引擎驅動。熄屏後無法維持播放,需 iOS 18 以上支援。"
+ }
+ }
+ }
+ },
"Auto tap disabled" : {
"extractionState" : "manual",
"localizations" : {
diff --git a/VoidLink/Stream/Connection.m b/VoidLink/Stream/Connection.m
index ad2e2626..2acf3dc4 100644
--- a/VoidLink/Stream/Connection.m
+++ b/VoidLink/Stream/Connection.m
@@ -51,6 +51,7 @@ static float volume = 1.0;
static int audioFrameSize;
static bool useSystemAudioEngine;
+static bool audioSessionInterrupted;
static AVAudioEngine *audioEngine;
static AVAudioPlayerNode *audioPlayerNode;
static AVAudioPCMBuffer *pcmBuffer;
@@ -304,7 +305,8 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
error:nil];
if(tempSettings.redirectMic) if(@available(iOS 13.0, *)) [session setAllowHapticsAndSystemSoundsDuringRecording:YES error:nil];
[session setActive:YES error:nil];
-
+ audioSessionInterrupted = false;
+
AudioEngineInit(audioConfig.sampleRate, audioConfig.channelCount);
return 0;
@@ -423,7 +425,7 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
float* fbuf = (float*)audioBuffer;
- if(audioConfig.channelCount <= 2) {
+ if(useSystemAudioEngine){
// 创建 AVAudioPCMBuffer
AVAudioFrameCount frameCount = decodeLen;
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFormat frameCapacity:frameCount];
@@ -437,7 +439,7 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
}
}
// 播放
- [audioPlayerNode scheduleBuffer:buffer completionHandler:nil];
+ if(!audioSessionInterrupted) [audioPlayerNode scheduleBuffer:buffer completionHandler:nil];
}
else{
@@ -458,6 +460,7 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
}
}
+
}
}
@@ -534,6 +537,7 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
LiInterruptConnection();
[audioPlayerNode stop];
[audioEngine stop];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
// 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
@@ -545,6 +549,27 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
});
}
+- (void)handleAudioSessionInterruption:(NSNotification *)notification {
+ NSDictionary *info = notification.userInfo;
+ AVAudioSessionInterruptionType type =
+ [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
+
+ switch (type) {
+ case AVAudioSessionInterruptionTypeBegan:
+ audioSessionInterrupted = true;
+ [audioPlayerNode stop];
+ [audioEngine stop];
+ break;
+ case AVAudioSessionInterruptionTypeEnded:
+ AudioEngineInit(audioConfig.sampleRate, audioConfig.channelCount);
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ audioSessionInterrupted = false;
+ });
+ default:
+ break;
+ }
+}
+
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id)callbacks
{
self = [super init];
@@ -662,7 +687,12 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
_clCallbacks.setMotionEventState = ClSetMotionEventState;
_clCallbacks.setControllerLED = ClSetControllerLED;
-
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleAudioSessionInterruption:)
+ name:AVAudioSessionInterruptionNotification
+ object:nil];
+
return self;
}
diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m
index 963c7dc7..ec78583c 100644
--- a/VoidLink/ViewControllers/MainFrameViewController.m
+++ b/VoidLink/ViewControllers/MainFrameViewController.m
@@ -850,6 +850,7 @@ static NSMutableSet* hostList;
}
int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
+
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
if (numberOfChannels >= 8) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
@@ -861,6 +862,7 @@ static NSMutableSet* hostList;
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
}
+ Connection.useSystemAudioEngine = streamSettings.audioConfig.intValue == 2;
switch (streamSettings.preferredCodec) {
case CODEC_PREF_AV1:
@@ -1191,7 +1193,24 @@ static NSMutableSet* hostList;
[settingsViewController.emulatedControllerTypeSelector setEnabled:!_settingsExpandedInStreamView];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.citrixX1MouseStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.externalDisplayModeStack];
- [settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioConfigStack];
+
+ if(settingsViewController.audioConfigSelector.numberOfSegments>2){
+ if(_settingsExpandedInStreamView){
+ if(settingsViewController.audioConfigSelector.selectedSegmentIndex>=2){
+ [settingsViewController.audioConfigSelector setEnabled:false];
+ }
+ else {
+ [settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:2];
+ [settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:3];
+ }
+ }
+ else{
+ [settingsViewController.audioConfigSelector setEnabled:true];
+ [settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:2];
+ [settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:3];
+ }
+ }
+
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.duckOtherAppStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.pipStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.appThemeStack];
diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m
index 9f71d5cc..f10ec842 100644
--- a/VoidLink/ViewControllers/SettingsViewController.m
+++ b/VoidLink/ViewControllers/SettingsViewController.m
@@ -895,7 +895,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[self addSetting:self.muteInBackgroundStack ofId:@"muteInBackgroundStack" withInfoTag:NO withDynamicLabel:NO to:audioSection];
// [self addSetting:self.audioEngineStack ofId:@"audioEngineStack" withInfoTag:YES withDynamicLabel:NO to:audioSection];
// cancel audio engine selector due to system engine is unable to playback multi-channel audio
- [self addSetting:self.audioConfigStack ofId:@"audioConfigStack" withInfoTag:NO withDynamicLabel:NO to:audioSection];
+ [self addSetting:self.audioConfigStack ofId:@"audioConfigStack" withInfoTag:YES withDynamicLabel:NO to:audioSection];
[audioSection addToParentStack:_parentStack];
// [audioSection setExpanded:NO];
@@ -1442,6 +1442,10 @@ BOOL isCustomResolution(int resolutionSelected) {
tipText = [LocalizationHelper localizedStringForKey:@"physicaStickMinOffsetTip"];
showOnlineDocAction = false;
}
+ if([sender.superview.accessibilityIdentifier isEqualToString: @"audioConfigStack"]){
+ tipText = [LocalizationHelper localizedStringForKey:@"audioConfigStackTip"];
+ showOnlineDocAction = false;
+ }
UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message:tipText preferredStyle:UIAlertControllerStyleAlert];
@@ -1927,22 +1931,30 @@ BOOL isCustomResolution(int resolutionSelected) {
self.audioEngineSelector.selectedSegmentIndex = self->tempSettings.audioEngine.intValue;
if (@available(iOS 18.0, tvOS 18.0, *)) {}else{
- [self.audioConfigSelector removeSegmentAtIndex:1 animated:false];
- [self.audioConfigSelector removeSegmentAtIndex:1 animated:false]; // segment 2 goes away when you remove index 2
+ [self.audioConfigSelector removeSegmentAtIndex:2 animated:false];
+ [self.audioConfigSelector removeSegmentAtIndex:2 animated:false]; // segment 2 goes away when you remove index 2
+ /*
[self.audioConfigSelector setTitle:[LocalizationHelper localizedStringForKey:@"Stereo (surround sound available for iOS18+)"] forSegmentAtIndex:0];
- [self.audioConfigSelector setEnabled:NO];
+ [self.audioConfigSelector setEnabled:NO];*/
}
switch ([self->tempSettings.audioConfig integerValue]) {
case 2:
[self.audioConfigSelector setSelectedSegmentIndex:0];
break;
- case 6:
+ case 3:
[self.audioConfigSelector setSelectedSegmentIndex:1];
break;
- case 8:
+ case 6:
[self.audioConfigSelector setSelectedSegmentIndex:2];
break;
+ case 8:
+ [self.audioConfigSelector setSelectedSegmentIndex:3];
+ break;
}
+ // 2 - stereo (system)
+ // 3 - stereo (SDL)
+ // 6 - 5.1 (SDL)
+ // 8 - 7.1 (SDL)
// Unlock Display Orientation setting
bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired] || [self isIPhone];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation working
@@ -3332,7 +3344,12 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger framerate = [self getChosenFrameRate];
- NSInteger audioConfig = [@[@2, @6, @8][[self.audioConfigSelector selectedSegmentIndex]] integerValue];
+ NSInteger audioConfig = [@[@2, @3, @6, @8][[self.audioConfigSelector selectedSegmentIndex]] integerValue];
+ // 2 - stereo (system)
+ // 3 - stereo (SDL)
+ // 6 - 5.1 (SDL)
+ // 8 - 7.1 (SDL)
+
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
NSInteger framePacingMode = [self.framePacingModeSelector selectedSegmentIndex];
NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex];
diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m
index c44bf79d..6bca3197 100644
--- a/VoidLink/ViewControllers/StreamFrameViewController.m
+++ b/VoidLink/ViewControllers/StreamFrameViewController.m
@@ -376,7 +376,6 @@
}
else [micHandler stopTappingWithStopEngine:false];
- Connection.useSystemAudioEngine = _settings.audioEngine.intValue == SystemAudioEngine;
Connection.muteInBackground = _settings.muteInBackground;
if(!viewJustLoaded) [_controllerSupport updateControllerSupport:self.streamConfig delegate:self];
diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings
index e4561e20..a2459e32 100644
--- a/VoidLink/mul.lproj/iPad.xcstrings
+++ b/VoidLink/mul.lproj/iPad.xcstrings
@@ -3110,7 +3110,37 @@
}
},
"iZL-dA-jk9.segmentTitles[1]" : {
- "comment" : "Class = \"UISegmentedControl\"; iZL-dA-jk9.segmentTitles[1] = \"5.1‑channel\"; ObjectID = \"iZL-dA-jk9\";",
+ "comment" : "Class = \"UISegmentedControl\"; iZL-dA-jk9.segmentTitles[1] = \"Stereo(SDL)\"; ObjectID = \"iZL-dA-jk9\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Stereo(SDL)"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "双声道(SDL)"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "雙聲道(SDL)"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "雙聲道(SDL)"
+ }
+ }
+ }
+ },
+ "iZL-dA-jk9.segmentTitles[2]" : {
+ "comment" : "Class = \"UISegmentedControl\"; iZL-dA-jk9.segmentTitles[2] = \"5.1‑channel\"; ObjectID = \"iZL-dA-jk9\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
@@ -3139,8 +3169,8 @@
}
}
},
- "iZL-dA-jk9.segmentTitles[2]" : {
- "comment" : "Class = \"UISegmentedControl\"; iZL-dA-jk9.segmentTitles[2] = \"7.1‑channel\"; ObjectID = \"iZL-dA-jk9\";",
+ "iZL-dA-jk9.segmentTitles[3]" : {
+ "comment" : "Class = \"UISegmentedControl\"; iZL-dA-jk9.segmentTitles[3] = \"7.1‑channel\"; ObjectID = \"iZL-dA-jk9\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings
index 9fa0a3d3..7b564f15 100644
--- a/VoidLink/mul.lproj/iPhone.xcstrings
+++ b/VoidLink/mul.lproj/iPhone.xcstrings
@@ -2408,7 +2408,37 @@
}
},
"hfR-K8-Ti4.segmentTitles[1]" : {
- "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[1] = \"5.1-channel\"; ObjectID = \"hfR-K8-Ti4\";",
+ "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[1] = \"Stereo(SDL)\"; ObjectID = \"hfR-K8-Ti4\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Stereo(SDL)"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "双声道(SDL)"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "雙聲道(SDL)"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "雙聲道(SDL)"
+ }
+ }
+ }
+ },
+ "hfR-K8-Ti4.segmentTitles[2]" : {
+ "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[2] = \"5.1-channel\"; ObjectID = \"hfR-K8-Ti4\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
@@ -2425,20 +2455,20 @@
},
"zh-Hant" : {
"stringUnit" : {
- "state" : "needs_review",
+ "state" : "translated",
"value" : "5.1聲道"
}
},
"zh-HK" : {
"stringUnit" : {
- "state" : "needs_review",
+ "state" : "translated",
"value" : "5.1聲道"
}
}
}
},
- "hfR-K8-Ti4.segmentTitles[2]" : {
- "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[2] = \"7.1-channel\"; ObjectID = \"hfR-K8-Ti4\";",
+ "hfR-K8-Ti4.segmentTitles[3]" : {
+ "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[3] = \"7.1-channel\"; ObjectID = \"hfR-K8-Ti4\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
@@ -2455,13 +2485,13 @@
},
"zh-Hant" : {
"stringUnit" : {
- "state" : "needs_review",
+ "state" : "translated",
"value" : "7.1聲道"
}
},
"zh-HK" : {
"stringUnit" : {
- "state" : "needs_review",
+ "state" : "translated",
"value" : "7.1聲道"
}
}