@libraz/libsonare 1.5.0 → 1.5.2

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.js CHANGED
@@ -732,12 +732,19 @@ var Mixer = class _Mixer {
732
732
  return this.mixer.toSceneJson();
733
733
  }
734
734
  /**
735
- * Maximum processor tail length (samples) in the compiled mixer graph. Lazily
735
+ * Longest audible serial processor-tail path to the master, in samples. Lazily
736
736
  * compiles the routing graph if the topology is dirty.
737
737
  */
738
738
  tailSamples() {
739
739
  return this.mixer.tailSamples();
740
740
  }
741
+ /**
742
+ * Reported latency (samples) of the compiled mixer graph, for aligning
743
+ * dry/wet material. Lazily compiles the routing graph if the topology is dirty.
744
+ */
745
+ latencySamples() {
746
+ return this.mixer.latencySamples();
747
+ }
741
748
  /**
742
749
  * Drain delayed / tail audio by processing a zero-input block of `numSamples`
743
750
  * frames after the host stops feeding strip inputs. Returns the mixed stereo
@@ -1190,7 +1197,7 @@ function latencyCompensatedVoiceChange(changer, samples, channels, blockFrames)
1190
1197
  const start = latencyFrames * 2;
1191
1198
  return processed.slice(start, start + samples.length);
1192
1199
  }
1193
- function voiceChangeRealtime(samples, options = {}) {
1200
+ function voiceChangeRealtime(samples, sampleRate = 48e3, preset = "neutral-monitor", options = {}) {
1194
1201
  assertSamples("voiceChangeRealtime", samples, options.validate !== false);
1195
1202
  const channels = options.channels ?? 1;
1196
1203
  if (channels !== 1 && channels !== 2) {
@@ -1199,9 +1206,8 @@ function voiceChangeRealtime(samples, options = {}) {
1199
1206
  if (channels === 2 && samples.length % 2 !== 0) {
1200
1207
  throw new Error("voiceChangeRealtime: stereo input length must be a multiple of 2.");
1201
1208
  }
1202
- const sampleRate = options.sampleRate ?? 48e3;
1203
1209
  const blockSize = Math.max(1, Math.floor(options.blockSize ?? 512));
1204
- const changer = new RealtimeVoiceChanger(options.preset ?? "neutral-monitor");
1210
+ const changer = new RealtimeVoiceChanger(preset);
1205
1211
  try {
1206
1212
  changer.prepare(sampleRate, blockSize, channels);
1207
1213
  return latencyCompensatedVoiceChange(changer, samples, channels, blockSize);
@@ -1586,6 +1592,74 @@ function vqt(samples, sampleRate = 22050, hopLength = 512, fmin = 32.70319566257
1586
1592
  }
1587
1593
  return requireModule9().vqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave, gamma);
1588
1594
  }
1595
+ function validateCqtInverse(fnName, magnitude, nBins, nFrames, sampleRate, hopLength, fmin, binsPerOctave, nIter, options) {
1596
+ assertSampleRate(fnName, sampleRate);
1597
+ validatePositiveIntegers(fnName, { nBins, nFrames, hopLength, binsPerOctave, nIter });
1598
+ if (nIter > 256) {
1599
+ throw new RangeError(`${fnName}: nIter must be at most 256`);
1600
+ }
1601
+ validateFrequencyBounds(fnName, fmin);
1602
+ if (fmin === 0) {
1603
+ throw new RangeError(`${fnName}: fmin must be positive`);
1604
+ }
1605
+ if (magnitude.length !== nBins * nFrames) {
1606
+ throw new RangeError(`${fnName}: magnitude length must equal nBins * nFrames`);
1607
+ }
1608
+ assertSamples(fnName, magnitude, options.validate !== false);
1609
+ }
1610
+ function cqtToAudio(magnitude, nBins, nFrames, sampleRate = 22050, hopLength = 512, fmin = 32.70319566257483, binsPerOctave = 12, nIter = 32, options = {}) {
1611
+ validateCqtInverse(
1612
+ "cqtToAudio",
1613
+ magnitude,
1614
+ nBins,
1615
+ nFrames,
1616
+ sampleRate,
1617
+ hopLength,
1618
+ fmin,
1619
+ binsPerOctave,
1620
+ nIter,
1621
+ options
1622
+ );
1623
+ return requireModule9().cqtToAudio(
1624
+ magnitude,
1625
+ nBins,
1626
+ nFrames,
1627
+ sampleRate,
1628
+ hopLength,
1629
+ fmin,
1630
+ binsPerOctave,
1631
+ nIter
1632
+ );
1633
+ }
1634
+ function vqtToAudio(magnitude, nBins, nFrames, sampleRate = 22050, hopLength = 512, fmin = 32.70319566257483, binsPerOctave = 12, gamma = 0, nIter = 32, options = {}) {
1635
+ validateCqtInverse(
1636
+ "vqtToAudio",
1637
+ magnitude,
1638
+ nBins,
1639
+ nFrames,
1640
+ sampleRate,
1641
+ hopLength,
1642
+ fmin,
1643
+ binsPerOctave,
1644
+ nIter,
1645
+ options
1646
+ );
1647
+ assertFiniteScalar("vqtToAudio", gamma, "gamma");
1648
+ if (gamma < 0) {
1649
+ throw new RangeError("vqtToAudio: gamma must be non-negative");
1650
+ }
1651
+ return requireModule9().vqtToAudio(
1652
+ magnitude,
1653
+ nBins,
1654
+ nFrames,
1655
+ sampleRate,
1656
+ hopLength,
1657
+ fmin,
1658
+ binsPerOctave,
1659
+ gamma,
1660
+ nIter
1661
+ );
1662
+ }
1589
1663
  function analyzeSections(samples, sampleRate = 22050, options = {}) {
1590
1664
  validateMusicSamples("analyzeSections", samples, sampleRate, options);
1591
1665
  validatePositiveIntegers("analyzeSections", {
@@ -1650,11 +1724,11 @@ function fourierTempogram(onsetEnvelope2, sampleRate = 22050, hopLength = 512, w
1650
1724
  validatePositiveIntegers("fourierTempogram", { hopLength, winLength });
1651
1725
  return requireModule9().fourierTempogram(onsetEnvelope2, sampleRate, hopLength, winLength);
1652
1726
  }
1653
- function tempogramRatio(tempogramData, winLength = 384, sampleRate = 22050, hopLength = 512, options = {}) {
1727
+ function tempogramRatio(tempogramData, winLength = 384, sampleRate = 22050, hopLength = 512, factors, options = {}) {
1654
1728
  assertSampleRate("tempogramRatio", sampleRate);
1655
1729
  assertSamples("tempogramRatio", tempogramData, options.validate !== false, "tempogramData");
1656
1730
  validatePositiveIntegers("tempogramRatio", { winLength, hopLength });
1657
- return requireModule9().tempogramRatio(tempogramData, winLength, sampleRate, hopLength);
1731
+ return requireModule9().tempogramRatio(tempogramData, winLength, sampleRate, hopLength, factors);
1658
1732
  }
1659
1733
  function lufs(samples, sampleRate = 22050, options = {}) {
1660
1734
  assertSampleRate("lufs", sampleRate);
@@ -2836,16 +2910,37 @@ var ClipPageStreamer = class {
2836
2910
  throw new Error("pageFrames and numSamples must be positive");
2837
2911
  }
2838
2912
  const lastPage = Math.ceil(source.numSamples / source.pageFrames) - 1;
2913
+ const previous = this.sources.get(source.clipId);
2914
+ if (previous) {
2915
+ this.resetState(previous);
2916
+ }
2839
2917
  this.sources.set(source.clipId, {
2840
2918
  source,
2841
2919
  lastPage,
2842
- resident: new Set(initialResidentPages)
2920
+ generation: 0,
2921
+ lastFrontier: null,
2922
+ resident: new Map(Array.from(initialResidentPages, (page) => [page, 0]))
2843
2923
  });
2844
2924
  }
2845
2925
  /** Stop tracking a clip. Does not close its binding (the caller owns that). */
2846
2926
  removeSource(clipId) {
2927
+ const state = this.sources.get(clipId);
2928
+ if (state) {
2929
+ this.resetState(state);
2930
+ }
2847
2931
  this.sources.delete(clipId);
2848
2932
  }
2933
+ /**
2934
+ * Explicitly start a new playback generation after a host seek/loop. Resident
2935
+ * pages are evicted and any older in-flight fetch is cleared when it settles.
2936
+ * The next miss establishes the new bounded window.
2937
+ */
2938
+ resetSource(clipId) {
2939
+ const state = this.sources.get(clipId);
2940
+ if (state) {
2941
+ this.resetState(state);
2942
+ }
2943
+ }
2849
2944
  /**
2850
2945
  * Drain pending page-miss requests, fetch the missing pages plus their
2851
2946
  * read-ahead window, and evict out-of-window pages. Resolves once this round's
@@ -2866,10 +2961,7 @@ var ClipPageStreamer = class {
2866
2961
  continue;
2867
2962
  }
2868
2963
  const page = Math.floor(request.sample / state.source.pageFrames);
2869
- const previous = frontiers.get(request.clipId);
2870
- if (previous === void 0 || page > previous) {
2871
- frontiers.set(request.clipId, page);
2872
- }
2964
+ frontiers.set(request.clipId, page);
2873
2965
  }
2874
2966
  const fetches = [];
2875
2967
  for (const [clipId, frontier] of frontiers) {
@@ -2888,14 +2980,20 @@ var ClipPageStreamer = class {
2888
2980
  }
2889
2981
  this.closed = true;
2890
2982
  for (const state of this.sources.values()) {
2983
+ this.resetState(state);
2891
2984
  state.source.binding.close();
2892
2985
  }
2893
2986
  this.sources.clear();
2894
2987
  }
2895
2988
  serviceFrontier(state, frontier) {
2989
+ if (state.lastFrontier !== null && frontier < state.lastFrontier) {
2990
+ this.resetState(state);
2991
+ }
2992
+ state.lastFrontier = frontier;
2993
+ const generation = state.generation;
2896
2994
  const low = Math.max(0, frontier - this.retainBehindPages);
2897
2995
  const high = Math.min(state.lastPage, frontier + this.readAheadPages);
2898
- for (const page of state.resident) {
2996
+ for (const page of state.resident.keys()) {
2899
2997
  if (page < low || page > high) {
2900
2998
  state.source.binding.provider.clear(page);
2901
2999
  state.resident.delete(page);
@@ -2903,21 +3001,25 @@ var ClipPageStreamer = class {
2903
3001
  }
2904
3002
  const fetches = [];
2905
3003
  for (let page = low; page <= high; ++page) {
2906
- if (state.resident.has(page)) {
3004
+ if (state.resident.get(page) === generation) {
2907
3005
  continue;
2908
3006
  }
2909
- state.resident.add(page);
3007
+ state.resident.set(page, generation);
2910
3008
  const pageIndex = page;
2911
3009
  fetches.push(
2912
3010
  state.source.binding.supplyPage(pageIndex).then(
2913
3011
  (ok) => {
2914
- if (!ok) {
3012
+ if (state.generation !== generation) {
3013
+ state.source.binding.provider.clear(pageIndex);
3014
+ } else if (!ok && state.resident.get(pageIndex) === generation) {
2915
3015
  state.resident.delete(pageIndex);
2916
3016
  }
2917
3017
  return ok;
2918
3018
  },
2919
3019
  (error) => {
2920
- state.resident.delete(pageIndex);
3020
+ if (state.resident.get(pageIndex) === generation) {
3021
+ state.resident.delete(pageIndex);
3022
+ }
2921
3023
  throw error;
2922
3024
  }
2923
3025
  )
@@ -2925,6 +3027,14 @@ var ClipPageStreamer = class {
2925
3027
  }
2926
3028
  return fetches;
2927
3029
  }
3030
+ resetState(state) {
3031
+ state.generation += 1;
3032
+ state.lastFrontier = null;
3033
+ for (const page of state.resident.keys()) {
3034
+ state.source.binding.provider.clear(page);
3035
+ }
3036
+ state.resident.clear();
3037
+ }
2928
3038
  };
2929
3039
  async function attachOpfsClipStream(streamer, engine, options) {
2930
3040
  const { clipId, primePages = 1, ...providerOptions } = options;
@@ -3532,8 +3642,10 @@ var Project = class _Project {
3532
3642
  * built-in oscillator synth so a MIDI-only arrangement bounces to audible
3533
3643
  * audio. Pass a {@link BuiltinSynthBinding} (or an array of them) to choose
3534
3644
  * the patch and MIDI destination; omit it (or pass `{}`) for one
3535
- * default-destination sine patch. An explicitly empty array `[]` (or
3536
- * `undefined` / `null`) produces zero bindings, so MIDI tracks render silently.
3645
+ * default-destination sine patch. Because the parameter defaults to `{}`,
3646
+ * omission and explicit `undefined` both create that one default binding.
3647
+ * Use an explicitly empty array `[]` (or runtime `null`) for zero bindings,
3648
+ * so MIDI tracks render silently.
3537
3649
  *
3538
3650
  * Like {@link bounce}, omitting `totalFrames` auto-derives the render length
3539
3651
  * from the arrangement plus the synth's release tail.
@@ -3558,9 +3670,10 @@ var Project = class _Project {
3558
3670
  * string (`'saw-lead'` / `'va:saw-lead'`; see {@link synthPresetNames}), or
3559
3671
  * an array of either; each object entry may carry a `destinationId` binding
3560
3672
  * convenience (default 0), which is not part of the NativeSynth patch itself.
3561
- * An explicitly empty array (or `undefined` / `null`) produces zero bindings.
3562
- * Unknown preset names throw. Deterministic for a fixed project + options +
3563
- * patch.
3673
+ * Because the parameter defaults to `{}`, omission and explicit `undefined`
3674
+ * both create one default binding. Use an explicitly empty array `[]` (or
3675
+ * runtime `null`) for zero bindings. Unknown preset names throw.
3676
+ * Deterministic for a fixed project + options + patch.
3564
3677
  */
3565
3678
  bounceWithSynthInstrument(instrument = {}, options = {}) {
3566
3679
  return this.native.bounceWithSynthInstrument(instrument, options);
@@ -3602,8 +3715,10 @@ var Project = class _Project {
3602
3715
  * honored. Programs the SoundFont does not cover — including bouncing with
3603
3716
  * no SoundFont loaded at all — play through the built-in synthesizer GM
3604
3717
  * fallback bank (the data-free floor; see {@link soundFontManifest} for the
3605
- * per-program backend). An explicitly empty array `[]` (or `undefined` /
3606
- * `null`) produces zero bindings, so MIDI tracks render silently.
3718
+ * per-program backend). Because the parameter defaults to `{}`, omission and
3719
+ * explicit `undefined` both create one default binding. Use an explicitly
3720
+ * empty array `[]` (or runtime `null`) for zero bindings, so MIDI tracks
3721
+ * render silently.
3607
3722
  */
3608
3723
  bounceWithSf2Instrument(instrument = {}, options = {}) {
3609
3724
  return this.native.bounceWithSf2Instrument(instrument, options);
@@ -3749,6 +3864,10 @@ var Project = class _Project {
3749
3864
  trackCount() {
3750
3865
  return this.native.trackCount();
3751
3866
  }
3867
+ /** Number of clips in the project. */
3868
+ clipCount() {
3869
+ return this.native.clipCount();
3870
+ }
3752
3871
  /** Number of audio sources registered on the project. */
3753
3872
  sourceCount() {
3754
3873
  return this.native.sourceCount();
@@ -3875,14 +3994,14 @@ var SYNTH_MOD_DESTINATIONS = [
3875
3994
  // src/realtime_engine.ts
3876
3995
  var EXPECTED_ENGINE_ABI_VERSION = 3;
3877
3996
  function engineCapabilities() {
3878
- const abiVersion = getSonareModule().engineAbiVersion();
3997
+ const abiVersion2 = getSonareModule().engineAbiVersion();
3879
3998
  const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
3880
3999
  const atomics = typeof globalThis.Atomics === "object";
3881
4000
  const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
3882
4001
  return {
3883
- engineAbiVersion: abiVersion,
4002
+ engineAbiVersion: abiVersion2,
3884
4003
  expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
3885
- abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
4004
+ abiCompatible: abiVersion2 === EXPECTED_ENGINE_ABI_VERSION,
3886
4005
  sharedArrayBuffer,
3887
4006
  atomics,
3888
4007
  audioWorklet,
@@ -4528,6 +4647,8 @@ var StreamAnalyzer = class {
4528
4647
  config.computeSpectral ?? defaults.computeSpectral,
4529
4648
  config.emitEveryNFrames ?? defaults.emitEveryNFrames,
4530
4649
  config.magnitudeDownsample ?? defaults.magnitudeDownsample,
4650
+ config.maxPendingFrames ?? defaults.maxPendingFrames,
4651
+ config.maxProgressionEntries ?? defaults.maxProgressionEntries,
4531
4652
  config.keyUpdateIntervalSec ?? defaults.keyUpdateIntervalSec,
4532
4653
  config.bpmUpdateIntervalSec ?? defaults.bpmUpdateIntervalSec,
4533
4654
  config.window ?? defaults.window,
@@ -4543,7 +4664,8 @@ var StreamAnalyzer = class {
4543
4664
  this.analyzer.process(samples);
4544
4665
  }
4545
4666
  /**
4546
- * Process audio samples with explicit sample offset.
4667
+ * Process audio samples with a contiguous explicit sample offset. A gap,
4668
+ * seek, or switch from `process()` requires `reset()` first.
4547
4669
  *
4548
4670
  * @param samples - Audio samples (mono, float32)
4549
4671
  * @param sampleOffset - Cumulative sample count at start of this chunk
@@ -4611,6 +4733,10 @@ var StreamAnalyzer = class {
4611
4733
  totalFrames: s.totalFrames,
4612
4734
  totalSamples: s.totalSamples,
4613
4735
  durationSeconds: s.durationSeconds,
4736
+ pendingFrames: s.pendingFrames,
4737
+ droppedOutputFrames: s.droppedOutputFrames,
4738
+ droppedChordProgressionEntries: s.droppedChordProgressionEntries,
4739
+ droppedBarProgressionEntries: s.droppedBarProgressionEntries,
4614
4740
  estimate: {
4615
4741
  bpm: s.estimate.bpm,
4616
4742
  bpmConfidence: s.estimate.bpmConfidence,
@@ -4972,6 +5098,12 @@ function version() {
4972
5098
  }
4973
5099
  return module.version();
4974
5100
  }
5101
+ function abiVersion() {
5102
+ if (!module) {
5103
+ throw new Error("Module not initialized. Call init() first.");
5104
+ }
5105
+ return module.abiVersion();
5106
+ }
4975
5107
  function engineAbiVersion() {
4976
5108
  if (!module) {
4977
5109
  throw new Error("Module not initialized. Call init() first.");
@@ -5043,6 +5175,7 @@ export {
5043
5175
  StreamingEqualizer,
5044
5176
  StreamingMasteringChain,
5045
5177
  StreamingRetune,
5178
+ abiVersion,
5046
5179
  amplitudeToDb,
5047
5180
  analyze,
5048
5181
  analyzeBpm,
@@ -5062,6 +5195,7 @@ export {
5062
5195
  chromaCens,
5063
5196
  chromaCqt,
5064
5197
  cqt,
5198
+ cqtToAudio,
5065
5199
  createOpfsClipPageProvider,
5066
5200
  createOpfsClipPageWorker,
5067
5201
  cyclicTempogram,
@@ -5233,6 +5367,7 @@ export {
5233
5367
  voiceChangerAbiVersion,
5234
5368
  voiceCharacterPresetId,
5235
5369
  vqt,
5370
+ vqtToAudio,
5236
5371
  waveformPeakPyramid,
5237
5372
  waveformPeaks,
5238
5373
  zeroCrossingRate,