Skip to main content

mittens_engine/engine/ecs/component/
music_note.rs

1use super::{Component, ComponentRef};
2use crate::engine::ecs::{ComponentId, World};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub(crate) enum NotePitch {
7    A,
8    B,
9    C,
10    D,
11    E,
12    F,
13    G,
14}
15
16#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
17pub struct MusicNote {
18    duration: f32,
19    #[serde(default = "MusicNote::default_velocity")]
20    velocity: f32,
21    pitch: NotePitch,
22    octave: u16,
23}
24
25impl Default for MusicNote {
26    fn default() -> Self {
27        Self {
28            duration: 0.25,
29            velocity: 1.0,
30            pitch: NotePitch::A,
31            octave: 4,
32        }
33    }
34}
35
36impl MusicNote {
37    fn default_velocity() -> f32 {
38        1.0
39    }
40
41    pub fn duration_beats(&self) -> f32 {
42        self.duration
43    }
44
45    pub fn velocity(&self) -> f32 {
46        self.velocity
47    }
48
49    pub fn octave(&self) -> u16 {
50        self.octave
51    }
52
53    pub fn pitch_name(&self) -> &'static str {
54        match self.pitch {
55            NotePitch::A => "a",
56            NotePitch::B => "b",
57            NotePitch::C => "c",
58            NotePitch::D => "d",
59            NotePitch::E => "e",
60            NotePitch::F => "f",
61            NotePitch::G => "g",
62        }
63    }
64
65    pub fn with_duration_beats(mut self, duration_beats: f32) -> Self {
66        self.duration = duration_beats;
67        self
68    }
69
70    pub fn with_velocity(mut self, velocity: f32) -> Self {
71        self.velocity = if velocity.is_finite() {
72            velocity.max(0.0)
73        } else {
74            1.0
75        };
76        self
77    }
78
79    pub fn with_octave(mut self, octave: u16) -> Self {
80        self.octave = octave;
81        self
82    }
83
84    pub fn a(octave: u16, duration_beats: f32) -> Self {
85        Self {
86            duration: duration_beats,
87            velocity: 1.0,
88            pitch: NotePitch::A,
89            octave,
90        }
91    }
92
93    pub fn b(octave: u16, duration_beats: f32) -> Self {
94        Self {
95            duration: duration_beats,
96            velocity: 1.0,
97            pitch: NotePitch::B,
98            octave,
99        }
100    }
101
102    pub fn c(octave: u16, duration_beats: f32) -> Self {
103        Self {
104            duration: duration_beats,
105            velocity: 1.0,
106            pitch: NotePitch::C,
107            octave,
108        }
109    }
110
111    pub fn d(octave: u16, duration_beats: f32) -> Self {
112        Self {
113            duration: duration_beats,
114            velocity: 1.0,
115            pitch: NotePitch::D,
116            octave,
117        }
118    }
119
120    pub fn e(octave: u16, duration_beats: f32) -> Self {
121        Self {
122            duration: duration_beats,
123            velocity: 1.0,
124            pitch: NotePitch::E,
125            octave,
126        }
127    }
128
129    pub fn f(octave: u16, duration_beats: f32) -> Self {
130        Self {
131            duration: duration_beats,
132            velocity: 1.0,
133            pitch: NotePitch::F,
134            octave,
135        }
136    }
137
138    pub fn g(octave: u16, duration_beats: f32) -> Self {
139        Self {
140            duration: duration_beats,
141            velocity: 1.0,
142            pitch: NotePitch::G,
143            octave,
144        }
145    }
146
147    pub(crate) fn from_pitch(duration_beats: f32, pitch: NotePitch, octave: u16) -> Self {
148        Self {
149            duration: duration_beats,
150            velocity: 1.0,
151            pitch,
152            octave,
153        }
154    }
155
156    pub(crate) fn pitch(&self) -> NotePitch {
157        self.pitch
158    }
159}
160
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
162pub struct MusicNoteComponent {
163    pub note: MusicNote,
164
165    /// Pre-authored target audio source (`audio_source` per spec §6.4.1).
166    /// Durable ref preserved through serde / MMS dump. `target_resolved` is
167    /// the runtime cache filled by `resolve_target`. When both are None,
168    /// dispatch falls back to ancestor-walk per docs/spec/audio-sources.md
169    /// §6.6 rank 5 (the executor caches the result back here).
170    #[serde(skip)]
171    pub target_source: Option<ComponentRef>,
172
173    /// Runtime cache of the resolved target. Skipped by serde — fresh
174    /// loads re-resolve from `target_source` (or ancestor walk) on first
175    /// use, then this stays populated.
176    #[serde(skip)]
177    pub target_resolved: Option<ComponentId>,
178
179    /// Pre-authored default beat (`scheduled_beat` per spec §6.4.1). When
180    /// None, `.play()` fires immediately (beat_offset = 0).
181    #[serde(default)]
182    pub scheduled_beat: Option<f64>,
183
184    /// Fire `AudioSchedulePlay` automatically when this component initializes.
185    /// Defaults to false — silent unless explicitly triggered.
186    #[serde(default)]
187    pub play_on_attach: bool,
188
189    #[serde(skip)]
190    component: Option<ComponentId>,
191}
192
193impl MusicNoteComponent {
194    pub fn new(note: MusicNote) -> Self {
195        Self {
196            note,
197            target_source: None,
198            target_resolved: None,
199            scheduled_beat: None,
200            play_on_attach: false,
201            component: None,
202        }
203    }
204
205    pub fn from_note(note: MusicNote) -> Self {
206        Self::new(note)
207    }
208
209    pub fn with_target_source(mut self, source: ComponentRef) -> Self {
210        self.target_source = Some(source);
211        self
212    }
213
214    pub fn with_scheduled_beat(mut self, beat: f64) -> Self {
215        self.scheduled_beat = Some(beat);
216        self
217    }
218
219    pub fn with_play_on_attach(mut self, on: bool) -> Self {
220        self.play_on_attach = on;
221        self
222    }
223
224    pub fn id(&self) -> Option<ComponentId> {
225        self.component
226    }
227
228    /// Resolve a target for this note following the current runtime
229    /// precedence: cached → explicit `target_source` (Guid/Query).
230    /// Returns `None` when nothing resolves — the executor's subtree /
231    /// ancestor audio-source walk handles that case and writes the result
232    /// back to `target_resolved` afterward.
233    pub fn resolve_target(&mut self, world: &mut World) -> Option<ComponentId> {
234        if self.target_resolved.is_some() {
235            return self.target_resolved;
236        }
237        if let Some(src) = self.target_source.as_ref() {
238            let resolved = match src {
239                ComponentRef::Guid(uuid) => world.component_id_by_guid(*uuid),
240                ComponentRef::Query(selector) => {
241                    let roots: Vec<ComponentId> = world
242                        .all_components()
243                        .filter(|&cid| world.parent_of(cid).is_none())
244                        .collect();
245                    roots
246                        .into_iter()
247                        .find_map(|root| world.find_component(root, selector))
248                }
249            };
250            self.target_resolved = resolved;
251            if resolved.is_some() {
252                return resolved;
253            }
254        }
255        None
256    }
257}
258
259impl Default for MusicNoteComponent {
260    fn default() -> Self {
261        Self::new(MusicNote::default())
262    }
263}
264
265impl Component for MusicNoteComponent {
266    fn set_id(&mut self, component: ComponentId) {
267        self.component = Some(component);
268    }
269
270    fn name(&self) -> &'static str {
271        "music_note"
272    }
273
274    fn as_any(&self) -> &dyn std::any::Any {
275        self
276    }
277
278    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
279        self
280    }
281
282    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
283        if !self.play_on_attach {
284            return;
285        }
286        // Per docs/spec/audio-sources.md §6.4: play_on_attach fires
287        // AudioSchedulePlay when the component initializes. Target resolution
288        // is deferred to the executor — `target_resolved` is filled either
289        // by `resolve_target` (when `target_source` is set) or by the
290        // executor's ancestor-walk writeback (when neither is set).
291        // Passing `component` (self id) here lets the executor walk both
292        // subtree and ancestor scopes.
293        let target = self.target_resolved.unwrap_or(component);
294        emit.push_intent_now(
295            component,
296            crate::engine::ecs::IntentValue::AudioSchedulePlay {
297                component_ids: vec![target],
298                beat_offset: 0.0,
299                beat_context: self.scheduled_beat,
300                note: Some(self.note),
301                gain: None,
302                rate: None,
303                duration: None,
304            },
305        );
306    }
307
308    fn to_mms_ast(
309        &self,
310        _world: &crate::engine::ecs::World,
311    ) -> crate::scripting::ast::ComponentExpression {
312        use crate::engine::ecs::component::ce_helpers::*;
313        let pitch = self.note.pitch_name();
314        let mut c = ce_call(
315            "MusicNote",
316            pitch,
317            vec![
318                num(self.note.octave() as f64),
319                num(self.note.duration_beats() as f64),
320            ],
321        );
322        if (self.note.velocity() - 1.0).abs() > f32::EPSILON {
323            c = c.with_call("velocity", vec![num(self.note.velocity() as f64)]);
324        }
325        if self.play_on_attach {
326            c = c.with_call("play_on_attach", vec![]);
327        }
328        if let Some(b) = self.scheduled_beat {
329            c = c.with_call("at_beat", vec![num(b)]);
330        }
331        c
332    }
333}