88 lines
3.3 KiB
JavaScript
88 lines
3.3 KiB
JavaScript
/**
|
|
* Notenmuster der Hintergrundmusik: ein pompöser Marsch (eigene Komposition, CC0),
|
|
* der die Propaganda-Stimmung überzeichnet. Ohne Web-Audio-Abhängigkeit, damit sich
|
|
* Rhythmus und Harmonie testen lassen; gespielt wird er von `systems/Music.js`.
|
|
*
|
|
* Raster: 16 Schritte (Sechzehntel) pro Takt, 8 Takte, Viervierteltakt.
|
|
* @module logic/marchPattern
|
|
*/
|
|
|
|
export const TEMPO_BPM = 112;
|
|
export const STEPS_PER_BAR = 16;
|
|
export const BARS = 8;
|
|
export const TOTAL_STEPS = STEPS_PER_BAR * BARS;
|
|
|
|
/** Dauer eines Sechzehntels in Sekunden. */
|
|
export const STEP_SECONDS = 60 / TEMPO_BPM / 4;
|
|
|
|
/** Akkordgrundtöne (MIDI) je Takt: I I IV V I IV V I in C-Dur. */
|
|
export const CHORD_ROOTS = Object.freeze([48, 48, 53, 55, 48, 53, 55, 48]);
|
|
|
|
/** Dur-Dreiklang über dem Grundton. */
|
|
const MAJOR_TRIAD = Object.freeze([0, 4, 7]);
|
|
|
|
/**
|
|
* Fanfarenmelodie: [Takt, Schritt, MIDI-Ton, Länge in Schritten].
|
|
* @type {readonly (readonly [number, number, number, number])[]}
|
|
*/
|
|
const MELODY = Object.freeze([
|
|
[0, 0, 67, 3], [0, 3, 67, 1], [0, 4, 72, 4], [0, 8, 76, 6], [0, 14, 72, 2],
|
|
[1, 0, 74, 4], [1, 4, 72, 4], [1, 8, 71, 4], [1, 12, 67, 4],
|
|
[2, 0, 69, 3], [2, 3, 69, 1], [2, 4, 72, 4], [2, 8, 77, 6], [2, 14, 76, 2],
|
|
[3, 0, 74, 4], [3, 4, 71, 4], [3, 8, 67, 8],
|
|
[4, 0, 67, 3], [4, 3, 67, 1], [4, 4, 72, 4], [4, 8, 76, 4], [4, 12, 79, 4],
|
|
[5, 0, 81, 6], [5, 6, 77, 2], [5, 8, 76, 4], [5, 12, 72, 4],
|
|
[6, 0, 74, 3], [6, 3, 76, 1], [6, 4, 74, 4], [6, 8, 71, 4], [6, 12, 67, 4],
|
|
[7, 0, 72, 8],
|
|
]);
|
|
|
|
/**
|
|
* @typedef {Object} MusicEvent
|
|
* @property {'kick'|'snare'|'bass'|'stab'|'lead'} instrument
|
|
* @property {number[]} notes MIDI-Töne (leer bei Schlagzeug).
|
|
* @property {number} lengthSteps
|
|
* @property {number} velocity 0..1
|
|
*/
|
|
|
|
/**
|
|
* Alle Ereignisse eines Schritts.
|
|
* @param {number} step 0..TOTAL_STEPS-1 (größere Werte werden umgebrochen).
|
|
* @returns {MusicEvent[]}
|
|
*/
|
|
export function eventsAtStep(step) {
|
|
const s = ((step % TOTAL_STEPS) + TOTAL_STEPS) % TOTAL_STEPS;
|
|
const bar = Math.floor(s / STEPS_PER_BAR);
|
|
const pos = s % STEPS_PER_BAR;
|
|
const root = CHORD_ROOTS[bar];
|
|
const events = [];
|
|
|
|
// „Umm“ auf 1 und 3 (Bass + große Trommel), „ta“ auf 2 und 4 (Akkord + kleine Trommel).
|
|
if (pos === 0 || pos === 8) {
|
|
events.push({ instrument: 'kick', notes: [], lengthSteps: 2, velocity: 1 });
|
|
events.push({ instrument: 'bass', notes: [pos === 0 ? root - 12 : root - 5], lengthSteps: 4, velocity: 0.9 });
|
|
}
|
|
if (pos === 4 || pos === 12) {
|
|
events.push({ instrument: 'snare', notes: [], lengthSteps: 1, velocity: 0.8 });
|
|
events.push({ instrument: 'stab', notes: MAJOR_TRIAD.map((i) => root + 12 + i), lengthSteps: 2, velocity: 0.8 });
|
|
}
|
|
// Trommelwirbel in der zweiten Hälfte des letzten Takts leitet die Wiederholung ein.
|
|
if (bar === BARS - 1 && pos >= 8 && pos !== 12) {
|
|
events.push({ instrument: 'snare', notes: [], lengthSteps: 1, velocity: 0.35 + (pos - 8) * 0.07 });
|
|
}
|
|
for (const [mBar, mStep, note, length] of MELODY) {
|
|
if (mBar === bar && mStep === pos) {
|
|
events.push({ instrument: 'lead', notes: [note], lengthSteps: length, velocity: 0.8 });
|
|
}
|
|
}
|
|
return events;
|
|
}
|
|
|
|
/**
|
|
* Frequenz eines MIDI-Tons (A4 = 69 = 440 Hz).
|
|
* @param {number} midi
|
|
* @returns {number}
|
|
*/
|
|
export function midiToFrequency(midi) {
|
|
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
}
|