131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
/**
|
|
* Spielt den Marsch aus `logic/marchPattern.js` mit synthetischen Instrumenten.
|
|
*
|
|
* Zeitplanung nach dem Muster „Vorausplanen mit zwei Uhren“: Ein grober JavaScript-
|
|
* Timer plant alle 25 ms die Noten der nächsten ~120 ms exakt auf der Audio-Uhr ein.
|
|
* Direkt im Timer zu spielen würde hörbar wackeln, weil JavaScript-Timer ungenau sind.
|
|
* @module systems/Music
|
|
*/
|
|
|
|
import { eventsAtStep, midiToFrequency, STEP_SECONDS, TOTAL_STEPS } from '../logic/marchPattern.js';
|
|
|
|
const SCHEDULER_INTERVAL_MS = 25;
|
|
const LOOKAHEAD_SECONDS = 0.12;
|
|
/** Grundlautstärke der Musik vor dem Musik-Regler. */
|
|
const MUSIC_LEVEL = 0.55;
|
|
/** Liegt die Planung weiter zurück (Tab im Hintergrund), wird neu synchronisiert statt nachgeholt. */
|
|
const MAX_LAG_SECONDS = 0.25;
|
|
|
|
export class MarchMusic {
|
|
/**
|
|
* @param {AudioContext} ctx
|
|
* @param {AudioNode} destination Musik-Bus.
|
|
* @param {AudioBuffer} noiseBuffer
|
|
*/
|
|
constructor(ctx, destination, noiseBuffer) {
|
|
this.ctx = ctx;
|
|
this.noiseBuffer = noiseBuffer;
|
|
this.out = ctx.createGain();
|
|
this.out.gain.value = MUSIC_LEVEL;
|
|
this.out.connect(destination);
|
|
this.timer = null;
|
|
this.step = 0;
|
|
this.nextTime = 0;
|
|
}
|
|
|
|
start() {
|
|
if (this.timer !== null) return;
|
|
this.step = 0;
|
|
this.nextTime = this.ctx.currentTime + 0.1;
|
|
this.timer = setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
}
|
|
|
|
/** Blendet aus und gibt die Knoten frei. */
|
|
stop() {
|
|
if (this.timer !== null) clearInterval(this.timer);
|
|
this.timer = null;
|
|
const now = this.ctx.currentTime;
|
|
this.out.gain.setTargetAtTime(0, now, 0.08);
|
|
const out = this.out;
|
|
setTimeout(() => out.disconnect(), 600);
|
|
}
|
|
|
|
/** @private */
|
|
schedule() {
|
|
const now = this.ctx.currentTime;
|
|
if (this.nextTime < now - MAX_LAG_SECONDS) this.nextTime = now + 0.05;
|
|
while (this.nextTime < now + LOOKAHEAD_SECONDS) {
|
|
for (const event of eventsAtStep(this.step)) this.play(event, this.nextTime);
|
|
this.nextTime += STEP_SECONDS;
|
|
this.step = (this.step + 1) % TOTAL_STEPS;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('../logic/marchPattern.js').MusicEvent} event
|
|
* @param {number} time Audio-Zeit.
|
|
* @private
|
|
*/
|
|
play(event, time) {
|
|
const length = event.lengthSteps * STEP_SECONDS;
|
|
switch (event.instrument) {
|
|
case 'kick':
|
|
this.voice('sine', 150, time, 0.22, 0.9 * event.velocity, 45);
|
|
break;
|
|
case 'snare':
|
|
this.snare(time, 0.35 * event.velocity);
|
|
break;
|
|
case 'bass':
|
|
this.voice('triangle', midiToFrequency(event.notes[0]), time, length * 0.9, 0.35 * event.velocity);
|
|
break;
|
|
case 'stab':
|
|
for (const note of event.notes) {
|
|
this.voice('sawtooth', midiToFrequency(note), time, length * 0.7, 0.05 * event.velocity, undefined, 1400);
|
|
}
|
|
break;
|
|
case 'lead':
|
|
this.voice('square', midiToFrequency(event.notes[0]), time, length * 0.92, 0.06 * event.velocity, undefined, 2600);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
voice(type, freq, time, duration, volume, freqEnd, lowpass) {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.setValueAtTime(freq, time);
|
|
if (freqEnd) osc.frequency.exponentialRampToValueAtTime(freqEnd, time + duration);
|
|
gain.gain.setValueAtTime(0.0001, time);
|
|
gain.gain.exponentialRampToValueAtTime(volume, time + 0.01);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, time + duration);
|
|
let node = osc.connect(gain);
|
|
if (lowpass) {
|
|
const filter = this.ctx.createBiquadFilter();
|
|
filter.type = 'lowpass';
|
|
filter.frequency.value = lowpass;
|
|
node = gain.connect(filter);
|
|
}
|
|
node.connect(this.out);
|
|
osc.start(time);
|
|
osc.stop(time + duration + 0.05);
|
|
}
|
|
|
|
/** @private */
|
|
snare(time, volume) {
|
|
const source = this.ctx.createBufferSource();
|
|
source.buffer = this.noiseBuffer;
|
|
const filter = this.ctx.createBiquadFilter();
|
|
filter.type = 'highpass';
|
|
filter.frequency.value = 1500;
|
|
const gain = this.ctx.createGain();
|
|
gain.gain.setValueAtTime(volume, time);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, time + 0.12);
|
|
source.connect(filter).connect(gain).connect(this.out);
|
|
source.start(time, Math.random() * 0.5);
|
|
source.stop(time + 0.15);
|
|
}
|
|
}
|