char-slop/chordpad.vibed.software

git clone https://git.t4t.associates/char-slop/chordpad.vibed.software

Charlotte Sominitial commit3c7bc6e

main
3.3 KiB120 linesraw
1import { assertEquals } from "@std/assert";
2import { voiceTrack } from "../project/harmony.ts";
3import {
4  newChordTrack,
5  newNoteTrack,
6  newProject,
7  placeChord,
8  projectKey,
9} from "../project/model.ts";
10import { projectMidi } from "./export.ts";
11import { parseMidi } from "./smf.ts";
12import { writeMidi } from "./write.ts";
13
14const notesOf = (bytes: Uint8Array) =>
15  parseMidi(bytes).map(p => ({
16    name: p.name,
17    channel: p.channel,
18    notes: p.notes.map(n => [n.start, n.dur, n.pitch, Math.round(n.vel * 127)]),
19  }));
20
21Deno.test("written notes read back as they went in", () => {
22  const bytes = writeMidi({
23    bpm: 120,
24    beatsPerBar: 4,
25    tracks: [
26      {
27        name: "lead",
28        channel: 0,
29        program: 4,
30        notes: [
31          { start: 0, dur: 1, pitch: 60, vel: 1 },
32          { start: 1, dur: 1 / 3, pitch: 62, vel: 0.5 },
33          { start: 4 / 3, dur: 1 / 3, pitch: 64, vel: 0.5 },
34          { start: 0.25, dur: 0.25, pitch: 72, vel: 0.75 },
35        ],
36      },
37      {
38        name: "bass ♭",
39        channel: 1,
40        program: 4,
41        notes: [{ start: 2, dur: 2, pitch: 36, vel: 0.6 }],
42      },
43    ],
44  });
45  const round = (xs: number[][]) => xs.map(x => x.map(v => Math.round(v * 1000) / 1000));
46  const parts = notesOf(bytes).map(p => ({ ...p, notes: round(p.notes) }));
47  assertEquals(parts, [
48    {
49      name: "lead",
50      channel: 0,
51      notes: [
52        [0, 1, 60, 127],
53        [0.25, 0.25, 72, 95],
54        [1, 0.333, 62, 64],
55        [1.333, 0.333, 64, 64],
56      ],
57    },
58    { name: "bass ♭", channel: 1, notes: [[2, 2, 36, 76]] },
59  ]);
60});
61
62Deno.test("repeated pitches stay separate notes", () => {
63  const bytes = writeMidi({
64    bpm: 100,
65    beatsPerBar: 4,
66    tracks: [
67      {
68        name: "t",
69        channel: 0,
70        program: 0,
71        notes: [
72          { start: 0, dur: 1, pitch: 60, vel: 0.8 },
73          { start: 1, dur: 1, pitch: 60, vel: 0.8 }, // starts as the first ends
74          { start: 2, dur: 2, pitch: 60, vel: 0.8 },
75          { start: 3, dur: 1, pitch: 60, vel: 0.8 }, // starts while the one before still sounds
76        ],
77      },
78    ],
79  });
80  assertEquals(
81    parseMidi(bytes)[0].notes.map(n => [n.start, n.dur]),
82    [
83      [0, 1],
84      [1, 1],
85      [2, 1],
86      [3, 1],
87    ],
88  );
89});
90
91Deno.test("a project exports its chords as voiced, and its notes, a track each", () => {
92  const p = newProject();
93  const chords = newChordTrack("harmony");
94  for (const [start, symbol] of [
95    [0, "C"],
96    [4, "Am7"],
97    [8, "bVI"],
98  ] as const) {
99    placeChord(chords, { id: symbol, start, dur: 4, symbol });
100  }
101  chords.chords[1].notes = [45, 60, 64, 67]; // hand-voiced
102  const melody = newNoteTrack("tune");
103  melody.notes.push({ id: "n", start: 1.5, dur: 0.5, pitch: 76, vel: 0.8 });
104  p.tracks = [chords, melody];
105
106  const [harmony, tune] = parseMidi(projectMidi(p));
107  const voiced = voiceTrack(chords, projectKey(p));
108  const at = (beat: number) => harmony.notes.filter(n => n.start === beat).map(n => n.pitch);
109  assertEquals([harmony.name, tune.name], ["harmony", "tune"]);
110  assertEquals(
111    [0, 4, 8].map(at),
112    voiced.map(v => [...v.notes].sort((a, b) => a - b)),
113  );
114  assertEquals(at(4), [45, 60, 64, 67]);
115  assertEquals(
116    tune.notes.map(n => [n.start, n.dur, n.pitch]),
117    [[1.5, 0.5, 76]],
118  );
119  assertEquals([harmony.channel, tune.channel], [0, 1]);
120});