diff --git a/.gitignore b/.gitignore index 43aad9d..b2a97f6 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/crates/soundgen-core/src/channel.rs b/crates/soundgen-core/src/channel.rs index ba70f02..2c5ca3f 100644 --- a/crates/soundgen-core/src/channel.rs +++ b/crates/soundgen-core/src/channel.rs @@ -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_sweep: Option, + vibrato: Option, 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(); + } } } diff --git a/crates/soundgen-feedback/src/lib.rs b/crates/soundgen-feedback/src/lib.rs index 7859ff7..76aab6c 100644 --- a/crates/soundgen-feedback/src/lib.rs +++ b/crates/soundgen-feedback/src/lib.rs @@ -317,6 +317,7 @@ mod tests { frequency: FrequencyAutomation::fixed(440.0), envelope: None, filter: None, + vibrato: None, volume: 0.5, pan: 0.0, }], diff --git a/crates/soundgen-fmt/src/lib.rs b/crates/soundgen-fmt/src/lib.rs index e0691d6..b6a534c 100644 --- a/crates/soundgen-fmt/src/lib.rs +++ b/crates/soundgen-fmt/src/lib.rs @@ -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, #[serde(default)] filter: Option, + /// Optional vibrato/frequency modulation (for bird chirps, sirens, etc.). + #[serde(default)] + vibrato: Option, #[serde(default = "default_volume")] volume: f32, #[serde(default)] @@ -93,6 +113,8 @@ pub enum ChannelSpec { envelope: Option, #[serde(default)] filter: Option, + #[serde(default)] + vibrato: Option, #[serde(default = "default_volume")] volume: f32, #[serde(default)] @@ -109,6 +131,8 @@ pub enum ChannelSpec { envelope: Option, #[serde(default)] filter: Option, + #[serde(default)] + vibrato: Option, #[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, }], diff --git a/crates/soundgen-fmt/src/preset.rs b/crates/soundgen-fmt/src/preset.rs index 2a7e19e..7273dd5 100644 --- a/crates/soundgen-fmt/src/preset.rs +++ b/crates/soundgen-fmt/src/preset.rs @@ -172,6 +172,7 @@ mod tests { frequency: FrequencyAutomation::fixed(440.0), envelope: None, filter: None, + vibrato: None, volume: 0.5, pan: 0.0, }], diff --git a/crates/soundgen-fmt/src/renderer.rs b/crates/soundgen-fmt/src/renderer.rs index a73af0d..674659b 100644 --- a/crates/soundgen-fmt/src/renderer.rs +++ b/crates/soundgen-fmt/src/renderer.rs @@ -1,10 +1,11 @@ //! Render a [`SoundSpec`] to interleaved stereo `Vec`. 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 { @@ -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, filter: &Option, + 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, }], diff --git a/crates/soundgen-fmt/tests/integration.rs b/crates/soundgen-fmt/tests/integration.rs index 35d08b4..8ec99e2 100644 --- a/crates/soundgen-fmt/tests/integration.rs +++ b/crates/soundgen-fmt/tests/integration.rs @@ -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, }, diff --git a/crates/soundgen-gui/src/app.rs b/crates/soundgen-gui/src/app.rs index 500dfba..cd8fd51 100644 --- a/crates/soundgen-gui/src/app.rs +++ b/crates/soundgen-gui/src/app.rs @@ -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, }); diff --git a/crates/soundgen-gui/src/channel_panel.rs b/crates/soundgen-gui/src/channel_panel.rs index 93ea5a3..0a5f214 100644 --- a/crates/soundgen-gui/src/channel_panel.rs +++ b/crates/soundgen-gui/src/channel_panel.rs @@ -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) -> 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 +} diff --git a/crates/soundgen-mcp/src/server.rs b/crates/soundgen-mcp/src/server.rs index 30e9a1c..b702eea 100644 --- a/crates/soundgen-mcp/src/server.rs +++ b/crates/soundgen-mcp/src/server.rs @@ -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 = arguments - .get("names") - .and_then(|n| n.as_array()) + let specs: Vec = 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" => { diff --git a/crates/soundgen-mcp/src/tools.rs b/crates/soundgen-mcp/src/tools.rs index 8eb7f54..2b1b1e6 100644 --- a/crates/soundgen-mcp/src/tools.rs +++ b/crates/soundgen-mcp/src/tools.rs @@ -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::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!({ diff --git a/crates/soundgen-runtime/src/bank.rs b/crates/soundgen-runtime/src/bank.rs index 869d397..042a329 100644 --- a/crates/soundgen-runtime/src/bank.rs +++ b/crates/soundgen-runtime/src/bank.rs @@ -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, }],