@waveform-playlist/core 11.3.1 → 12.1.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/dist/index.d.mts +125 -10
- package/dist/index.d.ts +125 -10
- package/dist/index.js +301 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2,10 +2,50 @@
|
|
|
2
2
|
var MAX_CANVAS_WIDTH = 1e3;
|
|
3
3
|
|
|
4
4
|
// src/types/clip.ts
|
|
5
|
+
function createClipFromTicks(options) {
|
|
6
|
+
const { startTick, ticksToSeconds, bpm, ppqn } = options;
|
|
7
|
+
if (startTick < 0) {
|
|
8
|
+
throw new Error("createClipFromTicks: startTick must be non-negative");
|
|
9
|
+
}
|
|
10
|
+
let toSeconds;
|
|
11
|
+
if (ticksToSeconds) {
|
|
12
|
+
toSeconds = ticksToSeconds;
|
|
13
|
+
} else if (bpm !== void 0 && ppqn !== void 0) {
|
|
14
|
+
toSeconds = (tick) => tick * 60 / (ppqn * bpm);
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"createClipFromTicks: either ticksToSeconds callback or both bpm and ppqn are required"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const sampleRate = options.audioBuffer?.sampleRate ?? options.sampleRate ?? options.waveformData?.sample_rate;
|
|
21
|
+
if (sampleRate === void 0) {
|
|
22
|
+
throw new Error("createClipFromTicks: sampleRate is required when audioBuffer is not provided");
|
|
23
|
+
}
|
|
24
|
+
const startSample = Math.round(toSeconds(startTick) * sampleRate);
|
|
25
|
+
return createClip({
|
|
26
|
+
audioBuffer: options.audioBuffer,
|
|
27
|
+
startSample,
|
|
28
|
+
startTick,
|
|
29
|
+
durationSamples: options.durationSamples,
|
|
30
|
+
offsetSamples: options.offsetSamples,
|
|
31
|
+
gain: options.gain,
|
|
32
|
+
name: options.name,
|
|
33
|
+
color: options.color,
|
|
34
|
+
fadeIn: options.fadeIn,
|
|
35
|
+
fadeOut: options.fadeOut,
|
|
36
|
+
waveformData: options.waveformData,
|
|
37
|
+
sampleRate: options.sampleRate,
|
|
38
|
+
sourceDurationSamples: options.sourceDurationSamples,
|
|
39
|
+
midiNotes: options.midiNotes,
|
|
40
|
+
midiChannel: options.midiChannel,
|
|
41
|
+
midiProgram: options.midiProgram
|
|
42
|
+
});
|
|
43
|
+
}
|
|
5
44
|
function createClip(options) {
|
|
6
45
|
const {
|
|
7
46
|
audioBuffer,
|
|
8
47
|
startSample,
|
|
48
|
+
startTick,
|
|
9
49
|
offsetSamples = 0,
|
|
10
50
|
gain = 1,
|
|
11
51
|
name,
|
|
@@ -39,6 +79,7 @@ function createClip(options) {
|
|
|
39
79
|
id: generateId(),
|
|
40
80
|
audioBuffer,
|
|
41
81
|
startSample,
|
|
82
|
+
startTick,
|
|
42
83
|
durationSamples,
|
|
43
84
|
offsetSamples,
|
|
44
85
|
sampleRate,
|
|
@@ -85,6 +126,7 @@ function createClipFromSeconds(options) {
|
|
|
85
126
|
return createClip({
|
|
86
127
|
audioBuffer,
|
|
87
128
|
startSample: Math.round(startTime * sampleRate),
|
|
129
|
+
startTick: options.startTick,
|
|
88
130
|
durationSamples: Math.round(duration * sampleRate),
|
|
89
131
|
offsetSamples: Math.round(offset * sampleRate),
|
|
90
132
|
sampleRate,
|
|
@@ -234,14 +276,6 @@ function samplesToTicks(samples, bpm, sampleRate, ppqn = PPQN) {
|
|
|
234
276
|
function snapToGrid(ticks, gridSizeTicks) {
|
|
235
277
|
return Math.round(ticks / gridSizeTicks) * gridSizeTicks;
|
|
236
278
|
}
|
|
237
|
-
function ticksToBarBeatLabel(ticks, timeSignature, ppqn = PPQN) {
|
|
238
|
-
const barTicks = ticksPerBar(timeSignature, ppqn);
|
|
239
|
-
const beatTicks = ticksPerBeat(timeSignature, ppqn);
|
|
240
|
-
const bar = Math.floor(ticks / barTicks) + 1;
|
|
241
|
-
const beatInBar = Math.floor(ticks % barTicks / beatTicks) + 1;
|
|
242
|
-
if (beatInBar === 1) return `${bar}`;
|
|
243
|
-
return `${bar}.${beatInBar}`;
|
|
244
|
-
}
|
|
245
279
|
|
|
246
280
|
// src/utils/dBUtils.ts
|
|
247
281
|
var DEFAULT_FLOOR = -100;
|
|
@@ -277,11 +311,12 @@ function gainToNormalized(gain, floor = DEFAULT_FLOOR) {
|
|
|
277
311
|
|
|
278
312
|
// src/utils/musicalTicks.ts
|
|
279
313
|
function snapToTicks(snapTo, timeSignature, ppqn = 960) {
|
|
314
|
+
const ts = timeSignature;
|
|
280
315
|
switch (snapTo) {
|
|
281
316
|
case "bar":
|
|
282
|
-
return ticksPerBar(
|
|
317
|
+
return ticksPerBar(ts, ppqn);
|
|
283
318
|
case "beat":
|
|
284
|
-
return ticksPerBeat(
|
|
319
|
+
return ticksPerBeat(ts, ppqn);
|
|
285
320
|
case "1/2":
|
|
286
321
|
return ppqn * 2;
|
|
287
322
|
case "1/4":
|
|
@@ -307,22 +342,20 @@ function snapToTicks(snapTo, timeSignature, ppqn = 960) {
|
|
|
307
342
|
var MIN_PIXELS_PER_UNIT = 8;
|
|
308
343
|
var MIN_PIXELS_PER_LABEL = 60;
|
|
309
344
|
function computeMusicalTicks(params) {
|
|
310
|
-
const {
|
|
311
|
-
|
|
312
|
-
|
|
345
|
+
const { meterEntries, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;
|
|
346
|
+
const firstMeter = meterEntries[0] ?? { tick: 0, numerator: 4, denominator: 4 };
|
|
347
|
+
if (ticksPerPixel <= 0 || ppqn <= 0 || firstMeter.denominator <= 0) {
|
|
348
|
+
return { ticks: [], pixelsPerQuarterNote: 0, zoomLevel: "coarse" };
|
|
313
349
|
}
|
|
314
|
-
const
|
|
315
|
-
const tpBar = ticksPerBar(timeSignature, ppqn);
|
|
350
|
+
const pixelsPerQuarterNote = ppqn / ticksPerPixel;
|
|
316
351
|
const tpEighth = ppqn / 2;
|
|
317
352
|
const tpSixteenth = ppqn / 4;
|
|
318
|
-
const pixelsPerBar = tpBar / ticksPerPixel;
|
|
319
|
-
const pixelsPerBeat = tpBeat / ticksPerPixel;
|
|
320
353
|
const pixelsPerEighth = tpEighth / ticksPerPixel;
|
|
321
354
|
const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;
|
|
322
355
|
let zoomLevel;
|
|
323
|
-
if (
|
|
356
|
+
if (pixelsPerQuarterNote * 4 < MIN_PIXELS_PER_UNIT) {
|
|
324
357
|
zoomLevel = "coarse";
|
|
325
|
-
} else if (
|
|
358
|
+
} else if (pixelsPerQuarterNote < MIN_PIXELS_PER_UNIT) {
|
|
326
359
|
zoomLevel = "bar";
|
|
327
360
|
} else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {
|
|
328
361
|
zoomLevel = "beat";
|
|
@@ -331,64 +364,255 @@ function computeMusicalTicks(params) {
|
|
|
331
364
|
} else {
|
|
332
365
|
zoomLevel = "sixteenth";
|
|
333
366
|
}
|
|
334
|
-
let
|
|
335
|
-
let
|
|
367
|
+
let coarseQuarterNoteStep;
|
|
368
|
+
let coarseQuarterNotes = 0;
|
|
336
369
|
if (zoomLevel === "coarse") {
|
|
337
|
-
|
|
338
|
-
while (
|
|
339
|
-
|
|
370
|
+
coarseQuarterNotes = 2;
|
|
371
|
+
while (coarseQuarterNotes * ppqn / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
|
|
372
|
+
coarseQuarterNotes *= 2;
|
|
340
373
|
}
|
|
341
|
-
|
|
342
|
-
coarseBarStep = multiplier;
|
|
343
|
-
} else if (zoomLevel === "bar") {
|
|
344
|
-
stepTicks = tpBar;
|
|
345
|
-
} else if (zoomLevel === "beat") {
|
|
346
|
-
stepTicks = tpBeat;
|
|
347
|
-
} else if (zoomLevel === "eighth") {
|
|
348
|
-
stepTicks = tpEighth;
|
|
349
|
-
} else {
|
|
350
|
-
stepTicks = tpSixteenth;
|
|
374
|
+
coarseQuarterNoteStep = coarseQuarterNotes;
|
|
351
375
|
}
|
|
352
376
|
const startTick = startPixel * ticksPerPixel;
|
|
353
377
|
const endTick = endPixel * ticksPerPixel;
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
const segments = [];
|
|
379
|
+
{
|
|
380
|
+
let cumulativeBars = 0;
|
|
381
|
+
for (let i = 0; i < meterEntries.length; i++) {
|
|
382
|
+
const meter = meterEntries[i];
|
|
383
|
+
const segmentStart = meter.tick;
|
|
384
|
+
const segmentEnd = i + 1 < meterEntries.length ? meterEntries[i + 1].tick : Number.MAX_SAFE_INTEGER;
|
|
385
|
+
const ts = [meter.numerator, meter.denominator];
|
|
386
|
+
const tpBar = ticksPerBar(ts, ppqn);
|
|
387
|
+
segments.push({
|
|
388
|
+
segmentStartTick: segmentStart,
|
|
389
|
+
segmentEndTick: segmentEnd,
|
|
390
|
+
meter,
|
|
391
|
+
barOffset: cumulativeBars
|
|
392
|
+
});
|
|
393
|
+
if (segmentEnd !== Number.MAX_SAFE_INTEGER) {
|
|
394
|
+
const segmentLen = segmentEnd - segmentStart;
|
|
395
|
+
cumulativeBars += Math.floor(segmentLen / tpBar);
|
|
396
|
+
}
|
|
360
397
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
398
|
+
}
|
|
399
|
+
const ticks = [];
|
|
400
|
+
for (const { segmentStartTick, segmentEndTick, meter, barOffset } of segments) {
|
|
401
|
+
const ts = [meter.numerator, meter.denominator];
|
|
402
|
+
const tpBeat = ticksPerBeat(ts, ppqn);
|
|
403
|
+
const tpBar = ticksPerBar(ts, ppqn);
|
|
404
|
+
let stepTicks;
|
|
405
|
+
if (zoomLevel === "coarse") {
|
|
406
|
+
stepTicks = coarseQuarterNotes * ppqn;
|
|
407
|
+
} else if (zoomLevel === "bar") {
|
|
408
|
+
stepTicks = tpBar;
|
|
409
|
+
} else if (zoomLevel === "beat") {
|
|
410
|
+
stepTicks = tpBeat;
|
|
411
|
+
} else if (zoomLevel === "eighth") {
|
|
412
|
+
stepTicks = tpEighth;
|
|
366
413
|
} else {
|
|
367
|
-
|
|
414
|
+
stepTicks = tpSixteenth;
|
|
368
415
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
416
|
+
const segmentTickStart = Math.max(segmentStartTick, startTick);
|
|
417
|
+
const segmentTickEnd = Math.min(segmentEndTick - 1, endTick);
|
|
418
|
+
if (segmentTickStart > segmentTickEnd) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const offsetIntoSegment = segmentTickStart - segmentStartTick;
|
|
422
|
+
const firstStepOffset = Math.floor(offsetIntoSegment / stepTicks) * stepTicks;
|
|
423
|
+
const firstStepTick = segmentStartTick + firstStepOffset;
|
|
424
|
+
for (let tick = firstStepTick; tick <= segmentTickEnd && tick < segmentEndTick; tick += stepTicks) {
|
|
425
|
+
const pixel = tick / ticksPerPixel;
|
|
426
|
+
if (pixel < startPixel || pixel > endPixel) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const tickOffsetInSegment = tick - segmentStartTick;
|
|
430
|
+
let type;
|
|
431
|
+
if (tickOffsetInSegment % tpBar === 0) {
|
|
432
|
+
type = "major";
|
|
433
|
+
} else if (tickOffsetInSegment % tpBeat === 0) {
|
|
434
|
+
type = "minor";
|
|
435
|
+
} else {
|
|
436
|
+
type = "minorMinor";
|
|
437
|
+
}
|
|
438
|
+
const barIndexInSegment = Math.floor(tickOffsetInSegment / tpBar);
|
|
439
|
+
const barIndex = barOffset + barIndexInSegment;
|
|
440
|
+
let label;
|
|
441
|
+
if (type === "major") {
|
|
442
|
+
label = `${barIndex + 1}`;
|
|
443
|
+
} else if (type === "minor" && tpBeat / ticksPerPixel >= MIN_PIXELS_PER_LABEL) {
|
|
444
|
+
const beatInBar = Math.floor(tickOffsetInSegment % tpBar / tpBeat) + 1;
|
|
445
|
+
label = `${barIndex + 1}.${beatInBar}`;
|
|
446
|
+
}
|
|
447
|
+
ticks.push({ pixel, type, barIndex, ...label !== void 0 ? { label } : {} });
|
|
375
448
|
}
|
|
376
|
-
ticks.push({ pixel, type, barIndex, ...label !== void 0 ? { label } : {} });
|
|
377
449
|
}
|
|
450
|
+
ticks.sort((a, b) => a.pixel - b.pixel);
|
|
378
451
|
const result = {
|
|
379
452
|
ticks,
|
|
380
|
-
|
|
381
|
-
pixelsPerBeat,
|
|
453
|
+
pixelsPerQuarterNote,
|
|
382
454
|
zoomLevel,
|
|
383
|
-
...
|
|
455
|
+
...coarseQuarterNoteStep !== void 0 ? { coarseQuarterNoteStep } : {}
|
|
384
456
|
};
|
|
385
457
|
return result;
|
|
386
458
|
}
|
|
387
|
-
function snapTickToGrid(tick, snapTo,
|
|
459
|
+
function snapTickToGrid(tick, snapTo, meterEntries, ppqn = 960) {
|
|
388
460
|
if (snapTo === "off") return tick;
|
|
389
|
-
|
|
461
|
+
let meter = meterEntries[0] ?? { tick: 0, numerator: 4, denominator: 4 };
|
|
462
|
+
for (const entry of meterEntries) {
|
|
463
|
+
if (entry.tick <= tick) {
|
|
464
|
+
meter = entry;
|
|
465
|
+
} else {
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const ts = [meter.numerator, meter.denominator];
|
|
470
|
+
const gridSize = snapToTicks(snapTo, ts, ppqn);
|
|
390
471
|
if (gridSize <= 0) return tick;
|
|
391
|
-
|
|
472
|
+
const offset = tick - meter.tick;
|
|
473
|
+
return meter.tick + Math.round(offset / gridSize) * gridSize;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/utils/meterDetection.ts
|
|
477
|
+
function detectMeterChanges(beats, firstBeatTick, ppqn) {
|
|
478
|
+
const DEFAULT_NUMERATOR = 4;
|
|
479
|
+
const DENOMINATOR = 4;
|
|
480
|
+
const defaultResult = [
|
|
481
|
+
{ tick: 0, numerator: DEFAULT_NUMERATOR, denominator: DENOMINATOR }
|
|
482
|
+
];
|
|
483
|
+
if (beats.length === 0) {
|
|
484
|
+
return defaultResult;
|
|
485
|
+
}
|
|
486
|
+
const firstDownbeatIndex = beats.findIndex((b) => b.beat === 1);
|
|
487
|
+
if (firstDownbeatIndex === -1) {
|
|
488
|
+
return defaultResult;
|
|
489
|
+
}
|
|
490
|
+
const bars = [];
|
|
491
|
+
let barStartBeatIndex = firstDownbeatIndex;
|
|
492
|
+
for (let i = firstDownbeatIndex + 1; i < beats.length; i++) {
|
|
493
|
+
if (beats[i].beat === 1) {
|
|
494
|
+
bars.push({ beatIndex: barStartBeatIndex, count: i - barStartBeatIndex });
|
|
495
|
+
barStartBeatIndex = i;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (bars.length === 0) {
|
|
499
|
+
return defaultResult;
|
|
500
|
+
}
|
|
501
|
+
const rawEntries = [];
|
|
502
|
+
let prevNumerator = -1;
|
|
503
|
+
for (const bar of bars) {
|
|
504
|
+
const numerator = bar.count;
|
|
505
|
+
if (numerator !== prevNumerator) {
|
|
506
|
+
const tick = firstBeatTick + bar.beatIndex * ppqn;
|
|
507
|
+
rawEntries.push({ tick, numerator, denominator: DENOMINATOR });
|
|
508
|
+
prevNumerator = numerator;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (rawEntries.length === 0) {
|
|
512
|
+
return defaultResult;
|
|
513
|
+
}
|
|
514
|
+
if (rawEntries[0].tick === 0) {
|
|
515
|
+
return rawEntries;
|
|
516
|
+
}
|
|
517
|
+
const tick0Entry = {
|
|
518
|
+
tick: 0,
|
|
519
|
+
numerator: rawEntries[0].numerator,
|
|
520
|
+
denominator: DENOMINATOR
|
|
521
|
+
};
|
|
522
|
+
const rest = rawEntries[0].numerator === tick0Entry.numerator ? rawEntries.slice(1) : rawEntries;
|
|
523
|
+
return [tick0Entry, ...rest];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/utils/peaksGenerator.ts
|
|
527
|
+
function generatePeaks(samples, samplesPerPixel, bits = 16) {
|
|
528
|
+
const numPeaks = Math.ceil(samples.length / samplesPerPixel);
|
|
529
|
+
const peakArray = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);
|
|
530
|
+
const maxValue = 2 ** (bits - 1);
|
|
531
|
+
for (let i = 0; i < numPeaks; i++) {
|
|
532
|
+
const start = i * samplesPerPixel;
|
|
533
|
+
const end = Math.min(start + samplesPerPixel, samples.length);
|
|
534
|
+
let min = 0;
|
|
535
|
+
let max = 0;
|
|
536
|
+
for (let j = start; j < end; j++) {
|
|
537
|
+
const value = samples[j];
|
|
538
|
+
if (value < min) min = value;
|
|
539
|
+
if (value > max) max = value;
|
|
540
|
+
}
|
|
541
|
+
peakArray[i * 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
542
|
+
peakArray[i * 2 + 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
543
|
+
}
|
|
544
|
+
return peakArray;
|
|
545
|
+
}
|
|
546
|
+
function appendPeaks(existingPeaks, newSamples, samplesPerPixel, totalSamplesProcessed, bits = 16) {
|
|
547
|
+
const maxValue = 2 ** (bits - 1);
|
|
548
|
+
const remainder = totalSamplesProcessed % samplesPerPixel;
|
|
549
|
+
let offset = 0;
|
|
550
|
+
if (remainder > 0 && existingPeaks.length > 0) {
|
|
551
|
+
const samplesToComplete = samplesPerPixel - remainder;
|
|
552
|
+
const endIndex = Math.min(samplesToComplete, newSamples.length);
|
|
553
|
+
let min = existingPeaks[existingPeaks.length - 2] / maxValue;
|
|
554
|
+
let max = existingPeaks[existingPeaks.length - 1] / maxValue;
|
|
555
|
+
for (let i = 0; i < endIndex; i++) {
|
|
556
|
+
const value = newSamples[i];
|
|
557
|
+
if (value < min) min = value;
|
|
558
|
+
if (value > max) max = value;
|
|
559
|
+
}
|
|
560
|
+
const updated = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length);
|
|
561
|
+
updated.set(existingPeaks);
|
|
562
|
+
updated[existingPeaks.length - 2] = Math.max(-maxValue, Math.floor(min * maxValue));
|
|
563
|
+
updated[existingPeaks.length - 1] = Math.min(maxValue - 1, Math.floor(max * maxValue));
|
|
564
|
+
offset = endIndex;
|
|
565
|
+
const newPeaks2 = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
566
|
+
const result2 = new (bits === 8 ? Int8Array : Int16Array)(updated.length + newPeaks2.length);
|
|
567
|
+
result2.set(updated);
|
|
568
|
+
result2.set(newPeaks2, updated.length);
|
|
569
|
+
return result2;
|
|
570
|
+
}
|
|
571
|
+
const newPeaks = generatePeaks(newSamples.slice(offset), samplesPerPixel, bits);
|
|
572
|
+
const result = new (bits === 8 ? Int8Array : Int16Array)(existingPeaks.length + newPeaks.length);
|
|
573
|
+
result.set(existingPeaks);
|
|
574
|
+
result.set(newPeaks, existingPeaks.length);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/utils/audioBufferUtils.ts
|
|
579
|
+
function concatenateAudioData(chunks) {
|
|
580
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
581
|
+
const result = new Float32Array(totalLength);
|
|
582
|
+
let offset = 0;
|
|
583
|
+
for (const chunk of chunks) {
|
|
584
|
+
result.set(chunk, offset);
|
|
585
|
+
offset += chunk.length;
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
function createAudioBuffer(audioContext, channelData, sampleRate, channelCount = 1) {
|
|
590
|
+
const channels = channelData instanceof Float32Array ? [channelData] : channelData;
|
|
591
|
+
const length = channels[0]?.length ?? 0;
|
|
592
|
+
const buffer = audioContext.createBuffer(channelCount, length, sampleRate);
|
|
593
|
+
for (let ch = 0; ch < Math.min(channelCount, channels.length); ch++) {
|
|
594
|
+
buffer.copyToChannel(new Float32Array(channels[ch]), ch);
|
|
595
|
+
}
|
|
596
|
+
return buffer;
|
|
597
|
+
}
|
|
598
|
+
function appendToAudioBuffer(audioContext, existingBuffer, newSamples, sampleRate) {
|
|
599
|
+
if (!existingBuffer) {
|
|
600
|
+
return createAudioBuffer(audioContext, [newSamples], sampleRate);
|
|
601
|
+
}
|
|
602
|
+
const existingData = existingBuffer.getChannelData(0);
|
|
603
|
+
const combined = concatenateAudioData([existingData, newSamples]);
|
|
604
|
+
return createAudioBuffer(audioContext, [combined], sampleRate);
|
|
605
|
+
}
|
|
606
|
+
function calculateDuration(sampleCount, sampleRate) {
|
|
607
|
+
return sampleCount / sampleRate;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/utils/latency.ts
|
|
611
|
+
function audibleLatencySamples(outputLatency, lookAhead, sampleRate) {
|
|
612
|
+
const total = outputLatency + lookAhead;
|
|
613
|
+
if (!Number.isFinite(total) || !Number.isFinite(sampleRate)) return 0;
|
|
614
|
+
if (total <= 0 || sampleRate <= 0) return 0;
|
|
615
|
+
return Math.floor(total * sampleRate);
|
|
392
616
|
}
|
|
393
617
|
|
|
394
618
|
// src/clipTimeHelpers.ts
|
|
@@ -548,8 +772,12 @@ export {
|
|
|
548
772
|
MAX_CANVAS_WIDTH,
|
|
549
773
|
MIN_PIXELS_PER_UNIT,
|
|
550
774
|
PPQN,
|
|
775
|
+
appendPeaks,
|
|
776
|
+
appendToAudioBuffer,
|
|
551
777
|
applyFadeIn,
|
|
552
778
|
applyFadeOut,
|
|
779
|
+
audibleLatencySamples,
|
|
780
|
+
calculateDuration,
|
|
553
781
|
clipDurationTime,
|
|
554
782
|
clipEndTime,
|
|
555
783
|
clipOffsetTime,
|
|
@@ -557,16 +785,21 @@ export {
|
|
|
557
785
|
clipStartTime,
|
|
558
786
|
clipsOverlap,
|
|
559
787
|
computeMusicalTicks,
|
|
788
|
+
concatenateAudioData,
|
|
789
|
+
createAudioBuffer,
|
|
560
790
|
createClip,
|
|
561
791
|
createClipFromSeconds,
|
|
792
|
+
createClipFromTicks,
|
|
562
793
|
createTimeline,
|
|
563
794
|
createTrack,
|
|
564
795
|
dBToNormalized,
|
|
796
|
+
detectMeterChanges,
|
|
565
797
|
exponentialCurve,
|
|
566
798
|
findGaps,
|
|
567
799
|
gainToDb,
|
|
568
800
|
gainToNormalized,
|
|
569
801
|
generateCurve,
|
|
802
|
+
generatePeaks,
|
|
570
803
|
getClipsAtSample,
|
|
571
804
|
getClipsInRange,
|
|
572
805
|
getShortcutLabel,
|
|
@@ -588,7 +821,6 @@ export {
|
|
|
588
821
|
sortClipsByTime,
|
|
589
822
|
ticksPerBar,
|
|
590
823
|
ticksPerBeat,
|
|
591
|
-
ticksToBarBeatLabel,
|
|
592
824
|
ticksToSamples,
|
|
593
825
|
trackChannelCount
|
|
594
826
|
};
|