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<f32> instead of i16.

Build counter 70 -> 71. Pure diagnostic; no behavioural
change.
This commit is contained in:
EdisonJwa
2026-05-17 02:17:37 +08:00
parent 6e0bf21295
commit 2735c55c97
3 changed files with 65 additions and 1 deletions
@@ -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)")
}
+1 -1
View File
@@ -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
@@ -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 })
}
}