@@ -2,6 +2,7 @@ use glam::Vec3;
22use rand:: prelude:: * ;
33use std:: time:: Duration ;
44
5+ /// Basic oscillator shape used by synths in both native and web front-ends.
56#[ derive( Clone , Copy , Debug ) ]
67pub enum Waveform {
78 Sine ,
@@ -10,13 +11,15 @@ pub enum Waveform {
1011 Triangle ,
1112}
1213
14+ /// Static configuration for a voice: color, waveform and initial position.
1315#[ derive( Clone , Debug ) ]
1416pub struct VoiceConfig {
1517 pub color_rgb : [ f32 ; 3 ] ,
1618 pub waveform : Waveform ,
1719 pub base_position : Vec3 ,
1820}
1921
22+ /// Scheduled note event produced by the music engine.
2023#[ derive( Clone , Debug , Default ) ]
2124pub struct NoteEvent {
2225 pub voice_index : usize ,
@@ -26,12 +29,14 @@ pub struct NoteEvent {
2629 pub duration_sec : f32 ,
2730}
2831
32+ /// Mutable runtime state per voice.
2933#[ derive( Clone , Debug ) ]
3034pub struct VoiceState {
3135 pub position : Vec3 ,
3236 pub muted : bool ,
3337}
3438
39+ /// Global engine parameters controlling tempo and scale.
3540#[ derive( Clone , Debug ) ]
3641pub struct EngineParams {
3742 pub bpm : f32 ,
@@ -47,8 +52,10 @@ impl Default for EngineParams {
4752 }
4853}
4954
55+ /// Default five-note scale centered around middle C.
5056pub const C_MAJOR_PENTATONIC : & [ i32 ] = & [ 0 , 2 , 4 , 7 , 9 , 12 ] ;
5157
58+ /// Random generative scheduler producing `NoteEvent`s on an eighth-note grid.
5259pub struct MusicEngine {
5360 pub voices : Vec < VoiceState > ,
5461 pub configs : Vec < VoiceConfig > ,
@@ -59,6 +66,7 @@ pub struct MusicEngine {
5966}
6067
6168impl MusicEngine {
69+ /// Construct a new engine with voices derived from the provided configs.
6270 pub fn new ( configs : Vec < VoiceConfig > , params : EngineParams , seed : u64 ) -> Self {
6371 let voices = configs
6472 . iter ( )
@@ -86,35 +94,41 @@ impl MusicEngine {
8694 }
8795 }
8896
97+ /// Set beats-per-minute for the internal scheduler.
8998 pub fn set_bpm ( & mut self , bpm : f32 ) {
9099 self . params . bpm = bpm;
91100 }
92101
102+ /// Toggle mute flag for a voice.
93103 pub fn toggle_mute ( & mut self , voice_index : usize ) {
94104 if let Some ( v) = self . voices . get_mut ( voice_index) {
95105 v. muted = !v. muted ;
96106 }
97107 }
98108
109+ /// Explicitly set mute flag for a voice.
99110 pub fn set_voice_muted ( & mut self , voice_index : usize , muted : bool ) {
100111 if let Some ( v) = self . voices . get_mut ( voice_index) {
101112 v. muted = muted;
102113 }
103114 }
104115
116+ /// Update the engine-space position of a voice.
105117 pub fn set_voice_position ( & mut self , voice_index : usize , pos : Vec3 ) {
106118 if let Some ( v) = self . voices . get_mut ( voice_index) {
107119 v. position = pos;
108120 }
109121 }
110122
123+ /// Reseed the per-voice RNG. If `seed` is None, a new random seed is chosen.
111124 pub fn reseed_voice ( & mut self , voice_index : usize , seed : Option < u64 > ) {
112125 if let Some ( r) = self . rngs . get_mut ( voice_index) {
113126 let new_seed = seed. unwrap_or_else ( || r. gen ( ) ) ;
114127 * r = StdRng :: seed_from_u64 ( new_seed) ;
115128 }
116129 }
117130
131+ /// Solo a voice. Toggling solo on the same voice clears solo mode.
118132 pub fn toggle_solo ( & mut self , voice_index : usize ) {
119133 match self . solo_index {
120134 Some ( idx) if idx == voice_index => {
@@ -133,6 +147,7 @@ impl MusicEngine {
133147 }
134148 }
135149
150+ /// Advance the scheduler by `dt`, pushing any newly scheduled `NoteEvent`s into `out_events`.
136151 pub fn tick ( & mut self , dt : Duration , now_sec : f64 , out_events : & mut Vec < NoteEvent > ) {
137152 let seconds_per_beat = 60.0 / self . params . bpm as f64 ;
138153 self . beat_accum += dt. as_secs_f64 ( ) ;
@@ -143,32 +158,21 @@ impl MusicEngine {
143158 }
144159 }
145160
161+ /// Schedule a single grid step for all voices.
146162 fn schedule_step ( & mut self , now_sec : f64 , out_events : & mut Vec < NoteEvent > ) {
147163 for ( i, voice) in self . voices . iter ( ) . enumerate ( ) {
148164 if voice. muted {
149165 continue ;
150166 }
151- // Probability to trigger per eighth note varies per voice
152- let prob = match i {
153- 0 => 0.4 ,
154- 1 => 0.6 ,
155- _ => 0.3 ,
156- } ;
157- if self . rngs [ i] . gen :: < f32 > ( ) < prob {
158- let degree = * self . params . scale . choose ( & mut self . rngs [ i] ) . unwrap_or ( & 0 ) ;
159- let octave = match i {
160- 0 => -1 ,
161- 1 => 0 ,
162- _ => 1 ,
163- } ;
167+ let prob = MusicEngine :: voice_trigger_probability ( i) ;
168+ let rng = & mut self . rngs [ i] ;
169+ if rng. gen :: < f32 > ( ) < prob {
170+ let degree = * self . params . scale . choose ( rng) . unwrap_or ( & 0 ) ;
171+ let octave = MusicEngine :: voice_octave_offset ( i) ;
164172 let midi = 60 + degree + octave * 12 ; // around middle C
165173 let freq = midi_to_hz ( midi as f32 ) ;
166- let vel = 0.4 + self . rngs [ i] . gen :: < f32 > ( ) * 0.6 ;
167- let dur = match i {
168- 0 => 0.4 ,
169- 1 => 0.25 ,
170- _ => 0.6 ,
171- } + self . rngs [ i] . gen :: < f32 > ( ) * 0.2 ;
174+ let vel = 0.4 + rng. gen :: < f32 > ( ) * 0.6 ;
175+ let dur = MusicEngine :: voice_base_duration ( i) + rng. gen :: < f32 > ( ) * 0.2 ;
172176 out_events. push ( NoteEvent {
173177 voice_index : i,
174178 frequency_hz : freq,
@@ -179,13 +183,42 @@ impl MusicEngine {
179183 }
180184 }
181185 }
186+
187+ #[ inline]
188+ fn voice_trigger_probability ( voice_index : usize ) -> f32 {
189+ match voice_index {
190+ 0 => 0.4 ,
191+ 1 => 0.6 ,
192+ _ => 0.3 ,
193+ }
194+ }
195+
196+ #[ inline]
197+ fn voice_octave_offset ( voice_index : usize ) -> i32 {
198+ match voice_index {
199+ 0 => -1 ,
200+ 1 => 0 ,
201+ _ => 1 ,
202+ }
203+ }
204+
205+ #[ inline]
206+ fn voice_base_duration ( voice_index : usize ) -> f32 {
207+ match voice_index {
208+ 0 => 0.4 ,
209+ 1 => 0.25 ,
210+ _ => 0.6 ,
211+ }
212+ }
182213}
183214
215+ /// Convert a MIDI note number to Hertz (A4=440 Hz).
184216pub fn midi_to_hz ( midi : f32 ) -> f32 {
185217 440.0 * ( 2.0_f32 ) . powf ( ( midi - 69.0 ) / 12.0 )
186218}
187219
188220#[ cfg( test) ]
221+ #[ allow( dead_code) ]
189222mod tests {
190223 use super :: * ;
191224 use std:: time:: Duration ;
@@ -195,6 +228,39 @@ mod tests {
195228 }
196229
197230 #[ test]
231+ fn engine_initializes_from_configs ( ) {
232+ let configs = vec ! [
233+ VoiceConfig {
234+ color_rgb: [ 1.0 , 0.0 , 0.0 ] ,
235+ waveform: Waveform :: Sine ,
236+ base_position: Vec3 :: new( -1.0 , 0.0 , 0.0 ) ,
237+ } ,
238+ VoiceConfig {
239+ color_rgb: [ 0.0 , 1.0 , 0.0 ] ,
240+ waveform: Waveform :: Saw ,
241+ base_position: Vec3 :: new( 1.0 , 0.0 , 0.0 ) ,
242+ } ,
243+ ] ;
244+ let params = EngineParams :: default ( ) ;
245+ let engine = MusicEngine :: new ( configs. clone ( ) , params, 42 ) ;
246+ assert_eq ! ( engine. voices. len( ) , configs. len( ) ) ;
247+ assert_eq ! ( engine. configs. len( ) , configs. len( ) ) ;
248+ assert_eq ! ( engine. voices[ 0 ] . position, configs[ 0 ] . base_position) ;
249+ assert_eq ! ( engine. voices[ 1 ] . position, configs[ 1 ] . base_position) ;
250+ }
251+
252+ #[ test]
253+ fn set_voice_position_updates_state ( ) {
254+ let configs = vec ! [ VoiceConfig {
255+ color_rgb: [ 1.0 , 0.0 , 0.0 ] ,
256+ waveform: Waveform :: Sine ,
257+ base_position: Vec3 :: new( 0.0 , 0.0 , 0.0 ) ,
258+ } ] ;
259+ let params = EngineParams :: default ( ) ;
260+ let mut engine = MusicEngine :: new ( configs, params, 1 ) ;
261+ engine. set_voice_position ( 0 , Vec3 :: new ( 2.0 , 0.0 , -1.0 ) ) ;
262+ assert_eq ! ( engine. voices[ 0 ] . position, Vec3 :: new( 2.0 , 0.0 , -1.0 ) ) ;
263+ }
198264 fn midi_to_hz_references ( ) {
199265 assert ! ( approx_eq( midi_to_hz( 69.0 ) , 440.0 , 0.01 ) ) ;
200266 // Middle C ≈ 261.6256 Hz
0 commit comments