Skip to content

Commit 4691526

Browse files
committed
Refactoring
1 parent 16c2941 commit 4691526

File tree

10 files changed

+304
-248
lines changed

10 files changed

+304
-248
lines changed

assets/scales.json

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
{
2-
"C_major": ["C4", "D4", "E4", "F4", "G4", "A4", "B4"],
3-
"A_minor": ["A3", "B3", "C4", "D4", "E4", "F4", "G4"],
4-
"D_major": ["D4", "E4", "F#4", "G4", "A4", "B4", "C#5"],
5-
"G_major": ["G3", "A3", "B3", "C4", "D4", "E4", "F#4"],
6-
"F_major": ["F3", "G3", "A3", "Bb3", "C4", "D4", "E4"],
7-
"E_minor": ["E3", "F#3", "G3", "A3", "B3", "C4", "D4"]
2+
"C_major": ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"],
3+
"C_minor": ["C4", "D4", "D#4", "F4", "G4", "G#4", "A#4", "C5"],
4+
"C#_major": ["C#4", "D#4", "F4", "F#4", "G#4", "A#4", "C5", "C#5"],
5+
"C#_minor": ["C#4", "D#4", "E4", "F#4", "G#4", "A4", "B4", "C#5"],
6+
"D_major": ["D4", "E4", "F#4", "G4", "A4", "B4", "C#5", "D5"],
7+
"D_minor": ["D4", "E4", "F4", "G4", "A4", "A#4", "C5", "D5"],
8+
"D#_major": ["D#4", "F4", "G4", "G#4", "A#4", "C5", "D5", "D#5"],
9+
"D#_minor": ["D#4", "F4", "F#4", "G#4", "A#4", "B4", "C#5", "D#5"],
10+
"E_major": ["E4", "F#4", "G#4", "A4", "B4", "C#5", "D#5", "E5"],
11+
"E_minor": ["E4", "F#4", "G4", "A4", "B4", "C5", "D5", "E5"],
12+
"F_major": ["F4", "G4", "A4", "A#4", "C5", "D5", "E5", "F5"],
13+
"F_minor": ["F4", "G4", "G#4", "A#4", "C5", "C#5", "D#5", "F5"],
14+
"F#_major": ["F#4", "G#4", "A#4", "B4", "C#5", "D#5", "F5", "F#5"],
15+
"F#_minor": ["F#4", "G#4", "A4", "B4", "C#5", "D5", "E5", "F#5"],
16+
"G_major": ["G4", "A4", "B4", "C5", "D5", "E5", "F#5", "G5"],
17+
"G_minor": ["G4", "A4", "A#4", "C5", "D5", "D#5", "F5", "G5"],
18+
"G#_major": ["G#4", "A#4", "C5", "C#5", "D#5", "F5", "G5", "G#5"],
19+
"G#_minor": ["G#4", "A#4", "B4", "C#5", "D#5", "E5", "F#5", "G#5"],
20+
"A_major": ["A4", "B4", "C#5", "D5", "E5", "F#5", "G#5", "A5"],
21+
"A_minor": ["A4", "B4", "C5", "D5", "E5", "F5", "G5", "A5"],
22+
"A#_major": ["A#4", "C5", "D5", "D#5", "F5", "G5", "A5", "A#5"],
23+
"A#_minor": ["A#4", "C5", "C#5", "D#5", "F5", "F#5", "G#5", "A#5"],
24+
"B_major": ["B4", "C#5", "D#5", "E5", "F#5", "G#5", "A#5", "B5"],
25+
"B_minor": ["B4", "C#5", "D5", "E5", "F#5", "G5", "A5", "B5"]
826
}

assets/word-styles.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"noun": ["", "bold", [100, 250], "Arial", ""],
3-
"verb": ["italic", "", [50, 300], "Arial", "uppercase"],
4-
"adjective": ["italic", "bold", [150, 350], "Times New Roman", ""],
5-
"adverb": ["italic", "bold", [100, 250], "Times New Roman", ""],
6-
"expression": ["", "", [100, 390], "Comic Sans MS", "uppercase"],
7-
"default": ["", "", [100, 400], "Times New Roman", ""]
2+
"noun": ["", "bold", [100, 300], "Arial", ""],
3+
"verb": ["italic", "", [50, 350], "Arial", "uppercase"],
4+
"adjective": ["italic", "bold", [150, 400], "Times New Roman", ""],
5+
"adverb": ["italic", "bold", [100, 300], "Times New Roman", ""],
6+
"expression": ["", "", [100, 400], "Comic Sans MS", "uppercase"],
7+
"default": ["", "", [100, 450], "Times New Roman", ""]
88
}

src/scene-audio.js renamed to src-words2music/audio.js

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
export let instruments = {};
1+
// Some simple synthetic instruments
2+
export const metroSynth = new Tone.MembraneSynth().toDestination();
23

4+
// Load tonejs-instruments sample library
5+
export let instruments = {};
36
export async function loadIntsruments() {
47
instruments = await SampleLibrary.load({
58
instruments: ['piano', 'bass-electric', 'bassoon', 'cello', 'clarinet', 'contrabass', 'french-horn', 'guitar-acoustic', 'guitar-electric', 'guitar-nylon', 'harp', 'organ', 'saxophone', 'trombone', 'trumpet', 'tuba', 'violin'],
@@ -9,29 +12,62 @@ export async function loadIntsruments() {
912
await Tone.loaded();
1013
}
1114

12-
export async function playDotSound(dot, instrument, volume = 0, scale = null, snapTime = null) {
13-
const minPitch = 220; // lowest frequency (Hz)
14-
const maxPitch = 880; // highest frequency (Hz)
15+
let lastSoundTime = 0;
1516

16-
const normFactor = Math.max(Math.min(1 - (dot.y / dot.height), 1), 0); // 1 at top, 0 at bottom
17-
var frequency = minPitch + normFactor * (maxPitch - minPitch);
18-
if (scale!== null) {
19-
// frequency = noteId2frequency(frequency2noteId(frequency, true));
20-
frequency = snapFrequencyToScale(frequency, scale);
17+
export async function playDotSound(y, instrument, scale = null, length = "8n", volume = 0, snapTime = null) {
18+
if (Tone.now() - lastSoundTime < 0.03) {
19+
console.log("Too soon to play sound");
20+
return;
2121
}
2222

23-
// Set volume if specified
23+
// Handle frequency
24+
var frequency;
25+
if (scale === "all") {
26+
let yNorm;
27+
if (typeof y === "number") {
28+
yNorm = y;
29+
} else {
30+
yNorm = Math.max(Math.min((1 - y.y / y.height), 1), 0); // 1 at top, 0 at bottom
31+
yNorm = 55 + yNorm * (880 - 55);
32+
}
33+
frequency = noteId2frequency(frequency2noteId(yNorm, true));
34+
} else if (typeof scale === "string" || typeof scale === "number") {
35+
frequency = scale;
36+
} else if (Array.isArray(scale)) {
37+
let yNorm;
38+
if (typeof y === "number") {
39+
yNorm = y;
40+
} else {
41+
yNorm = Math.max(Math.min((1 - y.y / y.height), 1), 0); // 1 at top, 0 at bottom
42+
}
43+
frequency = snapPos2scale(yNorm, scale);
44+
}
45+
46+
// Handle instrument
47+
if (typeof instrument === "string") {
48+
instrument = instruments[instrument]
49+
} else if (typeof instrument === "object") {
50+
instrument = instrument;
51+
}
52+
53+
// Handle volume
2454
if (volume !== null) {
25-
instruments[instrument].volume.value = volume; // in dB (e.g., -12)
55+
instrument.volume.value = volume; // in dB (e.g., -12)
56+
}
57+
instrument.toDestination();
58+
59+
// Handle length
60+
if (length == null) {
61+
length = "8n";
2662
}
27-
instruments[instrument].toDestination();
2863

64+
// Handle rhythm snapping
2965
if (snapTime == null) {
30-
instruments[instrument].triggerAttackRelease(frequency, "8n");
66+
instrument.triggerAttackRelease(frequency, "8n");
3167
return;
3268
}
3369
const time = snap2subdivision(snapTime);
34-
instruments[instrument].triggerAttackRelease(frequency, "4n", time);
70+
instrument.triggerAttackRelease(frequency, length, time);
3571
}
3672

3773
export async function playBaseSound(dot) {
@@ -51,17 +87,15 @@ export async function playBaseSound(dot) {
5187
synth.triggerAttackRelease("C3", "8n", time);
5288
}
5389

54-
function snap2subdivision(subdivision = "8n") {
90+
function snap2subdivision(subdivision = "8n", tol = 0.03) {
5591
const now = Tone.now();
56-
console.log("snapping")
5792
const nextTime = Tone.Transport.nextSubdivision(subdivision);
5893

5994
// Check current time is not close to the subdivision
6095
const nowTime = nextTime - Tone.Time(subdivision).toSeconds();
6196

62-
if (Math.abs(nowTime - now) < 0.03) {
63-
// console.log("Current time is close to the subdivision, using it");
64-
return now + 0.03;
97+
if (Math.abs(nowTime - now) < tol) {
98+
return now + 0.001;
6599
}
66100

67101
// return nextTime;
@@ -127,17 +161,18 @@ export async function initialseSound() {
127161
}
128162
}
129163

130-
function snapFrequencyToScale(frequency, currentScale = ["C4", "D4", "E4", "F4", "G4", "A4", "B4"]) {
131-
console.log("Snapping frequency to scale:", frequency, currentScale);
164+
function snapPos2scale(y, currentScale = ["C4", "D4", "E4", "F4", "G4", "A4", "B4"]) {
132165
const scaleFreqs = currentScale.map(note => Tone.Frequency(note).toFrequency());
166+
// Lerp y to the bound of the scale
167+
const freq = Math.min(...scaleFreqs) + y * (Math.max(...scaleFreqs) - Math.min(...scaleFreqs));
133168

134169
let closest = scaleFreqs[0];
135-
let minDiff = Math.abs(frequency - closest);
170+
let minDiff = Math.abs(freq - closest);
136171

137-
for (let f of scaleFreqs) {
138-
const diff = Math.abs(frequency - f);
172+
for (let scaleFreq of scaleFreqs) {
173+
const diff = Math.abs(freq - scaleFreq);
139174
if (diff < minDiff) {
140-
closest = f;
175+
closest = scaleFreq;
141176
minDiff = diff;
142177
}
143178
}
File renamed without changes.

src/scrolling-scene.js renamed to src-words2music/scene.js

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Dot } from "./dot-physics.js"
2-
import { getWordType, addWordToCanvas } from "./word-processing.js"
3-
import { playDotSound, playBaseSound } from "./scene-audio.js";
4-
import { loadIntsruments, instruments } from "./scene-audio.js";
1+
import { Dot } from "./dot.js"
2+
import { getWordType, addWordToCanvas } from "./words.js"
3+
import { playDotSound, playBaseSound } from "./audio.js";
4+
import { loadIntsruments, instruments, metroSynth } from "./audio.js";
55

66
let fontStyles = {};
77
let scales = {}; // where all scales will be stored
88
let currentScale = []; // active scale during playback
9+
910
export class ScrollingScene {
1011
constructor(canvas, inputElement) {
1112
this.mainCtx = canvas.getContext("2d");
@@ -23,39 +24,10 @@ export class ScrollingScene {
2324
this.wordsCanvas.width = this.width * 5;
2425
this.wordsCanvas.height = this.height;
2526
this.wordsCtx = this.wordsCanvas.getContext("2d", { willReadFrequently: true });
26-
// document.body.appendChild(this.wordsCanvas);
2727
this.inputElement = inputElement;
2828
this.dot = new Dot(1, 5);
29-
this.dotXTarget = this.dot.x;
30-
29+
this.dotXTarget = this.dot.x; // where the dot should be
3130
this.clearSentenceBuffer = false;
32-
this.inputElement.addEventListener("input", e => {
33-
const value = e.target.value;
34-
const lastChar = value[value.length - 1];
35-
36-
if (this.clearSentenceBuffer) {
37-
this.sentenceBuffer = "";
38-
this.clearSentenceBuffer = false;
39-
}
40-
41-
if ((lastChar === " " || lastChar === "\n")) {
42-
this.wordNeedsUpdate = true;
43-
this.sentenceBuffer = this.sentenceBuffer + " " + value;
44-
input.value = "";
45-
const punctuationRegex = /[.,!?;:]/;
46-
if (punctuationRegex.test(this.wordBuffer)) {
47-
this.clearSentenceBuffer = true;
48-
}
49-
} else {
50-
this.wordBuffer = value;
51-
}
52-
53-
const wordBufferContainer = document.getElementById("word-buffer-container");
54-
wordBufferContainer.innerText = `Word: ${this.wordBuffer}`;
55-
const sentenceBufferContainer = document.getElementById("sentence-buffer-container");
56-
sentenceBufferContainer.innerText = `Sentence: ${this.sentenceBuffer}`;
57-
});
58-
5931
this.updateScene = false;
6032
}
6133

@@ -70,10 +42,14 @@ export class ScrollingScene {
7042
}
7143

7244
addWordToCanvas(this.wordsCtx, this.wordBuffer, this, this.fontStyle);
73-
const wordTypeContainer = document.getElementById("word-type-container");
74-
wordTypeContainer.innerText = wordType;
7545
this.wordBuffer = "";
7646
this.wordNeedsUpdate = false;
47+
48+
// Debugging info
49+
const wordTypeContainer = document.getElementById("word-type-container");
50+
if (wordTypeContainer) {
51+
wordTypeContainer.innerText = wordType;
52+
}
7753
}
7854

7955
const wordsImgBuffer = this.wordsCtx.getImageData(0, 0, this.width, this.height);
@@ -97,24 +73,17 @@ export class ScrollingScene {
9773
Tone.Transport.start();
9874
this.metronomeStarted = true;
9975
}
76+
10077
// Handle dot effects
101-
// const soundOn = document.getElementById("toggleSound").checked;
102-
// if (soundOn) {
10378
if (this.dot.hasCollidedWithGround) {
104-
try {
105-
// playBaseSound(this.dot);
106-
playDotSound(this.dot, "piano", 0, currentScale, "4n");
107-
} catch (e) {
108-
console.warning("Error playing sound:", e);
109-
}
79+
// Ground bounce
80+
this.dot.colour = "red";
11081
} else if (this.dot.inFloatingMode) {
11182
// Floating mode
11283
this.dot.colour = "green";
113-
playDotSound(this.dot, "guitar-nylon", -20, currentScale, "16n");
11484
} else if (this.dot.hasCollided) {
11585
// Collision
11686
this.dot.colour = "red";
117-
playDotSound(this.dot, "piano", 0, currentScale, "8n");
11887
} else {
11988
// Normal mode
12089
this.dot.colour = "blue";
@@ -124,6 +93,24 @@ export class ScrollingScene {
12493
this.mainCtx.fillStyle = "black";
12594
// drawCursor(this.mainCtx, this.cursorX, this.cursorY);
12695
}
96+
97+
play() {
98+
if (this.dot.hasCollidedWithGround) {
99+
// Ground bounce
100+
try {
101+
playDotSound(this.dot, "piano", "C2", "2n", -10, "4n");
102+
} catch (e) {
103+
console.warning("Error playing sound:", e);
104+
}
105+
} else if (this.dot.inFloatingMode) {
106+
// Floating mode
107+
playDotSound(this.dot, new Tone.MembraneSynth().toDestination(),
108+
"all", "16n", -20, "16n");
109+
} else if (this.dot.hasCollided) {
110+
// Collision
111+
playDotSound(this.dot, "piano", currentScale, "8n", 0, "8n");
112+
}
113+
}
127114
}
128115

129116
function drawCursor(ctx, cursorX, cursorY) {
@@ -213,9 +200,46 @@ function traceWordOutline(startX, startY, word) {
213200
}
214201

215202
export async function setupScene(scene, bpm = 120) {
216-
// Hide start button
217-
const startButton = document.getElementById("updateScene");
218-
startButton.style.display = "none";
203+
// User input
204+
scene.inputElement.addEventListener("input", e => {
205+
const value = e.target.value;
206+
const lastChar = value[value.length - 1];
207+
208+
if (scene.clearSentenceBuffer) {
209+
scene.sentenceBuffer = "";
210+
scene.clearSentenceBuffer = false;
211+
}
212+
213+
if ((lastChar === " " || lastChar === "\n")) {
214+
scene.wordNeedsUpdate = true;
215+
scene.sentenceBuffer = scene.sentenceBuffer + " " + value;
216+
input.value = "";
217+
const punctuationRegex = /[.,!?;:]/;
218+
if (punctuationRegex.test(scene.wordBuffer)) {
219+
scene.clearSentenceBuffer = true;
220+
}
221+
} else {
222+
scene.wordBuffer = value;
223+
}
224+
225+
// Debugging info
226+
const wordBufferContainer = document.getElementById("word-buffer-container");
227+
if (wordBufferContainer) {
228+
wordBufferContainer.innerText = `Word: ${scene.wordBuffer}`;
229+
}
230+
const sentenceBufferContainer = document.getElementById("sentence-buffer-container");
231+
if (sentenceBufferContainer) {
232+
sentenceBufferContainer.innerText = `Sentence: ${scene.sentenceBuffer}`;
233+
}
234+
});
235+
236+
scene.inputElement.addEventListener("keydown", (e) => {
237+
if (!(e.key === "Backspace" || e.key === "Delete")) {
238+
return;
239+
}
240+
playDotSound(null, "piano", "C7", "4n", -10, "8n");
241+
});
242+
219243
// Scene & audio
220244
scene.bpm = bpm;
221245
scene.lastTimestamp = null;
@@ -233,11 +257,10 @@ export async function setupScene(scene, bpm = 120) {
233257
Object.assign(instruments, loaded);
234258
console.log("SampleLibrary loaded: ", Object.keys(instruments));
235259
await Tone.loaded();
236-
instruments["piano"].toDestination();
237-
instruments["piano"].triggerAttackRelease("C4", "8n", Tone.now());
238-
const metroSynth = new Tone.MembraneSynth().toDestination();
260+
239261
Tone.Transport.scheduleRepeat((time) => {
240-
metroSynth.triggerAttackRelease("C3", "8n", time);
262+
metroSynth.triggerAttackRelease("G2", "8n", time);
263+
// metroSynth.triggerAttackRelease(currentScale[0], "8n", time);
241264
}, "4n");
242265
Tone.Transport.scheduleRepeat(() => {
243266
const pos = Tone.Transport.position.split('.')[0];
@@ -285,9 +308,6 @@ export async function setupScene(scene, bpm = 120) {
285308
scene.dot.maxSpeed =
286309
Math.sqrt(2 * Math.abs(scene.dot.g) * (scene.dot.height - scene.dot.y - scene.dot.r / scene.dot.yScale));
287310
console.log("max speed:", scene.dot.maxSpeed);
288-
289-
// Show start button
290-
startButton.style.display = "inline";
291311
}
292312

293313
async function loadFontStyles(src) {

0 commit comments

Comments
 (0)