User confirmed at +72 the symptom is 'voice + constant clicks +
choppy fragments'. The diagnostic data conclusively pointed to
iOS VPIO render-callback timing as the cause:
* frames_changes=60+ per 100 callbacks at cb>=1800
iOS keeps switching num_frames between 960 and 1104
on roughly 60% of callbacks
* peak_out_i16 is sensible (2500-16870, never clipping)
when AudioHandler returns content
* input_was_zero=true on most callbacks during active speech
AudioHandler keeps entering buffering_samples state
The cause: previous render callback called fill_buffer
synchronously every iOS audio thread invocation. With iOS
calling at irregular rates with irregular sizes, AudioHandler's
jitter buffer (sized around 20 ms Opus frames) cannot satisfy
arbitrary-sized requests and falls back to returning silence
(&[] empty slice) on misaligned reads. The silent gaps in the
middle of the output buffer create discontinuities = audible
clicks; the missing-tail content produces choppy fragments.
Fix (architectural): decouple the AudioHandler decoder from the
VPIO render callback via a lock-free SPSC ring buffer.
Producer (tokio task, 50 Hz):
every 20 ms:
fill_buffer(scratch_stereo_f32, 1920 = 20 ms stereo)
downmix L+R -> mono i16 (960 samples)
ring_buffer.push_slice(mono_i16)
Consumer (VPIO render callback, iOS audio thread):
every callback:
pop num_frames samples from ring buffer into out
zero-fill tail on underrun
Why it works:
* Producer always asks AudioHandler for a stable 20 ms chunk
(perfectly aligned with internal Opus frame size). No more
buffering_samples false-triggers.
* Consumer pulls whatever iOS asks for whenever iOS schedules
it; ring buffer's 200 ms depth absorbs the callback jitter.
* This is the standard pattern every production VoIP audio
engine uses (WebRTC, Discord, FaceTime) to bridge bursty
Opus decoders to bursty platform audio callbacks.
Implementation:
* New dep: rtrb 0.3.4 (RustAudio realtime-safe SPSC ring
buffer, 6.8M downloads, lock-free push/pop with no
allocation on the audio thread).
* RING_BUFFER_SAMPLES = 9600 (200 ms mono i16 at 48 kHz).
Sized for 10x producer ticks of headroom.
* PRODUCER_TICK_MS = 20 (matches Opus 50 Hz packet rate).
set_missed_tick_behavior(Skip) to avoid burst catch-up on
runtime stalls.
* Producer task spawned in IosVoiceUnit::start, shutdown
via tokio::oneshot when IosVoiceUnit drops.
* Render callback is now just: pop into out, zero-fill tail,
apply mute then gain.
* Gain applied CONSUMER-side so user volume changes take
effect within one callback (<= 200 ms latency).
* Underrun diagnostics: count underrun callbacks + total
zero-filled samples, log every 100 callbacks.
Threading + safety:
* rtrb is lock-free SPSC. Audio thread never blocks.
* Producer can block briefly on Arc<Mutex<AudioHandler>>
contention with the inbound forwarder (handle_packet), but
not with the audio thread.
* Producer task is owned by tokio runtime; explicit shutdown
channel ensures it exits when the engine stops.
Build verify:
* Linux host: cargo check clean in 4.06s (downloads rtrb 0.3.4).
* iOS Mac: cargo check clean in 2.14s.
Build counter 72 -> 73.
138 lines
6.0 KiB
YAML
138 lines
6.0 KiB
YAML
name: chanora_flutter
|
|
description: "Chanora — cross-platform voice client for TeamSpeak-compatible servers."
|
|
# The following line prevents the package from being accidentally published to
|
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
|
|
# The following defines the version and build number for your application.
|
|
# A version number is three numbers separated by dots, like 1.2.43
|
|
# followed by an optional build number separated by a +.
|
|
# Both the version and the builder number may be overridden in flutter
|
|
# build by specifying --build-name and --build-number, respectively.
|
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
# Read more about iOS versioning at
|
|
# 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+73
|
|
|
|
environment:
|
|
sdk: ^3.11.5
|
|
|
|
# Dependencies specify other packages that your package needs in order to work.
|
|
# To automatically upgrade your package dependencies to the latest versions
|
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
# dependencies can be manually updated by changing the version numbers below to
|
|
# the latest version available on pub.dev. To see which dependencies have newer
|
|
# versions available, run `flutter pub outdated`.
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
flutter_localizations:
|
|
sdk: flutter
|
|
intl: any
|
|
|
|
# The following adds the Cupertino Icons font to your application.
|
|
# Use with the CupertinoIcons class for iOS style icons.
|
|
cupertino_icons: ^1.0.8
|
|
flutter_rust_bridge: 2.12.0
|
|
freezed_annotation: ^3.1.0
|
|
connectivity_plus: ^6.1.0
|
|
path_provider: ^2.1.4
|
|
# audio_session 0.2.3 (MIT, Ryan Heise, 865k downloads) gives us
|
|
# programmatic access to AVAudioSession on iOS + AudioManager on
|
|
# Android. We use:
|
|
#
|
|
# * AVAudioSession.availableInputs \u2014 enumerate real input ports
|
|
# (Built-in Mic, Bluetooth HFP, wired headset, USB).
|
|
# * AVAudioSession.currentRoute \u2014 inputs + outputs of the
|
|
# active route.
|
|
# * AVAudioSession.setPreferredInput(port) \u2014 switch input
|
|
# (also drives matching output for HFP devices).
|
|
# * AVAudioSession.overrideOutputAudioPort(speaker|none) \u2014
|
|
# toggle the built-in receiver/earpiece vs speakerphone.
|
|
# * AVAudioSession.routeChangeStream \u2014 live notifications when
|
|
# the user plugs / unplugs / connects a device.
|
|
#
|
|
# This is the right primitive for a VoIP-style 'pick speaker /
|
|
# receiver / AirPods / wired' picker. Discord, WhatsApp, FaceTime
|
|
# all do exactly this. We previously tried audio_router 1.1.1
|
|
# whose iOS path is AVRoutePickerView (the AirPlay button) \u2014 the
|
|
# wrong UI: only lists AirPlay output destinations, not the
|
|
# speaker/receiver/Bluetooth choices we actually want.
|
|
audio_session: ^0.2.3
|
|
# package_info_plus 8.x (Flutter Community Plus, BSD-3, ~3M
|
|
# downloads) exposes the platform-canonical app version + build
|
|
# number at runtime so the About dialog (and any future
|
|
# diagnostic export) can display "v1.0.0-rc.8+60" sourced from
|
|
# the SAME pubspec.yaml field that drives CFBundleShortVersionString
|
|
# + CFBundleVersion on iOS and versionName + versionCode on
|
|
# Android. Single source of truth means the version on the
|
|
# iPhone screen always matches the commit that built it; no
|
|
# more "am I testing the right build?" question during the
|
|
# iOS audio test cycle.
|
|
package_info_plus: ^8.0.0
|
|
|
|
dev_dependencies:
|
|
flutter_test:
|
|
sdk: flutter
|
|
|
|
# The "flutter_lints" package below contains a set of recommended lints to
|
|
# encourage good coding practices. The lint set provided by the package is
|
|
# activated in the `analysis_options.yaml` file located at the root of your
|
|
# package. See that file for information about deactivating specific lint
|
|
# rules and activating additional ones.
|
|
flutter_lints: ^6.0.0
|
|
freezed: ^3.2.5
|
|
build_runner: ^2.15.0
|
|
|
|
# For information on the generic Dart part of this file, see the
|
|
# following page: https://dart.dev/tools/pub/pubspec
|
|
|
|
# The following section is specific to Flutter packages.
|
|
flutter:
|
|
|
|
# Generate localization bindings from lib/l10n/*.arb at build time.
|
|
# Per DEC-015 (register v0.9.5) Chanora ships English + Chinese
|
|
# Simplified at MVP with an i18n-ready architecture for additional
|
|
# languages.
|
|
generate: true
|
|
|
|
# The following line ensures that the Material Icons font is
|
|
# included with your application, so that you can use the icons in
|
|
# the material Icons class.
|
|
uses-material-design: true
|
|
|
|
# To add assets to your application, add an assets section, like this:
|
|
# assets:
|
|
# - images/a_dot_burr.jpeg
|
|
# - images/a_dot_ham.jpeg
|
|
|
|
# An image asset can refer to one or more resolution-specific "variants", see
|
|
# https://flutter.dev/to/resolution-aware-images
|
|
|
|
# For details regarding adding assets from package dependencies, see
|
|
# https://flutter.dev/to/asset-from-package
|
|
|
|
# To add custom fonts to your application, add a fonts section here,
|
|
# in this "flutter" section. Each entry in this list should have a
|
|
# "family" key with the font family name, and a "fonts" key with a
|
|
# list giving the asset and other descriptors for the font. For
|
|
# example:
|
|
# fonts:
|
|
# - family: Schyler
|
|
# fonts:
|
|
# - asset: fonts/Schyler-Regular.ttf
|
|
# - asset: fonts/Schyler-Italic.ttf
|
|
# style: italic
|
|
# - family: Trajan Pro
|
|
# fonts:
|
|
# - asset: fonts/TrajanPro.ttf
|
|
# - asset: fonts/TrajanPro_Bold.ttf
|
|
# weight: 700
|
|
#
|
|
# For details regarding fonts from package dependencies,
|
|
# see https://flutter.dev/to/font-from-package
|