@hraness/dawg 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/DAWG.md +300 -35
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +37 -12
- package/core/expression.ts +1241 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +536 -56
- package/core/sdk/eval-child.ts +38 -3
- package/core/sdk/print.ts +445 -14
- package/core/sdk/v1.ts +1709 -23
- package/core/sections.ts +2046 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +27 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +25 -0
- package/guides/tempo.md +26 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +2 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +29 -4
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +21 -3
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +36 -2
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +2 -0
- package/src/audio/arrange.ts +471 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +12 -2
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/convolution.ts +43 -5
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/space.ts +197 -50
- package/src/audio/engine.ts +175 -20
- package/src/audio/live.ts +33 -6
- package/src/audio/loudness.ts +551 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +87 -16
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/sampler.ts +108 -19
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +86 -0
- package/src/audio/wav.ts +448 -59
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +934 -0
- package/src/commands/help.ts +281 -24
- package/src/commands/master.ts +361 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +964 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +826 -47
- package/src/render.ts +109 -7
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +58 -5
- package/src/tui/euclid.ts +50 -6
- package/src/tui/fader.ts +409 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +656 -16
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +95 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +414 -41
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +79 -12
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
package/core/midi.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard MIDI File (SMF) export.
|
|
3
|
+
*
|
|
4
|
+
* Format 1: track 0 is the conductor track with the time-signature (FF 58)
|
|
5
|
+
* and tempo (FF 51) meta events, then one track per score track with its
|
|
6
|
+
* notes. The file's division is the score's ticks per quarter, so score
|
|
7
|
+
* ticks are written as they are.
|
|
8
|
+
*
|
|
9
|
+
* SMF tempo is constant between events, so ramps are written as steps
|
|
10
|
+
* (every `rampStepTicks`, a sixteenth by default) and a fermata as a slower
|
|
11
|
+
* tempo over its beat. Each step's tempo is the exact average over the step,
|
|
12
|
+
* so every step boundary lands at the same second as in the rendered WAV.
|
|
13
|
+
*
|
|
14
|
+
* Notes are written as performed (core/expression.ts): articulation
|
|
15
|
+
* lengths and velocities, humanize, the velocity curve and mono/legato
|
|
16
|
+
* voicing. The sustain pedal is written as CC64 (127 down, 64 half, 0 up)
|
|
17
|
+
* while each note keeps its key length, as a real pedal recording does.
|
|
18
|
+
* Pitch expression (glide, bend, per-note vibrato) and per-note cents are
|
|
19
|
+
* written as pitch bend (range ±24 semitones via RPN 0) on notes that sound
|
|
20
|
+
* alone on their track; SMF pitch bend is per channel, so notes inside a
|
|
21
|
+
* chord play at their written pitch.
|
|
22
|
+
*
|
|
23
|
+
* Tuning (core/tuning.ts) is written with the MIDI Tuning Standard: each
|
|
24
|
+
* tuned track gets a real-time single-note tuning change SysEx (F0 7F 7F
|
|
25
|
+
* 08 02) carrying every key's frequency as a tuning program, selected on
|
|
26
|
+
* its channel with RPN 3 (tuning program select). Synths that honour MTS
|
|
27
|
+
* play the tuning; others play 12-TET keys. Section starts are FF 06
|
|
28
|
+
* marker events on the conductor track.
|
|
29
|
+
*
|
|
30
|
+
* Pure and dependency-free apart from the score modules.
|
|
31
|
+
*/
|
|
32
|
+
import { isDrumInstrument } from "./drums.ts";
|
|
33
|
+
import {
|
|
34
|
+
performanceTimingFor,
|
|
35
|
+
performNotes,
|
|
36
|
+
type PedalState,
|
|
37
|
+
type PerformanceTiming,
|
|
38
|
+
type PerformedNote,
|
|
39
|
+
} from "./expression.ts";
|
|
40
|
+
import {
|
|
41
|
+
barStartTick,
|
|
42
|
+
clickTicksOf,
|
|
43
|
+
fermataSpan,
|
|
44
|
+
loopTicksOf,
|
|
45
|
+
meterSegments,
|
|
46
|
+
performedNotes,
|
|
47
|
+
timeMapFor,
|
|
48
|
+
type TimeScore,
|
|
49
|
+
} from "./tempo.ts";
|
|
50
|
+
import type { TrackScore } from "./score.ts";
|
|
51
|
+
import { resolveTuning, type TuningTable } from "./tuning.ts";
|
|
52
|
+
|
|
53
|
+
export type MidiOptions = Readonly<{
|
|
54
|
+
/** Ticks per tempo step inside a ramp; default a sixteenth note. */
|
|
55
|
+
rampStepTicks?: number;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
const MAX_DIVISION = 0x7fff;
|
|
59
|
+
|
|
60
|
+
/** Breakpoints and tempos (microseconds per quarter) for the conductor. */
|
|
61
|
+
export function midiTempoEvents(
|
|
62
|
+
score: TimeScore,
|
|
63
|
+
options: MidiOptions = {},
|
|
64
|
+
): readonly Readonly<{ tick: number; usPerQuarter: number }>[] {
|
|
65
|
+
const tpb = score.ticksPerBeat;
|
|
66
|
+
const map = timeMapFor(score);
|
|
67
|
+
const constantUs = (bpm: number) => Math.round(60_000_000 / bpm);
|
|
68
|
+
if (!map) return [{ tick: 0, usPerQuarter: constantUs(score.tempoBpm) }];
|
|
69
|
+
const end = loopTicksOf(score);
|
|
70
|
+
const step = Math.max(1, Math.round(options.rampStepTicks ?? tpb / 4));
|
|
71
|
+
const points = new Set<number>([0]);
|
|
72
|
+
let previous = 0;
|
|
73
|
+
for (const event of score.time?.tempo ?? []) {
|
|
74
|
+
if (event.ramp)
|
|
75
|
+
for (let tick = previous + step; tick < event.tick; tick += step)
|
|
76
|
+
points.add(tick);
|
|
77
|
+
points.add(event.tick);
|
|
78
|
+
previous = event.tick;
|
|
79
|
+
}
|
|
80
|
+
for (const fermata of score.time?.fermatas ?? []) {
|
|
81
|
+
points.add(fermata.tick);
|
|
82
|
+
points.add(fermata.tick + fermataSpan(score, fermata.tick));
|
|
83
|
+
}
|
|
84
|
+
const ticks = [...points].filter((tick) => tick >= 0).sort((a, b) => a - b);
|
|
85
|
+
const out: { tick: number; usPerQuarter: number }[] = [];
|
|
86
|
+
for (let index = 0; index < ticks.length; index += 1) {
|
|
87
|
+
const tick = Math.round(ticks[index]!);
|
|
88
|
+
const next = ticks[index + 1];
|
|
89
|
+
let us: number;
|
|
90
|
+
if (next === undefined || tick >= end) us = constantUs(map.bpm(tick));
|
|
91
|
+
else {
|
|
92
|
+
const seconds = map.seconds(next) - map.seconds(tick);
|
|
93
|
+
us = Math.round((seconds * 1_000_000 * tpb) / (next - tick));
|
|
94
|
+
}
|
|
95
|
+
us = Math.min(0xffffff, Math.max(1, us));
|
|
96
|
+
if (out.length > 0 && out[out.length - 1]!.usPerQuarter === us) continue;
|
|
97
|
+
out.push({ tick, usPerQuarter: us });
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function varLen(value: number): number[] {
|
|
103
|
+
let v = Math.max(0, Math.round(value));
|
|
104
|
+
const bytes = [v & 0x7f];
|
|
105
|
+
v >>>= 7;
|
|
106
|
+
while (v > 0) {
|
|
107
|
+
bytes.unshift((v & 0x7f) | 0x80);
|
|
108
|
+
v >>>= 7;
|
|
109
|
+
}
|
|
110
|
+
return bytes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type TimedEvent = Readonly<{ tick: number; order: number; bytes: number[] }>;
|
|
114
|
+
|
|
115
|
+
function trackChunk(
|
|
116
|
+
events: TimedEvent[],
|
|
117
|
+
name: string | undefined,
|
|
118
|
+
endTick: number,
|
|
119
|
+
): number[] {
|
|
120
|
+
const sorted = [...events].sort(
|
|
121
|
+
(a, b) => a.tick - b.tick || a.order - b.order,
|
|
122
|
+
);
|
|
123
|
+
const body: number[] = [];
|
|
124
|
+
if (name !== undefined) {
|
|
125
|
+
const text = [...new TextEncoder().encode(name)].slice(0, 127);
|
|
126
|
+
body.push(0, 0xff, 0x03, ...varLen(text.length), ...text);
|
|
127
|
+
}
|
|
128
|
+
let last = 0;
|
|
129
|
+
for (const event of sorted) {
|
|
130
|
+
body.push(...varLen(event.tick - last), ...event.bytes);
|
|
131
|
+
last = event.tick;
|
|
132
|
+
}
|
|
133
|
+
body.push(...varLen(Math.max(0, endTick - last)), 0xff, 0x2f, 0);
|
|
134
|
+
return [
|
|
135
|
+
0x4d,
|
|
136
|
+
0x54,
|
|
137
|
+
0x72,
|
|
138
|
+
0x6b,
|
|
139
|
+
(body.length >>> 24) & 0xff,
|
|
140
|
+
(body.length >>> 16) & 0xff,
|
|
141
|
+
(body.length >>> 8) & 0xff,
|
|
142
|
+
body.length & 0xff,
|
|
143
|
+
...body,
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CC64: Readonly<Record<PedalState, number>> = {
|
|
148
|
+
down: 127,
|
|
149
|
+
half: 64,
|
|
150
|
+
up: 0,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function log2(value: number): number {
|
|
154
|
+
return Math.round(Math.log2(value));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** MTS frequency words (semitone, 14-bit fraction) for keys 0..127. */
|
|
158
|
+
export function mtsKeys(table: TuningTable): readonly number[][] {
|
|
159
|
+
const out: number[][] = [];
|
|
160
|
+
for (let key = 0; key < 128; key++) {
|
|
161
|
+
const hz = table.hz[key] ?? 0;
|
|
162
|
+
if (!(hz > 0)) {
|
|
163
|
+
out.push([0x7f, 0x7f, 0x7f]);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const value = 69 + 12 * Math.log2(hz / 440);
|
|
167
|
+
let semitone = Math.floor(value);
|
|
168
|
+
let fraction = Math.round((value - semitone) * 16384);
|
|
169
|
+
if (fraction >= 16384) {
|
|
170
|
+
semitone += 1;
|
|
171
|
+
fraction = 0;
|
|
172
|
+
}
|
|
173
|
+
if (semitone < 0) out.push([0, 0, 0]);
|
|
174
|
+
else if (semitone > 127 || (semitone === 127 && fraction > 16382))
|
|
175
|
+
out.push([0x7f, 0x7f, 0x7e]);
|
|
176
|
+
else out.push([semitone, (fraction >> 7) & 0x7f, fraction & 0x7f]);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The SysEx and RPN events that tune a channel at tick 0. */
|
|
182
|
+
function tuningEvents(
|
|
183
|
+
keys: readonly number[][],
|
|
184
|
+
program: number,
|
|
185
|
+
channel: number,
|
|
186
|
+
): TimedEvent[] {
|
|
187
|
+
const events: TimedEvent[] = [];
|
|
188
|
+
for (let first = 0; first < 128; first += 64) {
|
|
189
|
+
const data = [0x7f, 0x7f, 0x08, 0x02, program, 64];
|
|
190
|
+
for (let key = first; key < first + 64; key++)
|
|
191
|
+
data.push(key, ...keys[key]!);
|
|
192
|
+
data.push(0xf7);
|
|
193
|
+
events.push({
|
|
194
|
+
tick: 0,
|
|
195
|
+
order: -2,
|
|
196
|
+
bytes: [0xf0, ...varLen(data.length), ...data],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const cc = 0xb0 | channel;
|
|
200
|
+
events.push({
|
|
201
|
+
tick: 0,
|
|
202
|
+
order: -1,
|
|
203
|
+
bytes: [cc, 101, 0, 0, cc, 100, 3, 0, cc, 6, program, 0, cc, 101, 127],
|
|
204
|
+
});
|
|
205
|
+
events.push({ tick: 0, order: -1, bytes: [cc, 100, 127] });
|
|
206
|
+
return events;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Pitch-bend range written with RPN 0: ±24 semitones covers octave falls. */
|
|
210
|
+
export const MIDI_BEND_RANGE_SEMITONES = 24;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Pitch bend for notes with pitch expression (glide, bend, vibrato, cents).
|
|
214
|
+
* SMF pitch bend is per channel, so only notes that sound alone on their
|
|
215
|
+
* track get it (a mono line, a lead); notes under a chord stay at the key.
|
|
216
|
+
* The curve is sampled every 1/64 note and written when it changes; the
|
|
217
|
+
* bend returns to centre when the note ends. RPN 0 sets the range first.
|
|
218
|
+
*/
|
|
219
|
+
function pitchBendEvents(
|
|
220
|
+
notes: readonly PerformedNote[],
|
|
221
|
+
channel: number,
|
|
222
|
+
timing: PerformanceTiming,
|
|
223
|
+
at: (tick: number) => number,
|
|
224
|
+
): TimedEvent[] {
|
|
225
|
+
const sorted = [...notes].sort((a, b) => a.startTick - b.startTick);
|
|
226
|
+
const events: TimedEvent[] = [];
|
|
227
|
+
const step = Math.max(1, timing.ticksPerBeat / 16);
|
|
228
|
+
const range = MIDI_BEND_RANGE_SEMITONES * 100;
|
|
229
|
+
const seconds = (tick: number) =>
|
|
230
|
+
timing.secondsAt
|
|
231
|
+
? timing.secondsAt(tick)
|
|
232
|
+
: (tick * 60) / (timing.tempoBpm * timing.ticksPerBeat);
|
|
233
|
+
const bend = (tick: number, order: number, cents: number) => {
|
|
234
|
+
const value = Math.min(
|
|
235
|
+
16383,
|
|
236
|
+
Math.max(0, Math.round(8192 + (cents / range) * 8192)),
|
|
237
|
+
);
|
|
238
|
+
events.push({
|
|
239
|
+
tick,
|
|
240
|
+
order,
|
|
241
|
+
bytes: [0xe0 | channel, value & 0x7f, (value >>> 7) & 0x7f],
|
|
242
|
+
});
|
|
243
|
+
return value;
|
|
244
|
+
};
|
|
245
|
+
sorted.forEach((note, index) => {
|
|
246
|
+
const cents = note.performance?.cents;
|
|
247
|
+
if (!cents && !note.cents) return;
|
|
248
|
+
const stop = note.startTick + note.durationTicks;
|
|
249
|
+
const overlaps = sorted.some(
|
|
250
|
+
(other, j) =>
|
|
251
|
+
j !== index &&
|
|
252
|
+
other.startTick < stop &&
|
|
253
|
+
other.startTick + other.durationTicks > note.startTick,
|
|
254
|
+
);
|
|
255
|
+
if (overlaps) return;
|
|
256
|
+
const start = seconds(note.startTick);
|
|
257
|
+
const centsAtTick = (tick: number) =>
|
|
258
|
+
(note.cents ?? 0) + (cents ? cents(seconds(tick) - start) : 0);
|
|
259
|
+
// Before the note-on at the same tick.
|
|
260
|
+
let last = bend(at(note.startTick), 0.7, centsAtTick(note.startTick));
|
|
261
|
+
for (let tick = note.startTick + step; tick < stop; tick += step) {
|
|
262
|
+
const value = Math.min(
|
|
263
|
+
16383,
|
|
264
|
+
Math.max(0, Math.round(8192 + (centsAtTick(tick) / range) * 8192)),
|
|
265
|
+
);
|
|
266
|
+
if (value === last || at(tick) === at(note.startTick)) continue;
|
|
267
|
+
last = bend(at(tick), 0.7, centsAtTick(tick));
|
|
268
|
+
}
|
|
269
|
+
if (last !== 8192) bend(at(stop), 0.2, 0);
|
|
270
|
+
});
|
|
271
|
+
if (events.length === 0) return events;
|
|
272
|
+
const cc = 0xb0 | channel;
|
|
273
|
+
events.push({
|
|
274
|
+
tick: 0,
|
|
275
|
+
order: -1,
|
|
276
|
+
bytes: [
|
|
277
|
+
cc,
|
|
278
|
+
101,
|
|
279
|
+
0,
|
|
280
|
+
0,
|
|
281
|
+
cc,
|
|
282
|
+
100,
|
|
283
|
+
0,
|
|
284
|
+
0,
|
|
285
|
+
cc,
|
|
286
|
+
6,
|
|
287
|
+
MIDI_BEND_RANGE_SEMITONES,
|
|
288
|
+
0,
|
|
289
|
+
cc,
|
|
290
|
+
38,
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
cc,
|
|
294
|
+
101,
|
|
295
|
+
127,
|
|
296
|
+
0,
|
|
297
|
+
cc,
|
|
298
|
+
100,
|
|
299
|
+
127,
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
return events;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Encode a score as a format-1 Standard MIDI File. */
|
|
306
|
+
export function scoreToMidi(
|
|
307
|
+
score: TrackScore,
|
|
308
|
+
options: MidiOptions = {},
|
|
309
|
+
): Uint8Array {
|
|
310
|
+
// Keep the division in range; scale ticks if a score ever exceeds it.
|
|
311
|
+
const scale =
|
|
312
|
+
score.ticksPerBeat > MAX_DIVISION ? MAX_DIVISION / score.ticksPerBeat : 1;
|
|
313
|
+
const division = Math.round(score.ticksPerBeat * scale);
|
|
314
|
+
const at = (tick: number) => Math.max(0, Math.round(tick * scale));
|
|
315
|
+
const end = loopTicksOf(score);
|
|
316
|
+
|
|
317
|
+
const conductor: TimedEvent[] = [];
|
|
318
|
+
for (const segment of meterSegments(score)) {
|
|
319
|
+
const clocks = Math.round(
|
|
320
|
+
(clickTicksOf(segment, score.ticksPerBeat) / score.ticksPerBeat) * 24,
|
|
321
|
+
);
|
|
322
|
+
conductor.push({
|
|
323
|
+
tick: at(segment.tick),
|
|
324
|
+
order: 0,
|
|
325
|
+
bytes: [
|
|
326
|
+
0xff,
|
|
327
|
+
0x58,
|
|
328
|
+
4,
|
|
329
|
+
segment.beatsPerBar,
|
|
330
|
+
log2(segment.beatUnit),
|
|
331
|
+
Math.min(255, Math.max(1, clocks)),
|
|
332
|
+
8,
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
for (const event of midiTempoEvents(score, options)) {
|
|
337
|
+
const us = event.usPerQuarter;
|
|
338
|
+
conductor.push({
|
|
339
|
+
tick: at(event.tick),
|
|
340
|
+
order: 1,
|
|
341
|
+
bytes: [0xff, 0x51, 3, (us >>> 16) & 0xff, (us >>> 8) & 0xff, us & 0xff],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
for (const section of score.sections ?? []) {
|
|
345
|
+
const tick = barStartTick(score, section.startBar);
|
|
346
|
+
if (tick >= end) continue;
|
|
347
|
+
const text = [...new TextEncoder().encode(section.name)].slice(0, 127);
|
|
348
|
+
conductor.push({
|
|
349
|
+
tick: at(tick),
|
|
350
|
+
order: 2,
|
|
351
|
+
bytes: [0xff, 0x06, ...varLen(text.length), ...text],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const chunks: number[][] = [trackChunk(conductor, "dawg", at(end))];
|
|
355
|
+
const programs = new Map<string, number>();
|
|
356
|
+
const placed = performedNotes(score);
|
|
357
|
+
const timing = performanceTimingFor(score);
|
|
358
|
+
let melodic = 0;
|
|
359
|
+
for (const track of score.tracks) {
|
|
360
|
+
const drum = isDrumInstrument(track.instrument) || track.kit !== undefined;
|
|
361
|
+
let channel: number;
|
|
362
|
+
if (drum) channel = 9;
|
|
363
|
+
else {
|
|
364
|
+
channel = melodic % 15;
|
|
365
|
+
if (channel >= 9) channel += 1;
|
|
366
|
+
melodic += 1;
|
|
367
|
+
}
|
|
368
|
+
const events: TimedEvent[] = [];
|
|
369
|
+
// The pedal is written as CC64, so notes are performed without it and
|
|
370
|
+
// keep the key's length.
|
|
371
|
+
const notes = performNotes(
|
|
372
|
+
track.pedal ? { ...track, pedal: undefined } : track,
|
|
373
|
+
placed.filter((note) => note.trackId === track.id),
|
|
374
|
+
timing,
|
|
375
|
+
);
|
|
376
|
+
const table =
|
|
377
|
+
score.tuning || track.tuning
|
|
378
|
+
? resolveTuning(score.tuning, track.tuning, score.key)
|
|
379
|
+
: undefined;
|
|
380
|
+
if (table && !drum) {
|
|
381
|
+
const keys = mtsKeys(table);
|
|
382
|
+
let program = programs.get(keys.join(","));
|
|
383
|
+
if (program === undefined && programs.size < 128) {
|
|
384
|
+
program = programs.size;
|
|
385
|
+
programs.set(keys.join(","), program);
|
|
386
|
+
}
|
|
387
|
+
if (program !== undefined) {
|
|
388
|
+
events.push(...tuningEvents(keys, program, channel));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const event of track.pedal ?? []) {
|
|
392
|
+
if (event.tick > end) continue;
|
|
393
|
+
events.push({
|
|
394
|
+
tick: at(event.tick),
|
|
395
|
+
// After note-offs, before note-ons at the same tick.
|
|
396
|
+
order: 0.5,
|
|
397
|
+
bytes: [0xb0 | channel, 64, CC64[event.state]],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (!drum) events.push(...pitchBendEvents(notes, channel, timing, at));
|
|
401
|
+
for (const note of notes) {
|
|
402
|
+
if (note.pitch < 0 || note.pitch > 127) continue;
|
|
403
|
+
const start = at(note.startTick);
|
|
404
|
+
const stop = Math.max(
|
|
405
|
+
start + 1,
|
|
406
|
+
at(Math.min(note.startTick + note.durationTicks, end)),
|
|
407
|
+
);
|
|
408
|
+
const velocity = Math.min(
|
|
409
|
+
127,
|
|
410
|
+
Math.max(1, Math.round(note.velocity * 127)),
|
|
411
|
+
);
|
|
412
|
+
events.push({
|
|
413
|
+
tick: start,
|
|
414
|
+
order: 1,
|
|
415
|
+
bytes: [0x90 | channel, note.pitch, velocity],
|
|
416
|
+
});
|
|
417
|
+
// Note-offs sort before note-ons at the same tick so repeats retrigger.
|
|
418
|
+
events.push({
|
|
419
|
+
tick: stop,
|
|
420
|
+
order: 0,
|
|
421
|
+
bytes: [0x80 | channel, note.pitch, 0],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
chunks.push(trackChunk(events, track.name, at(end)));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const header = [
|
|
428
|
+
0x4d,
|
|
429
|
+
0x54,
|
|
430
|
+
0x68,
|
|
431
|
+
0x64,
|
|
432
|
+
0,
|
|
433
|
+
0,
|
|
434
|
+
0,
|
|
435
|
+
6,
|
|
436
|
+
0,
|
|
437
|
+
1,
|
|
438
|
+
(chunks.length >>> 8) & 0xff,
|
|
439
|
+
chunks.length & 0xff,
|
|
440
|
+
(division >>> 8) & 0xff,
|
|
441
|
+
division & 0xff,
|
|
442
|
+
];
|
|
443
|
+
const total = header.length + chunks.reduce((sum, c) => sum + c.length, 0);
|
|
444
|
+
const out = new Uint8Array(total);
|
|
445
|
+
out.set(header, 0);
|
|
446
|
+
let offset = header.length;
|
|
447
|
+
for (const chunk of chunks) {
|
|
448
|
+
out.set(chunk, offset);
|
|
449
|
+
offset += chunk.length;
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
package/core/rhythm.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
type Note,
|
|
23
23
|
type Track,
|
|
24
24
|
} from "./score.ts";
|
|
25
|
+
import { NOTE_EXPRESSION_FIELDS } from "./expression.ts";
|
|
26
|
+
import { loopTicksOf } from "./tempo.ts";
|
|
25
27
|
|
|
26
28
|
/** MIDI pitch a row's voice plays on `track`, or undefined when unknown. */
|
|
27
29
|
export function rhythmVoicePitch(
|
|
@@ -70,7 +72,7 @@ export function expandTrackRow(
|
|
|
70
72
|
`track ${track.id} has no voice "${row.voice}"`,
|
|
71
73
|
"invalid-track",
|
|
72
74
|
);
|
|
73
|
-
const loopTicks = score
|
|
75
|
+
const loopTicks = loopTicksOf(score);
|
|
74
76
|
const prefix = `rh.${trackHash(track.id)}.${idVoice(row.voice)}.`;
|
|
75
77
|
return expandRow(row, { ticksPerBeat: score.ticksPerBeat, loopTicks }).map(
|
|
76
78
|
(hit, index) => ({
|
|
@@ -229,11 +231,14 @@ function sameNotes(
|
|
|
229
231
|
): boolean {
|
|
230
232
|
if (actual.length !== expected.length) return false;
|
|
231
233
|
const key = (note: Note) =>
|
|
232
|
-
`${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}`;
|
|
234
|
+
`${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}|${note.cents ?? 0}`;
|
|
233
235
|
const want = new Map<string, number>();
|
|
234
236
|
for (const note of expected)
|
|
235
237
|
want.set(key(note), (want.get(key(note)) ?? 0) + 1);
|
|
236
238
|
for (const note of actual) {
|
|
239
|
+
// Expression is a hand edit: the generator cannot reproduce it.
|
|
240
|
+
if (NOTE_EXPRESSION_FIELDS.some((field) => note[field] !== undefined))
|
|
241
|
+
return false;
|
|
237
242
|
const left = want.get(key(note));
|
|
238
243
|
if (!left) return false;
|
|
239
244
|
want.set(key(note), left - 1);
|