Skip to content

Commit 822b080

Browse files
Tokclaude
andcommitted
amen+tts: 3x2 amen panel w/ glass panes, bump TTS error logging
Amen panel reshape: - ModuleKind::AmenSampler is now 3x2 (was 3x1). The 3x1 height never fit the waveform + slice-wheel additions. - Sample-picker combo box is now capped at (available - 70) width so RND + LD fit inside the 3-cell module without overflowing. - Rearranged lower half of the panel into a cleaner two-row layout: - Slice wheel on the left, and a stacked column on the right holding SLICES buttons, REV/LOOP toggles, and the tempo row (SRC BPM DragValue + STRETCH toggle). - A single knob row underneath with three glass-pane groups: LEVEL (volume / pitch), REGION (start / end), SHAPE (gate / stutter). Uses widgets::even_group_width for consistent 3-pane distribution. - Top half unchanged (picker, metadata, waveform, AUTO/RESET). TTS diagnostics (src/llm/tts.rs): - speak_neutts's HTTP failure paths now log at warn level (was debug). The common mode is "NeuTTS /synthesize returned 4xx" when voice_ref points at a missing file — the user sees that in the log without needing --log debug. - Log the response code + first 200 chars of the error body on non-2xx status. - Log "pushed N/M samples" on success so we can confirm audio actually reached the ring buffer. - Log distinct reasons on empty body / decode failure / zero samples after decode. 480 tests still passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 89c9cfd commit 822b080

3 files changed

Lines changed: 119 additions & 75 deletions

File tree

src/llm/tts.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,61 @@ pub fn speak_neutts(text: &str, tts: &TtsModuleState, tts_tx: &Arc<Mutex<Produce
4949
.send_json(&payload)
5050
{
5151
Ok(resp) => resp,
52+
Err(ureq::Error::Status(code, resp)) => {
53+
let body = resp.into_string().unwrap_or_default();
54+
log::warn!(
55+
"NeuTTS /synthesize returned {} — voice_ref may be missing or invalid. Body: {}",
56+
code,
57+
body.chars().take(200).collect::<String>()
58+
);
59+
return;
60+
}
5261
Err(e) => {
53-
log::debug!("NeuTTS server unreachable: {}", e);
62+
log::warn!("NeuTTS server unreachable: {}", e);
5463
return;
5564
}
5665
};
5766

5867
// Read WAV bytes from response body.
5968
let mut wav_bytes = Vec::new();
60-
if client.into_reader().read_to_end(&mut wav_bytes).is_err() {
69+
if let Err(e) = client.into_reader().read_to_end(&mut wav_bytes) {
70+
log::warn!("NeuTTS: failed to read response body: {}", e);
71+
return;
72+
}
73+
if wav_bytes.is_empty() {
74+
log::warn!("NeuTTS: server returned empty body");
6175
return;
6276
}
6377

6478
if let Some(mut samples) = read_wav_f32_bytes(&wav_bytes) {
79+
if samples.is_empty() {
80+
log::warn!("NeuTTS: returned WAV decoded to zero samples");
81+
return;
82+
}
6583
if pitch_snap && let Some(hz) = detect_pitch_hz(&samples, 44100.0) {
6684
let detected_midi = (12.0 * (hz / 440.0).log2() + 69.0).round() as u8;
6785
let snapped_midi = crate::state::snap_to_scale(detected_midi, root_note, scale);
6886
let shift = snapped_midi as f32 - detected_midi as f32;
6987
samples = resample_pitch_shift(&samples, shift);
7088
}
89+
let sample_count = samples.len();
7190
let mut tx = tts_tx.lock();
91+
let mut pushed = 0usize;
7292
for s in &samples {
73-
let _ = tx.push(*s);
93+
if tx.push(*s).is_ok() {
94+
pushed += 1;
95+
}
7496
}
97+
log::info!(
98+
"NeuTTS: pushed {}/{} samples to ring buffer",
99+
pushed,
100+
sample_count
101+
);
102+
} else {
103+
log::warn!(
104+
"NeuTTS: failed to decode WAV response ({} bytes)",
105+
wav_bytes.len()
106+
);
75107
}
76108
});
77109
}

src/state/rack.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ impl ModuleKind {
194194
Self::DrumKit909 => (4, 3),
195195
Self::HooverLead => (4, 2),
196196
Self::An1xVoice => (6, 6),
197-
Self::AmenSampler => (3, 1),
197+
Self::AmenSampler => (3, 2),
198198
Self::NoiseVoice => (2, 1),
199199
Self::GranularTexture => (3, 1),
200200
Self::LlmAgent => (3, 2),

src/ui/panels/amen.rs

Lines changed: 83 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,11 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
411411
.and_then(|n| n.to_str())
412412
.unwrap_or("(pick sample)")
413413
.to_string();
414+
// Leave room for RND + LD buttons (two small buttons ~48px total
415+
// plus item_spacing). Using a fixed cap keeps the picker from
416+
// pushing outside the 3-cell module width on narrow rack layouts.
414417
egui::ComboBox::from_id_source("amen_sample_picker")
415-
.width(ui.available_width() - 30.0)
418+
.width((ui.available_width() - 70.0).max(60.0))
416419
.selected_text(egui::RichText::new(current_name).monospace().size(8.0))
417420
.show_ui(ui, |ui| {
418421
for sp in &samples {
@@ -549,11 +552,15 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
549552
}
550553
});
551554

552-
// ── Slice wheel + reverse indicator ──────────────────────────────────────
555+
// ── Slice wheel + slices/rev/loop + tempo stacked column ───────────────
556+
// The slice wheel sits on the left; the column to its right holds the
557+
// SLICES buttons, REV/LOOP toggles, and the tempo row (SRC BPM +
558+
// STRETCH). Packing all three into that column frees the lower half
559+
// of the panel for a single knob row.
553560
ui.horizontal(|ui| {
554561
draw_slice_wheel(ui, slice_count, active, reverse, loop_mode, 56.0);
555562
ui.vertical(|ui| {
556-
// SLICES selector (cycles through 1/2/4/8/16)
563+
// SLICES selector
557564
ui.horizontal(|ui| {
558565
ui.label(
559566
egui::RichText::new("SLICES")
@@ -562,8 +569,7 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
562569
.color(theme::SMOKE),
563570
);
564571
for &n in &[1u8, 2, 4, 8, 16] {
565-
let active_sel = slice_count == n;
566-
let col = if active_sel {
572+
let col = if slice_count == n {
567573
theme::CHALK
568574
} else {
569575
theme::IRON
@@ -585,7 +591,7 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
585591
}
586592
}
587593
});
588-
// REV / LOOP toggles
594+
// REV / LOOP
589595
ui.horizontal(|ui| {
590596
if widgets::toggle_button(ui, if reverse { "REV" } else { "FWD" }, &mut reverse) {
591597
changed = true;
@@ -598,75 +604,81 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
598604
changed = true;
599605
}
600606
});
607+
// SRC BPM + STRETCH — tempo-matching controls. STRETCH on =
608+
// resample-based stretch to sequencer.bpm (pitches the sample).
609+
ui.horizontal(|ui| {
610+
ui.label(
611+
egui::RichText::new("BPM")
612+
.monospace()
613+
.size(7.5)
614+
.color(theme::SMOKE),
615+
);
616+
let mut v = source_bpm;
617+
if ui
618+
.add(egui::DragValue::new(&mut v).range(40.0..=300.0).speed(0.5))
619+
.changed()
620+
{
621+
source_bpm = v;
622+
changed = true;
623+
}
624+
if widgets::toggle_button(
625+
ui,
626+
if bpm_stretch { "STRETCH" } else { "FREE" },
627+
&mut bpm_stretch,
628+
) {
629+
changed = true;
630+
}
631+
});
601632
});
602633
});
603634

604-
// ── Sliders row 1: volume / pitch ────────────────────────────────────────
605-
ui.horizontal(|ui| {
606-
if widgets::param_control(ui, "VOLUME", &mut vol, ParamMode::Free, ctrl).0 {
607-
changed = true;
608-
}
609-
let mut pitch_norm = (pitch + 24.0) / 48.0;
610-
if widgets::param_control(ui, "PITCH", &mut pitch_norm, ParamMode::Free, ctrl).0 {
611-
pitch = pitch_norm * 48.0 - 24.0;
612-
changed = true;
613-
}
614-
});
615-
616-
// ── Sliders row 2: region offsets ────────────────────────────────────────
617-
ui.horizontal(|ui| {
618-
if widgets::param_control(ui, "START", &mut start_offset, ParamMode::Free, ctrl).0 {
619-
if start_offset >= end_offset {
620-
start_offset = (end_offset - 0.01).max(0.0);
621-
}
622-
changed = true;
623-
}
624-
if widgets::param_control(ui, "END", &mut end_offset, ParamMode::Free, ctrl).0 {
625-
if end_offset <= start_offset {
626-
end_offset = (start_offset + 0.01).min(1.0);
627-
}
628-
changed = true;
629-
}
630-
});
631-
632-
// ── Sliders row 3: gate + stutter ────────────────────────────────────────
633-
ui.horizontal(|ui| {
634-
if widgets::param_control(ui, "GATE", &mut gate, ParamMode::Free, ctrl).0 {
635-
changed = true;
636-
}
637-
let mut stutter_norm = stutter as f32 / 4.0;
638-
if widgets::param_control(ui, "STUTTER", &mut stutter_norm, ParamMode::Free, ctrl).0 {
639-
stutter = (stutter_norm * 4.0).round().clamp(0.0, 4.0) as u8;
640-
changed = true;
641-
}
642-
});
643-
644-
// ── BPM stretch row ──────────────────────────────────────────────────────
645-
// SRC BPM is the sample's original tempo; STRETCH toggles resample-based
646-
// tempo matching to sequencer.bpm (non-pitch-preserving, classic
647-
// drumbreak treatment — the sample pitches up/down to match).
635+
// ── Knob row — three glass-pane groups on one line ──────────────────────
636+
// LEVEL (vol/pitch) · REGION (start/end) · SHAPE (gate/stutter).
637+
// even_group_width evenly distributes the 3 panes across the panel's
638+
// available width, respecting the inter-group gap.
639+
let gw = widgets::even_group_width(ui, 3);
648640
ui.horizontal(|ui| {
649-
ui.label(
650-
egui::RichText::new("SRC BPM")
651-
.monospace()
652-
.size(7.5)
653-
.color(theme::SMOKE),
654-
);
655-
let mut v = source_bpm;
656-
if ui
657-
.add(egui::DragValue::new(&mut v).range(40.0..=300.0).speed(0.5))
658-
.changed()
659-
{
660-
source_bpm = v;
661-
changed = true;
662-
}
663-
if widgets::toggle_button(
664-
ui,
665-
if bpm_stretch { "STRETCH" } else { "FREE" },
666-
&mut bpm_stretch,
667-
) {
668-
changed = true;
669-
}
641+
widgets::glass_group_fill(ui, gw, gw, |ui| {
642+
ui.horizontal(|ui| {
643+
if widgets::param_control(ui, "VOLUME", &mut vol, ParamMode::Free, ctrl).0 {
644+
changed = true;
645+
}
646+
let mut pitch_norm = (pitch + 24.0) / 48.0;
647+
if widgets::param_control(ui, "PITCH", &mut pitch_norm, ParamMode::Free, ctrl).0 {
648+
pitch = pitch_norm * 48.0 - 24.0;
649+
changed = true;
650+
}
651+
});
652+
});
653+
widgets::glass_group_fill(ui, gw, gw, |ui| {
654+
ui.horizontal(|ui| {
655+
if widgets::param_control(ui, "START", &mut start_offset, ParamMode::Free, ctrl).0 {
656+
if start_offset >= end_offset {
657+
start_offset = (end_offset - 0.01).max(0.0);
658+
}
659+
changed = true;
660+
}
661+
if widgets::param_control(ui, "END", &mut end_offset, ParamMode::Free, ctrl).0 {
662+
if end_offset <= start_offset {
663+
end_offset = (start_offset + 0.01).min(1.0);
664+
}
665+
changed = true;
666+
}
667+
});
668+
});
669+
widgets::glass_group_fill(ui, gw, gw, |ui| {
670+
ui.horizontal(|ui| {
671+
if widgets::param_control(ui, "GATE", &mut gate, ParamMode::Free, ctrl).0 {
672+
changed = true;
673+
}
674+
let mut stutter_norm = stutter as f32 / 4.0;
675+
if widgets::param_control(ui, "STUTTER", &mut stutter_norm, ParamMode::Free, ctrl).0
676+
{
677+
stutter = (stutter_norm * 4.0).round().clamp(0.0, 4.0) as u8;
678+
changed = true;
679+
}
680+
});
681+
});
670682
});
671683

672684
if changed {

0 commit comments

Comments
 (0)