@waveform-playlist/core 11.3.1 → 12.0.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,247 @@ 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;
415
+ }
416
+ const segmentTickStart = Math.max(segmentStartTick, startTick);
417
+ const segmentTickEnd = Math.min(segmentEndTick - 1, endTick);
418
+ if (segmentTickStart > segmentTickEnd) {
419
+ continue;
368
420
  }
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);
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;
392
608
  }
393
609
 
394
610
  // src/clipTimeHelpers.ts
@@ -548,8 +764,11 @@ export {
548
764
  MAX_CANVAS_WIDTH,
549
765
  MIN_PIXELS_PER_UNIT,
550
766
  PPQN,
767
+ appendPeaks,
768
+ appendToAudioBuffer,
551
769
  applyFadeIn,
552
770
  applyFadeOut,
771
+ calculateDuration,
553
772
  clipDurationTime,
554
773
  clipEndTime,
555
774
  clipOffsetTime,
@@ -557,16 +776,21 @@ export {
557
776
  clipStartTime,
558
777
  clipsOverlap,
559
778
  computeMusicalTicks,
779
+ concatenateAudioData,
780
+ createAudioBuffer,
560
781
  createClip,
561
782
  createClipFromSeconds,
783
+ createClipFromTicks,
562
784
  createTimeline,
563
785
  createTrack,
564
786
  dBToNormalized,
787
+ detectMeterChanges,
565
788
  exponentialCurve,
566
789
  findGaps,
567
790
  gainToDb,
568
791
  gainToNormalized,
569
792
  generateCurve,
793
+ generatePeaks,
570
794
  getClipsAtSample,
571
795
  getClipsInRange,
572
796
  getShortcutLabel,
@@ -588,7 +812,6 @@ export {
588
812
  sortClipsByTime,
589
813
  ticksPerBar,
590
814
  ticksPerBeat,
591
- ticksToBarBeatLabel,
592
815
  ticksToSamples,
593
816
  trackChannelCount
594
817
  };