@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.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(timeSignature, ppqn);
317
+ return ticksPerBar(ts, ppqn);
283
318
  case "beat":
284
- return ticksPerBeat(timeSignature, ppqn);
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 { timeSignature, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;
311
- if (ticksPerPixel <= 0 || ppqn <= 0 || timeSignature[1] <= 0) {
312
- return { ticks: [], pixelsPerBar: 0, pixelsPerBeat: 0, zoomLevel: "coarse" };
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 tpBeat = ticksPerBeat(timeSignature, ppqn);
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 (pixelsPerBar < MIN_PIXELS_PER_UNIT) {
356
+ if (pixelsPerQuarterNote * 4 < MIN_PIXELS_PER_UNIT) {
324
357
  zoomLevel = "coarse";
325
- } else if (pixelsPerBeat < MIN_PIXELS_PER_UNIT) {
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 stepTicks;
335
- let coarseBarStep;
367
+ let coarseQuarterNoteStep;
368
+ let coarseQuarterNotes = 0;
336
369
  if (zoomLevel === "coarse") {
337
- let multiplier = 2;
338
- while (tpBar * multiplier / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
339
- multiplier *= 2;
370
+ coarseQuarterNotes = 2;
371
+ while (coarseQuarterNotes * ppqn / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
372
+ coarseQuarterNotes *= 2;
340
373
  }
341
- stepTicks = tpBar * multiplier;
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 firstStep = Math.floor(startTick / stepTicks) * stepTicks;
355
- const ticks = [];
356
- for (let tick = firstStep; tick <= endTick; tick += stepTicks) {
357
- const pixel = tick / ticksPerPixel;
358
- if (pixel < startPixel || pixel > endPixel) {
359
- continue;
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
- let type;
362
- if (tick % tpBar === 0) {
363
- type = "major";
364
- } else if (tick % tpBeat === 0) {
365
- type = "minor";
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
- type = "minorMinor";
414
+ stepTicks = tpSixteenth;
368
415
  }
369
- const barIndex = Math.floor(tick / tpBar);
370
- let label;
371
- if (type === "major") {
372
- label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
373
- } else if (type === "minor" && pixelsPerBeat >= MIN_PIXELS_PER_LABEL) {
374
- label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
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
- pixelsPerBar,
381
- pixelsPerBeat,
453
+ pixelsPerQuarterNote,
382
454
  zoomLevel,
383
- ...coarseBarStep !== void 0 ? { coarseBarStep } : {}
455
+ ...coarseQuarterNoteStep !== void 0 ? { coarseQuarterNoteStep } : {}
384
456
  };
385
457
  return result;
386
458
  }
387
- function snapTickToGrid(tick, snapTo, timeSignature, ppqn = 960) {
459
+ function snapTickToGrid(tick, snapTo, meterEntries, ppqn = 960) {
388
460
  if (snapTo === "off") return tick;
389
- const gridSize = snapToTicks(snapTo, timeSignature, ppqn);
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
- return Math.round(tick / gridSize) * gridSize;
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
  };