From 2735c55c978809934bdd19842d0992d3c4f469c0 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sun, 17 May 2026 02:17:37 +0800 Subject: [PATCH] diag(audio,ios): log actual VPIO + AVAudioSession state post-init (rc.8+71) Per external review (helpful checklist from ChatGPT-style analysis pointing out we never verified that iOS actually accepted our preferred sample rate / channels / format): preferredSampleRate and preferredIOBufferDuration are HINTS, not guarantees. iOS may substitute its own values if the hardware can't satisfy our preference. If VPIO is running at 44.1 kHz Float32 stereo while our render callback writes 48 kHz Int16 mono into the buffer, the symptoms would match what user reports (broken playback, pitch shifted, severe distortion) and our previous diagnostics wouldn't catch it because they only sampled signal-level metrics. This commit adds two diagnostic emissions to verify: 1. AppDelegate.swift::activateAudioSession: after setActive succeeds, log the ACTUAL session state \u2014 category, mode, sampleRate, ioBufferDuration, current route (inputs + outputs), outputVolume. Lets us see whether iOS honoured our .default + .defaultToSpeaker setup and which physical route it picked at launch. 2. ios_voice_unit.rs::IosVoiceUnit::start: after unit.start() succeeds, log the actual OUTPUT and INPUT stream formats VPIO accepted (sample_rate, channels, sample_format, flags). If these differ from our requested 48 kHz Int16 mono, we have a format-substitution problem. Three possible outcomes from the next test: * Both diagnostics confirm 48 kHz Int16 mono on both buses and the session sampleRate=48000 -> format is correct; the playback breakage is somewhere else (e.g. AudioHandler jitter buffer behaviour, route binding, or hardware mixer). * Session sampleRate != 48000 -> we need to insert a sample rate converter or pin AVAudioSession's setPreferredSampleRate(48000) explicitly in Swift before setActive. * VPIO substituted Float32 for our Int16 request -> our render callback is writing i16 magnitudes into a Float32 buffer which would explain the distortion. Fix: write Float32 directly using data::Interleaved instead of i16. Build counter 70 -> 71. Pure diagnostic; no behavioural change. --- .../ios/Runner/AppDelegate.swift | 22 ++++++++++ apps/chanora_flutter/pubspec.yaml | 2 +- crates/chanora_audio/src/ios_voice_unit.rs | 42 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/apps/chanora_flutter/ios/Runner/AppDelegate.swift b/apps/chanora_flutter/ios/Runner/AppDelegate.swift index b3d7415..ce11818 100644 --- a/apps/chanora_flutter/ios/Runner/AppDelegate.swift +++ b/apps/chanora_flutter/ios/Runner/AppDelegate.swift @@ -153,6 +153,28 @@ import AVFoundation do { try AVAudioSession.sharedInstance().setActive(true, options: []) NSLog("chanora_flutter: AVAudioSession activated on foreground") + // Read back the ACTUAL session state. preferredSampleRate / + // preferredIOBufferDuration are hints; iOS may pick something + // else depending on hardware + currently-engaged effects. + // Without these we can't tell whether VPIO is running at + // 48 kHz mono (what our render callback assumes) or at e.g. + // 44.1 kHz (which would explain the user's broken playback + // \u2014 our render callback would be writing samples at the + // wrong rate, causing pitch + timing artifacts). + let s = AVAudioSession.sharedInstance() + let route = s.currentRoute + let outs = route.outputs.map { "\($0.portType.rawValue)/\($0.portName)" }.joined(separator: ",") + let ins = route.inputs.map { "\($0.portType.rawValue)/\($0.portName)" }.joined(separator: ",") + NSLog( + "chanora_flutter: AVAudioSession actual: " + + "category=\(s.category.rawValue) " + + "mode=\(s.mode.rawValue) " + + "sampleRate=\(s.sampleRate) " + + "ioBufferDuration=\(String(format: "%.4f", s.ioBufferDuration)) " + + "outputs=[\(outs)] " + + "inputs=[\(ins)] " + + "outputVolume=\(s.outputVolume)" + ) } catch { NSLog("chanora_flutter: AVAudioSession setActive failed: \(error)") } diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index 71e63ab..2ca8cf0 100644 --- a/apps/chanora_flutter/pubspec.yaml +++ b/apps/chanora_flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-rc.8+70 +version: 1.0.0-rc.8+71 environment: sdk: ^3.11.5 diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index bcb5884..d95397e 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -579,6 +579,48 @@ impl IosVoiceUnit { "ios VPIO audio unit started" ); + // Read back the ACTUAL stream format VPIO accepted on each + // bus (iOS sometimes substitutes its own format if the + // hardware can't satisfy our preference) and the actual + // AVAudioSession sample rate + IO buffer duration. Without + // these we can't tell whether our 48 kHz Int16 mono format + // was honoured or silently downgraded to e.g. 44.1 kHz + // Float32 (which would cause our render callback to write + // i16 values into a buffer iOS interprets as f32 = severe + // distortion). Diagnostic prompted by external review + // pointing out that 'preferredSampleRate' is a hint, not + // a guarantee \u2014 must verify post-init. + match unit.output_stream_format() { + Ok(fmt) => info!( + target: "chanora_audio", + sample_rate = fmt.sample_rate, + channels = fmt.channels, + sample_format = ?fmt.sample_format, + flags = ?fmt.flags, + "ios VPIO actual OUTPUT stream format (post-init)" + ), + Err(e) => warn!( + target: "chanora_audio", + error = %e, + "ios VPIO output_stream_format read failed" + ), + } + match unit.input_stream_format() { + Ok(fmt) => info!( + target: "chanora_audio", + sample_rate = fmt.sample_rate, + channels = fmt.channels, + sample_format = ?fmt.sample_format, + flags = ?fmt.flags, + "ios VPIO actual INPUT stream format (post-init)" + ), + Err(e) => warn!( + target: "chanora_audio", + error = %e, + "ios VPIO input_stream_format read failed" + ), + } + Ok(Self { unit }) } }