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:
@@ -6,10 +6,15 @@ release/
|
||||
# WAV output (generated, not source)
|
||||
*.wav
|
||||
/sfx_output/
|
||||
/training_sounds/
|
||||
|
||||
# User-generated sound specs
|
||||
/sound.json
|
||||
|
||||
# Feedback database (contains user ratings, keep local)
|
||||
/feedback.db
|
||||
/feedback_dataset.jsonl
|
||||
|
||||
# IDE / editor
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Produces stereo samples (left, right) with volume and pan applied.
|
||||
|
||||
use crate::effect::{Envelope, Filter, Sweep};
|
||||
use crate::effect::{Envelope, Filter, Sweep, Vibrato};
|
||||
use crate::generator::{Generator, Voice};
|
||||
use crate::voice::VoiceKind;
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct ChannelRenderer {
|
||||
initial_frequency: f32,
|
||||
filter: Option<Filter>,
|
||||
filter_sweep: Option<Sweep>,
|
||||
vibrato: Option<Vibrato>,
|
||||
volume: f32,
|
||||
pan: f32,
|
||||
}
|
||||
@@ -27,6 +28,7 @@ impl ChannelRenderer {
|
||||
initial_frequency: 440.0,
|
||||
filter: None,
|
||||
filter_sweep: None,
|
||||
vibrato: None,
|
||||
volume: 1.0,
|
||||
pan: 0.0,
|
||||
}
|
||||
@@ -58,6 +60,12 @@ impl ChannelRenderer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set vibrato (LFO frequency modulation).
|
||||
pub fn with_vibrato(mut self, vibrato: Vibrato) -> Self {
|
||||
self.vibrato = Some(vibrato);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_volume(mut self, volume: f32) -> Self {
|
||||
self.volume = volume.clamp(0.0, 1.0);
|
||||
self
|
||||
@@ -83,6 +91,9 @@ impl ChannelRenderer {
|
||||
if let Some(s) = &mut self.filter_sweep {
|
||||
s.trigger();
|
||||
}
|
||||
if let Some(v) = &mut self.vibrato {
|
||||
v.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the envelope (note off).
|
||||
@@ -101,11 +112,19 @@ impl ChannelRenderer {
|
||||
#[inline]
|
||||
pub fn tick(&mut self) -> (f32, f32) {
|
||||
// Update frequency from sweep
|
||||
let mut current_freq = self.initial_frequency;
|
||||
if let Some(sweep) = &mut self.freq_sweep {
|
||||
let freq = sweep.tick();
|
||||
self.voice.set_frequency(freq);
|
||||
current_freq = sweep.tick();
|
||||
}
|
||||
|
||||
// Apply vibrato (LFO frequency modulation)
|
||||
if let Some(vib) = &mut self.vibrato {
|
||||
let ratio = vib.tick();
|
||||
current_freq *= ratio;
|
||||
}
|
||||
|
||||
self.voice.set_frequency(current_freq);
|
||||
|
||||
// Tick voice
|
||||
let mut sample = self.voice.tick();
|
||||
|
||||
@@ -148,6 +167,9 @@ impl ChannelRenderer {
|
||||
if let Some(f) = &mut self.filter {
|
||||
f.reset();
|
||||
}
|
||||
if let Some(v) = &mut self.vibrato {
|
||||
v.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -317,6 +317,7 @@ mod tests {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
|
||||
@@ -37,6 +37,23 @@ pub struct EnvelopeSpec {
|
||||
pub release: f32,
|
||||
}
|
||||
|
||||
/// Vibrato/tremolo spec — LFO for frequency modulation (bird chirps, sirens, etc.).
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VibratoSpec {
|
||||
/// LFO frequency in Hz (e.g., 5-20 for bird chirps).
|
||||
pub rate: f32,
|
||||
/// Depth in cents (e.g., 200-800 for bird chirps, 50 for subtle vibrato).
|
||||
#[serde(default = "default_vibrato_depth")]
|
||||
pub depth: f32,
|
||||
/// Delay before vibrato starts (seconds). Default 0.
|
||||
#[serde(default)]
|
||||
pub delay: f32,
|
||||
}
|
||||
|
||||
fn default_vibrato_depth() -> f32 {
|
||||
100.0
|
||||
}
|
||||
|
||||
/// Filter spec with optional cutoff automation.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FilterSpec {
|
||||
@@ -81,6 +98,9 @@ pub enum ChannelSpec {
|
||||
envelope: Option<EnvelopeSpec>,
|
||||
#[serde(default)]
|
||||
filter: Option<FilterSpec>,
|
||||
/// Optional vibrato/frequency modulation (for bird chirps, sirens, etc.).
|
||||
#[serde(default)]
|
||||
vibrato: Option<VibratoSpec>,
|
||||
#[serde(default = "default_volume")]
|
||||
volume: f32,
|
||||
#[serde(default)]
|
||||
@@ -93,6 +113,8 @@ pub enum ChannelSpec {
|
||||
envelope: Option<EnvelopeSpec>,
|
||||
#[serde(default)]
|
||||
filter: Option<FilterSpec>,
|
||||
#[serde(default)]
|
||||
vibrato: Option<VibratoSpec>,
|
||||
#[serde(default = "default_volume")]
|
||||
volume: f32,
|
||||
#[serde(default)]
|
||||
@@ -109,6 +131,8 @@ pub enum ChannelSpec {
|
||||
envelope: Option<EnvelopeSpec>,
|
||||
#[serde(default)]
|
||||
filter: Option<FilterSpec>,
|
||||
#[serde(default)]
|
||||
vibrato: Option<VibratoSpec>,
|
||||
#[serde(default = "default_volume")]
|
||||
volume: f32,
|
||||
#[serde(default)]
|
||||
@@ -267,6 +291,7 @@ mod tests {
|
||||
release: 0.2,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.8,
|
||||
pan: -0.5,
|
||||
}],
|
||||
|
||||
@@ -172,6 +172,7 @@ mod tests {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Render a [`SoundSpec`] to interleaved stereo `Vec<f32>`.
|
||||
use soundgen_core::{
|
||||
ChannelRenderer, Envelope, Filter, FilterType, FrequencyAutomation, NoiseMode, Sweep, VoiceKind,
|
||||
ChannelRenderer, Envelope, Filter, FilterType, FrequencyAutomation, NoiseMode, Sweep, Vibrato,
|
||||
VoiceKind,
|
||||
};
|
||||
use VoiceKind as VK;
|
||||
|
||||
use crate::{ChannelSpec, EnvelopeSpec, FilterSpec, SoundSpec};
|
||||
use crate::{ChannelSpec, EnvelopeSpec, FilterSpec, SoundSpec, VibratoSpec};
|
||||
|
||||
/// Render a [`SoundSpec`] to interleaved stereo samples (L, R, L, R, ...).
|
||||
pub fn render_spec(spec: &SoundSpec) -> Vec<f32> {
|
||||
@@ -30,25 +31,43 @@ fn build_channel(spec: &ChannelSpec, sr: f32, duration: f32) -> ChannelRenderer
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
let duty_cycle = soundgen_core::voice::DutyCycle::from_percent(*duty);
|
||||
let voice = VK::Pulse(soundgen_core::voice::PulseChannel::new(sr, duty_cycle));
|
||||
build_renderer(
|
||||
voice, sr, duration, frequency, envelope, filter, *volume, *pan,
|
||||
voice,
|
||||
sr,
|
||||
duration,
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato.as_ref(),
|
||||
*volume,
|
||||
*pan,
|
||||
)
|
||||
}
|
||||
ChannelSpec::Triangle {
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
let voice = VK::Triangle(soundgen_core::voice::TriangleChannel::new(sr));
|
||||
build_renderer(
|
||||
voice, sr, duration, frequency, envelope, filter, *volume, *pan,
|
||||
voice,
|
||||
sr,
|
||||
duration,
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato.as_ref(),
|
||||
*volume,
|
||||
*pan,
|
||||
)
|
||||
}
|
||||
ChannelSpec::Noise {
|
||||
@@ -56,6 +75,7 @@ fn build_channel(spec: &ChannelSpec, sr: f32, duration: f32) -> ChannelRenderer
|
||||
frequency,
|
||||
envelope,
|
||||
filter,
|
||||
vibrato: _,
|
||||
volume,
|
||||
pan,
|
||||
} => {
|
||||
@@ -65,10 +85,9 @@ fn build_channel(spec: &ChannelSpec, sr: f32, duration: f32) -> ChannelRenderer
|
||||
};
|
||||
let noise = soundgen_core::voice::NoiseChannel::new(sr, noise_mode);
|
||||
let voice = VK::Noise(noise);
|
||||
// Noise "frequency" is a static clock rate — wrap as fixed automation.
|
||||
let freq_auto = FrequencyAutomation::fixed(*frequency);
|
||||
build_renderer(
|
||||
voice, sr, duration, &freq_auto, envelope, filter, *volume, *pan,
|
||||
voice, sr, duration, &freq_auto, envelope, filter, None, *volume, *pan,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,6 +101,7 @@ fn build_renderer(
|
||||
frequency: &FrequencyAutomation,
|
||||
envelope: &Option<EnvelopeSpec>,
|
||||
filter: &Option<FilterSpec>,
|
||||
vibrato: Option<&VibratoSpec>,
|
||||
volume: f32,
|
||||
pan: f32,
|
||||
) -> ChannelRenderer {
|
||||
@@ -95,6 +115,11 @@ fn build_renderer(
|
||||
cr = cr.with_freq_sweep(frequency.to_sweep(sr, duration));
|
||||
}
|
||||
|
||||
// Vibrato
|
||||
if let Some(vib) = vibrato {
|
||||
cr = cr.with_vibrato(Vibrato::new(sr, vib.rate, vib.depth));
|
||||
}
|
||||
|
||||
// Envelope
|
||||
if let Some(env) = envelope {
|
||||
cr = cr.with_envelope(Envelope::adsr(
|
||||
@@ -150,6 +175,7 @@ mod tests {
|
||||
release: 0.0,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -178,6 +204,7 @@ mod tests {
|
||||
release: 0.0,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -223,6 +250,7 @@ mod tests {
|
||||
release: 0.0,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -255,6 +283,7 @@ mod tests {
|
||||
cutoff_sweep: None,
|
||||
q: 0.707,
|
||||
}),
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
|
||||
@@ -41,6 +41,7 @@ fn test_render_spec_to_wav_and_verify() {
|
||||
release: 0.1,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.6,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -141,6 +142,7 @@ fn test_json_spec_roundtrip() {
|
||||
}),
|
||||
q: 0.707,
|
||||
}),
|
||||
vibrato: None,
|
||||
volume: 0.7,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -170,6 +172,7 @@ fn test_wav_file_format_correct() {
|
||||
release: 0.0,
|
||||
}),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -204,6 +207,7 @@ fn test_multi_channel_render() {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: Some(EnvelopeSpec::default()),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: -0.5,
|
||||
},
|
||||
@@ -211,6 +215,7 @@ fn test_multi_channel_render() {
|
||||
frequency: FrequencyAutomation::fixed(220.0),
|
||||
envelope: Some(EnvelopeSpec::default()),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.4,
|
||||
pan: 0.5,
|
||||
},
|
||||
@@ -219,6 +224,7 @@ fn test_multi_channel_render() {
|
||||
frequency: 5000.0,
|
||||
envelope: Some(EnvelopeSpec::default()),
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.3,
|
||||
pan: 0.0,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::io::{self, BufRead, Write};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use soundgen_feedback::FeedbackDB;
|
||||
use soundgen_fmt::PresetRegistry;
|
||||
use soundgen_fmt::{PresetRegistry, SoundSpec};
|
||||
|
||||
use crate::tools;
|
||||
|
||||
@@ -168,12 +168,12 @@ fn handle_tools_call(
|
||||
tools::render_sound(&spec_json, out_path, db_ref)
|
||||
}
|
||||
"generate_batch" => {
|
||||
let names: Vec<String> = arguments
|
||||
.get("names")
|
||||
.and_then(|n| n.as_array())
|
||||
let specs: Vec<SoundSpec> = arguments
|
||||
.get("specs")
|
||||
.and_then(|s| s.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.filter_map(|v| serde_json::from_value(v.clone()).ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -182,15 +182,14 @@ fn handle_tools_call(
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Extract DB from mutex for the call
|
||||
if let Some(mutex) = db_mutex {
|
||||
if let Ok(mut db) = mutex.lock() {
|
||||
tools::generate_batch(&names, out_dir, Some(&mut db))
|
||||
tools::generate_batch(&specs, out_dir, Some(&mut db))
|
||||
} else {
|
||||
tools::generate_batch(&names, out_dir, None)
|
||||
tools::generate_batch(&specs, out_dir, None)
|
||||
}
|
||||
} else {
|
||||
tools::generate_batch(&names, out_dir, None)
|
||||
tools::generate_batch(&specs, out_dir, None)
|
||||
}
|
||||
}
|
||||
"get_reference_sounds" => {
|
||||
|
||||
@@ -144,7 +144,6 @@ pub fn render_sound(
|
||||
file_size
|
||||
);
|
||||
|
||||
// Include reference examples from feedback DB
|
||||
if let Some(db) = feedback_db {
|
||||
if let Ok(refs) = db.search_similar(&spec.name, 3) {
|
||||
let rated: Vec<_> = refs.iter().filter(|r| r.rating > 0).collect();
|
||||
@@ -166,14 +165,15 @@ pub fn render_sound(
|
||||
|
||||
ToolResult::ok(response)
|
||||
}
|
||||
/// Generate a batch of sounds by name and store them in the feedback DB.
|
||||
|
||||
/// Generate a batch of sounds from custom specs and store them in the feedback DB.
|
||||
pub fn generate_batch(
|
||||
names: &[String],
|
||||
specs: &[SoundSpec],
|
||||
out_dir: &str,
|
||||
mut feedback_db: Option<&mut soundgen_feedback::FeedbackDB>,
|
||||
) -> ToolResult {
|
||||
if names.is_empty() {
|
||||
return ToolResult::err("No sound names provided".to_string());
|
||||
if specs.is_empty() {
|
||||
return ToolResult::err("No sound specs provided".to_string());
|
||||
}
|
||||
|
||||
let dir = Path::new(out_dir);
|
||||
@@ -185,51 +185,31 @@ pub fn generate_batch(
|
||||
let mut errors = Vec::new();
|
||||
let mut db_saved = false;
|
||||
|
||||
for name in names {
|
||||
let spec = SoundSpec {
|
||||
name: name.clone(),
|
||||
duration: 0.3,
|
||||
sample_rate: 44100,
|
||||
channels: vec![soundgen_fmt::ChannelSpec::Pulse {
|
||||
duty: 50,
|
||||
frequency: soundgen_core::FrequencyAutomation {
|
||||
start: 200.0,
|
||||
end: 800.0,
|
||||
curve: soundgen_core::SweepCurve::Exponential,
|
||||
},
|
||||
envelope: Some(soundgen_fmt::EnvelopeSpec {
|
||||
attack: 0.01,
|
||||
decay: 0.1,
|
||||
sustain: 0.0,
|
||||
release: 0.15,
|
||||
}),
|
||||
filter: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
};
|
||||
|
||||
let samples = render_spec(&spec);
|
||||
let wav_path = dir.join(format!("{}.wav", name));
|
||||
for spec in specs {
|
||||
let samples = render_spec(spec);
|
||||
let wav_path = dir.join(format!("{}.wav", spec.name));
|
||||
|
||||
if let Err(e) = write_wav(&wav_path, &samples, spec.sample_rate) {
|
||||
errors.push(format!("{}: {}", name, e));
|
||||
errors.push(format!("{}: {}", spec.name, e));
|
||||
continue;
|
||||
}
|
||||
|
||||
let wav_path_str = wav_path.display().to_string();
|
||||
|
||||
if let Some(db) = feedback_db.as_deref_mut() {
|
||||
if let Err(e) = db.add(name, &spec, Some(&wav_path_str)) {
|
||||
errors.push(format!("{} (DB): {}", name, e));
|
||||
if let Err(e) = db.add(&spec.name, spec, Some(&wav_path_str)) {
|
||||
errors.push(format!("{} (DB): {}", spec.name, e));
|
||||
} else {
|
||||
db_saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
results.push(format!(
|
||||
" {} → {} ({:.2}s)",
|
||||
name, wav_path_str, spec.duration
|
||||
" {} → {} ({:.2}s, {}ch)",
|
||||
spec.name,
|
||||
wav_path_str,
|
||||
spec.duration,
|
||||
spec.channels.len()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -246,6 +226,7 @@ pub fn generate_batch(
|
||||
|
||||
ToolResult::ok(response)
|
||||
}
|
||||
|
||||
/// Get reference sounds from the feedback DB for a given name.
|
||||
pub fn get_reference_sounds(
|
||||
name: &str,
|
||||
@@ -263,7 +244,6 @@ pub fn get_reference_sounds(
|
||||
Err(e) => return ToolResult::err(format!("DB search error: {}", e)),
|
||||
};
|
||||
|
||||
// Filter by min_rating
|
||||
entries.retain(|e| e.rating >= min_rating);
|
||||
entries.truncate(limit);
|
||||
|
||||
@@ -358,21 +338,21 @@ pub fn tool_definitions() -> Vec<serde_json::Value> {
|
||||
}),
|
||||
serde_json::json!({
|
||||
"name": "generate_batch",
|
||||
"description": "Generate multiple sounds by name and save them to a directory. Sounds are stored in the feedback DB (unrated) for later evaluation in the GUI Training tab.",
|
||||
"description": "Generate multiple sounds from custom SoundSpec JSON objects and save them to a directory. Each sound is stored in the feedback DB (unrated) for later evaluation in the GUI Training tab. Provide full specs — NOT just names — so each sound is unique.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"names": {
|
||||
"specs": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Sound names to generate (e.g., [\"missile_launch\", \"sword_swing\"])"
|
||||
"items": { "type": "object" },
|
||||
"description": "Array of SoundSpec JSON objects, each with a unique name, duration, and channels"
|
||||
},
|
||||
"out_dir": {
|
||||
"type": "string",
|
||||
"description": "Output directory for WAV files"
|
||||
}
|
||||
},
|
||||
"required": ["names", "out_dir"]
|
||||
"required": ["specs", "out_dir"]
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
|
||||
@@ -132,6 +132,7 @@ mod tests {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -156,6 +157,7 @@ mod tests {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 0.5,
|
||||
pan: 0.0,
|
||||
}],
|
||||
@@ -182,6 +184,7 @@ mod tests {
|
||||
frequency: FrequencyAutomation::fixed(440.0),
|
||||
envelope: None,
|
||||
filter: None,
|
||||
vibrato: None,
|
||||
volume: 1.0,
|
||||
pan: 0.0,
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user