Skip to content

Commit

Permalink
Merge branch 'soundfont'
Browse files Browse the repository at this point in the history
  • Loading branch information
threedalpeng committed Feb 20, 2024
2 parents 0e25dd0 + 9b13b39 commit e4cc416
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 30 deletions.
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"@steeze-ui/heroicons": "^2.3.0",
"@steeze-ui/svelte-icon": "^1.5.0",
"idb": "^8.0.0",
"mini-svg-data-uri": "^1.4.4"
"mini-svg-data-uri": "^1.4.4",
"smplr": "^0.12.2"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.0",
Expand Down
46 changes: 28 additions & 18 deletions src/lib/device/metronome/metronome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class Metronome {
this.beatPerBar = beatPerBar;
this.bpm = bpm;
this.timer.signatureUnit = signatureUnit;
this.timer.onTick(this.#onTick.bind(this));
this.timer.onAudioTick(this.#scheduleAudio.bind(this));
}

get beatPerBar() {
Expand Down Expand Up @@ -60,40 +58,43 @@ class Metronome {
get isRunning() {
return this.#isRunning;
}
start() {
start(): void {
if (!this.#isRunning) {
this.#isRunning = true;
this.timer.start();
}
}

#stopScheduling: (() => void) | null = null;
schedule() {
this.#stopScheduling = this.timer.loop(
{ start: 0, duration: this.#notesPerBeat },
this.#onTick.bind(this),
this.#scheduleAudio.bind(this)
);
}

#currentBeat = 0;
#barPassed = 0;
#onTick({ time, tickPassed }: TickState) {
if (tickPassed % this.#ticksPerBeat !== 0) {
return;
}
this.#currentBeat = ((tickPassed / this.#ticksPerBeat) % this.timer.beatPerBar) + 1;
this.#currentBeat += 1;
this.#onBeatCallbacks.forEach((cb) => cb(this.state));
if (this.#currentBeat % this.timer.beatPerBar === 1) {
this.#onBarCallbacks.forEach((cb) => cb(this.state));
}

if (this.#currentBeat >= this.timer.beatPerBar) {
this.#barPassed++;
this.#currentBeat = 0;
}
}

#masterGain: GainNode | null = null;
get #ticksPerBeat() {
return this.timer.convert(1, 'beat', 'tick');
get #notesPerBeat() {
return this.timer.convert(1, 'beat', 'note');
}
#currentBeatInAudioTick = 0;
#scheduleAudio({ audioCtx, time, tickPassed }: AudioTickState) {
if (tickPassed % this.#ticksPerBeat !== 0) {
return;
}
const beatNum = (tickPassed / this.#ticksPerBeat) % this.timer.beatPerBar;

if (!this.#masterGain) {
this.#masterGain = audioCtx.createGain();
this.#masterGain.connect(audioCtx.destination);
Expand All @@ -103,7 +104,7 @@ class Metronome {
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(this.#masterGain);
if (beatNum === 0) {
if (this.#currentBeatInAudioTick % this.timer.beatPerBar === 0) {
osc.frequency.value = 880;
} else {
osc.frequency.value = 440;
Expand All @@ -115,18 +116,22 @@ class Metronome {
osc.addEventListener('ended', () => {
gain.disconnect();
});
this.#currentBeatInAudioTick += 1;
}

stop() {
if (this.#isRunning) {
this.#isRunning = false;
this.timer.stop();
if (this.#stopScheduling) this.#stopScheduling();
this.clearSchedule();
this.#currentBeat = 0;
this.#currentBeatInAudioTick = 0;
this.#barPassed = 0;
}
}

toggle() {
toggle(): void {
if (this.#isRunning) {
this.stop();
} else {
Expand Down Expand Up @@ -164,10 +169,15 @@ class Metronome {
this.#onOptionChangeCallbacks.delete(cb);
}

destroy() {
this.stop();
clearSchedule() {
this.timer.clearSchedule();
this.#onBeatCallbacks.clear();
this.#onBarCallbacks.clear();
}

destroy() {
this.stop();
this.clearSchedule();
this.#onOptionChangeCallbacks.clear();
}
}
Expand Down
47 changes: 45 additions & 2 deletions src/lib/timer/tick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class AudioClockTimer {
node.start(0);
}
if (!this.#isRunning) {
this.#startCallbacks.forEach((cb) => cb());
this.#isRunning = true;
// delay initial lookhead
this.#nextTick = this.audioCtx.currentTime + 0.1;
Expand All @@ -83,6 +84,14 @@ export class AudioClockTimer {
}
}

#startCallbacks: Set<() => any> = new Set();
onStart(cb: () => any) {
this.#startCallbacks.add(cb);
}
removeStart(cb: () => any) {
this.#startCallbacks.delete(cb);
}

#audioTickCallbacks: Set<AudioTickCallback> = new Set();
onAudioTick(cb: AudioTickCallback) {
this.#audioTickCallbacks.add(cb);
Expand Down Expand Up @@ -135,10 +144,15 @@ export class AudioClockTimer {
}
}

clearSchedule() {
this.#audioTickCallbacks.clear();
this.#tickCallbacks.clear();
}

destroy() {
this.stop();
this.#audioTickCallbacks.clear();
this.#tickCallbacks.clear();
this.stop();
}
}

Expand Down Expand Up @@ -273,7 +287,7 @@ export class TempoTimer extends AudioClockTimer {
}
};
const onAudioTick: AudioTickCallback = (state) => {
if (currentTime + start >= state.time) {
if (currentTime + start <= state.time) {
audioCb(state);
this.removeAudioTick(onAudioTick);
}
Expand All @@ -285,4 +299,33 @@ export class TempoTimer extends AudioClockTimer {
}
this.onAudioTick(onAudioTick);
}

loop(time: { start: number; duration?: number }, cb: TickCallback, audioCb: AudioTickCallback) {
const start = this.convert(time.start, 'note', 'second');
const duration = time.duration ? this.convert(time.duration, 'note', 'second') : -1;

let currentTime = this.currentTime;
let tickSchedule = currentTime + start;
let audioTickSchedule = currentTime + start;
const onStart: TickCallback = (state) => {
while (tickSchedule <= state.time) {
cb(state);
tickSchedule += duration;
}
};
const onAudioTick: AudioTickCallback = (state) => {
if (audioTickSchedule <= state.time) {
audioCb(state);
audioTickSchedule += duration;
}
};
const stop = () => {
this.removeAudioTick(onAudioTick);
this.removeTick(onStart);
};

this.onTick(onStart);
this.onAudioTick(onAudioTick);
return stop;
}
}
1 change: 0 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script>
import '$/app.css';
// import { pwaInfo } from 'virtual:pwa-info';
// $: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
</script>

Expand Down
39 changes: 33 additions & 6 deletions src/routes/tools/render-sample/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
import RandomBoxOptions from '$/lib/practice/RandomBox/RandomBoxOptions.svelte';
import { getRandomBoxContext } from '$/lib/practice/RandomBox/context';
import type { PracticeBoard, PracticeScore } from '$/lib/practice/types';
import { getPitchFromFingerPosition } from '$/utils/music/pitch';
import { getPitchFromFingerPosition, numberingPitch } from '$/utils/music/pitch';
import { onDestroy, onMount } from 'svelte';
import { practice } from './data';
import { Soundfont, CacheStorage } from 'smplr';
const metronome = getMetronomeContext();
const randomBox = getRandomBoxContext<(typeof practice.scores)[number]>();
Expand All @@ -25,10 +26,19 @@
function replaceScore() {
let score = randomBox.open();
const schedule = () => {
scheduleScore(score);
timer.removeTick(schedule);
if (!guitarSoundfont) {
guitarSoundfont = new Soundfont(timer.audioCtx!!, {
instrument: 'acoustic_guitar_steel',
storage: new CacheStorage()
});
}
// preload soundfont
guitarSoundfont.load.then(() => {
metronome.schedule();
scheduleScore(score);
});
};
timer.onTick(schedule);
timer.onStart(schedule);
return score;
}
Expand All @@ -46,6 +56,9 @@
};
}) as FingerInfo[];
let guitarSoundfont: Soundfont | null = null;
let previousTime = 0;
function scheduleScore(score: PracticeScore) {
/** Now scheduling */
Expand All @@ -57,7 +70,11 @@
currentActiveFingers.clear();
currentBoard = board;
},
({ audioCtx }) => {}
({ audioCtx }) => {
// guitarSoundfont!!.start({
// note: 50 + 12
// });
}
);
});
Expand All @@ -69,6 +86,7 @@
);
return { ...note, pitch };
});
for (let i = 0; i < notes.length; i++) {
const note = notes[i];
const nextThreeFingers = notes.slice(i + 1, i + 4).map((n) => n.position);
Expand All @@ -83,8 +101,17 @@
currentActiveFingers = currentActiveFingers;
};
},
({ audioCtx }) => {
({ audioCtx, time }) => {
// play audio with pitch
if (note.pitch) {
guitarSoundfont!!.start({
note: numberingPitch(note.pitch) + 12,
time: time,
duration: note.time.duration
? timer.convert(note.time.duration, 'note', 'second')
: note.time.duration
});
}
}
);
}
Expand Down
Loading

0 comments on commit e4cc416

Please sign in to comment.