@absolutejs/voice 0.0.22-beta.630 → 0.0.22-beta.631

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.
@@ -1721,6 +1721,7 @@ var createTimeStretcher = () => {
1721
1721
  };
1722
1722
 
1723
1723
  // src/client/audioPlayer.ts
1724
+ var activeAudioPlayers = new Set;
1724
1725
  var DEFAULT_LOOKAHEAD_MS = 15;
1725
1726
  var DEFAULT_VOLUME = 1;
1726
1727
  var DEFAULT_PLAYBACK_RATE = 1;
@@ -1729,6 +1730,7 @@ var MAX_PLAYBACK_RATE = 2;
1729
1730
  var STRETCH_BYPASS_EPSILON = 0.01;
1730
1731
  var ANALYSER_FFT_SIZE = 256;
1731
1732
  var PCM_BYTE_MIDPOINT = 128;
1733
+ var GAP_EPSILON_S = 0.03;
1732
1734
  var createInitialState3 = () => ({
1733
1735
  activeSourceCount: 0,
1734
1736
  error: null,
@@ -1800,6 +1802,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1800
1802
  let interruptPromise = null;
1801
1803
  let resolveInterruptPromise = null;
1802
1804
  let interruptFallbackTimer = null;
1805
+ let concurrencyCounted = false;
1806
+ let observedPeakConcurrency = 0;
1807
+ let scheduledDurationMs = 0;
1808
+ let gapCount = 0;
1809
+ let totalGapMs = 0;
1810
+ let maxGapMs = 0;
1811
+ let errorCount = 0;
1812
+ let hasScheduledAnyChunk = false;
1813
+ const sampleConcurrency = () => {
1814
+ if (activeAudioPlayers.size > observedPeakConcurrency) {
1815
+ observedPeakConcurrency = activeAudioPlayers.size;
1816
+ }
1817
+ };
1818
+ const markPlayerActive = () => {
1819
+ if (concurrencyCounted) {
1820
+ return;
1821
+ }
1822
+ concurrencyCounted = true;
1823
+ activeAudioPlayers.add(sampleConcurrency);
1824
+ for (const sample of activeAudioPlayers) {
1825
+ sample();
1826
+ }
1827
+ };
1828
+ const markPlayerInactive = () => {
1829
+ if (!concurrencyCounted) {
1830
+ return;
1831
+ }
1832
+ concurrencyCounted = false;
1833
+ activeAudioPlayers.delete(sampleConcurrency);
1834
+ };
1803
1835
  const notify = () => {
1804
1836
  for (const subscriber of subscribers) {
1805
1837
  subscriber();
@@ -1913,7 +1945,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1913
1945
  });
1914
1946
  maybeResolveInterrupt();
1915
1947
  };
1916
- const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1948
+ const earliestStart = context.currentTime + lookaheadSeconds;
1949
+ const startAt = Math.max(earliestStart, queueEndTime);
1950
+ if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
1951
+ const gapMs = (earliestStart - queueEndTime) * 1000;
1952
+ gapCount += 1;
1953
+ totalGapMs += gapMs;
1954
+ maxGapMs = Math.max(maxGapMs, gapMs);
1955
+ }
1956
+ hasScheduledAnyChunk = true;
1957
+ scheduledDurationMs += buffer.duration / rate * 1000;
1958
+ sampleConcurrency();
1917
1959
  queueEndTime = startAt + buffer.duration / rate;
1918
1960
  sourceNodes.add(node);
1919
1961
  setState({
@@ -1981,6 +2023,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1981
2023
  queuedChunkCount: state.queuedChunkCount + nextChunks.length
1982
2024
  });
1983
2025
  } catch (error) {
2026
+ errorCount += 1;
1984
2027
  setState({
1985
2028
  error: error instanceof Error ? error.message : String(error)
1986
2029
  });
@@ -2004,6 +2047,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2004
2047
  return state.activeSourceCount;
2005
2048
  },
2006
2049
  close: async () => {
2050
+ markPlayerInactive();
2007
2051
  unsubscribeSource();
2008
2052
  stopQueuedPlayback({ forceClear: true });
2009
2053
  clearInterruptTimer();
@@ -2043,6 +2087,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2043
2087
  return Math.sqrt(sumSquares / analyserBuffer.length);
2044
2088
  },
2045
2089
  getSnapshot: () => state,
2090
+ getIntegritySummary: () => {
2091
+ const chunksReceived = source.assistantAudio.length;
2092
+ const chunksScheduled = state.processedChunkCount;
2093
+ return {
2094
+ chunksReceived,
2095
+ chunksScheduled,
2096
+ errorCount,
2097
+ gapCount,
2098
+ maxConcurrentPlayers: observedPeakConcurrency,
2099
+ maxGapMs: Math.round(maxGapMs),
2100
+ ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
2101
+ scheduledDurationMs: Math.round(scheduledDurationMs),
2102
+ totalGapMs: Math.round(totalGapMs)
2103
+ };
2104
+ },
2046
2105
  interrupt: async () => {
2047
2106
  const startedAt = Date.now();
2048
2107
  const context = await ensureAudioContext();
@@ -2096,6 +2155,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2096
2155
  return;
2097
2156
  }
2098
2157
  await audioContext.suspend();
2158
+ markPlayerInactive();
2099
2159
  setState({
2100
2160
  activeSourceCount: sourceNodes.size,
2101
2161
  isActive: false,
@@ -2126,6 +2186,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2126
2186
  if (context.state === "suspended") {
2127
2187
  await context.resume();
2128
2188
  }
2189
+ markPlayerActive();
2129
2190
  setState({
2130
2191
  activeSourceCount: sourceNodes.size,
2131
2192
  isActive: true,
@@ -2133,6 +2194,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2133
2194
  });
2134
2195
  await queueSync();
2135
2196
  } catch (error) {
2197
+ errorCount += 1;
2198
+ markPlayerInactive();
2136
2199
  setState({
2137
2200
  error: error instanceof Error ? error.message : String(error),
2138
2201
  isActive: false,
@@ -528,6 +528,7 @@ var createTimeStretcher = () => {
528
528
  };
529
529
 
530
530
  // src/client/audioPlayer.ts
531
+ var activeAudioPlayers = new Set;
531
532
  var DEFAULT_LOOKAHEAD_MS = 15;
532
533
  var DEFAULT_VOLUME = 1;
533
534
  var DEFAULT_PLAYBACK_RATE = 1;
@@ -536,6 +537,7 @@ var MAX_PLAYBACK_RATE = 2;
536
537
  var STRETCH_BYPASS_EPSILON = 0.01;
537
538
  var ANALYSER_FFT_SIZE = 256;
538
539
  var PCM_BYTE_MIDPOINT = 128;
540
+ var GAP_EPSILON_S = 0.03;
539
541
  var createInitialState = () => ({
540
542
  activeSourceCount: 0,
541
543
  error: null,
@@ -607,6 +609,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
607
609
  let interruptPromise = null;
608
610
  let resolveInterruptPromise = null;
609
611
  let interruptFallbackTimer = null;
612
+ let concurrencyCounted = false;
613
+ let observedPeakConcurrency = 0;
614
+ let scheduledDurationMs = 0;
615
+ let gapCount = 0;
616
+ let totalGapMs = 0;
617
+ let maxGapMs = 0;
618
+ let errorCount = 0;
619
+ let hasScheduledAnyChunk = false;
620
+ const sampleConcurrency = () => {
621
+ if (activeAudioPlayers.size > observedPeakConcurrency) {
622
+ observedPeakConcurrency = activeAudioPlayers.size;
623
+ }
624
+ };
625
+ const markPlayerActive = () => {
626
+ if (concurrencyCounted) {
627
+ return;
628
+ }
629
+ concurrencyCounted = true;
630
+ activeAudioPlayers.add(sampleConcurrency);
631
+ for (const sample of activeAudioPlayers) {
632
+ sample();
633
+ }
634
+ };
635
+ const markPlayerInactive = () => {
636
+ if (!concurrencyCounted) {
637
+ return;
638
+ }
639
+ concurrencyCounted = false;
640
+ activeAudioPlayers.delete(sampleConcurrency);
641
+ };
610
642
  const notify = () => {
611
643
  for (const subscriber of subscribers) {
612
644
  subscriber();
@@ -720,7 +752,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
720
752
  });
721
753
  maybeResolveInterrupt();
722
754
  };
723
- const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
755
+ const earliestStart = context.currentTime + lookaheadSeconds;
756
+ const startAt = Math.max(earliestStart, queueEndTime);
757
+ if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
758
+ const gapMs = (earliestStart - queueEndTime) * 1000;
759
+ gapCount += 1;
760
+ totalGapMs += gapMs;
761
+ maxGapMs = Math.max(maxGapMs, gapMs);
762
+ }
763
+ hasScheduledAnyChunk = true;
764
+ scheduledDurationMs += buffer.duration / rate * 1000;
765
+ sampleConcurrency();
724
766
  queueEndTime = startAt + buffer.duration / rate;
725
767
  sourceNodes.add(node);
726
768
  setState({
@@ -788,6 +830,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
788
830
  queuedChunkCount: state.queuedChunkCount + nextChunks.length
789
831
  });
790
832
  } catch (error) {
833
+ errorCount += 1;
791
834
  setState({
792
835
  error: error instanceof Error ? error.message : String(error)
793
836
  });
@@ -811,6 +854,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
811
854
  return state.activeSourceCount;
812
855
  },
813
856
  close: async () => {
857
+ markPlayerInactive();
814
858
  unsubscribeSource();
815
859
  stopQueuedPlayback({ forceClear: true });
816
860
  clearInterruptTimer();
@@ -850,6 +894,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
850
894
  return Math.sqrt(sumSquares / analyserBuffer.length);
851
895
  },
852
896
  getSnapshot: () => state,
897
+ getIntegritySummary: () => {
898
+ const chunksReceived = source.assistantAudio.length;
899
+ const chunksScheduled = state.processedChunkCount;
900
+ return {
901
+ chunksReceived,
902
+ chunksScheduled,
903
+ errorCount,
904
+ gapCount,
905
+ maxConcurrentPlayers: observedPeakConcurrency,
906
+ maxGapMs: Math.round(maxGapMs),
907
+ ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
908
+ scheduledDurationMs: Math.round(scheduledDurationMs),
909
+ totalGapMs: Math.round(totalGapMs)
910
+ };
911
+ },
853
912
  interrupt: async () => {
854
913
  const startedAt = Date.now();
855
914
  const context = await ensureAudioContext();
@@ -903,6 +962,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
903
962
  return;
904
963
  }
905
964
  await audioContext.suspend();
965
+ markPlayerInactive();
906
966
  setState({
907
967
  activeSourceCount: sourceNodes.size,
908
968
  isActive: false,
@@ -933,6 +993,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
933
993
  if (context.state === "suspended") {
934
994
  await context.resume();
935
995
  }
996
+ markPlayerActive();
936
997
  setState({
937
998
  activeSourceCount: sourceNodes.size,
938
999
  isActive: true,
@@ -940,6 +1001,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
940
1001
  });
941
1002
  await queueSync();
942
1003
  } catch (error) {
1004
+ errorCount += 1;
1005
+ markPlayerInactive();
943
1006
  setState({
944
1007
  error: error instanceof Error ? error.message : String(error),
945
1008
  isActive: false,
@@ -1471,6 +1471,20 @@ export type VoiceAudioPlayerSource = {
1471
1471
  assistantAudio: VoiceStreamState["assistantAudio"];
1472
1472
  subscribe: (subscriber: () => void) => () => void;
1473
1473
  };
1474
+ /** Post-call playback integrity, for verifying the assistant audio actually
1475
+ * played cleanly (no overlap, no dropped/stalled packets) — emit it as a trace
1476
+ * so a garbled call is provable, not guessed. `ok` is the all-clear roll-up. */
1477
+ export type VoiceAudioIntegrity = {
1478
+ ok: boolean;
1479
+ chunksReceived: number;
1480
+ chunksScheduled: number;
1481
+ scheduledDurationMs: number;
1482
+ gapCount: number;
1483
+ totalGapMs: number;
1484
+ maxGapMs: number;
1485
+ maxConcurrentPlayers: number;
1486
+ errorCount: number;
1487
+ };
1474
1488
  export type VoiceAudioPlayer = {
1475
1489
  close: () => Promise<void>;
1476
1490
  error: string | null;
@@ -1478,6 +1492,8 @@ export type VoiceAudioPlayer = {
1478
1492
  * driving a visualizer from the actual voice. 0 when idle / no analyser. */
1479
1493
  getOutputLevel: () => number;
1480
1494
  getSnapshot: () => VoiceAudioPlayerState;
1495
+ /** Post-call playback-integrity roll-up (overlap, gaps, drops). */
1496
+ getIntegritySummary: () => VoiceAudioIntegrity;
1481
1497
  activeSourceCount: number;
1482
1498
  isActive: boolean;
1483
1499
  isPlaying: boolean;
@@ -1731,6 +1731,7 @@ var createTimeStretcher = () => {
1731
1731
  };
1732
1732
 
1733
1733
  // src/client/audioPlayer.ts
1734
+ var activeAudioPlayers = new Set;
1734
1735
  var DEFAULT_LOOKAHEAD_MS = 15;
1735
1736
  var DEFAULT_VOLUME = 1;
1736
1737
  var DEFAULT_PLAYBACK_RATE = 1;
@@ -1739,6 +1740,7 @@ var MAX_PLAYBACK_RATE = 2;
1739
1740
  var STRETCH_BYPASS_EPSILON = 0.01;
1740
1741
  var ANALYSER_FFT_SIZE = 256;
1741
1742
  var PCM_BYTE_MIDPOINT = 128;
1743
+ var GAP_EPSILON_S = 0.03;
1742
1744
  var createInitialState = () => ({
1743
1745
  activeSourceCount: 0,
1744
1746
  error: null,
@@ -1810,6 +1812,36 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1810
1812
  let interruptPromise = null;
1811
1813
  let resolveInterruptPromise = null;
1812
1814
  let interruptFallbackTimer = null;
1815
+ let concurrencyCounted = false;
1816
+ let observedPeakConcurrency = 0;
1817
+ let scheduledDurationMs = 0;
1818
+ let gapCount = 0;
1819
+ let totalGapMs = 0;
1820
+ let maxGapMs = 0;
1821
+ let errorCount = 0;
1822
+ let hasScheduledAnyChunk = false;
1823
+ const sampleConcurrency = () => {
1824
+ if (activeAudioPlayers.size > observedPeakConcurrency) {
1825
+ observedPeakConcurrency = activeAudioPlayers.size;
1826
+ }
1827
+ };
1828
+ const markPlayerActive = () => {
1829
+ if (concurrencyCounted) {
1830
+ return;
1831
+ }
1832
+ concurrencyCounted = true;
1833
+ activeAudioPlayers.add(sampleConcurrency);
1834
+ for (const sample of activeAudioPlayers) {
1835
+ sample();
1836
+ }
1837
+ };
1838
+ const markPlayerInactive = () => {
1839
+ if (!concurrencyCounted) {
1840
+ return;
1841
+ }
1842
+ concurrencyCounted = false;
1843
+ activeAudioPlayers.delete(sampleConcurrency);
1844
+ };
1813
1845
  const notify = () => {
1814
1846
  for (const subscriber of subscribers) {
1815
1847
  subscriber();
@@ -1923,7 +1955,17 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1923
1955
  });
1924
1956
  maybeResolveInterrupt();
1925
1957
  };
1926
- const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1958
+ const earliestStart = context.currentTime + lookaheadSeconds;
1959
+ const startAt = Math.max(earliestStart, queueEndTime);
1960
+ if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
1961
+ const gapMs = (earliestStart - queueEndTime) * 1000;
1962
+ gapCount += 1;
1963
+ totalGapMs += gapMs;
1964
+ maxGapMs = Math.max(maxGapMs, gapMs);
1965
+ }
1966
+ hasScheduledAnyChunk = true;
1967
+ scheduledDurationMs += buffer.duration / rate * 1000;
1968
+ sampleConcurrency();
1927
1969
  queueEndTime = startAt + buffer.duration / rate;
1928
1970
  sourceNodes.add(node);
1929
1971
  setState({
@@ -1991,6 +2033,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1991
2033
  queuedChunkCount: state.queuedChunkCount + nextChunks.length
1992
2034
  });
1993
2035
  } catch (error) {
2036
+ errorCount += 1;
1994
2037
  setState({
1995
2038
  error: error instanceof Error ? error.message : String(error)
1996
2039
  });
@@ -2014,6 +2057,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2014
2057
  return state.activeSourceCount;
2015
2058
  },
2016
2059
  close: async () => {
2060
+ markPlayerInactive();
2017
2061
  unsubscribeSource();
2018
2062
  stopQueuedPlayback({ forceClear: true });
2019
2063
  clearInterruptTimer();
@@ -2053,6 +2097,21 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2053
2097
  return Math.sqrt(sumSquares / analyserBuffer.length);
2054
2098
  },
2055
2099
  getSnapshot: () => state,
2100
+ getIntegritySummary: () => {
2101
+ const chunksReceived = source.assistantAudio.length;
2102
+ const chunksScheduled = state.processedChunkCount;
2103
+ return {
2104
+ chunksReceived,
2105
+ chunksScheduled,
2106
+ errorCount,
2107
+ gapCount,
2108
+ maxConcurrentPlayers: observedPeakConcurrency,
2109
+ maxGapMs: Math.round(maxGapMs),
2110
+ ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
2111
+ scheduledDurationMs: Math.round(scheduledDurationMs),
2112
+ totalGapMs: Math.round(totalGapMs)
2113
+ };
2114
+ },
2056
2115
  interrupt: async () => {
2057
2116
  const startedAt = Date.now();
2058
2117
  const context = await ensureAudioContext();
@@ -2106,6 +2165,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2106
2165
  return;
2107
2166
  }
2108
2167
  await audioContext.suspend();
2168
+ markPlayerInactive();
2109
2169
  setState({
2110
2170
  activeSourceCount: sourceNodes.size,
2111
2171
  isActive: false,
@@ -2136,6 +2196,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2136
2196
  if (context.state === "suspended") {
2137
2197
  await context.resume();
2138
2198
  }
2199
+ markPlayerActive();
2139
2200
  setState({
2140
2201
  activeSourceCount: sourceNodes.size,
2141
2202
  isActive: true,
@@ -2143,6 +2204,8 @@ var createVoiceAudioPlayer = (source, options = {}) => {
2143
2204
  });
2144
2205
  await queueSync();
2145
2206
  } catch (error) {
2207
+ errorCount += 1;
2208
+ markPlayerInactive();
2146
2209
  setState({
2147
2210
  error: error instanceof Error ? error.message : String(error),
2148
2211
  isActive: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.630",
3
+ "version": "0.0.22-beta.631",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",