Skip to content

Commit 43a1313

Browse files
committed
refactor(runtime)!: params-based create_audio_callback; simplify sink to Arc<dyn OutputSink>
BREAKING CHANGE: create_audio_callback now takes AudioCallbackParams. Sink type is Arc<dyn OutputSink> (was Arc<Box<dyn OutputSink>>).
1 parent 06382b2 commit 43a1313

7 files changed

Lines changed: 95 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5151

5252
[0.1.0]: https://github.com/hate/keyless/releases/tag/v0.1.0
5353

54+
## [0.2.0] - 2025-11-04
55+
56+
### Breaking
57+
- keyless-runtime: `create_audio_callback` now takes `AudioCallbackParams` instead of many args.
58+
- keyless-runtime: sink type simplified to `Arc<dyn OutputSink>`.
59+
60+
[0.2.0]: https://github.com/hate/keyless/releases/tag/v0.2.0
61+

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ members = [
1212
resolver = "3"
1313

1414
[workspace.package]
15-
version = "0.1.0"
15+
version = "0.2.0"
1616
edition = "2024"
1717
license = "MIT"
1818
repository = "https://github.com/hate/keyless"

keyless-runtime/src/pipeline/callback.rs

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,52 @@ use super::events::process_transcription_events;
1313
use crate::ptt::handlers::{handle_ptt_pressed, handle_ptt_released};
1414
use crate::workers;
1515

16+
/// Type alias for the audio callback closure.
17+
pub type AudioCallback = Box<dyn FnMut(&[f32]) + Send>;
18+
19+
/// Grouped transmit channels for the audio callback.
20+
pub struct CallbackTx {
21+
/// RMS audio level → TUI gauge (0..100).
22+
pub tx_level: mpsc::SyncSender<u16>,
23+
/// Log lines → TUI status panel.
24+
pub tx_log: mpsc::SyncSender<String>,
25+
/// Spectrum bars (EQ) → TUI visualizer.
26+
pub tx_spec: mpsc::SyncSender<Vec<u16>>,
27+
/// Live preview text → TUI preview area.
28+
pub tx_preview: mpsc::SyncSender<String>,
29+
/// VAD open/closed → TUI indicator.
30+
pub tx_vad: mpsc::SyncSender<bool>,
31+
}
32+
33+
/// Parameters required to construct the audio callback.
34+
pub struct AudioCallbackParams {
35+
/// EQ configuration for spectrum computation.
36+
pub eq_cfg: EqConfig,
37+
/// Voice Activity Detection thresholds.
38+
pub vad: VadThresholds,
39+
/// Whisper worker/transcriber handle.
40+
pub transcriber: workers::whisper::WhisperHandle,
41+
/// Output destination for final transcriptions.
42+
pub sink: Arc<dyn OutputSink>,
43+
/// Sound effects player for PTT/VAD cues.
44+
pub sfx: Arc<SfxPlayer>,
45+
/// Shared PTT hold flag (hotkey state).
46+
pub hold_flag: Arc<AtomicBool>,
47+
/// Grouped outbound channels to the TUI.
48+
pub tx: CallbackTx,
49+
}
50+
1651
/// Create the audio frame callback with PTT state machine.
17-
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
18-
pub fn create_audio_callback(
19-
eq_cfg: EqConfig,
20-
vad_config: &VadThresholds,
21-
transcriber: workers::whisper::WhisperHandle,
22-
sink: Arc<Box<dyn OutputSink>>,
23-
sfx: Arc<SfxPlayer>,
24-
hold_flag: Arc<AtomicBool>,
25-
tx_level: mpsc::SyncSender<u16>,
26-
tx_log: mpsc::SyncSender<String>,
27-
tx_spec: mpsc::SyncSender<Vec<u16>>,
28-
tx_preview: mpsc::SyncSender<String>,
29-
tx_vad: mpsc::SyncSender<bool>,
30-
) -> Box<dyn FnMut(&[f32]) + Send> {
52+
pub fn create_audio_callback(params: AudioCallbackParams) -> AudioCallback {
53+
let AudioCallbackParams {
54+
eq_cfg,
55+
vad,
56+
transcriber,
57+
sink,
58+
sfx,
59+
hold_flag,
60+
tx,
61+
} = params;
3162
// Clone inner transcriber handle (needed for closure capture; Arc is cheap).
3263
let trans_clone = transcriber.inner_clone();
3364

@@ -37,10 +68,10 @@ pub fn create_audio_callback(
3768
let mut held_prev = false;
3869
// VAD gate: hysteresis-based voice activity detection (prevents flickering).
3970
let mut vad = VadGate::new(
40-
vad_config.start_db,
41-
vad_config.stop_db,
42-
vad_config.min_duration_ms,
43-
vad_config.max_silence_ms,
71+
vad.start_db,
72+
vad.stop_db,
73+
vad.min_duration_ms,
74+
vad.max_silence_ms,
4475
100.0, // Sample rate in Hz (used for timing calculations).
4576
);
4677

@@ -53,7 +84,7 @@ pub fn create_audio_callback(
5384
// RMS = sqrt(sum(samples²) / count); normalized to [0, 100] for gauge display.
5485
let rms = (frame.iter().map(|s| s * s).sum::<f32>() / frame.len() as f32).sqrt();
5586
let level = (rms * 100.0).clamp(0.0, 100.0) as u16;
56-
let _ = tx_level.try_send(level);
87+
let _ = tx.tx_level.try_send(level);
5788
// Convert to dB for VAD (20 * log10(rms)); max(1e-8) prevents log(0) = -inf.
5889
let level_db = 20.0 * (rms.max(1e-8)).log10();
5990

@@ -73,17 +104,17 @@ pub fn create_audio_callback(
73104
// Hotkey is released (PTT not held).
74105
if held_prev {
75106
// JUST released (transition): finalize the segment (flush transcriber, emit Final).
76-
handle_ptt_released(&trans_clone, &sink, &tx_log, &tx_preview, &sfx);
107+
handle_ptt_released(&trans_clone, &sink, &tx.tx_log, &tx.tx_preview, &sfx);
77108
sfx.play_release();
78109
held_prev = false;
79110
}
80111
// CRITICAL: Always drain events even when not held, to catch asynchronous Final events
81112
// from the inference thread (they arrive later, after hotkey is released).
82-
process_transcription_events(&trans_clone, &sink, &tx_log, &tx_preview, &sfx);
113+
process_transcription_events(&trans_clone, &sink, &tx.tx_log, &tx.tx_preview, &sfx);
83114

84115
// Show flat/empty visualizer when idle (only send if state changed to avoid spam).
85116
if !last_spec_was_zero {
86-
let _ = tx_spec.try_send(zero_spectrum.clone());
117+
let _ = tx.tx_spec.try_send(zero_spectrum.clone());
87118
last_spec_was_zero = true;
88119
}
89120
return;
@@ -92,7 +123,7 @@ pub fn create_audio_callback(
92123
// Hotkey is held (PTT active).
93124
if !held_prev {
94125
// JUST pressed (transition): start a new dictation session.
95-
handle_ptt_pressed(&tx_log, &tx_preview);
126+
handle_ptt_pressed(&tx.tx_log, &tx.tx_preview);
96127
sfx.play_press();
97128
// Reset VAD timers (don't carry over from previous session; fresh start).
98129
vad.reset();
@@ -119,19 +150,19 @@ pub fn create_audio_callback(
119150
level_db = %level_db,
120151
"VAD gate state changed"
121152
);
122-
let _ = tx_log.try_send(format!(
153+
let _ = tx.tx_log.try_send(format!(
123154
"VAD {} at {:.1} dB",
124155
if gate_open { "OPEN" } else { "CLOSED" },
125156
level_db
126157
));
127-
let _ = tx_vad.try_send(gate_open);
158+
let _ = tx.tx_vad.try_send(gate_open);
128159
}
129160

130161
// Update EQ visualizer and transcription based on VAD state.
131162
if open_now {
132163
// Compute bars only when VAD is open (reduces CPU when gate is closed; skip FFT).
133164
let out = compute_bars(frame, &eq_cfg, &mut eq_state);
134-
let _ = tx_spec.try_send(out);
165+
let _ = tx.tx_spec.try_send(out);
135166
last_spec_was_zero = false;
136167

137168
// Push audio to transcriber (non-blocking lock; ignore if poisoned).
@@ -140,14 +171,14 @@ pub fn create_audio_callback(
140171
&& let Err(e) = t.push_audio(frame)
141172
{
142173
error!(error = %e, "failed to push audio to transcriber (backpressure)");
143-
let _ = tx_log.try_send(format!("push_audio error: {}", e));
174+
let _ = tx.tx_log.try_send(format!("push_audio error: {}", e));
144175
}
145176
// Drain Partial events (show live preview while speaking).
146-
process_transcription_events(&trans_clone, &sink, &tx_log, &tx_preview, &sfx);
177+
process_transcription_events(&trans_clone, &sink, &tx.tx_log, &tx.tx_preview, &sfx);
147178
} else {
148179
// Gate closed: show empty spectrum only if it changed (skip compute to save CPU).
149180
if !last_spec_was_zero {
150-
let _ = tx_spec.try_send(zero_spectrum.clone());
181+
let _ = tx.tx_spec.try_send(zero_spectrum.clone());
151182
last_spec_was_zero = true;
152183
}
153184
}

keyless-runtime/src/pipeline/events.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tracing::{error, info};
1111
/// Process transcription events: deliver Final events, show Partial as preview.
1212
pub fn process_transcription_events<T: RealtimeTranscriber>(
1313
transcriber: &Arc<Mutex<T>>,
14-
sink: &Arc<Box<dyn OutputSink>>,
14+
sink: &Arc<dyn OutputSink>,
1515
tx_log: &mpsc::SyncSender<String>,
1616
tx_preview: &mpsc::SyncSender<String>,
1717
sfx: &Arc<SfxPlayer>,

keyless-runtime/src/pipeline/startup.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ pub fn finalize_pipeline_from_artifacts(
209209
};
210210
let provider = keyless_output::DefaultOutputProvider;
211211
// Provider creates sink based on output_mode (runtime polymorphism).
212-
let sink: Box<dyn keyless_core::output::OutputSink> = provider.provide(config)?;
212+
let sink_box: Box<dyn keyless_core::output::OutputSink> = provider.provide(config)?;
213213

214214
// Extract mic name: prefer config, fallback to system default, fallback to placeholder.
215215
let mic_name = config
@@ -221,19 +221,24 @@ pub fn finalize_pipeline_from_artifacts(
221221
// Build callback using provided transcriber and eq cfg
222222
let transcriber = artifacts.transcriber.clone();
223223
let sfx = Arc::new(keyless_audio::SfxPlayer::new());
224-
let callback = callback::create_audio_callback(
225-
artifacts.eq_cfg,
226-
&vad_config,
227-
transcriber.clone(),
228-
Arc::new(sink),
229-
Arc::clone(&sfx),
230-
hold_flag,
231-
tx_level.clone(),
232-
tx_log.clone(),
224+
let sink: Arc<dyn keyless_core::output::OutputSink> = sink_box.into();
225+
let tx = callback::CallbackTx {
226+
tx_level: tx_level.clone(),
227+
tx_log: tx_log.clone(),
233228
tx_spec,
234229
tx_preview,
235230
tx_vad,
236-
);
231+
};
232+
let params = callback::AudioCallbackParams {
233+
eq_cfg: artifacts.eq_cfg,
234+
vad: vad_config,
235+
transcriber: transcriber.clone(),
236+
sink: Arc::clone(&sink),
237+
sfx: Arc::clone(&sfx),
238+
hold_flag,
239+
tx,
240+
};
241+
let callback = callback::create_audio_callback(params);
237242

238243
// Start audio input stream (non-Send component; must be created on main thread).
239244
let dev_name_for_log = selected_device

keyless-runtime/src/ptt/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tracing::{debug, error, info};
1111
/// Handle PTT release: finalize segment and drain/deliver Final events.
1212
pub fn handle_ptt_released<T: RealtimeTranscriber>(
1313
transcriber: &Arc<Mutex<T>>,
14-
sink: &Arc<Box<dyn OutputSink>>,
14+
sink: &Arc<dyn OutputSink>,
1515
tx_log: &mpsc::SyncSender<String>,
1616
tx_preview: &mpsc::SyncSender<String>,
1717
sfx: &Arc<SfxPlayer>,

0 commit comments

Comments
 (0)