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:
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user