import { assertEquals } from "@std/assert"; import { voiceTrack } from "../project/harmony.ts"; import { newChordTrack, newNoteTrack, newProject, placeChord, projectKey, } from "../project/model.ts"; import { projectMidi } from "./export.ts"; import { parseMidi } from "./smf.ts"; import { writeMidi } from "./write.ts"; const notesOf = (bytes: Uint8Array) => parseMidi(bytes).map(p => ({ name: p.name, channel: p.channel, notes: p.notes.map(n => [n.start, n.dur, n.pitch, Math.round(n.vel * 127)]), })); Deno.test("written notes read back as they went in", () => { const bytes = writeMidi({ bpm: 120, beatsPerBar: 4, tracks: [ { name: "lead", channel: 0, program: 4, notes: [ { start: 0, dur: 1, pitch: 60, vel: 1 }, { start: 1, dur: 1 / 3, pitch: 62, vel: 0.5 }, { start: 4 / 3, dur: 1 / 3, pitch: 64, vel: 0.5 }, { start: 0.25, dur: 0.25, pitch: 72, vel: 0.75 }, ], }, { name: "bass ♭", channel: 1, program: 4, notes: [{ start: 2, dur: 2, pitch: 36, vel: 0.6 }], }, ], }); const round = (xs: number[][]) => xs.map(x => x.map(v => Math.round(v * 1000) / 1000)); const parts = notesOf(bytes).map(p => ({ ...p, notes: round(p.notes) })); assertEquals(parts, [ { name: "lead", channel: 0, notes: [ [0, 1, 60, 127], [0.25, 0.25, 72, 95], [1, 0.333, 62, 64], [1.333, 0.333, 64, 64], ], }, { name: "bass ♭", channel: 1, notes: [[2, 2, 36, 76]] }, ]); }); Deno.test("repeated pitches stay separate notes", () => { const bytes = writeMidi({ bpm: 100, beatsPerBar: 4, tracks: [ { name: "t", channel: 0, program: 0, notes: [ { start: 0, dur: 1, pitch: 60, vel: 0.8 }, { start: 1, dur: 1, pitch: 60, vel: 0.8 }, // starts as the first ends { start: 2, dur: 2, pitch: 60, vel: 0.8 }, { start: 3, dur: 1, pitch: 60, vel: 0.8 }, // starts while the one before still sounds ], }, ], }); assertEquals( parseMidi(bytes)[0].notes.map(n => [n.start, n.dur]), [ [0, 1], [1, 1], [2, 1], [3, 1], ], ); }); Deno.test("a project exports its chords as voiced, and its notes, a track each", () => { const p = newProject(); const chords = newChordTrack("harmony"); for (const [start, symbol] of [ [0, "C"], [4, "Am7"], [8, "bVI"], ] as const) { placeChord(chords, { id: symbol, start, dur: 4, symbol }); } chords.chords[1].notes = [45, 60, 64, 67]; // hand-voiced const melody = newNoteTrack("tune"); melody.notes.push({ id: "n", start: 1.5, dur: 0.5, pitch: 76, vel: 0.8 }); p.tracks = [chords, melody]; const [harmony, tune] = parseMidi(projectMidi(p)); const voiced = voiceTrack(chords, projectKey(p)); const at = (beat: number) => harmony.notes.filter(n => n.start === beat).map(n => n.pitch); assertEquals([harmony.name, tune.name], ["harmony", "tune"]); assertEquals( [0, 4, 8].map(at), voiced.map(v => [...v.notes].sort((a, b) => a - b)), ); assertEquals(at(4), [45, 60, 64, 67]); assertEquals( tune.notes.map(n => [n.start, n.dur, n.pitch]), [[1.5, 0.5, 76]], ); assertEquals([harmony.channel, tune.channel], [0, 1]); });