Add dart doc comments to poke_notification_service, poke_preferences_service, link_trust_service, voice_settings, snapshot_view covering public API.
271 lines
9.9 KiB
Dart
271 lines
9.9 KiB
Dart
// 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] when the user saves or
|
|
/// requests a key bind.
|
|
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;
|
|
}
|
|
|
|
/// Dialog for configuring voice transmission and audio processing settings.
|
|
///
|
|
/// Surfaces transmit mode selection (continuous, PTT, voice-activity),
|
|
/// PTT release-tail slider, key-bind request, and a full audio processing
|
|
/// panel covering noise suppression, echo cancellation, AGC, HPF, and VAD
|
|
/// backend selection.
|
|
///
|
|
/// On mobile hosts, the voice-activity segment is hidden when no
|
|
/// Chanora-owned VAD pipeline is available (iOS, macOS, web).
|
|
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,
|
|
this.onInputDeviceChanged,
|
|
this.onOutputDeviceChanged,
|
|
});
|
|
|
|
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;
|
|
final ValueChanged<String?>? onInputDeviceChanged;
|
|
final ValueChanged<String?>? onOutputDeviceChanged;
|
|
|
|
@override
|
|
State<VoiceSettingsDialog> createState() => _VoiceSettingsDialogState();
|
|
}
|
|
|
|
class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|
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<rust.BridgeTransmitMode>(
|
|
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'),
|
|
AudioDeviceListTile(
|
|
label: 'Input',
|
|
kind: AudioDeviceKind.input,
|
|
onDeviceChanged: widget.onInputDeviceChanged,
|
|
),
|
|
AudioDeviceListTile(
|
|
label: 'Output',
|
|
kind: AudioDeviceKind.output,
|
|
onDeviceChanged: widget.onOutputDeviceChanged,
|
|
),
|
|
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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|