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:
@@ -7,9 +7,6 @@
|
||||
// - VAD backend
|
||||
// - 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 '../l10n/generated/app_localizations.dart';
|
||||
@@ -22,26 +19,6 @@ import 'voice_platform.dart';
|
||||
import 'voice_settings_controls.dart';
|
||||
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].
|
||||
class VoiceSettingsResult {
|
||||
const VoiceSettingsResult({
|
||||
@@ -106,7 +83,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
rust.BridgeAudioProcessingConfig _buildConfig() {
|
||||
return _audioProcessing.buildConfig(
|
||||
base: widget.initialAudioConfig,
|
||||
isAndroid: _isAndroid,
|
||||
isAndroid: isAndroidHost,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,92 +166,10 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
const Divider(height: 24),
|
||||
const VoiceSectionHeader('Audio processing'),
|
||||
|
||||
// Android HW/SW selector
|
||||
if (_isAndroid) ...[
|
||||
const VoiceSubHeader('Processing backend'),
|
||||
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),
|
||||
),
|
||||
AudioProcessingPanel(
|
||||
config: _audioProcessing,
|
||||
update: (mutation) => setState(mutation),
|
||||
),
|
||||
|
||||
if (isTalkPowerBlocked(
|
||||
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 ────────────────────────────────
|
||||
if (_mode == rust.BridgeTransmitMode.ptt &&
|
||||
widget.pttLevel.isNotEmpty) ...[
|
||||
@@ -318,14 +197,14 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
],
|
||||
|
||||
// ── Audio output route picker (mobile only) ─────────────
|
||||
if (_isAndroid || _isIos) ...[
|
||||
if (isAndroidHost || isIosHost) ...[
|
||||
const Divider(height: 24),
|
||||
const VoiceSectionHeader('Audio output'),
|
||||
const AudioOutputTile(),
|
||||
],
|
||||
|
||||
// ── Audio devices (desktop only, SRS-026) ──────────────
|
||||
if (!_isAndroid && !_isIos) ...[
|
||||
if (!isAndroidHost && !isIosHost) ...[
|
||||
const Divider(height: 24),
|
||||
const VoiceSectionHeader('Audio devices'),
|
||||
const AudioDeviceListTile(
|
||||
|
||||
Reference in New Issue
Block a user