refactor(flutter): extract shared AudioProcessingPanel widget (TODO-013)
Deduplicate audio processing toggle UI from voice_compact.dart and voice_settings.dart into shared AudioProcessingPanel widget in voice_settings_controls.dart. Centralize platform detection getters in voice_platform.dart.
This commit is contained in:
@@ -26,21 +26,6 @@ import 'voice_settings_controls.dart';
|
|||||||
import 'voice_status_summary.dart';
|
import 'voice_status_summary.dart';
|
||||||
import '../src/rust/api.dart' as rust;
|
import '../src/rust/api.dart' as rust;
|
||||||
|
|
||||||
bool get _isIos {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isIOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isMacOS {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isMacOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isDesktopSileroVadHost {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isWindows || Platform.isLinux;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two-line status chip that summarises the current voice state.
|
/// Two-line status chip that summarises the current voice state.
|
||||||
/// Tap to open the voice details modal.
|
/// Tap to open the voice details modal.
|
||||||
class VoiceStatusChip extends StatelessWidget {
|
class VoiceStatusChip extends StatelessWidget {
|
||||||
@@ -737,130 +722,12 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
// Android HW/SW selector.
|
AudioProcessingPanel(
|
||||||
if (Platform.isAndroid) ...[
|
config: _audioProcessing,
|
||||||
const VoiceSubHeader('Processing backend'),
|
dense: true,
|
||||||
SegmentedButton<bool>(
|
update: (mutation) {
|
||||||
style: voiceSegmentedButtonStyle(theme),
|
setState(mutation);
|
||||||
segments: androidProcessingSegments,
|
|
||||||
selected: {_audioProcessing.preferHardware},
|
|
||||||
onSelectionChanged: (s) {
|
|
||||||
setState(() => _audioProcessing.preferHardware = s.first);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_audioProcessing.preferHardware
|
|
||||||
? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.'
|
|
||||||
: 'Software mode applies the full WebRTC APM stage set.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (_isIos) ...[
|
|
||||||
Text(
|
|
||||||
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
|
|
||||||
'hidden here; only settings that still affect the shipping '
|
|
||||||
'iOS path are shown.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
],
|
|
||||||
if (!_isIos &&
|
|
||||||
(!Platform.isAndroid ||
|
|
||||||
androidShowsNsControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
dense: true,
|
|
||||||
label: 'Noise suppression',
|
|
||||||
subtitle: 'Wiener filter',
|
|
||||||
value: _audioProcessing.nsEnabled,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() => _audioProcessing.nsEnabled = v);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!Platform.isAndroid ||
|
|
||||||
androidShowsAecControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
dense: true,
|
|
||||||
label: 'Echo cancellation',
|
|
||||||
subtitle: Platform.isAndroid
|
|
||||||
? (_audioProcessing.preferHardware
|
|
||||||
? 'Prefers device/OS effect; falls back to WebRTC AEC3'
|
|
||||||
: 'WebRTC AEC3 · adaptive filter')
|
|
||||||
: (_isMacOS
|
|
||||||
? 'Managed by platform VPIO'
|
|
||||||
: 'WebRTC AEC3 · adaptive filter'),
|
|
||||||
value: _isMacOS ? true : _audioProcessing.aecEnabled,
|
|
||||||
onChanged: _isMacOS
|
|
||||||
? null
|
|
||||||
: (v) {
|
|
||||||
setState(() => _audioProcessing.aecEnabled = v);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!Platform.isAndroid ||
|
|
||||||
androidShowsAgcControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
dense: true,
|
|
||||||
label: 'Auto gain control',
|
|
||||||
subtitle: 'AGC2 · -18 dBFS target',
|
|
||||||
value: _audioProcessing.agcEnabled,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() => _audioProcessing.agcEnabled = v);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!Platform.isAndroid || androidShowsHpfControl(_audioProcessing))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
dense: true,
|
|
||||||
label: 'High-pass filter',
|
|
||||||
subtitle: '80 Hz · DC removal',
|
|
||||||
value: _audioProcessing.hpfEnabled,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() => _audioProcessing.hpfEnabled = v);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!Platform.isAndroid ||
|
|
||||||
androidShowsLimiterControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
dense: true,
|
|
||||||
label: 'Peak limiter',
|
|
||||||
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
|
|
||||||
value: _audioProcessing.limiterEnabled,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() => _audioProcessing.limiterEnabled = v);
|
|
||||||
_notifyAudioConfig();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// VAD backend.
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Voice activity detection (VAD)',
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
SegmentedButton<rust.BridgeVadBackend>(
|
|
||||||
style: voiceSegmentedButtonStyle(theme),
|
|
||||||
segments: _isDesktopSileroVadHost
|
|
||||||
? desktopVadBackendSegments
|
|
||||||
: vadBackendSegments,
|
|
||||||
selected: {_audioProcessing.vadBackend},
|
|
||||||
onSelectionChanged: (s) {
|
|
||||||
setState(() => _audioProcessing.vadBackend = s.first);
|
|
||||||
_notifyAudioConfig();
|
_notifyAudioConfig();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ import 'dart:io' show Platform;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
/// True when the host is a touch-only mobile platform without a
|
// TODO(refactor): Scattered Platform.isX checks exist across ~6 Dart files.
|
||||||
/// hardware keyboard the user would bind a PTT key on.
|
// Centralize all platform checks here and update call sites to use these
|
||||||
bool get isTouchOnlyPttHost {
|
// getters instead of raw Platform.isAndroid/isIOS/etc.
|
||||||
if (kIsWeb) return false;
|
bool get _notWeb => !kIsWeb;
|
||||||
return Platform.isIOS || Platform.isAndroid;
|
|
||||||
}
|
bool get isTouchOnlyPttHost => _notWeb && (Platform.isIOS || Platform.isAndroid);
|
||||||
|
|
||||||
|
bool get isAndroidHost => _notWeb && Platform.isAndroid;
|
||||||
|
|
||||||
|
bool get isIosHost => _notWeb && Platform.isIOS;
|
||||||
|
|
||||||
|
bool get isMacOsHost => _notWeb && Platform.isMacOS;
|
||||||
|
|
||||||
|
bool get isDesktopSileroVadHost => _notWeb && (Platform.isWindows || Platform.isLinux);
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
// - VAD backend
|
// - VAD backend
|
||||||
// - platform audio-processing mode selection where available
|
// - platform audio-processing mode selection where available
|
||||||
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../l10n/generated/app_localizations.dart';
|
import '../l10n/generated/app_localizations.dart';
|
||||||
@@ -22,26 +19,6 @@ import 'voice_platform.dart';
|
|||||||
import 'voice_settings_controls.dart';
|
import 'voice_settings_controls.dart';
|
||||||
import '../src/rust/api.dart' as rust;
|
import '../src/rust/api.dart' as rust;
|
||||||
|
|
||||||
bool get _isAndroid {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isAndroid;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isIos {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isIOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isMacOS {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isMacOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isDesktopSileroVadHost {
|
|
||||||
if (kIsWeb) return false;
|
|
||||||
return Platform.isWindows || Platform.isLinux;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result returned by [VoiceSettingsDialog].
|
/// Result returned by [VoiceSettingsDialog].
|
||||||
class VoiceSettingsResult {
|
class VoiceSettingsResult {
|
||||||
const VoiceSettingsResult({
|
const VoiceSettingsResult({
|
||||||
@@ -106,7 +83,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|||||||
rust.BridgeAudioProcessingConfig _buildConfig() {
|
rust.BridgeAudioProcessingConfig _buildConfig() {
|
||||||
return _audioProcessing.buildConfig(
|
return _audioProcessing.buildConfig(
|
||||||
base: widget.initialAudioConfig,
|
base: widget.initialAudioConfig,
|
||||||
isAndroid: _isAndroid,
|
isAndroid: isAndroidHost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,92 +166,10 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
const VoiceSectionHeader('Audio processing'),
|
const VoiceSectionHeader('Audio processing'),
|
||||||
|
|
||||||
// Android HW/SW selector
|
AudioProcessingPanel(
|
||||||
if (_isAndroid) ...[
|
config: _audioProcessing,
|
||||||
const VoiceSubHeader('Processing backend'),
|
update: (mutation) => setState(mutation),
|
||||||
SegmentedButton<bool>(
|
),
|
||||||
style: voiceSegmentedButtonStyle(theme),
|
|
||||||
segments: androidProcessingSegments,
|
|
||||||
selected: {_audioProcessing.preferHardware},
|
|
||||||
onSelectionChanged: (s) =>
|
|
||||||
setState(() => _audioProcessing.preferHardware = s.first),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_audioProcessing.preferHardware
|
|
||||||
? 'Android hardware mode still falls back to WebRTC APM per stage when device effects are missing, so these controls remain available.'
|
|
||||||
: 'Android software mode applies the full WebRTC APM control set.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
|
|
||||||
// DSP toggles
|
|
||||||
const VoiceSubHeader('DSP stages'),
|
|
||||||
if (_isIos) ...[
|
|
||||||
Text(
|
|
||||||
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
|
|
||||||
'hidden here; only settings that still affect the shipping '
|
|
||||||
'iOS path are shown.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
if (!_isIos &&
|
|
||||||
(!_isAndroid || androidShowsNsControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
label: 'Noise suppression (NS)',
|
|
||||||
subtitle: 'Wiener filter · stationary noise',
|
|
||||||
value: _audioProcessing.nsEnabled,
|
|
||||||
onChanged: (v) =>
|
|
||||||
setState(() => _audioProcessing.nsEnabled = v),
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!_isAndroid || androidShowsAecControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
label: 'Echo cancellation (AEC3)',
|
|
||||||
subtitle: _isAndroid
|
|
||||||
? (_audioProcessing.preferHardware
|
|
||||||
? 'Prefers device/OS effect; WebRTC AEC3 fallback when binding is unavailable'
|
|
||||||
: 'WebRTC AEC3 · adaptive filter')
|
|
||||||
: (_isMacOS
|
|
||||||
? 'Managed by platform VPIO'
|
|
||||||
: 'WebRTC AEC3 · adaptive filter'),
|
|
||||||
value: _isMacOS ? true : _audioProcessing.aecEnabled,
|
|
||||||
onChanged: _isMacOS
|
|
||||||
? null
|
|
||||||
: (v) => setState(() => _audioProcessing.aecEnabled = v),
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!_isAndroid || androidShowsAgcControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
label: 'Auto gain control (AGC2)',
|
|
||||||
subtitle: 'RNN VAD-gated · -18 dBFS target',
|
|
||||||
value: _audioProcessing.agcEnabled,
|
|
||||||
onChanged: (v) =>
|
|
||||||
setState(() => _audioProcessing.agcEnabled = v),
|
|
||||||
),
|
|
||||||
if (!_isAndroid || androidShowsHpfControl(_audioProcessing))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
label: 'High-pass filter (HPF)',
|
|
||||||
subtitle: '80 Hz Butterworth · DC removal',
|
|
||||||
value: _audioProcessing.hpfEnabled,
|
|
||||||
onChanged: (v) =>
|
|
||||||
setState(() => _audioProcessing.hpfEnabled = v),
|
|
||||||
),
|
|
||||||
if (!_isIos &&
|
|
||||||
(!_isAndroid || androidShowsLimiterControl(_audioProcessing)))
|
|
||||||
AudioProcessingToggleRow(
|
|
||||||
label: 'Peak limiter',
|
|
||||||
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
|
|
||||||
value: _audioProcessing.limiterEnabled,
|
|
||||||
onChanged: (v) =>
|
|
||||||
setState(() => _audioProcessing.limiterEnabled = v),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (isTalkPowerBlocked(
|
if (isTalkPowerBlocked(
|
||||||
talkPower: widget.talkPower,
|
talkPower: widget.talkPower,
|
||||||
@@ -289,22 +184,6 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── VAD ────────────────────────────────────────────────
|
|
||||||
const Divider(height: 24),
|
|
||||||
const VoiceSectionHeader('Voice activity detection (VAD)'),
|
|
||||||
|
|
||||||
const VoiceSubHeader('Backend'),
|
|
||||||
SegmentedButton<rust.BridgeVadBackend>(
|
|
||||||
style: voiceSegmentedButtonStyle(theme),
|
|
||||||
segments: _isDesktopSileroVadHost
|
|
||||||
? desktopVadBackendSegments
|
|
||||||
: vadBackendSegments,
|
|
||||||
selected: {_audioProcessing.vadBackend},
|
|
||||||
onSelectionChanged: (s) =>
|
|
||||||
setState(() => _audioProcessing.vadBackend = s.first),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// ── PTT capability badge ────────────────────────────────
|
// ── PTT capability badge ────────────────────────────────
|
||||||
if (_mode == rust.BridgeTransmitMode.ptt &&
|
if (_mode == rust.BridgeTransmitMode.ptt &&
|
||||||
widget.pttLevel.isNotEmpty) ...[
|
widget.pttLevel.isNotEmpty) ...[
|
||||||
@@ -318,14 +197,14 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// ── Audio output route picker (mobile only) ─────────────
|
// ── Audio output route picker (mobile only) ─────────────
|
||||||
if (_isAndroid || _isIos) ...[
|
if (isAndroidHost || isIosHost) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
const VoiceSectionHeader('Audio output'),
|
const VoiceSectionHeader('Audio output'),
|
||||||
const AudioOutputTile(),
|
const AudioOutputTile(),
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── Audio devices (desktop only, SRS-026) ──────────────
|
// ── Audio devices (desktop only, SRS-026) ──────────────
|
||||||
if (!_isAndroid && !_isIos) ...[
|
if (!isAndroidHost && !isIosHost) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
const VoiceSectionHeader('Audio devices'),
|
const VoiceSectionHeader('Audio devices'),
|
||||||
const AudioDeviceListTile(
|
const AudioDeviceListTile(
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'dart:io' show Platform;
|
|||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'audio_processing_config_state.dart';
|
||||||
|
import 'voice_platform.dart';
|
||||||
import '../src/rust/api.dart' as rust;
|
import '../src/rust/api.dart' as rust;
|
||||||
|
|
||||||
/// Shared compact style for voice settings segmented buttons.
|
/// Shared compact style for voice settings segmented buttons.
|
||||||
@@ -230,3 +232,173 @@ class AudioProcessingToggleRow extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef AudioFieldUpdater = void Function(VoidCallback mutation);
|
||||||
|
|
||||||
|
class AudioProcessingPanel extends StatelessWidget {
|
||||||
|
const AudioProcessingPanel({
|
||||||
|
super.key,
|
||||||
|
required this.config,
|
||||||
|
required this.update,
|
||||||
|
this.dense = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AudioProcessingConfigState config;
|
||||||
|
final AudioFieldUpdater update;
|
||||||
|
final bool dense;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isAndroid = isAndroidHost;
|
||||||
|
final isIos = isIosHost;
|
||||||
|
final isMacOS = isMacOsHost;
|
||||||
|
final isDesktopSilero = isDesktopSileroVadHost;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (isAndroid) ...[
|
||||||
|
if (!dense) const VoiceSubHeader('Processing backend'),
|
||||||
|
if (dense) _compactLabel(context, 'Processing backend'),
|
||||||
|
SegmentedButton<bool>(
|
||||||
|
style: voiceSegmentedButtonStyle(theme),
|
||||||
|
segments: androidProcessingSegments,
|
||||||
|
selected: {config.preferHardware},
|
||||||
|
onSelectionChanged: (s) {
|
||||||
|
update(() => config.preferHardware = s.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
config.preferHardware
|
||||||
|
? (dense
|
||||||
|
? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.'
|
||||||
|
: 'Android hardware mode still falls back to WebRTC APM per stage when device effects are missing, so these controls remain available.')
|
||||||
|
: (dense
|
||||||
|
? 'Software mode applies the full WebRTC APM stage set.'
|
||||||
|
: 'Android software mode applies the full WebRTC APM control set.'),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!dense) const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (!dense) const VoiceSubHeader('DSP stages'),
|
||||||
|
|
||||||
|
if (isIos) ...[
|
||||||
|
Text(
|
||||||
|
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
|
||||||
|
'hidden here; only settings that still affect the shipping '
|
||||||
|
'iOS path are shown.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (!isIos &&
|
||||||
|
(!isAndroid || androidShowsNsControl(config)))
|
||||||
|
AudioProcessingToggleRow(
|
||||||
|
dense: dense,
|
||||||
|
label: dense ? 'Noise suppression' : 'Noise suppression (NS)',
|
||||||
|
subtitle: dense
|
||||||
|
? 'Wiener filter'
|
||||||
|
: 'Wiener filter · stationary noise',
|
||||||
|
value: config.nsEnabled,
|
||||||
|
onChanged: (v) => update(() => config.nsEnabled = v),
|
||||||
|
),
|
||||||
|
if (!isIos &&
|
||||||
|
(!isAndroid || androidShowsAecControl(config)))
|
||||||
|
AudioProcessingToggleRow(
|
||||||
|
dense: dense,
|
||||||
|
label: dense ? 'Echo cancellation' : 'Echo cancellation (AEC3)',
|
||||||
|
subtitle: _aecSubtitle(isAndroid, isMacOS, config),
|
||||||
|
value: isMacOS ? true : config.aecEnabled,
|
||||||
|
onChanged:
|
||||||
|
isMacOS ? null : (v) => update(() => config.aecEnabled = v),
|
||||||
|
),
|
||||||
|
if (!isIos &&
|
||||||
|
(!isAndroid || androidShowsAgcControl(config)))
|
||||||
|
AudioProcessingToggleRow(
|
||||||
|
dense: dense,
|
||||||
|
label: dense ? 'Auto gain control' : 'Auto gain control (AGC2)',
|
||||||
|
subtitle: dense
|
||||||
|
? 'AGC2 · -18 dBFS target'
|
||||||
|
: 'RNN VAD-gated · -18 dBFS target',
|
||||||
|
value: config.agcEnabled,
|
||||||
|
onChanged: (v) => update(() => config.agcEnabled = v),
|
||||||
|
),
|
||||||
|
if (!isAndroid || androidShowsHpfControl(config))
|
||||||
|
AudioProcessingToggleRow(
|
||||||
|
dense: dense,
|
||||||
|
label: dense ? 'High-pass filter' : 'High-pass filter (HPF)',
|
||||||
|
subtitle: dense
|
||||||
|
? '80 Hz · DC removal'
|
||||||
|
: '80 Hz Butterworth · DC removal',
|
||||||
|
value: config.hpfEnabled,
|
||||||
|
onChanged: (v) => update(() => config.hpfEnabled = v),
|
||||||
|
),
|
||||||
|
if (!isIos &&
|
||||||
|
(!isAndroid || androidShowsLimiterControl(config)))
|
||||||
|
AudioProcessingToggleRow(
|
||||||
|
dense: dense,
|
||||||
|
label: 'Peak limiter',
|
||||||
|
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
|
||||||
|
value: config.limiterEnabled,
|
||||||
|
onChanged: (v) => update(() => config.limiterEnabled = v),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!dense) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
const VoiceSectionHeader('Voice activity detection (VAD)'),
|
||||||
|
const VoiceSubHeader('Backend'),
|
||||||
|
] else ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Voice activity detection (VAD)',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
],
|
||||||
|
SegmentedButton<rust.BridgeVadBackend>(
|
||||||
|
style: voiceSegmentedButtonStyle(theme),
|
||||||
|
segments:
|
||||||
|
isDesktopSilero ? desktopVadBackendSegments : vadBackendSegments,
|
||||||
|
selected: {config.vadBackend},
|
||||||
|
onSelectionChanged: (s) {
|
||||||
|
update(() => config.vadBackend = s.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!dense) const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _aecSubtitle(bool isAndroid, bool isMacOS, AudioProcessingConfigState c) {
|
||||||
|
if (isAndroid) {
|
||||||
|
return c.preferHardware
|
||||||
|
? 'Prefers device/OS effect; falls back to WebRTC AEC3'
|
||||||
|
: 'WebRTC AEC3 · adaptive filter';
|
||||||
|
}
|
||||||
|
if (isMacOS) return 'Managed by platform VPIO';
|
||||||
|
return 'WebRTC AEC3 · adaptive filter';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _compactLabel(BuildContext context, String text) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user