Skip to content

Commit cda2278

Browse files
committed
Updated sound engine, should be much more stable now
1 parent 2299acf commit cda2278

File tree

8 files changed

+154
-31
lines changed

8 files changed

+154
-31
lines changed

assets/custom-questions.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
[
3+
"I don't know you.",
4+
"What's your name?"
5+
]
6+
]

assets/word-buffers.json

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
{
22
"nounCtx": {
3-
"canvas": "nounCanvas",
3+
"canvas": "nounCanvas",
44
"order": 1,
5-
"scrollSpeed": 1,
5+
"scrollSpeed": 1,
66
"cursor": ["cursorX", "cursorY"],
7-
"cursorOffset": [0, -20]
7+
"cursorOffset": [0, -20]
88
},
99
"adjCtx": {
10-
"canvas": "adjCanvas",
10+
"canvas": "adjCanvas",
1111
"order": 0,
12-
"scrollSpeed": 0.8,
12+
"scrollSpeed": 0.8,
1313
"cursor": ["adjCursorX", "adjCursorY"],
14-
"cursorOffset": [0, 0]
14+
"cursorOffset": [0, 0]
15+
},
16+
"questionCtx": {
17+
"canvas": "questionCanvas",
18+
"order": 2,
19+
"scrollSpeed": 0.2,
20+
"cursor": ["cursorX", "cursorY"],
21+
"cursorOffset": [0, 0]
1522
}
1623
}

assets/word-styles.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,10 @@
5858
"buffer": "nounCtx",
5959
"style": ["", "", [50, 250], "Times New Roman", "", "black", "alphabetic"],
6060
"offsetRatio": [1.0, 0.0]
61+
},
62+
"custom-sentence": {
63+
"buffer": "questionCtx",
64+
"style": ["", "", [40, 40], "monospace", "", "black", "alphabetic"],
65+
"offsetRatio": [1.0, 0.0]
6166
}
6267
}

src-words2music/audio.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1+
export const audioCtx = new (window.AudioContext || window.webkitAudioContext)({
2+
latencyHint: "interactive", // Use "interactive" for low latency
3+
}); // AudioContext for the browser
4+
console.log("AudioContext initialised:", audioCtx);
5+
16
// Some simple synthetic instruments
27
export const metroSynth = new Tone.MembraneSynth().toDestination();
8+
export const glitchSynth = new Tone.MembraneSynth().toDestination();
9+
export const teleportSynth = new Tone.MonoSynth({
10+
oscillator: { type: "sine" },
11+
envelope: {
12+
attack: 0.5,
13+
decay: 0.2,
14+
sustain: 0.1,
15+
release: 2
16+
},
17+
filter: {
18+
Q: 3,
19+
type: "lowpass",
20+
rolloff: -12
21+
}
22+
})
323

424
// Load tonejs-instruments sample library
525
export let instruments = {};
626
export async function loadIntsruments() {
727
instruments = await SampleLibrary.load({
8-
instruments: ['piano', 'bass-electric', 'bassoon', 'cello', 'clarinet', 'contrabass', 'french-horn', 'guitar-acoustic', 'guitar-electric', 'guitar-nylon', 'harp', 'organ', 'saxophone', 'trombone', 'trumpet', 'tuba', 'violin'],
28+
instruments: ['piano', 'bass-electric', 'bassoon', 'cello', 'clarinet', 'contrabass', 'french-horn', 'guitar-acoustic', 'guitar-electric', 'guitar-nylon', 'harp', 'organ', 'saxophone', 'trombone', 'trumpet', 'tuba', 'violin', 'xylophone'],
929
baseUrl: "src/tonejs-instruments/samples/",
1030
})
1131
console.log("SampleLibrary loaded: ", Object.keys(instruments));
@@ -15,11 +35,7 @@ export async function loadIntsruments() {
1535
let lastSoundTime = 0;
1636

1737
export async function playDotSound(y, instrument, scale = null, length = "8n", volume = 0, snapTime = null) {
18-
if (Tone.now() - lastSoundTime < 0.02) {
19-
console.log("Too soon to play sound");
20-
return;
21-
}
22-
lastSoundTime = Tone.now();
38+
await audioCtx.resume();
2339

2440
// Handle frequency
2541
var frequency;
@@ -68,6 +84,15 @@ export async function playDotSound(y, instrument, scale = null, length = "8n", v
6884
return;
6985
}
7086
const time = snap2subdivision(snapTime);
87+
try {
88+
const lastTime = y.lastQuickAudioTime;
89+
if (Math.abs(time - lastTime) < 0.01) {
90+
console.log("Skipping sound: too close to last sound");
91+
return;
92+
}
93+
y.lastQuickAudioTime = time;
94+
} catch (e) {
95+
}
7196
instrument.triggerAttackRelease(frequency, length, time);
7297
}
7398

src-words2music/dot.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ export class Dot {
22
constructor(x, radius) {
33
this.x = x;
44
this.y = 0;
5-
this.u = 0.3;
5+
this.u = 1;
6+
this.u0 = 1;
67
this.v = 0;
78
this.r = radius;
89
this.g = 12;
9-
this.viscosity = 4;
10+
this.vViscosity = 4;
11+
this.hViscosity = 2;
1012
this.xPrev = this.x;
1113
this.yPrev = this.y;
1214
this.uPrev = this.u;
@@ -32,11 +34,17 @@ export class Dot {
3234
}
3335

3436
// x-motion
37+
if (this.inSlowMo) {
38+
this.u -= this.hViscosity * this.u * deltaTime;
39+
this.u = Math.max(this.u, this.u0 * 0.5);
40+
} else {
41+
this.u += this.hViscosity * (this.u0 - this.u) * deltaTime;
42+
}
3543
this.xTarget = this.x + this.u * deltaTime;
3644

3745
// y-motion
3846
if (this.inSlowMo) {
39-
this.v -= this.viscosity * this.v * deltaTime;
47+
this.v -= this.vViscosity * this.v * deltaTime;
4048
this.v += (0.2 * this.g) * deltaTime;
4149
} else if (this.inFloatingMode) {
4250
this.v += (-0.5 * this.g) * deltaTime;
@@ -78,6 +86,13 @@ export class Dot {
7886
if (this.inFloatingMode || this.inSlowMo) {
7987
this.x = this.xTarget;
8088
this.y = this.yTarget;
89+
if (collisionResult.hasCollided == 1) {
90+
this.inFloatingMode = true;
91+
this.inSlowMo = false;
92+
} else if (collisionResult.hasCollided == 2) {
93+
this.inFloatingMode = false;
94+
this.inSlowMo = true;
95+
}
8196
return;
8297
}
8398

@@ -117,6 +132,8 @@ export class Dot {
117132
const randomFactor = Math.random();
118133
if (randomFactor < 0.7) {
119134
this.v *= 1 / 2;
135+
} else if (randomFactor < 0.8) {
136+
this.v *= 2;
120137
}
121138
// console.log(`Collision detected at: x = ${this.x.toFixed(2)}, y = ${this.y.toFixed(2)}, u = ${this.u.toFixed(2)}, v = ${this.v.toFixed(2)}`);
122139
}

src-words2music/scene.js

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Dot } from "./dot.js"
2-
import { loadFontStyles, addWordToCanvas, loadWordBuffers, wordBuffers } from "./words.js"
2+
import { loadFontStyles, addWord2canvas, loadWordBuffers, wordBuffers, loadCustomQuestions, customQuestions, printSentence2canvas } from "./words.js"
33
import { playDotSound } from "./audio.js";
4-
import { loadIntsruments, instruments, metroSynth } from "./audio.js";
4+
import { loadIntsruments, instruments, metroSynth, glitchSynth, teleportSynth } from "./audio.js";
55

66
let scales = {}; // where all scales will be stored
77
let currentScale = []; // active scale during playback
@@ -28,14 +28,20 @@ export class ScrollingScene {
2828
this.dotXTarget = this.dot.x; // where the dot should be
2929
this.clearSentenceBuffer = false;
3030
this.updateScene = false;
31+
this.hasPrintedSentence = false;
32+
this.lastQuickAudioTime = 0;
3133
}
3234

3335
simulate(deltaTime) {
3436
if (this.wordNeedsUpdate) {
35-
addWordToCanvas(this);
37+
addWord2canvas(this);
3638
this.wordBuffer = "";
3739
this.wordNeedsUpdate = false;
3840
}
41+
if (!this.hasPrintedSentence) {
42+
printSentence2canvas(customQuestions[0], this)
43+
this.hasPrintedSentence = true;
44+
}
3945
this.wordsCtx.clearRect(0, 0,
4046
this.wordsCtx.canvas.width, this.wordsCtx.canvas.height);
4147

@@ -101,12 +107,12 @@ export class ScrollingScene {
101107
console.warning("Error playing sound:", e);
102108
}
103109
} else if (this.dot.inSlowMo) {
104-
// Slow falling mode
105-
playDotSound(this.dot, new Tone.MembraneSynth().toDestination(),
106-
currentScale, "4n", -20, "8n");
110+
// playDotSound(this.dot, "harp",
111+
// currentScale, "4n", -20, "8n");
112+
playDotSound(this.dot, teleportSynth,
113+
currentScale, "4n", -10, "8n");
107114
} else if (this.dot.inFloatingMode) {
108-
// Floating mode
109-
playDotSound(this.dot, new Tone.MembraneSynth().toDestination(),
115+
playDotSound(this.dot, glitchSynth,
110116
"all", "16n", -20, "16n");
111117
} else if (this.dot.hasCollided) {
112118
// Collision
@@ -212,7 +218,7 @@ export async function setupScene(scene, bpm = 120) {
212218
Tone.Transport.loop = false;
213219

214220
const loaded = await SampleLibrary.load({
215-
instruments: ['piano', 'bass-electric', 'bassoon', 'cello', 'clarinet', 'contrabass', 'french-horn', 'guitar-acoustic', 'guitar-electric', 'guitar-nylon', 'harp', 'organ', 'saxophone', 'trombone', 'trumpet', 'tuba', 'violin'],
221+
instruments: ['piano', 'bass-electric', 'bassoon', 'cello', 'clarinet', 'contrabass', 'french-horn', 'guitar-acoustic', 'guitar-electric', 'guitar-nylon', 'harp', 'organ', 'saxophone', 'trombone', 'trumpet', 'tuba', 'violin', 'xylophone'],
216222
baseUrl: "src/tonejs-instruments/samples/",
217223
})
218224
Object.assign(instruments, loaded);
@@ -226,21 +232,28 @@ export async function setupScene(scene, bpm = 120) {
226232
}, "4n");
227233
Tone.Transport.scheduleRepeat(() => {
228234
const pos = Tone.Transport.position.split('.')[0];
229-
document.getElementById("metronome-container").textContent = `Metronome: ${pos}`;
235+
// show on screen
236+
const metronomeContainer = document.getElementById("metronome-container");
237+
if (metronomeContainer) {
238+
metronomeContainer.innerText = `Metronome: ${pos}`;
239+
}
230240
}, "8n");
231241

232242
await loadScales("assets/scales.json");
233243
Tone.Transport.scheduleRepeat(() => {
234244
const scaleName = pickRandomScale();
235245
const scaleContainer = document.getElementById("scale-container");
236-
scaleContainer.innerText = `Scale: ${scaleName}`;
237-
console.log("New scale:", currentScale);
246+
if (scaleContainer) {
247+
scaleContainer.innerText = `Scale: ${scaleName}`;
248+
}
238249
}, "2m");
239250

240251
// Words
241252
await loadFontStyles("assets/word-styles.json");
242253
await loadWordBuffers("assets/word-buffers.json", scene);
243254
scene.wordBufferNames = Object.keys(wordBuffers).sort((a, b) => wordBuffers[a].order - wordBuffers[b].order);
255+
await loadCustomQuestions("assets/custom-questions.json");
256+
console.log("Custom questions loaded:", customQuestions);
244257
scene.sentenceBuffer = "";
245258
scene.wordBuffer = "";
246259
scene.wordNeedsUpdate = false;

src-words2music/words.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export async function loadWordBuffers(src = "assets/word-buffers.json", scene) {
4040

4141
}
4242

43-
export function addWordToCanvas(scene, randomiseFontSize = true) {
43+
export let customQuestions = [];
44+
export async function loadCustomQuestions(src = "assets/cusotm-questions.json") {
45+
const response = await fetch(src);
46+
customQuestions = await response.json();
47+
}
48+
49+
export function addWord2canvas(scene, randomiseFontSize = true) {
4450
if (scene.wordBuffer.trim() === "") return;
4551

4652
const wordType = getWordType(scene.wordBuffer, scene.sentenceBuffer);
@@ -98,6 +104,49 @@ export function addWordToCanvas(scene, randomiseFontSize = true) {
98104
}
99105
}
100106

107+
export function printSentence2canvas(sentence, scene, wordType = "custom-sentence") {
108+
const fontInfo = fontStyles[wordType];
109+
const fontStyle = fontInfo["style"] || fontStyles["default"]["style"];
110+
console.log("Font info:", fontInfo, fontStyle);
111+
if (fontStyle[4] === "uppercase") {
112+
scene.wordBuffer = scene.wordBuffer.toUpperCase();
113+
} else if (fontStyle[4] === "lowercase") {
114+
scene.wordBuffer = scene.wordBuffer.toLowerCase();
115+
}
116+
117+
const fontSize = fontStyle[2][0];
118+
119+
const ctx = scene[fontInfo["buffer"]] || scene.wordsCtx;
120+
console.log(`${fontStyle[0]} ${fontStyle[1]} ${fontSize}px ${fontStyle[3]}`)
121+
ctx.font = `${fontStyle[0]} ${fontStyle[1]} ${fontSize}px ${fontStyle[3]}`;
122+
ctx.fillStyle = fontStyle[5] || "black";
123+
ctx.textBaseline = fontStyle[6] || "alphebatic";
124+
125+
// const wordWidth = ctx.measureText(scene.wordBuffer).width;
126+
127+
const metrics = ctx.measureText(sentence[0]);
128+
const lineHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
129+
130+
console.log("lineHeight:", lineHeight);
131+
132+
var cursorY = scene.height * 0.3;
133+
var maxWidth = 0;
134+
for (let i = 0; i < sentence.length; i++) {
135+
const phrase = sentence[i];
136+
const phraseWidth = ctx.measureText(phrase).width;
137+
if (phraseWidth > maxWidth) {
138+
maxWidth = phraseWidth;
139+
}
140+
}
141+
const cursorX = Math.round((scene.width - maxWidth) / 2);
142+
143+
for (let i = 0; i < sentence.length; i++) {
144+
const phrase = sentence[i];
145+
ctx.fillText(phrase, cursorX, cursorY);
146+
cursorY -= lineHeight;
147+
}
148+
}
149+
101150
// Find which word type a word is
102151
export function getWordType(word, sentence = "") {
103152
if (sentence.length === 0) {

src-words2music/words2music.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ScrollingScene, setupScene } from "./scene.js"
2+
import { audioCtx } from "./audio.js"
23

34
const inputBox = document.getElementById("input");
45
const canvas = document.getElementById("canvas");
@@ -29,7 +30,6 @@ function update(timestamp) {
2930
lastTimestamp = timestamp;
3031
}
3132
if (scene.dot.hasCollidedWithGround && !scene.metronomeStarted) {
32-
Tone.start();
3333
Tone.Transport.start();
3434
scene.metronomeStarted = true;
3535
}
@@ -62,9 +62,10 @@ function update(timestamp) {
6262
// Starting and stopping everything
6363
const startScene = document.getElementById("startScene");
6464
if (startScene) {
65-
startScene.addEventListener("click", () => {
65+
startScene.addEventListener("click", async () => {
6666
console.log("Starting scene");
67-
Tone.start();
67+
await Tone.start();
68+
await audioCtx.resume();
6869
scene.updateScene = !scene.updateScene;
6970
if (scene.updateScene) {
7071
startScene.value = "Pause";

0 commit comments

Comments
 (0)