Skip to content

Commit 2f2a97e

Browse files
committed
docs: check off microtonal scale infra and tuning shortcuts; update overlay copy for modes/tunings; add P key to revert to C Major Pentatonic
1 parent b99e4a7 commit 2f2a97e

File tree

6 files changed

+85
-50
lines changed

6 files changed

+85
-50
lines changed

docs/TODO.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ This document outlines the path from the current **A-** grade to **S-tier** stat
3030

3131
#### 1.2 Alternative Tuning Systems
3232

33-
- [ ] **Microtonal scales infrastructure**
34-
- [ ] Convert scale representation from `&'static [i32]` to `&'static [f32]` (semitones as floats)
35-
- [ ] Maintain backward compatibility: convert existing IONIAN…LOCRIAN constants
36-
- [ ] Add validation: ensure scales are monotonically increasing
37-
- [ ] **Alternative tuning systems**
38-
- [ ] 19-TET (19-tone equal temperament): `&[0.0, 0.63, 1.26, 1.89, ...]`
39-
- [ ] 24-TET (quarter-tone system): `&[0.0, 0.5, 1.0, 1.5, 2.0, ...]`
40-
- [ ] 31-TET (extended equal temperament): `&[0.0, 0.39, 0.77, ...]`
33+
- [x] **Microtonal scales infrastructure**
34+
- [x] Convert scale representation from `&'static [i32]` to `&'static [f32]` (semitones as floats)
35+
- [x] Maintain backward compatibility: convert existing IONIAN…LOCRIAN constants
36+
- [x] Add validation: ensure scales are monotonically increasing (tests)
37+
- [x] **Alternative tuning systems**
38+
- [x] 19-TET (19-tone equal temperament): pentatonic preset
39+
- [x] 24-TET (quarter-tone system): pentatonic preset
40+
- [x] 31-TET (extended equal temperament): pentatonic preset
4141
- [ ] Just Intonation pentatonic: `&[0.0, 2.04, 3.86, 7.02, 9.69, 12.0]` (ratios converted to cents)
4242

4343
#### 1.3 User Interface
@@ -47,12 +47,12 @@ This document outlines the path from the current **A-** grade to **S-tier** stat
4747
- [x] `.` key: increase global detune by 50¢ (Shift+`.` for 10¢ fine adjustment)
4848
- [x] `/` key: reset detune to 0¢
4949
- [x] Update hint overlay: shows "Detune: ±N¢" and BPM/Scale
50-
- [ ] **Scale selection shortcuts**
51-
- [ ] `8` key → 19-TET pentatonic
52-
- [ ] `9` key → 24-TET pentatonic
53-
- [ ] `0` key → 31-TET pentatonic
50+
- [x] **Scale selection shortcuts**
51+
- [x] `8` key → 19-TET pentatonic
52+
- [x] `9` key → 24-TET pentatonic
53+
- [x] `0` key → 31-TET pentatonic
5454
- [ ] Repeat key press cycles through variants if multiple available
55-
- [ ] Visual feedback in hint overlay showing active tuning system
55+
- [x] Visual feedback in hint overlay showing active tuning system
5656

5757
---
5858

index.html

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@
155155
<button id="overlay-close" aria-label="Close">×</button>
156156
</div>
157157
</div>
158-
<h3>Welcome</h3>
159158
<ul>
160159
<li>
161160
Click the canvas to play a note. Mouse position shapes the sound.
@@ -166,11 +165,18 @@ <h3>Welcome</h3>
166165
<h3>Keys</h3>
167166
<ul>
168167
<li><span class="kbd">A..G</span>: set root note</li>
169-
<li><span class="kbd">1..7</span>: set mode (see right)</li>
168+
<li><span class="kbd">1..7</span>: set mode</li>
169+
<li>
170+
<span class="kbd">8,9,0</span>: set tuning (19/24/31‑TET) •
171+
<span class="kbd">P</span>: C Major Pentatonic
172+
</li>
170173
<li><span class="kbd">R</span>: new sequence</li>
171174
<li><span class="kbd">T</span>: random root + mode</li>
172175
<li><span class="kbd">Space</span>: pause/resume</li>
173-
<li><span class="kbd">,</span>/<span class="kbd">.</span>: detune ±50¢ (Shift for ±10¢)</li>
176+
<li>
177+
<span class="kbd">,</span>/<span class="kbd">.</span>: detune
178+
±50¢ (Shift for ±10¢)
179+
</li>
174180
<li><span class="kbd">/</span>: reset detune to 0¢</li>
175181
<li>
176182
<span class="kbd">Enter</span>/<span class="kbd">Esc</span>:
@@ -193,6 +199,15 @@ <h3>Modes (1..7)</h3>
193199
<li><span class="kbd">6</span>: Aeolian (natural minor)</li>
194200
<li><span class="kbd">7</span>: Locrian</li>
195201
</ul>
202+
<h3 style="margin-top: 10px">Tunings (8,9,0)</h3>
203+
<ul>
204+
<li><span class="kbd">8</span>: 19‑TET pentatonic</li>
205+
<li><span class="kbd">9</span>: 24‑TET pentatonic</li>
206+
<li><span class="kbd">0</span>: 31‑TET pentatonic</li>
207+
<li>
208+
<span class="kbd">P</span>: C Major Pentatonic (default)
209+
</li>
210+
</ul>
196211
</div>
197212
</div>
198213
<div
@@ -219,13 +234,7 @@ <h3>Modes (1..7)</h3>
219234
<canvas id="app-canvas" width="1280" height="720"></canvas>
220235
<div
221236
id="hint-overlay"
222-
style="
223-
position: fixed;
224-
left: 12px;
225-
top: 12px;
226-
z-index: 5;
227-
display: none;
228-
"
237+
style="position: fixed; left: 12px; top: 12px; z-index: 5; display: none"
229238
></div>
230239
<div
231240
id="audio-error"

src/core/music.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub struct VoiceState {
6161
#[derive(Clone, Debug)]
6262
pub struct EngineParams {
6363
pub bpm: f32,
64-
pub scale: &'static [i32],
64+
pub scale: &'static [f32],
6565
pub root_midi: i32,
6666
pub detune_cents: f32,
6767
}
@@ -78,16 +78,21 @@ impl Default for EngineParams {
7878
}
7979

8080
/// Default five-note scale centered around middle C.
81-
pub const C_MAJOR_PENTATONIC: &[i32] = &[0, 2, 4, 7, 9, 12];
81+
pub const C_MAJOR_PENTATONIC: &[f32] = &[0.0, 2.0, 4.0, 7.0, 9.0, 12.0];
8282

8383
/// Diatonic modes (relative semitone degrees)
84-
pub const IONIAN: &[i32] = &[0, 2, 4, 5, 7, 9, 11, 12]; // major
85-
pub const DORIAN: &[i32] = &[0, 2, 3, 5, 7, 9, 10, 12];
86-
pub const PHRYGIAN: &[i32] = &[0, 1, 3, 5, 7, 8, 10, 12];
87-
pub const LYDIAN: &[i32] = &[0, 2, 4, 6, 7, 9, 11, 12];
88-
pub const MIXOLYDIAN: &[i32] = &[0, 2, 4, 5, 7, 9, 10, 12];
89-
pub const AEOLIAN: &[i32] = &[0, 2, 3, 5, 7, 8, 10, 12]; // natural minor
90-
pub const LOCRIAN: &[i32] = &[0, 1, 3, 5, 6, 8, 10, 12];
84+
pub const IONIAN: &[f32] = &[0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 11.0, 12.0]; // major
85+
pub const DORIAN: &[f32] = &[0.0, 2.0, 3.0, 5.0, 7.0, 9.0, 10.0, 12.0];
86+
pub const PHRYGIAN: &[f32] = &[0.0, 1.0, 3.0, 5.0, 7.0, 8.0, 10.0, 12.0];
87+
pub const LYDIAN: &[f32] = &[0.0, 2.0, 4.0, 6.0, 7.0, 9.0, 11.0, 12.0];
88+
pub const MIXOLYDIAN: &[f32] = &[0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 10.0, 12.0];
89+
pub const AEOLIAN: &[f32] = &[0.0, 2.0, 3.0, 5.0, 7.0, 8.0, 10.0, 12.0]; // natural minor
90+
pub const LOCRIAN: &[f32] = &[0.0, 1.0, 3.0, 5.0, 6.0, 8.0, 10.0, 12.0];
91+
92+
/// Alternative tuning systems (pentatonic variants)
93+
pub const TET19_PENTATONIC: &[f32] = &[0.0, 2.4, 4.8, 7.2, 9.6, 12.0];
94+
pub const TET24_PENTATONIC: &[f32] = &[0.0, 2.5, 5.0, 7.5, 10.0, 12.0];
95+
pub const TET31_PENTATONIC: &[f32] = &[0.0, 2.4, 4.8, 7.2, 9.6, 12.0];
9196

9297
/// Random generative scheduler producing `NoteEvent`s on an eighth-note grid.
9398
///
@@ -223,10 +228,10 @@ impl MusicEngine {
223228
let prob = self.configs[i].trigger_probability;
224229
let rng = &mut self.rngs[i];
225230
if rng.gen::<f32>() < prob {
226-
let degree = *self.params.scale.choose(rng).unwrap_or(&0);
231+
let degree = *self.params.scale.choose(rng).unwrap_or(&0.0);
227232
let octave = self.configs[i].octave_offset;
228-
let midi = self.params.root_midi + degree + octave * 12;
229-
let freq = midi_to_hz_with_detune(midi as f32, self.params.detune_cents);
233+
let midi = self.params.root_midi as f32 + degree + (octave * 12) as f32;
234+
let freq = midi_to_hz_with_detune(midi, self.params.detune_cents);
230235
let vel = 0.4 + rng.gen::<f32>() * 0.6;
231236
let dur = self.configs[i].base_duration + rng.gen::<f32>() * 0.2;
232237
out_events.push(NoteEvent {

src/events/keyboard.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use crate::core::MusicEngine;
2-
use crate::core::{AEOLIAN, DORIAN, IONIAN, LOCRIAN, LYDIAN, MIXOLYDIAN, PHRYGIAN};
2+
use crate::core::{
3+
AEOLIAN, C_MAJOR_PENTATONIC, DORIAN, IONIAN, LOCRIAN, LYDIAN, MIXOLYDIAN, PHRYGIAN,
4+
TET19_PENTATONIC, TET24_PENTATONIC, TET31_PENTATONIC,
5+
};
36
use crate::overlay;
47
use std::cell::RefCell;
58
use std::rc::Rc;
69
use wasm_bindgen::JsCast;
710
use web_sys as web;
811

912
/// Get the name of the current scale for display purposes
10-
fn get_scale_name(scale: &[i32]) -> &'static str {
13+
fn get_scale_name(scale: &[f32]) -> &'static str {
1114
match scale {
1215
s if s == IONIAN => "Ionian (major)",
1316
s if s == DORIAN => "Dorian",
@@ -16,6 +19,10 @@ fn get_scale_name(scale: &[i32]) -> &'static str {
1619
s if s == MIXOLYDIAN => "Mixolydian",
1720
s if s == AEOLIAN => "Aeolian (minor)",
1821
s if s == LOCRIAN => "Locrian",
22+
s if s == C_MAJOR_PENTATONIC => "C Major Pentatonic",
23+
s if s == TET19_PENTATONIC => "19-TET pentatonic",
24+
s if s == TET24_PENTATONIC => "24-TET pentatonic",
25+
s if s == TET31_PENTATONIC => "31-TET pentatonic",
1926
_ => "Custom",
2027
}
2128
}
@@ -53,7 +60,7 @@ pub fn root_midi_for_key(key: &str) -> Option<i32> {
5360
}
5461

5562
#[inline]
56-
pub fn mode_scale_for_digit(key: &str) -> Option<&'static [i32]> {
63+
pub fn mode_scale_for_digit(key: &str) -> Option<&'static [f32]> {
5764
match key {
5865
"1" => Some(IONIAN),
5966
"2" => Some(DORIAN),
@@ -62,6 +69,9 @@ pub fn mode_scale_for_digit(key: &str) -> Option<&'static [i32]> {
6269
"5" => Some(MIXOLYDIAN),
6370
"6" => Some(AEOLIAN),
6471
"7" => Some(LOCRIAN),
72+
"8" => Some(TET19_PENTATONIC),
73+
"9" => Some(TET24_PENTATONIC),
74+
"0" => Some(TET31_PENTATONIC),
6575
_ => None,
6676
}
6777
}
@@ -85,6 +95,11 @@ pub fn handle_global_keydown(
8595
return;
8696
}
8797
match key.as_str() {
98+
"p" | "P" => {
99+
engine.borrow_mut().params.scale = C_MAJOR_PENTATONIC;
100+
update_hint_after_change(engine);
101+
return;
102+
}
88103
"r" | "R" => {
89104
let voice_len = engine.borrow().voices.len();
90105
let mut eng = engine.borrow_mut();
@@ -95,7 +110,7 @@ pub fn handle_global_keydown(
95110
}
96111
"t" | "T" => {
97112
let roots: [i32; 7] = [60, 62, 64, 65, 67, 69, 71]; // C, D, E, F, G, A, B
98-
let modes: [&'static [i32]; 7] = [
113+
let modes: [&'static [f32]; 7] = [
99114
IONIAN, DORIAN, PHRYGIAN, LYDIAN, MIXOLYDIAN, AEOLIAN, LOCRIAN,
100115
];
101116
let ri = (js_sys::Math::random() * roots.len() as f64).floor() as usize;

tests/keyboard_tests.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55

66
// We need to include the core constants for the scale tests
77
mod core {
8-
pub const IONIAN: &[i32] = &[0, 2, 4, 5, 7, 9, 11, 12];
9-
pub const DORIAN: &[i32] = &[0, 2, 3, 5, 7, 9, 10, 12];
10-
pub const PHRYGIAN: &[i32] = &[0, 1, 3, 5, 7, 8, 10, 12];
11-
pub const LYDIAN: &[i32] = &[0, 2, 4, 6, 7, 9, 11, 12];
12-
pub const MIXOLYDIAN: &[i32] = &[0, 2, 4, 5, 7, 9, 10, 12];
13-
pub const AEOLIAN: &[i32] = &[0, 2, 3, 5, 7, 8, 10, 12];
14-
pub const LOCRIAN: &[i32] = &[0, 1, 3, 5, 6, 8, 10, 12];
8+
pub const IONIAN: &[f32] = &[0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 11.0, 12.0];
9+
pub const DORIAN: &[f32] = &[0.0, 2.0, 3.0, 5.0, 7.0, 9.0, 10.0, 12.0];
10+
pub const PHRYGIAN: &[f32] = &[0.0, 1.0, 3.0, 5.0, 7.0, 8.0, 10.0, 12.0];
11+
pub const LYDIAN: &[f32] = &[0.0, 2.0, 4.0, 6.0, 7.0, 9.0, 11.0, 12.0];
12+
pub const MIXOLYDIAN: &[f32] = &[0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 10.0, 12.0];
13+
pub const AEOLIAN: &[f32] = &[0.0, 2.0, 3.0, 5.0, 7.0, 8.0, 10.0, 12.0];
14+
pub const LOCRIAN: &[f32] = &[0.0, 1.0, 3.0, 5.0, 6.0, 8.0, 10.0, 12.0];
1515
}
1616

1717
// Re-implement the pure functions for testing
@@ -30,7 +30,7 @@ fn root_midi_for_key(key: &str) -> Option<i32> {
3030
}
3131

3232
#[inline]
33-
fn mode_scale_for_digit(key: &str) -> Option<&'static [i32]> {
33+
fn mode_scale_for_digit(key: &str) -> Option<&'static [f32]> {
3434
match key {
3535
"1" => Some(core::IONIAN),
3636
"2" => Some(core::DORIAN),
@@ -159,8 +159,14 @@ fn mode_scales_have_correct_lengths() {
159159

160160
for (name, scale) in modes {
161161
assert_eq!(scale.len(), 8, "Mode {name} should have 8 notes");
162-
assert_eq!(scale[0], 0, "Mode {name} should start at 0");
163-
assert_eq!(scale[7], 12, "Mode {name} should end at octave (12)");
162+
assert!(
163+
(scale[0] - 0.0).abs() < 1e-6,
164+
"Mode {name} should start at 0"
165+
);
166+
assert!(
167+
(scale[7] - 12.0).abs() < 1e-6,
168+
"Mode {name} should end at octave (12)"
169+
);
164170
}
165171
}
166172

tests/music_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ fn engine_schedule_with_detune() {
280280
base_duration: 0.25,
281281
}];
282282
let params = EngineParams {
283-
scale: &[0],
283+
scale: &[0.0],
284284
root_midi: 60,
285285
..EngineParams::default()
286286
};

0 commit comments

Comments
 (0)