Home Manual Reference Source Test

tests/tracks.test.js

  1. import { describe, it, expect, vi, beforeEach } from "vitest";
  2. import { FIXTURES, MOCKS, STUBS } from "./api.js";
  3. const { midiPart, midiStep3 } = FIXTURES;
  4. const { default: MidiWriter } = await MOCKS.midiWriter;
  5. const { generateMidi } = await import("../src/controller/tracks.js");
  6. /** @test {generateMidi} */
  7. describe("generateMidi", () => {
  8. const midiNoteArgs = () => MidiWriter.NoteEvent.mock.calls.map((c) => c[0]);
  9. const midiFirstTrack = () => MidiWriter.Track.mock.results[0].value;
  10. beforeEach(() => {
  11. vi.clearAllMocks();
  12. STUBS.tracksMidiWriter(MidiWriter);
  13. });
  14. /** @test {generateMidi} */
  15. it("emits per-voice tracks with header meta and one NoteEvent per sub-beat for a 2-voice 2-sub-beat input", () => {
  16. const out = generateMidi(
  17. [
  18. midiPart({
  19. solution: { note_0_0: 60, note_0_1: 62, note_1_0: 55, note_1_1: 57 },
  20. numSubBeats: 2,
  21. numerator: 2,
  22. }),
  23. ],
  24. midiStep3(2, [0, 42]),
  25. 120,
  26. );
  27. expect(out).toEqual({
  28. combinedB64: "TESTB64",
  29. voicesB64: ["TESTB64", "TESTB64"],
  30. });
  31. expect(MidiWriter.Track).toHaveBeenCalledTimes(4); // combined + per-voice
  32. expect(MidiWriter.NoteEvent).toHaveBeenCalledTimes(8); // 2 notes * 2 voices * 2 tracks
  33. const t = midiFirstTrack();
  34. expect(t.setTempo).toHaveBeenCalledWith(120);
  35. expect(t.setTimeSignature).toHaveBeenCalledWith(2, 4);
  36. expect(t.setKeySignature).toHaveBeenCalledWith("C");
  37. const pcArgs = MidiWriter.ProgramChangeEvent.mock.calls.map((c) => c[0]);
  38. expect(pcArgs).toContainEqual({ instrument: 0, channel: 0 });
  39. expect(pcArgs).toContainEqual({ instrument: 42, channel: 1 });
  40. expect(midiNoteArgs().map((a) => a.channel)).toEqual(
  41. expect.arrayContaining([1, 2]),
  42. );
  43. expect(midiNoteArgs()[0]).not.toHaveProperty("wait");
  44. });
  45. /** @test {generateMidi} */
  46. it("extends preceding note via ties as a single T384 event and emits Cm key signature in minor mode; accumulates rests and orphan-ties as wait T256", () => {
  47. generateMidi(
  48. [
  49. midiPart({
  50. solution: { note_0_0: 60, note_0_1: -2, note_0_2: -2 },
  51. numSubBeats: 3,
  52. numerator: 3,
  53. mode: "minor",
  54. }),
  55. ],
  56. midiStep3(),
  57. 120,
  58. );
  59. expect(MidiWriter.NoteEvent).toHaveBeenCalledTimes(2); // 1 merged note * 2 tracks
  60. expect(midiNoteArgs()[0]).toMatchObject({ duration: "T384" });
  61. expect(midiFirstTrack().setKeySignature).toHaveBeenCalledWith("Cm");
  62. vi.clearAllMocks();
  63. generateMidi(
  64. [
  65. midiPart({
  66. solution: { note_0_0: -1, note_0_1: -2, note_0_2: 60 },
  67. numSubBeats: 3,
  68. numerator: 3,
  69. }),
  70. ],
  71. midiStep3(),
  72. 120,
  73. );
  74. expect(midiNoteArgs()[0]).toMatchObject({ wait: "T256" });
  75. });
  76. /** @test {generateMidi} */
  77. it("returns null on empty parts input such as when all step-3 parts are UNSAT", () => {
  78. expect(generateMidi([], midiStep3(), 120)).toBeNull();
  79. });
  80. /** @test {generateMidi} */
  81. it("emits TimeSig and KeySig at every multi-part boundary; scans ties across boundaries so held notes extend and meta events flush after", () => {
  82. generateMidi(
  83. [
  84. midiPart({
  85. numSubBeats: 2,
  86. solution: { note_0_0: 60, note_0_1: -2 },
  87. numerator: 2,
  88. }),
  89. midiPart({
  90. numSubBeats: 2,
  91. solution: { note_0_0: -2, note_0_1: 64 },
  92. numerator: 3,
  93. key: 9,
  94. mode: "minor",
  95. }),
  96. midiPart({
  97. numSubBeats: 1,
  98. solution: { note_0_0: 67 },
  99. numerator: 6,
  100. denominator: 8,
  101. key: 7,
  102. }),
  103. ],
  104. midiStep3(),
  105. 120,
  106. );
  107. expect(midiNoteArgs().slice(0, 3)).toMatchObject([
  108. { pitch: ["C4"], duration: "T384" }, // held across p1->p2 boundary
  109. { pitch: ["E4"], duration: "T128" }, // p2 (after meta-event flush)
  110. { pitch: ["G4"], duration: "T64" }, // p3 (denom=8 -> ticks=64)
  111. ]);
  112. expect(
  113. MidiWriter.TimeSignatureEvent.mock.calls.map((c) => c.join("/")),
  114. ).toEqual(["3/4", "6/8", "3/4", "6/8"]);
  115. expect(MidiWriter.KeySignatureEvent.mock.calls.map((c) => c[0])).toEqual([
  116. "Am",
  117. "G",
  118. "Am",
  119. "G",
  120. ]);
  121. const t = midiFirstTrack();
  122. expect(t.setTimeSignature).toHaveBeenCalledWith(2, 4);
  123. expect(t.setKeySignature).toHaveBeenCalledWith("C");
  124. expect(t.addEvent.mock.calls.map((c) => c[0]._kind)).toEqual([
  125. "program",
  126. "note",
  127. "timesig",
  128. "keysig",
  129. "note",
  130. "timesig",
  131. "keysig",
  132. "note",
  133. ]);
  134. });
  135. });