Add vibrato support + fix feedback loop + GUI improvements
Vibrato (LFO frequency modulation): - New VibratoSpec in SoundSpec: rate (Hz), depth (cents), delay (s) - ChannelRenderer applies vibrato to frequency each tick - Supported on Pulse and Triangle channels - GUI: vibrato controls (enable, rate, depth) in channel panel - Enables bird chirps, sirens, wobbles Feedback loop fixes: - generate_batch now accepts full SoundSpec array (not just names) so each sound is unique - render_sound returns reference examples from feedback DB - Fixed GUI bug: clicking stars no longer erases feedback text - Feedback text auto-saves on Enter / focus loss - Replaced emoji buttons (✓/🗑) with text (Save/Del) - Green saved indicator when feedback exists in DB 99 tests passing, 0 warnings
This commit is contained in:
@@ -144,6 +144,7 @@ impl Default for SoundgenApp {
|
||||
frequency: soundgen_core::FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -940,27 +941,41 @@ impl SoundgenApp {
|
||||
.clicked()
|
||||
{
|
||||
rating = if rating == star { 0 } else { star };
|
||||
// Preserve existing feedback text when changing rating
|
||||
let existing_fb = self.rating_text.get(&id).cloned();
|
||||
let fb_opt = existing_fb
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
if let Some(db) = &mut self.feedback_db {
|
||||
let _ = db.update_rating(&id, rating, None);
|
||||
let _ = db.update_rating(&id, rating, fb_opt.as_deref());
|
||||
}
|
||||
self.feedback_dirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Feedback text
|
||||
// Feedback text — saves on Enter or focus loss
|
||||
let text = self
|
||||
.rating_text
|
||||
.entry(id.clone())
|
||||
.or_insert_with(|| entry.feedback.clone().unwrap_or_default());
|
||||
ui.add(
|
||||
let resp = ui.add(
|
||||
egui::TextEdit::singleline(text)
|
||||
.desired_width(150.0)
|
||||
.desired_width(120.0)
|
||||
.hint_text("feedback..."),
|
||||
);
|
||||
|
||||
// Save feedback button
|
||||
if ui.button("✓").on_hover_text("Save feedback").clicked() {
|
||||
// Save on Enter or when focus is lost (after editing)
|
||||
let should_save =
|
||||
resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
|
||||
let save_clicked = ui
|
||||
.button("Save")
|
||||
.on_hover_text("Save feedback (or press Enter)")
|
||||
.clicked();
|
||||
|
||||
if should_save || save_clicked {
|
||||
let fb = self.rating_text.get(&id).cloned();
|
||||
let fb_opt = if fb.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||
None
|
||||
@@ -970,11 +985,30 @@ impl SoundgenApp {
|
||||
if let Some(db) = &mut self.feedback_db {
|
||||
let _ = db.update_rating(&id, entry.rating, fb_opt.as_deref());
|
||||
}
|
||||
self.set_status("Feedback saved".to_string());
|
||||
self.set_status(format!("Feedback saved for {}", entry.name));
|
||||
}
|
||||
|
||||
// Show saved indicator if feedback exists in DB
|
||||
if entry.feedback.is_some()
|
||||
|| self
|
||||
.rating_text
|
||||
.get(&id)
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
ui.label(
|
||||
egui::RichText::new("saved")
|
||||
.small()
|
||||
.color(egui::Color32::from_rgb(100, 200, 120)),
|
||||
);
|
||||
}
|
||||
|
||||
// Delete button
|
||||
if ui.button("🗑").on_hover_text("Delete").clicked() {
|
||||
if ui
|
||||
.button("Del")
|
||||
.on_hover_text("Delete this sound from DB")
|
||||
.clicked()
|
||||
{
|
||||
if let Some(db) = &mut self.feedback_db {
|
||||
let _ = db.delete(&id);
|
||||
}
|
||||
@@ -1041,6 +1075,7 @@ impl SoundgenApp {
|
||||
release: 0.15,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -1132,6 +1167,7 @@ impl SoundgenApp {
|
||||
frequency: soundgen_core::FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -1169,6 +1205,7 @@ impl SoundgenApp {
|
||||
frequency: soundgen_core::FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
});
|
||||
@@ -1178,6 +1215,7 @@ impl SoundgenApp {
|
||||
frequency: soundgen_core::FrequencyAutomation::fixed(220.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.4,
|
||||
pan: 0.0,
|
||||
});
|
||||
@@ -1188,6 +1226,7 @@ impl SoundgenApp {
|
||||
frequency: 8000.0,
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.3,
|
||||
pan: 0.0,
|
||||
});
|
||||
|
||||
@@ -101,6 +101,7 @@ fn channel_controls(ui: &mut Ui, channel: &mut ChannelSpec, _index: usize) -> bo
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
@@ -131,12 +132,14 @@ fn channel_controls(ui: &mut Ui, channel: &mut ChannelSpec, _index: usize) -> bo
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
changed |= freq_controls(ui, frequency);
|
||||
changed |= envelope_controls(ui, envelope);
|
||||
changed |= filter_controls(ui, filter);
|
||||
changed |= vibrato_controls(ui, vibrato);
|
||||
changed |= vol_pan_controls(ui, volume, pan);
|
||||
}
|
||||
ChannelSpec::Noise {
|
||||
@@ -144,6 +147,7 @@ fn channel_controls(ui: &mut Ui, channel: &mut ChannelSpec, _index: usize) -> bo
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato: _,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
@@ -478,3 +482,64 @@ fn vol_pan_controls(ui: &mut Ui, volume: &mut f32, pan: &mut f32) -> bool {
|
||||
});
|
||||
changed
|
||||
}
|
||||
|
||||
fn vibrato_controls(ui: &mut Ui, vibrato: &mut Option<soundgen_fmt::VibratoSpec>) -> bool {
|
||||
use soundgen_fmt::VibratoSpec;
|
||||
let mut changed = false;
|
||||
|
||||
let has_vibrato = vibrato.is_some();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let mut enable = has_vibrato;
|
||||
if ui
|
||||
.checkbox(&mut enable, "Vibrato")
|
||||
.on_hover_text("LFO frequency modulation — for bird chirps, sirens, wobbles")
|
||||
.changed()
|
||||
{
|
||||
if enable && vibrato.is_none() {
|
||||
*vibrato = Some(VibratoSpec {
|
||||
rate: 10.0,
|
||||
depth: 200.0,
|
||||
delay: 0.0,
|
||||
});
|
||||
changed = true;
|
||||
} else if !enable && vibrato.is_some() {
|
||||
*vibrato = None;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(vib) = vibrato {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Rate:");
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(&mut vib.rate, 0.5..=50.0)
|
||||
.suffix(" Hz")
|
||||
.fixed_decimals(1),
|
||||
)
|
||||
.on_hover_text("LFO speed (5-15 Hz = bird chirps, 0.5-2 = subtle vibrato)")
|
||||
.changed()
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
ui.label("Depth:");
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(&mut vib.depth, 0.0..=1200.0)
|
||||
.suffix(" cents")
|
||||
.fixed_decimals(0),
|
||||
)
|
||||
.on_hover_text(
|
||||
"Pitch variation (100 = subtle, 600 = wide chirp, 1200 = full octave)",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user