// Voice settings dialog (SDD-097). Surfaces transmit mode, release // tail, and the full P1 audio processing configuration: // - Noise suppression (NS) // - Echo cancellation (AEC3) // - Automatic gain control (AGC2) // - High-pass filter (HPF) // - VAD backend // - platform audio-processing mode selection where available import 'package:flutter/material.dart'; import '../l10n/generated/app_localizations.dart'; import 'audio_device_list_tile.dart'; import 'audio_output_tile.dart'; import 'audio_processing_config_state.dart'; import 'ptt_capability_badge.dart'; import 'talk_power_warning.dart'; import 'voice_platform.dart'; import 'voice_settings_controls.dart'; import '../src/rust/api.dart' as rust; /// Result returned by [VoiceSettingsDialog]. class VoiceSettingsResult { const VoiceSettingsResult({ required this.mode, required this.releaseTailMs, required this.bindKeyRequested, required this.audioConfig, }); final rust.BridgeTransmitMode mode; final int releaseTailMs; final bool bindKeyRequested; final rust.BridgeAudioProcessingConfig audioConfig; } /// Voice + audio processing settings dialog. class VoiceSettingsDialog extends StatefulWidget { const VoiceSettingsDialog({ super.key, required this.initialMode, required this.initialReleaseTailMs, required this.initialAudioConfig, this.pttLevel = '', this.pttBackendId = '', this.pttBoundInputClass = '', this.talkPower, this.neededTalkPower, this.talkPowerGranted, }); final rust.BridgeTransmitMode initialMode; final int initialReleaseTailMs; final rust.BridgeAudioProcessingConfig initialAudioConfig; final String pttLevel; final String pttBackendId; final String pttBoundInputClass; final int? talkPower; final int? neededTalkPower; final bool? talkPowerGranted; @override State createState() => _VoiceSettingsDialogState(); } class _VoiceSettingsDialogState extends State { late rust.BridgeTransmitMode _mode; late double _releaseTail; late final AudioProcessingConfigState _audioProcessing; @override void initState() { super.initState(); _mode = widget.initialMode; _releaseTail = widget.initialReleaseTailMs.clamp(0, 500).toDouble(); _audioProcessing = AudioProcessingConfigState.fromConfig( widget.initialAudioConfig, ); } rust.BridgeAudioProcessingConfig _buildConfig() { return _audioProcessing.buildConfig( base: widget.initialAudioConfig, isAndroid: isAndroidHost, ); } @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); final theme = Theme.of(context); return AlertDialog( title: Text(l10n.voiceSettingsTitle), contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), content: SizedBox( width: 400, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Transmit mode ────────────────────────────────────── VoiceSectionHeader(l10n.voiceModeLabel), SegmentedButton( style: voiceSegmentedButtonStyle(theme), // DEC-030: hide the voice-activity segment on hosts // that ship no Chanora-owned VAD pipeline (iOS, // macOS, web). segments: transmitModeSegmentsFor( voiceActivityAvailable: voiceActivityTransmitAvailable, ), selected: {_mode}, onSelectionChanged: (s) => setState(() => _mode = s.first), ), // ── PTT options ──────────────────────────────────────── if (_mode == rust.BridgeTransmitMode.ptt) ...[ const Divider(height: 24), if (!isTouchOnlyPttHost) ...[ OutlinedButton.icon( icon: const Icon(Icons.keyboard), label: Text(l10n.voiceBindKeyAction), onPressed: () => Navigator.of(context).pop( VoiceSettingsResult( mode: _mode, releaseTailMs: _releaseTail.round(), bindKeyRequested: true, audioConfig: _buildConfig(), ), ), ), const SizedBox(height: 8), ], Text( l10n.voiceReleaseTailLabel, style: theme.textTheme.titleSmall, ), Row( children: [ Expanded( child: Slider( value: _releaseTail, min: 0, max: 500, divisions: 20, label: '${_releaseTail.round()}${l10n.voiceReleaseTailHint}', onChanged: (v) => setState(() => _releaseTail = v), ), ), SizedBox( width: 64, child: Text( '${_releaseTail.round()}${l10n.voiceReleaseTailHint}', style: theme.textTheme.bodySmall, textAlign: TextAlign.end, ), ), ], ), ], // ── Audio processing ─────────────────────────────────── const Divider(height: 24), const VoiceSectionHeader('Audio processing'), AudioProcessingPanel( config: _audioProcessing, update: (mutation) => setState(mutation), ), if (isTalkPowerBlocked( talkPower: widget.talkPower, neededTalkPower: widget.neededTalkPower, talkPowerGranted: widget.talkPowerGranted, )) ...[ const SizedBox(height: 8), TalkPowerWarning( talkPower: widget.talkPower, neededTalkPower: widget.neededTalkPower, talkPowerGranted: widget.talkPowerGranted, ), ], // ── PTT capability badge ──────────────────────────────── if (_mode == rust.BridgeTransmitMode.ptt && widget.pttLevel.isNotEmpty) ...[ const Divider(height: 24), const VoiceSectionHeader('PTT capability'), PttCapabilityBadge( level: widget.pttLevel, backendId: widget.pttBackendId, boundInputClass: widget.pttBoundInputClass, ), ], // ── Audio output route picker (mobile only) ───────────── if (isAndroidHost || isIosHost) ...[ const Divider(height: 24), const VoiceSectionHeader('Audio output'), const AudioOutputTile(), ], // ── Audio devices (desktop only, SRS-026) ────────────── if (!isAndroidHost && !isIosHost) ...[ const Divider(height: 24), const VoiceSectionHeader('Audio devices'), const AudioDeviceListTile( label: 'Input', kind: AudioDeviceKind.input, ), const AudioDeviceListTile( label: 'Output', kind: AudioDeviceKind.output, ), const SizedBox(height: 8), ], // ── Debug ────────────────────────────────────────────── const Divider(height: 24), const VoiceSectionHeader('Debug'), AudioProcessingToggleRow( label: 'WAV dump', subtitle: 'Record raw/processed mic to temp dir', value: _audioProcessing.debugWavDump, onChanged: (v) => setState(() => _audioProcessing.debugWavDump = v), ), const SizedBox(height: 8), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.closeAction), ), FilledButton( onPressed: () => Navigator.of(context).pop( VoiceSettingsResult( mode: _mode, releaseTailMs: _releaseTail.round(), bindKeyRequested: false, audioConfig: _buildConfig(), ), ), child: Text(l10n.pttConfigureSaveAction), ), ], ); } }