@waveform-playlist/core 11.3.0 → 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;
@@ -266,12 +300,313 @@ function normalizedToDb(normalized, floor = DEFAULT_FLOOR) {
266
300
  const clamped = Math.max(0, normalized);
267
301
  return clamped * -floor + floor;
268
302
  }
303
+ function gainToDb(gain) {
304
+ return 20 * Math.log10(Math.max(gain, 1e-4));
305
+ }
269
306
  function gainToNormalized(gain, floor = DEFAULT_FLOOR) {
270
307
  if (gain <= 0) return 0;
271
308
  const db = 20 * Math.log10(gain);
272
309
  return dBToNormalized(db, floor);
273
310
  }
274
311
 
312
+ // src/utils/musicalTicks.ts
313
+ function snapToTicks(snapTo, timeSignature, ppqn = 960) {
314
+ const ts = timeSignature;
315
+ switch (snapTo) {
316
+ case "bar":
317
+ return ticksPerBar(ts, ppqn);
318
+ case "beat":
319
+ return ticksPerBeat(ts, ppqn);
320
+ case "1/2":
321
+ return ppqn * 2;
322
+ case "1/4":
323
+ return ppqn;
324
+ case "1/8":
325
+ return ppqn / 2;
326
+ case "1/16":
327
+ return ppqn / 4;
328
+ case "1/32":
329
+ return ppqn / 8;
330
+ case "1/2T":
331
+ return Math.round(ppqn * 2 * 2 / 3);
332
+ case "1/4T":
333
+ return Math.round(ppqn * 2 / 3);
334
+ case "1/8T":
335
+ return Math.round(ppqn * 2 / 6);
336
+ case "1/16T":
337
+ return Math.round(ppqn * 2 / 12);
338
+ case "off":
339
+ return 0;
340
+ }
341
+ }
342
+ var MIN_PIXELS_PER_UNIT = 8;
343
+ var MIN_PIXELS_PER_LABEL = 60;
344
+ function computeMusicalTicks(params) {
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" };
349
+ }
350
+ const pixelsPerQuarterNote = ppqn / ticksPerPixel;
351
+ const tpEighth = ppqn / 2;
352
+ const tpSixteenth = ppqn / 4;
353
+ const pixelsPerEighth = tpEighth / ticksPerPixel;
354
+ const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;
355
+ let zoomLevel;
356
+ if (pixelsPerQuarterNote * 4 < MIN_PIXELS_PER_UNIT) {
357
+ zoomLevel = "coarse";
358
+ } else if (pixelsPerQuarterNote < MIN_PIXELS_PER_UNIT) {
359
+ zoomLevel = "bar";
360
+ } else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {
361
+ zoomLevel = "beat";
362
+ } else if (pixelsPerSixteenth < MIN_PIXELS_PER_UNIT) {
363
+ zoomLevel = "eighth";
364
+ } else {
365
+ zoomLevel = "sixteenth";
366
+ }
367
+ let coarseQuarterNoteStep;
368
+ let coarseQuarterNotes = 0;
369
+ if (zoomLevel === "coarse") {
370
+ coarseQuarterNotes = 2;
371
+ while (coarseQuarterNotes * ppqn / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
372
+ coarseQuarterNotes *= 2;
373
+ }
374
+ coarseQuarterNoteStep = coarseQuarterNotes;
375
+ }
376
+ const startTick = startPixel * ticksPerPixel;
377
+ const endTick = endPixel * ticksPerPixel;
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
+ }
397
+ }
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;
413
+ } else {
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;
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 } : {} });
448
+ }
449
+ }
450
+ ticks.sort((a, b) => a.pixel - b.pixel);
451
+ const result = {
452
+ ticks,
453
+ pixelsPerQuarterNote,
454
+ zoomLevel,
455
+ ...coarseQuarterNoteStep !== void 0 ? { coarseQuarterNoteStep } : {}
456
+ };
457
+ return result;
458
+ }
459
+ function snapTickToGrid(tick, snapTo, meterEntries, ppqn = 960) {
460
+ if (snapTo === "off") return tick;
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);
471
+ if (gridSize <= 0) return tick;
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
+
275
610
  // src/clipTimeHelpers.ts
276
611
  function clipStartTime(clip) {
277
612
  return clip.startSample / clip.sampleRate;
@@ -285,6 +620,12 @@ function clipOffsetTime(clip) {
285
620
  function clipDurationTime(clip) {
286
621
  return clip.durationSamples / clip.sampleRate;
287
622
  }
623
+ function trackChannelCount(track) {
624
+ return track.clips.reduce(
625
+ (max, clip) => Math.max(max, clip.audioBuffer?.numberOfChannels ?? 1),
626
+ 1
627
+ );
628
+ }
288
629
  function clipPixelWidth(startSample, durationSamples, samplesPerPixel) {
289
630
  return Math.floor((startSample + durationSamples) / samplesPerPixel) - Math.floor(startSample / samplesPerPixel);
290
631
  }
@@ -421,24 +762,35 @@ var getShortcutLabel = (shortcut) => {
421
762
  export {
422
763
  InteractionState,
423
764
  MAX_CANVAS_WIDTH,
765
+ MIN_PIXELS_PER_UNIT,
424
766
  PPQN,
767
+ appendPeaks,
768
+ appendToAudioBuffer,
425
769
  applyFadeIn,
426
770
  applyFadeOut,
771
+ calculateDuration,
427
772
  clipDurationTime,
428
773
  clipEndTime,
429
774
  clipOffsetTime,
430
775
  clipPixelWidth,
431
776
  clipStartTime,
432
777
  clipsOverlap,
778
+ computeMusicalTicks,
779
+ concatenateAudioData,
780
+ createAudioBuffer,
433
781
  createClip,
434
782
  createClipFromSeconds,
783
+ createClipFromTicks,
435
784
  createTimeline,
436
785
  createTrack,
437
786
  dBToNormalized,
787
+ detectMeterChanges,
438
788
  exponentialCurve,
439
789
  findGaps,
790
+ gainToDb,
440
791
  gainToNormalized,
441
792
  generateCurve,
793
+ generatePeaks,
442
794
  getClipsAtSample,
443
795
  getClipsInRange,
444
796
  getShortcutLabel,
@@ -454,11 +806,13 @@ export {
454
806
  samplesToTicks,
455
807
  secondsToPixels,
456
808
  secondsToSamples,
809
+ snapTickToGrid,
457
810
  snapToGrid,
811
+ snapToTicks,
458
812
  sortClipsByTime,
459
813
  ticksPerBar,
460
814
  ticksPerBeat,
461
- ticksToBarBeatLabel,
462
- ticksToSamples
815
+ ticksToSamples,
816
+ trackChannelCount
463
817
  };
464
818
  //# sourceMappingURL=index.mjs.map