feat(audio,bridge,flutter): v1 audio + PTT lifecycle implementation (SDD-094..097)
Implement the SDD-094 / SDD-095 / SDD-096 / SDD-097 detailed designs
committed in dfa84ee.
Rust side
- chanora_audio::TransmitMode enum (Ptt/Continuous/VoiceActivity) with
serde-friendly u8 repr (SDD-095).
- chanora_audio::TransmitModeSelector: lock-free Atomic-backed selector
that is the sole writer of transmit_active (per SAD-083), applying
hard_mute as a final clamp. VoiceActivity falls through to Continuous
for v1 (DEC-030 placeholder).
- chanora_audio::ReleaseTailTimer: tokio-task-owning struct driving the
selector's ptt_held input; default 200 ms tail, configurable 0–500 ms
with AtomicU32 hot read; pending JoinHandle held in a std::sync::Mutex
touched only on PTT edge transitions (SDD-096).
- chanora_storage: AudioMeta persisted as audio_meta.json next to
identity.dek; get/set_transmit_mode + get/set_release_tail_ms with
0..=500 clamp on write.
- chanora_core::ChanoraSession: voice_join(channel, password) and
voice_leave() are the new lifecycle entry points; ensure_audio_running
and shutdown_audio_if_idle are private helpers around the existing
Option<AudioEngine> field. SessionEvent::VoiceState carries the
in_channel / transmit_mode / mute / release_tail_ms tuple. Selector
state survives reconnect; supervisor rewires it to each fresh engine
gate.
- chanora_bridge: drop start_audio; add voice_join, voice_leave,
set/get_transmit_mode, set/get_release_tail_ms, set_hard_mute.
BridgeEvent::VoiceState mirrors the core event. AudioStarted/Stopped
kept for backwards compat but Flutter ignores them in the new UI.
Flutter side
- New apps/chanora_flutter/lib/widgets/voice_bar.dart replaces the
legacy _AudioControls widget. Renders channel pill, mode badge,
mute toggle, level meter, PttCapabilityBadge, leave button. No
manual Start affordance anywhere.
- New apps/chanora_flutter/lib/widgets/voice_settings.dart dialog with
TransmitMode radio group (VoiceActivity disabled with 'Coming soon'
trailing label per DEC-030), bind-key button, release-tail slider
0–500 ms step 25.
- main.dart: state fields _inChannel, _transmitMode, _hardMute,
_releaseTailMs driven by BridgeEvent_VoiceState. Channel-tap now
calls voiceJoin instead of moveToChannel. Removed _onStartAudio,
_audioStarted-gated branch, and the FilledButton.
- l10n: 11 new strings in app_en.arb + app_zh.arb.
Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored
(chanora_audio: +12 new tests for TransmitMode/Selector/ReleaseTail;
chanora_storage: +2 new tests for audio_meta round-trip).
- flutter analyze: 0 errors, 0 warnings; 6 infos are the Flutter 3.32
Radio.groupValue deprecation (pre-existing API usage).
- FRB Dart/Rust bindings regenerated via flutter_rust_bridge_codegen.
Follow-up (intentionally deferred)
- PttController and per-platform PTT backends still drive AudioTransmitGate
directly via the legacy set_ptt path; routing those key edges through
ChanoraSession::release_tail_timer().{key_down,key_up} so the tail
applies to native PTT input is a contained wiring change in a follow-up.
- Real audio-level RMS in BridgeAudioStats (current meter is binary).
- VoiceActivity backend (DEC-030).
This commit is contained in:
@@ -55,7 +55,7 @@ extension BridgeEventPatterns on BridgeEvent {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -66,7 +66,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted() when audioStarted !=
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case _:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -84,7 +85,7 @@ return pttCapability(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,}){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
@@ -95,7 +96,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted():
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped():
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that);}
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
@@ -109,7 +111,7 @@ return pttCapability(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,}){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -120,7 +122,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted() when audioStarted !=
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case _:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -137,7 +140,7 @@ return pttCapability(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -147,7 +150,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted() when audioStar
|
||||
return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -165,7 +169,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,}) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs) voiceState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
return connected(_that.serverName);case BridgeEvent_Lost():
|
||||
@@ -175,7 +179,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted():
|
||||
return audioStarted();case BridgeEvent_AudioStopped():
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);}
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -189,7 +194,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,}) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -199,7 +204,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted() when audioStar
|
||||
return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -686,6 +692,83 @@ as String,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class BridgeEvent_VoiceState extends BridgeEvent {
|
||||
const BridgeEvent_VoiceState({required this.inChannel, required this.transmitMode, required this.mute, required this.releaseTailMs}): super._();
|
||||
|
||||
|
||||
/// True when the session is currently joined to a voice
|
||||
/// channel and the audio engine is running.
|
||||
final bool inChannel;
|
||||
/// Active transmit mode.
|
||||
final BridgeTransmitMode transmitMode;
|
||||
/// True when the hard-mute clamp is engaged.
|
||||
final bool mute;
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
final int releaseTailMs;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BridgeEvent_VoiceStateCopyWith<BridgeEvent_VoiceState> get copyWith => _$BridgeEvent_VoiceStateCopyWithImpl<BridgeEvent_VoiceState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_VoiceState&&(identical(other.inChannel, inChannel) || other.inChannel == inChannel)&&(identical(other.transmitMode, transmitMode) || other.transmitMode == transmitMode)&&(identical(other.mute, mute) || other.mute == mute)&&(identical(other.releaseTailMs, releaseTailMs) || other.releaseTailMs == releaseTailMs));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,inChannel,transmitMode,mute,releaseTailMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BridgeEvent.voiceState(inChannel: $inChannel, transmitMode: $transmitMode, mute: $mute, releaseTailMs: $releaseTailMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BridgeEvent_VoiceStateCopyWith<$Res> implements $BridgeEventCopyWith<$Res> {
|
||||
factory $BridgeEvent_VoiceStateCopyWith(BridgeEvent_VoiceState value, $Res Function(BridgeEvent_VoiceState) _then) = _$BridgeEvent_VoiceStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BridgeEvent_VoiceStateCopyWithImpl<$Res>
|
||||
implements $BridgeEvent_VoiceStateCopyWith<$Res> {
|
||||
_$BridgeEvent_VoiceStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BridgeEvent_VoiceState _self;
|
||||
final $Res Function(BridgeEvent_VoiceState) _then;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? inChannel = null,Object? transmitMode = null,Object? mute = null,Object? releaseTailMs = null,}) {
|
||||
return _then(BridgeEvent_VoiceState(
|
||||
inChannel: null == inChannel ? _self.inChannel : inChannel // ignore: cast_nullable_to_non_nullable
|
||||
as bool,transmitMode: null == transmitMode ? _self.transmitMode : transmitMode // ignore: cast_nullable_to_non_nullable
|
||||
as BridgeTransmitMode,mute: null == mute ? _self.mute : mute // ignore: cast_nullable_to_non_nullable
|
||||
as bool,releaseTailMs: null == releaseTailMs ? _self.releaseTailMs : releaseTailMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
Reference in New Issue
Block a user