@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/src/audio/wav.ts
CHANGED
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
type TrackScore,
|
|
11
11
|
} from "../../core/score.ts";
|
|
12
12
|
import { drumVoiceForPitch, isDrumInstrument } from "../../core/drums.ts";
|
|
13
|
+
import {
|
|
14
|
+
performanceTimingFor,
|
|
15
|
+
tunedTiming,
|
|
16
|
+
performNotes,
|
|
17
|
+
type PerformedNote,
|
|
18
|
+
} from "../../core/expression.ts";
|
|
13
19
|
import {
|
|
14
20
|
planSamplerVoices,
|
|
15
21
|
renderSamplerVoices,
|
|
@@ -34,12 +40,16 @@ import {
|
|
|
34
40
|
import { synthKit } from "../../core/kits.ts";
|
|
35
41
|
import { kitDrumSample, kitTailSeconds, newKitVoiceState } from "./kits.ts";
|
|
36
42
|
import {
|
|
43
|
+
addReverbWet,
|
|
37
44
|
applyMonoChain,
|
|
38
45
|
applyStereoChain,
|
|
39
46
|
delayTailFor,
|
|
40
47
|
interpolateAutomation,
|
|
48
|
+
reverbActive,
|
|
41
49
|
reverbImpulse,
|
|
42
50
|
reverbTailFor,
|
|
51
|
+
reverbWet,
|
|
52
|
+
type ReverbWet,
|
|
43
53
|
} from "./effects/chain.ts";
|
|
44
54
|
import {
|
|
45
55
|
createOrbitBus,
|
|
@@ -49,12 +59,21 @@ import {
|
|
|
49
59
|
type OrbitBus,
|
|
50
60
|
} from "./effects/bus.ts";
|
|
51
61
|
import { tableFor, wavetableOscillator } from "./wavetable.ts";
|
|
62
|
+
import { noteHz, resolveTuning, type TuningTable } from "../../core/tuning.ts";
|
|
52
63
|
import {
|
|
53
64
|
duckGains,
|
|
54
65
|
duckSettings,
|
|
55
66
|
orbitOf,
|
|
56
67
|
type Ducker,
|
|
57
68
|
} from "./effects/duck.ts";
|
|
69
|
+
import {
|
|
70
|
+
loopSecondsOf,
|
|
71
|
+
performedNotes,
|
|
72
|
+
slowestBpmOf,
|
|
73
|
+
} from "../../core/tempo.ts";
|
|
74
|
+
import { sampleWarpFor, warpedSpan, type SampleWarp } from "./warp.ts";
|
|
75
|
+
import { applyMaster, type MasterReport } from "./master.ts";
|
|
76
|
+
import type { SongMaster } from "../../core/master.ts";
|
|
58
77
|
|
|
59
78
|
export type WavOptions = Readonly<{ sampleRate?: number; maxSeconds?: number }>;
|
|
60
79
|
|
|
@@ -71,6 +90,12 @@ export type RenderOptions = WavOptions &
|
|
|
71
90
|
* tracks render silent; scores without samplers are unaffected.
|
|
72
91
|
*/
|
|
73
92
|
samples?: SampleBank;
|
|
93
|
+
/**
|
|
94
|
+
* Ticks added to each note's start when seeding its noise. An arranged
|
|
95
|
+
* render window passes its origin so seeded voices sound as they do in
|
|
96
|
+
* a single pass of the whole song. Zero (the default) changes nothing.
|
|
97
|
+
*/
|
|
98
|
+
seedTick?: number;
|
|
74
99
|
}>;
|
|
75
100
|
|
|
76
101
|
/** Interleaved stereo 16-bit PCM plus its frame count. */
|
|
@@ -79,6 +104,8 @@ export type RenderedAudio = Readonly<{
|
|
|
79
104
|
channels: 2;
|
|
80
105
|
frames: number;
|
|
81
106
|
pcm: Int16Array;
|
|
107
|
+
/** Loudness the song master reached; absent without a master. */
|
|
108
|
+
master?: MasterReport;
|
|
82
109
|
}>;
|
|
83
110
|
|
|
84
111
|
/** Output channel count for every render and export. */
|
|
@@ -141,6 +168,16 @@ const MAX_DRUM_SECONDS = 0.6;
|
|
|
141
168
|
const CONTROL_SAMPLES = 32;
|
|
142
169
|
/** One-shot renders keep this much ring-out after the last bar. */
|
|
143
170
|
const ONE_SHOT_TAIL_SECONDS = 0.35;
|
|
171
|
+
|
|
172
|
+
function oneShotSeconds(
|
|
173
|
+
loopSeconds: number,
|
|
174
|
+
samplerTail: number,
|
|
175
|
+
reverbTail: number,
|
|
176
|
+
): number {
|
|
177
|
+
return (
|
|
178
|
+
loopSeconds + Math.max(ONE_SHOT_TAIL_SECONDS, samplerTail) + reverbTail
|
|
179
|
+
);
|
|
180
|
+
}
|
|
144
181
|
/** Loop renders fold at most this much tail back onto the loop start. */
|
|
145
182
|
const MAX_LOOP_TAIL_SECONDS = 8;
|
|
146
183
|
|
|
@@ -151,10 +188,14 @@ type RenderContext = Readonly<{
|
|
|
151
188
|
samplesPerTick: number;
|
|
152
189
|
tempoBpm: number;
|
|
153
190
|
irs?: ReadonlyMap<string, DecodedSample>;
|
|
191
|
+
/** Tempo map in samples (core/tempo.ts); absent: constant tempo. */
|
|
192
|
+
warp?: SampleWarp;
|
|
193
|
+
seedTick?: number;
|
|
154
194
|
}>;
|
|
155
195
|
|
|
156
196
|
/** Exact (fractional) loop length in frames at a sample rate. */
|
|
157
197
|
export function loopFrames(score: TrackScore, sampleRate: number): number {
|
|
198
|
+
if (score.time) return loopSecondsOf(score) * sampleRate;
|
|
158
199
|
return (score.bars * score.beatsPerBar * 60 * sampleRate) / score.tempoBpm;
|
|
159
200
|
}
|
|
160
201
|
|
|
@@ -193,8 +234,16 @@ type Stem = {
|
|
|
193
234
|
key: string;
|
|
194
235
|
left: Float64Array;
|
|
195
236
|
right: Float64Array;
|
|
237
|
+
/** Stem, pre-reverb pair and tail (counted in the cache budget). */
|
|
196
238
|
bytes: number;
|
|
197
239
|
used: number;
|
|
240
|
+
/** For a track with a reverb: its stem before the reverb, and the tail. */
|
|
241
|
+
room?: Readonly<{
|
|
242
|
+
/** `stemKey` of the track with its reverb mix and mix lane left out. */
|
|
243
|
+
key: string;
|
|
244
|
+
pre: Readonly<{ left: Float64Array; right: Float64Array }>;
|
|
245
|
+
wet: ReverbWet;
|
|
246
|
+
}>;
|
|
198
247
|
};
|
|
199
248
|
|
|
200
249
|
/**
|
|
@@ -244,11 +293,38 @@ export class StemRenderer {
|
|
|
244
293
|
return { stems: this.stems.size, bytes: this.cacheBytes };
|
|
245
294
|
}
|
|
246
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Seconds a one-shot render of `score` needs (song plus tail) before the
|
|
298
|
+
* renderer's cap, so callers can tell when a single pass would cut it.
|
|
299
|
+
*/
|
|
300
|
+
public oneShotSeconds(
|
|
301
|
+
score: TrackScore,
|
|
302
|
+
options: RenderOptions = {},
|
|
303
|
+
): number {
|
|
304
|
+
const sampleRate = clampSampleRate(options.sampleRate);
|
|
305
|
+
const bank = options.samples ?? EMPTY_SAMPLE_BANK;
|
|
306
|
+
const reverbTail = Math.max(
|
|
307
|
+
0,
|
|
308
|
+
...score.tracks.map((track) =>
|
|
309
|
+
reverbTailFor(track, sampleRate, bank.irs),
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
const samplerTail = Math.min(
|
|
313
|
+
MAX_LOOP_TAIL_SECONDS,
|
|
314
|
+
Math.max(
|
|
315
|
+
samplerTailSeconds(score, bank, sampleRate),
|
|
316
|
+
...score.tracks.map(synthTailSeconds),
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
return oneShotSeconds(loopSecondsOf(score), samplerTail, reverbTail);
|
|
320
|
+
}
|
|
321
|
+
|
|
247
322
|
public render(score: TrackScore, options: RenderOptions = {}): RenderedAudio {
|
|
248
323
|
const sampleRate = clampSampleRate(options.sampleRate);
|
|
249
324
|
const maxSeconds = Math.max(1, Math.min(60, options.maxSeconds ?? 30));
|
|
250
|
-
const loopSeconds = (score
|
|
325
|
+
const loopSeconds = loopSecondsOf(score);
|
|
251
326
|
const bank = options.samples ?? EMPTY_SAMPLE_BANK;
|
|
327
|
+
const warp = sampleWarpFor(score, sampleRate);
|
|
252
328
|
const reverbTail = Math.max(
|
|
253
329
|
0,
|
|
254
330
|
...score.tracks.map((track) =>
|
|
@@ -281,14 +357,17 @@ export class StemRenderer {
|
|
|
281
357
|
reverbTail +
|
|
282
358
|
Math.max(
|
|
283
359
|
0,
|
|
284
|
-
|
|
360
|
+
// The slowest tempo in the map gives the longest echoes.
|
|
361
|
+
...score.tracks.map((track) =>
|
|
362
|
+
delayTailFor(track, slowestBpmOf(score)),
|
|
363
|
+
),
|
|
285
364
|
),
|
|
286
365
|
);
|
|
287
366
|
samples = frames + Math.ceil(tailSeconds * sampleRate);
|
|
288
367
|
} else {
|
|
289
368
|
const seconds = Math.min(
|
|
290
369
|
maxSeconds,
|
|
291
|
-
loopSeconds
|
|
370
|
+
oneShotSeconds(loopSeconds, samplerTail, reverbTail),
|
|
292
371
|
);
|
|
293
372
|
frames = Math.max(1, Math.ceil(seconds * sampleRate));
|
|
294
373
|
samples = frames;
|
|
@@ -300,6 +379,8 @@ export class StemRenderer {
|
|
|
300
379
|
samplesPerTick: (sampleRate * 60) / (score.tempoBpm * score.ticksPerBeat),
|
|
301
380
|
tempoBpm: score.tempoBpm,
|
|
302
381
|
...(bank.irs ? { irs: bank.irs } : {}),
|
|
382
|
+
...(warp ? { warp } : {}),
|
|
383
|
+
...(options.seedTick ? { seedTick: options.seedTick } : {}),
|
|
303
384
|
};
|
|
304
385
|
this.renders += 1;
|
|
305
386
|
const { dry, dryR, left, right, mixL, mixR } = this.scratchFor(samples);
|
|
@@ -307,20 +388,34 @@ export class StemRenderer {
|
|
|
307
388
|
mixR.fill(0);
|
|
308
389
|
const tracks = new Map(score.tracks.map((track) => [track.id, track]));
|
|
309
390
|
const groups = new Map<string, Note[]>();
|
|
310
|
-
|
|
391
|
+
// Track time (rate, phase, cycle) places notes on the song timeline.
|
|
392
|
+
for (const note of performedNotes(score)) {
|
|
311
393
|
const group = groups.get(note.trackId);
|
|
312
394
|
if (group) group.push(note);
|
|
313
395
|
else groups.set(note.trackId, [note]);
|
|
314
396
|
}
|
|
397
|
+
// Note expression and track performance (core/expression.ts): the
|
|
398
|
+
// notes as played. A track with neither gets its notes back unchanged.
|
|
399
|
+
const timing = performanceTimingFor(score);
|
|
400
|
+
const performed = new Map<string, readonly PerformedNote[]>();
|
|
401
|
+
for (const [trackId, notes] of groups) {
|
|
402
|
+
const track = tracks.get(trackId);
|
|
403
|
+
performed.set(
|
|
404
|
+
trackId,
|
|
405
|
+
performNotes(track, notes, tunedTiming(timing, score, track)),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
315
408
|
// Orbit ducking is a gain on finished stems (src/audio/effects/duck.ts).
|
|
316
409
|
const duckers: Ducker[] = [];
|
|
317
|
-
for (const [trackId
|
|
410
|
+
for (const [trackId] of groups) {
|
|
318
411
|
const settings = duckSettings(tracks.get(trackId));
|
|
319
412
|
if (settings && isTrackAudible(score, trackId))
|
|
320
413
|
duckers.push({
|
|
321
414
|
trackId,
|
|
322
415
|
...settings,
|
|
323
|
-
onsets:
|
|
416
|
+
onsets: performed
|
|
417
|
+
.get(trackId)!
|
|
418
|
+
.map((note) => noteSpan(note, context).start),
|
|
324
419
|
});
|
|
325
420
|
}
|
|
326
421
|
const ducked = duckGains(
|
|
@@ -337,11 +432,16 @@ export class StemRenderer {
|
|
|
337
432
|
for (const [trackId, notes] of groups) {
|
|
338
433
|
if (!isTrackAudible(score, trackId)) continue;
|
|
339
434
|
const track = tracks.get(trackId);
|
|
435
|
+
// Stems key on the written notes (with the track's performance
|
|
436
|
+
// settings); the voices render the performed ones.
|
|
437
|
+
const played = performed.get(trackId)!;
|
|
340
438
|
const busOrbit = sharedOrbitOf(track);
|
|
341
439
|
const sampler = isSamplerInstrument(track?.instrument);
|
|
342
440
|
// Wavetable hook: the oscillator factory for a wavetable track (its
|
|
343
441
|
// table id joins the stem key), undefined for every other instrument.
|
|
344
442
|
const wavetable = track ? wavetableHook(track, bank, context) : undefined;
|
|
443
|
+
// The merged song and track tuning; undefined keeps 12-TET untouched.
|
|
444
|
+
const tuning = resolveTuning(score.tuning, track?.tuning, score.key);
|
|
345
445
|
const key = stemKey(
|
|
346
446
|
track,
|
|
347
447
|
notes,
|
|
@@ -351,9 +451,38 @@ export class StemRenderer {
|
|
|
351
451
|
track ? reverbImpulse(track, sampleRate, bank.irs)?.id : undefined,
|
|
352
452
|
);
|
|
353
453
|
let stem = this.stems.get(trackId);
|
|
454
|
+
const caching = this.maxCacheBytes > 0;
|
|
455
|
+
// A cached track with a reverb also keeps its pre-reverb pair and the
|
|
456
|
+
// tail before its mix gain, keyed without the mix: a mix-only edit
|
|
457
|
+
// re-adds the tail instead of re-running the voices and the room.
|
|
458
|
+
const room =
|
|
459
|
+
caching && track && busOrbit === undefined && reverbActive(track)
|
|
460
|
+
? stemKey(
|
|
461
|
+
roomTrack(track),
|
|
462
|
+
notes,
|
|
463
|
+
context,
|
|
464
|
+
sampler ? bank : undefined,
|
|
465
|
+
wavetable?.id,
|
|
466
|
+
reverbImpulse(track, sampleRate, bank.irs)?.id,
|
|
467
|
+
)
|
|
468
|
+
: undefined;
|
|
354
469
|
if (stem?.key === key) stem.used = this.renders;
|
|
355
|
-
else {
|
|
356
|
-
const
|
|
470
|
+
else if (track && room !== undefined && stem?.room?.key === room) {
|
|
471
|
+
const { pre, wet } = stem.room;
|
|
472
|
+
const target = {
|
|
473
|
+
left: Float64Array.from(pre.left),
|
|
474
|
+
right: Float64Array.from(pre.right),
|
|
475
|
+
};
|
|
476
|
+
addReverbWet(target.left, target.right, wet, track, context);
|
|
477
|
+
stem = {
|
|
478
|
+
...stem,
|
|
479
|
+
key,
|
|
480
|
+
left: target.left,
|
|
481
|
+
right: target.right,
|
|
482
|
+
used: this.renders,
|
|
483
|
+
};
|
|
484
|
+
this.store(trackId, stem);
|
|
485
|
+
} else {
|
|
357
486
|
const target = caching
|
|
358
487
|
? {
|
|
359
488
|
left: new Float64Array(samples),
|
|
@@ -365,16 +494,28 @@ export class StemRenderer {
|
|
|
365
494
|
const stereo = synthVoice && isStereoVoice(track);
|
|
366
495
|
if (stereo) dryR.fill(0);
|
|
367
496
|
if (sampler) {
|
|
368
|
-
if (track) renderSamplerNotes(dry,
|
|
497
|
+
if (track) renderSamplerNotes(dry, played, track, context, bank);
|
|
369
498
|
} else if (synthVoice && track) {
|
|
370
499
|
const gainAt = (tick: number) => trackGainAt(track, tick);
|
|
371
500
|
const voice = {
|
|
372
501
|
...context,
|
|
373
502
|
ticksPerBeat: score.ticksPerBeat,
|
|
374
503
|
...(wavetable ? { oscillatorFor: wavetable.oscillatorFor } : {}),
|
|
504
|
+
...(tuning ? { tuning } : {}),
|
|
375
505
|
};
|
|
376
|
-
|
|
506
|
+
const warp = context.warp;
|
|
507
|
+
for (const note of played) {
|
|
377
508
|
const { start, length } = noteSpan(note, context);
|
|
509
|
+
// The voice asks for `startTick + elapsed / samplesPerTick`;
|
|
510
|
+
// through a tempo map that sample's tick comes from the map.
|
|
511
|
+
const noteGainAt = warp
|
|
512
|
+
? (tick: number) =>
|
|
513
|
+
gainAt(
|
|
514
|
+
warp.tick(
|
|
515
|
+
start + (tick - note.startTick) * context.samplesPerTick,
|
|
516
|
+
),
|
|
517
|
+
)
|
|
518
|
+
: gainAt;
|
|
378
519
|
renderSynthNote(
|
|
379
520
|
dry,
|
|
380
521
|
stereo ? dryR : undefined,
|
|
@@ -383,14 +524,14 @@ export class StemRenderer {
|
|
|
383
524
|
start,
|
|
384
525
|
length,
|
|
385
526
|
voice,
|
|
386
|
-
|
|
527
|
+
noteGainAt,
|
|
387
528
|
);
|
|
388
529
|
}
|
|
389
530
|
} else {
|
|
390
531
|
const drums = isDrumInstrument(track?.instrument);
|
|
391
|
-
for (const note of
|
|
532
|
+
for (const note of played) {
|
|
392
533
|
if (drums) renderDrumNote(dry, note, track, context);
|
|
393
|
-
else renderToneNote(dry, note, track, context);
|
|
534
|
+
else renderToneNote(dry, note, track, context, tuning);
|
|
394
535
|
}
|
|
395
536
|
}
|
|
396
537
|
if (track) applyMonoChain(dry, track, context);
|
|
@@ -403,7 +544,27 @@ export class StemRenderer {
|
|
|
403
544
|
context,
|
|
404
545
|
stereo ? dryR : undefined,
|
|
405
546
|
);
|
|
406
|
-
|
|
547
|
+
let saved: Stem["room"];
|
|
548
|
+
if (track && room !== undefined) {
|
|
549
|
+
// Same chain, the reverb split in two (see `applyReverb`).
|
|
550
|
+
applyStereoChain(
|
|
551
|
+
target.left,
|
|
552
|
+
target.right,
|
|
553
|
+
track,
|
|
554
|
+
context,
|
|
555
|
+
true,
|
|
556
|
+
false,
|
|
557
|
+
);
|
|
558
|
+
const pre = {
|
|
559
|
+
left: Float64Array.from(target.left),
|
|
560
|
+
right: Float64Array.from(target.right),
|
|
561
|
+
};
|
|
562
|
+
const wet = reverbWet(target.left, target.right, track, context);
|
|
563
|
+
if (wet) {
|
|
564
|
+
addReverbWet(target.left, target.right, wet, track, context);
|
|
565
|
+
saved = { key: room, pre, wet };
|
|
566
|
+
}
|
|
567
|
+
} else if (track)
|
|
407
568
|
applyStereoChain(
|
|
408
569
|
target.left,
|
|
409
570
|
target.right,
|
|
@@ -415,8 +576,9 @@ export class StemRenderer {
|
|
|
415
576
|
key,
|
|
416
577
|
left: target.left,
|
|
417
578
|
right: target.right,
|
|
418
|
-
bytes: target.left.byteLength
|
|
579
|
+
bytes: target.left.byteLength * (saved ? 6 : 2),
|
|
419
580
|
used: this.renders,
|
|
581
|
+
...(saved ? { room: saved } : {}),
|
|
420
582
|
};
|
|
421
583
|
if (caching) this.store(trackId, stem);
|
|
422
584
|
}
|
|
@@ -426,17 +588,7 @@ export class StemRenderer {
|
|
|
426
588
|
buses.set(busOrbit, (bus = createOrbitBus(busOrbit, samples)));
|
|
427
589
|
sendToBus(bus, track, stem.left, stem.right, context, key);
|
|
428
590
|
}
|
|
429
|
-
|
|
430
|
-
if (gain)
|
|
431
|
-
for (let index = 0; index < samples; index += 1) {
|
|
432
|
-
mixL[index]! += stem.left[index]! * gain[index]!;
|
|
433
|
-
mixR[index]! += stem.right[index]! * gain[index]!;
|
|
434
|
-
}
|
|
435
|
-
else
|
|
436
|
-
for (let index = 0; index < samples; index += 1) {
|
|
437
|
-
mixL[index]! += stem.left[index]!;
|
|
438
|
-
mixR[index]! += stem.right[index]!;
|
|
439
|
-
}
|
|
591
|
+
addStem(mixL, mixR, stem.left, stem.right, samples, ducked.get(trackId));
|
|
440
592
|
}
|
|
441
593
|
// Bus returns join after every stem, in orbit order; the orbit's ducking
|
|
442
594
|
// applies to them as to its tracks.
|
|
@@ -455,12 +607,15 @@ export class StemRenderer {
|
|
|
455
607
|
wet = { key, ...renderOrbitBus(bus, context) };
|
|
456
608
|
if (this.maxCacheBytes > 0) this.buses.set(orbit, wet);
|
|
457
609
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
610
|
+
// (x * 1 === x, so an unducked bus adds its plain samples.)
|
|
611
|
+
addStem(
|
|
612
|
+
mixL,
|
|
613
|
+
mixR,
|
|
614
|
+
wet.left,
|
|
615
|
+
wet.right,
|
|
616
|
+
samples,
|
|
617
|
+
busGains.get(`bus:${orbit}`),
|
|
618
|
+
);
|
|
464
619
|
}
|
|
465
620
|
for (const orbit of [...this.buses.keys()])
|
|
466
621
|
if (!buses.has(orbit)) this.buses.delete(orbit);
|
|
@@ -468,24 +623,29 @@ export class StemRenderer {
|
|
|
468
623
|
// unsoloed tracks keep theirs so toggling them back is free.
|
|
469
624
|
for (const trackId of [...this.stems.keys()])
|
|
470
625
|
if (!groups.has(trackId)) this.evict(trackId);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
626
|
+
// Linear effects superpose, so folding the tail onto the start yields
|
|
627
|
+
// the steady state of the loop playing forever.
|
|
628
|
+
if (samples > frames) foldTail(mixL, mixR, frames, samples);
|
|
629
|
+
// The song master (src/audio/master.ts); none leaves the mix untouched.
|
|
630
|
+
const mastered = applyMaster(
|
|
631
|
+
mixL,
|
|
632
|
+
mixR,
|
|
633
|
+
frames,
|
|
634
|
+
sampleRate,
|
|
635
|
+
score.master,
|
|
636
|
+
options.loop === true,
|
|
637
|
+
);
|
|
638
|
+
// A master's output is a finished level, so its reduction to 16 bits is
|
|
639
|
+
// TPDF-dithered (seeded: renders stay byte-identical run to run).
|
|
640
|
+
const pcm = mastered
|
|
641
|
+
? toPcm(mastered.left, mastered.right, frames, true)
|
|
642
|
+
: toPcm(mixL, mixR, frames);
|
|
484
643
|
return Object.freeze({
|
|
485
644
|
sampleRate,
|
|
486
645
|
channels: RENDER_CHANNELS,
|
|
487
646
|
frames,
|
|
488
647
|
pcm,
|
|
648
|
+
...(mastered ? { master: mastered.report } : {}),
|
|
489
649
|
});
|
|
490
650
|
}
|
|
491
651
|
|
|
@@ -543,7 +703,122 @@ export class StemRenderer {
|
|
|
543
703
|
}
|
|
544
704
|
}
|
|
545
705
|
|
|
706
|
+
/*
|
|
707
|
+
* The mix loops live in small functions of their own: inside `render` (a
|
|
708
|
+
* large function the JIT optimizes late) they ran several times slower.
|
|
709
|
+
* Same operations in the same order, so the output is unchanged.
|
|
710
|
+
*/
|
|
711
|
+
|
|
712
|
+
/** Add a stem (times its ducking gain, if any) into the mix. */
|
|
713
|
+
function addStem(
|
|
714
|
+
mixL: Float64Array,
|
|
715
|
+
mixR: Float64Array,
|
|
716
|
+
left: Float64Array,
|
|
717
|
+
right: Float64Array,
|
|
718
|
+
samples: number,
|
|
719
|
+
gain: Float64Array | undefined,
|
|
720
|
+
): void {
|
|
721
|
+
if (gain)
|
|
722
|
+
for (let index = 0; index < samples; index += 1) {
|
|
723
|
+
mixL[index]! += left[index]! * gain[index]!;
|
|
724
|
+
mixR[index]! += right[index]! * gain[index]!;
|
|
725
|
+
}
|
|
726
|
+
else
|
|
727
|
+
for (let index = 0; index < samples; index += 1) {
|
|
728
|
+
mixL[index]! += left[index]!;
|
|
729
|
+
mixR[index]! += right[index]!;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Fold everything past `frames` back onto the loop start. */
|
|
734
|
+
function foldTail(
|
|
735
|
+
mixL: Float64Array,
|
|
736
|
+
mixR: Float64Array,
|
|
737
|
+
frames: number,
|
|
738
|
+
samples: number,
|
|
739
|
+
): void {
|
|
740
|
+
for (let index = frames; index < samples; index += 1) {
|
|
741
|
+
mixL[index % frames]! += mixL[index]!;
|
|
742
|
+
mixR[index % frames]! += mixR[index]!;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Interleaved 16-bit PCM of the first `frames` mix samples. */
|
|
747
|
+
/**
|
|
748
|
+
* The song master over an already summed 16-bit mix (an arrangement
|
|
749
|
+
* rendered in windows), so the whole song is limited and normalized as one
|
|
750
|
+
* pass is. Undefined when the master is absent or bypassed.
|
|
751
|
+
*/
|
|
752
|
+
export function masterSummedPcm(
|
|
753
|
+
pcm: Int16Array,
|
|
754
|
+
frames: number,
|
|
755
|
+
sampleRate: number,
|
|
756
|
+
master: SongMaster | undefined,
|
|
757
|
+
loop: boolean,
|
|
758
|
+
): { pcm: Int16Array; report: MasterReport } | undefined {
|
|
759
|
+
const left = new Float64Array(frames);
|
|
760
|
+
const right = new Float64Array(frames);
|
|
761
|
+
for (let index = 0; index < frames; index += 1) {
|
|
762
|
+
left[index] = pcm[index * 2]! / 32767;
|
|
763
|
+
right[index] = pcm[index * 2 + 1]! / 32767;
|
|
764
|
+
}
|
|
765
|
+
const mastered = applyMaster(left, right, frames, sampleRate, master, loop);
|
|
766
|
+
if (!mastered) return undefined;
|
|
767
|
+
return {
|
|
768
|
+
pcm: toPcm(mastered.left, mastered.right, frames, true),
|
|
769
|
+
report: mastered.report,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function toPcm(
|
|
774
|
+
mixL: Float64Array,
|
|
775
|
+
mixR: Float64Array,
|
|
776
|
+
frames: number,
|
|
777
|
+
dither = false,
|
|
778
|
+
): Int16Array {
|
|
779
|
+
const pcm = new Int16Array(frames * RENDER_CHANNELS);
|
|
780
|
+
if (dither) {
|
|
781
|
+
// Triangular dither of +-1 LSB: the sum of two uniform values, from a
|
|
782
|
+
// fixed-seed xorshift so every render of a score dithers the same way.
|
|
783
|
+
let state = 0x9e3779b9;
|
|
784
|
+
const uniform = () => {
|
|
785
|
+
state ^= state << 13;
|
|
786
|
+
state ^= state >>> 17;
|
|
787
|
+
state ^= state << 5;
|
|
788
|
+
return (state >>> 0) / 4_294_967_296;
|
|
789
|
+
};
|
|
790
|
+
for (let index = 0; index < frames; index += 1) {
|
|
791
|
+
pcm[index * 2] = clamp16(
|
|
792
|
+
Math.round(mixL[index]! * 32767 + uniform() - uniform()),
|
|
793
|
+
);
|
|
794
|
+
pcm[index * 2 + 1] = clamp16(
|
|
795
|
+
Math.round(mixR[index]! * 32767 + uniform() - uniform()),
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return pcm;
|
|
799
|
+
}
|
|
800
|
+
for (let index = 0; index < frames; index += 1) {
|
|
801
|
+
pcm[index * 2] = clamp16(mixL[index]! * 32767);
|
|
802
|
+
pcm[index * 2 + 1] = clamp16(mixR[index]! * 32767);
|
|
803
|
+
}
|
|
804
|
+
return pcm;
|
|
805
|
+
}
|
|
806
|
+
|
|
546
807
|
/** Everything a track's stem depends on, except mute and solo. */
|
|
808
|
+
/**
|
|
809
|
+
* The track as its reverb tail sees it: everything but the reverb's mix
|
|
810
|
+
* and mix lane, which only scale the tail.
|
|
811
|
+
*/
|
|
812
|
+
function roomTrack(track: Track): Track {
|
|
813
|
+
const lanes = { ...track.fxAutomation };
|
|
814
|
+
delete lanes["reverb-mix"];
|
|
815
|
+
return {
|
|
816
|
+
...track,
|
|
817
|
+
reverb: { ...track.reverb!, mix: 1 },
|
|
818
|
+
fxAutomation: lanes,
|
|
819
|
+
} as Track;
|
|
820
|
+
}
|
|
821
|
+
|
|
547
822
|
function stemKey(
|
|
548
823
|
track: Track | undefined,
|
|
549
824
|
notes: readonly Note[],
|
|
@@ -566,12 +841,20 @@ function stemKey(
|
|
|
566
841
|
context.score.beatsPerBar,
|
|
567
842
|
context.sampleRate,
|
|
568
843
|
context.samples,
|
|
844
|
+
// Tempo map, meter changes and fermatas move every note.
|
|
845
|
+
...(context.score.time ? [context.score.time] : []),
|
|
569
846
|
// Sampler stems also depend on the decoded files: a replaced or missing
|
|
570
847
|
// sample changes the key even when the score did not change.
|
|
571
848
|
...(bank && track?.sampler ? [samplerVoiceDigest(track, bank)] : []),
|
|
572
849
|
...(wavetableId ? [wavetableId] : []),
|
|
573
850
|
// A convolution reverb's impulse (its sha256 for a sample).
|
|
574
851
|
...(impulseId ? [impulseId] : []),
|
|
852
|
+
// The song tuning, and the key whose tonic is the default root.
|
|
853
|
+
...(context.score.tuning || track?.tuning
|
|
854
|
+
? [context.score.tuning ?? null, context.score.key]
|
|
855
|
+
: []),
|
|
856
|
+
// Seeded noise follows the window origin of an arranged render.
|
|
857
|
+
...(context.seedTick ? [`seed:${context.seedTick}`] : []),
|
|
575
858
|
]);
|
|
576
859
|
}
|
|
577
860
|
|
|
@@ -594,7 +877,11 @@ function renderSamplerNotes(
|
|
|
594
877
|
context: RenderContext,
|
|
595
878
|
bank: SampleBank,
|
|
596
879
|
): void {
|
|
597
|
-
const timing = {
|
|
880
|
+
const timing = {
|
|
881
|
+
score: context.score,
|
|
882
|
+
sampleRate: context.sampleRate,
|
|
883
|
+
...(context.warp ? { warp: context.warp } : {}),
|
|
884
|
+
};
|
|
598
885
|
renderSamplerVoices(
|
|
599
886
|
target,
|
|
600
887
|
planSamplerVoices(track, notes, bank, timing),
|
|
@@ -618,7 +905,8 @@ function noteSpan(
|
|
|
618
905
|
note: Note,
|
|
619
906
|
context: RenderContext,
|
|
620
907
|
): { start: number; length: number } {
|
|
621
|
-
const { score, sampleRate } = context;
|
|
908
|
+
const { score, sampleRate, warp } = context;
|
|
909
|
+
if (warp) return warpedSpan(warp, note.startTick, note.durationTicks);
|
|
622
910
|
const start = Math.max(
|
|
623
911
|
0,
|
|
624
912
|
Math.floor(
|
|
@@ -641,13 +929,18 @@ function renderToneNote(
|
|
|
641
929
|
note: Note,
|
|
642
930
|
track: Track | undefined,
|
|
643
931
|
context: RenderContext,
|
|
932
|
+
tuning?: TuningTable,
|
|
644
933
|
): void {
|
|
645
934
|
const { sampleRate, samples, samplesPerTick } = context;
|
|
646
935
|
const instrument = track?.instrument ?? "sine";
|
|
647
936
|
const { start, length } = noteSpan(note, context);
|
|
648
937
|
const end = Math.min(samples, start + length);
|
|
649
|
-
const frequency =
|
|
938
|
+
const frequency = noteHz(note.pitch, note.cents, tuning);
|
|
939
|
+
if (!(frequency > 0)) return;
|
|
650
940
|
const velocity = Math.max(0, Math.min(1, note.velocity));
|
|
941
|
+
const performance = (note as PerformedNote).performance;
|
|
942
|
+
const cents = performance?.cents;
|
|
943
|
+
let bent = 0;
|
|
651
944
|
for (let index = start; index < end; index += 1) {
|
|
652
945
|
const elapsed = index - start;
|
|
653
946
|
const remaining = end - index;
|
|
@@ -657,9 +950,24 @@ function renderToneNote(
|
|
|
657
950
|
Math.min(attack, release) *
|
|
658
951
|
velocity *
|
|
659
952
|
0.28 *
|
|
660
|
-
trackGainAt(
|
|
661
|
-
|
|
662
|
-
|
|
953
|
+
trackGainAt(
|
|
954
|
+
track,
|
|
955
|
+
context.warp
|
|
956
|
+
? context.warp.tick(index)
|
|
957
|
+
: note.startTick + elapsed / samplesPerTick,
|
|
958
|
+
);
|
|
959
|
+
// Plain notes keep the closed-form phase (byte-identical output).
|
|
960
|
+
let phase = (frequency * elapsed) / sampleRate;
|
|
961
|
+
if (cents) {
|
|
962
|
+
phase = bent;
|
|
963
|
+
bent +=
|
|
964
|
+
(frequency * 2 ** (cents(elapsed / sampleRate) / 1200)) / sampleRate;
|
|
965
|
+
}
|
|
966
|
+
const damp = performance?.damp;
|
|
967
|
+
const t = elapsed / sampleRate;
|
|
968
|
+
const held =
|
|
969
|
+
damp && t > damp.from ? Math.exp(-(t - damp.from) / damp.tau) : 1;
|
|
970
|
+
target[index]! += legacyWave(instrument, phase) * envelope * held;
|
|
663
971
|
}
|
|
664
972
|
}
|
|
665
973
|
|
|
@@ -685,7 +993,9 @@ function renderDrumNote(
|
|
|
685
993
|
start + Math.ceil((kit?.seconds ?? MAX_DRUM_SECONDS) * sampleRate),
|
|
686
994
|
);
|
|
687
995
|
const voice = drumVoiceForPitch(note.pitch);
|
|
688
|
-
const random = seededRandom(
|
|
996
|
+
const random = seededRandom(
|
|
997
|
+
`${note.id}:${note.startTick + (context.seedTick ?? 0)}`,
|
|
998
|
+
);
|
|
689
999
|
const velocity = Math.max(0, Math.min(1, note.velocity));
|
|
690
1000
|
let phase = 0;
|
|
691
1001
|
let previousNoise = 0;
|
|
@@ -751,7 +1061,12 @@ function renderDrumNote(
|
|
|
751
1061
|
release *
|
|
752
1062
|
velocity *
|
|
753
1063
|
0.5 *
|
|
754
|
-
trackGainAt(
|
|
1064
|
+
trackGainAt(
|
|
1065
|
+
track,
|
|
1066
|
+
context.warp
|
|
1067
|
+
? context.warp.tick(index)
|
|
1068
|
+
: note.startTick + (index - start) / samplesPerTick,
|
|
1069
|
+
);
|
|
755
1070
|
}
|
|
756
1071
|
}
|
|
757
1072
|
|
|
@@ -778,7 +1093,13 @@ function applyPan(
|
|
|
778
1093
|
for (let index = 0; index < dry.length; index += 1) {
|
|
779
1094
|
if (lane.length > 0 && index % CONTROL_SAMPLES === 0)
|
|
780
1095
|
[gainL, gainR] = gains(
|
|
781
|
-
interpolateAutomation(
|
|
1096
|
+
interpolateAutomation(
|
|
1097
|
+
lane,
|
|
1098
|
+
context.warp
|
|
1099
|
+
? context.warp.tick(index)
|
|
1100
|
+
: index / context.samplesPerTick,
|
|
1101
|
+
staticPan,
|
|
1102
|
+
),
|
|
782
1103
|
);
|
|
783
1104
|
const sample = dry[index]!;
|
|
784
1105
|
left[index] = sample * gainL;
|
|
@@ -816,16 +1137,29 @@ function wavetableHook(
|
|
|
816
1137
|
lane.length > 0
|
|
817
1138
|
? (tick: number) => interpolateAutomation(lane, tick, settings.wt ?? 0)
|
|
818
1139
|
: undefined;
|
|
1140
|
+
const warp = context.warp;
|
|
819
1141
|
return {
|
|
820
1142
|
id: table.id,
|
|
821
|
-
oscillatorFor: (note) =>
|
|
822
|
-
|
|
1143
|
+
oscillatorFor: (note) => {
|
|
1144
|
+
const span = noteSpan(note, context);
|
|
1145
|
+
return wavetableOscillator(table, settings, note, {
|
|
823
1146
|
sampleRate: context.sampleRate,
|
|
824
1147
|
samplesPerTick: context.samplesPerTick,
|
|
825
1148
|
secondsPerTick: context.samplesPerTick / context.sampleRate,
|
|
826
|
-
gate:
|
|
827
|
-
|
|
828
|
-
|
|
1149
|
+
gate: span.length / context.sampleRate,
|
|
1150
|
+
// Through a tempo map the lane is read at the sample's song tick.
|
|
1151
|
+
positionAt:
|
|
1152
|
+
warp && positionAt
|
|
1153
|
+
? (tick: number) =>
|
|
1154
|
+
positionAt(
|
|
1155
|
+
warp.tick(
|
|
1156
|
+
span.start +
|
|
1157
|
+
(tick - note.startTick) * context.samplesPerTick,
|
|
1158
|
+
),
|
|
1159
|
+
)
|
|
1160
|
+
: positionAt,
|
|
1161
|
+
});
|
|
1162
|
+
},
|
|
829
1163
|
};
|
|
830
1164
|
}
|
|
831
1165
|
|
|
@@ -861,3 +1195,58 @@ function writeAscii(target: Uint8Array, offset: number, value: string): void {
|
|
|
861
1195
|
function clamp16(value: number): number {
|
|
862
1196
|
return Math.max(-32_768, Math.min(32_767, Math.round(value)));
|
|
863
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
/** A named point in a WAV export: a sample frame and its label. */
|
|
1200
|
+
export type WavCue = Readonly<{ frame: number; label: string }>;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Append a `cue ` chunk and a `LIST adtl` chunk of `labl` labels, so DAWs
|
|
1204
|
+
* and DJ tools show the cues (dawg writes section starts). No cues returns
|
|
1205
|
+
* the bytes unchanged.
|
|
1206
|
+
*/
|
|
1207
|
+
export function withWavCues(
|
|
1208
|
+
bytes: Uint8Array,
|
|
1209
|
+
cues: readonly WavCue[],
|
|
1210
|
+
): Uint8Array {
|
|
1211
|
+
if (cues.length === 0 || bytes.byteLength < 12) return bytes;
|
|
1212
|
+
const encoder = new TextEncoder();
|
|
1213
|
+
const labels = cues.map((cue) =>
|
|
1214
|
+
encoder.encode(`${cue.label.slice(0, 200)}\0`),
|
|
1215
|
+
);
|
|
1216
|
+
const cueSize = 4 + 24 * cues.length;
|
|
1217
|
+
const lablSizes = labels.map((text) => 4 + text.byteLength);
|
|
1218
|
+
const adtlSize =
|
|
1219
|
+
4 + lablSizes.reduce((sum, size) => sum + 8 + size + (size % 2), 0);
|
|
1220
|
+
const base = bytes.byteLength + (bytes.byteLength % 2);
|
|
1221
|
+
const out = new Uint8Array(base + 8 + cueSize + 8 + adtlSize);
|
|
1222
|
+
out.set(bytes);
|
|
1223
|
+
const view = new DataView(out.buffer);
|
|
1224
|
+
let at = base;
|
|
1225
|
+
writeAscii(out, at, "cue ");
|
|
1226
|
+
view.setUint32(at + 4, cueSize, true);
|
|
1227
|
+
view.setUint32(at + 8, cues.length, true);
|
|
1228
|
+
at += 12;
|
|
1229
|
+
cues.forEach((cue, index) => {
|
|
1230
|
+
const frame = Math.max(0, Math.round(cue.frame));
|
|
1231
|
+
view.setUint32(at, index + 1, true);
|
|
1232
|
+
view.setUint32(at + 4, frame, true);
|
|
1233
|
+
writeAscii(out, at + 8, "data");
|
|
1234
|
+
view.setUint32(at + 12, 0, true);
|
|
1235
|
+
view.setUint32(at + 16, 0, true);
|
|
1236
|
+
view.setUint32(at + 20, frame, true);
|
|
1237
|
+
at += 24;
|
|
1238
|
+
});
|
|
1239
|
+
writeAscii(out, at, "LIST");
|
|
1240
|
+
view.setUint32(at + 4, adtlSize, true);
|
|
1241
|
+
writeAscii(out, at + 8, "adtl");
|
|
1242
|
+
at += 12;
|
|
1243
|
+
labels.forEach((text, index) => {
|
|
1244
|
+
writeAscii(out, at, "labl");
|
|
1245
|
+
view.setUint32(at + 4, lablSizes[index]!, true);
|
|
1246
|
+
view.setUint32(at + 8, index + 1, true);
|
|
1247
|
+
out.set(text, at + 12);
|
|
1248
|
+
at += 8 + lablSizes[index]! + (lablSizes[index]! % 2);
|
|
1249
|
+
});
|
|
1250
|
+
view.setUint32(4, out.byteLength - 8, true);
|
|
1251
|
+
return out;
|
|
1252
|
+
}
|