@sorisdk/web-audio 0.1.5 → 0.5.1

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.
Binary file
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, PackSource, MatchResult, AudioFingerprintType } from '@sorisdk/matcher';
2
- export { AudioFingerprintType, PackSource } from '@sorisdk/matcher';
1
+ import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, AudioMarkerDetection, PackSource, MatchResult, AudioFingerprintType } from '@sorisdk/matcher';
2
+ export { AudioFingerprintType, AudioMarkerConfig, AudioMarkerDetection, PackSource } from '@sorisdk/matcher';
3
3
  import { InitAfpgenOptions } from '@sorisdk/afpgen';
4
4
 
5
5
  declare const DEFAULT_BROWSER_AUDIOPACK_VERSION = "20220401000000";
@@ -52,7 +52,13 @@ interface InitMatchWindowWasmOptions {
52
52
  wasmInitInput?: unknown;
53
53
  }
54
54
 
55
- type DetectedMatch = Pick<MatchResult, "afpType" | "name" | "position" | "score">;
55
+ interface DetectedMatch extends Pick<MatchResult, "afpType" | "name" | "position" | "score"> {
56
+ audioMarker?: AudioMarkerDetection | null;
57
+ }
58
+ interface AudioMarkerEvent {
59
+ marker: string | null;
60
+ detection: AudioMarkerDetection;
61
+ }
56
62
  interface MaterialActivityRequestPayload {
57
63
  type: "material";
58
64
  material_id: string;
@@ -82,6 +88,7 @@ interface CreateMicrophoneMatcherOptions {
82
88
  mediaConstraints?: MediaStreamConstraints;
83
89
  stopTracksOnStop?: boolean;
84
90
  suspendContextOnStop?: boolean;
91
+ audiomarker?: boolean;
85
92
  activityReporter?: MicrophoneMatcherActivityReporterOptions;
86
93
  wasm?: CreateMicrophoneMatcherWasmOptions;
87
94
  }
@@ -97,6 +104,7 @@ interface MicrophoneMatcherEventMap extends Record<string, unknown> {
97
104
  match: {
98
105
  best: DetectedMatch;
99
106
  };
107
+ audiomarker: AudioMarkerEvent;
100
108
  campaign: MicrophoneMatcherCampaignEvent;
101
109
  nomatch: {};
102
110
  stop: {};
@@ -181,6 +189,8 @@ declare class MicrophoneMatcher {
181
189
  private readonly audioContext;
182
190
  private readonly stopTracksOnStop;
183
191
  private readonly suspendContextOnStop;
192
+ private readonly audiomarker;
193
+ private readonly captureSampleRate;
184
194
  private readonly audioGraphFactory;
185
195
  private readonly packSource;
186
196
  private readonly options;
@@ -195,8 +205,12 @@ declare class MicrophoneMatcher {
195
205
  private pendingMatch;
196
206
  private captureGeneration;
197
207
  private pcmBuffer;
208
+ private markerPcmBuffer;
198
209
  private resampleBuffer;
199
210
  private resampleOffset;
211
+ private latestAudioMarker;
212
+ private latestAudioMarkerDetection;
213
+ private audioMarkerRequestId;
200
214
  private hopSize;
201
215
  private diagnostics;
202
216
  constructor(options: CreateMicrophoneMatcherOptions);
@@ -226,6 +240,12 @@ declare class MicrophoneMatcher {
226
240
  private queueMatch;
227
241
  private runMatchLoop;
228
242
  private resetCaptureState;
243
+ private appendAudioMarkerSamples;
244
+ private beginAudioMarkerDetection;
245
+ private detectAudioMarkerForRequest;
246
+ private matchConfig;
247
+ private withAudioMarkerForRequest;
248
+ private emitAudioMarkerIfChanged;
229
249
  private normalizeInputSamples;
230
250
  private resampleToTargetRate;
231
251
  private cleanupAfterStartFailure;
@@ -236,6 +256,8 @@ declare class MicrophoneMatcher {
236
256
  }
237
257
  declare function createMicrophoneMatcher(options?: CreateMicrophoneMatcherOptions): Promise<MicrophoneMatcher>;
238
258
 
239
- declare function requestMicrophoneStream(mediaDevices: MediaDevices, mediaConstraints?: MediaStreamConstraints): Promise<MediaStream>;
259
+ declare function requestMicrophoneStream(mediaDevices: MediaDevices, mediaConstraints?: MediaStreamConstraints, options?: {
260
+ preferredSampleRate?: number;
261
+ }): Promise<MediaStream>;
240
262
 
241
- export { AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
263
+ export { type AudioMarkerEvent, AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
package/dist/index.js CHANGED
@@ -144,6 +144,7 @@ import {
144
144
  // src/internal.ts
145
145
  import { AudioFingerprintType } from "@sorisdk/matcher";
146
146
  var TARGET_SAMPLE_RATE = 16e3;
147
+ var AUDIO_MARKER_SAMPLE_RATE = 48e3;
147
148
  var DEFAULT_RUNTIME_CONFIG = {
148
149
  captureBufferSize: 4096,
149
150
  matchWindowMs: 3250,
@@ -157,14 +158,14 @@ var DEFAULT_RUNTIME_CONFIG = {
157
158
  matchConfig: void 0,
158
159
  initialFingerprintType: AudioFingerprintType.Livestream
159
160
  };
160
- function createBrowserAudioContext() {
161
- const AudioContextConstructor = globalThis.AudioContext;
161
+ function createBrowserAudioContext(sampleRate = TARGET_SAMPLE_RATE) {
162
+ const AudioContextConstructor = globalThis.AudioContext ?? globalThis.webkitAudioContext;
162
163
  if (typeof AudioContextConstructor !== "function") {
163
164
  throw new Error("AudioContext is not available in this environment");
164
165
  }
165
166
  try {
166
167
  return new AudioContextConstructor({
167
- sampleRate: TARGET_SAMPLE_RATE,
168
+ sampleRate,
168
169
  latencyHint: "interactive"
169
170
  });
170
171
  } catch {
@@ -491,7 +492,7 @@ async function createScriptProcessorGraph({
491
492
  }
492
493
  };
493
494
  }
494
- async function requestMicrophoneStream(mediaDevices, mediaConstraints) {
495
+ async function requestMicrophoneStream(mediaDevices, mediaConstraints, options) {
495
496
  if (!mediaDevices?.getUserMedia) {
496
497
  throw new Error("MediaDevices.getUserMedia is not available");
497
498
  }
@@ -499,7 +500,7 @@ async function requestMicrophoneStream(mediaDevices, mediaConstraints) {
499
500
  mediaConstraints ?? {
500
501
  audio: {
501
502
  channelCount: 1,
502
- sampleRate: 16e3,
503
+ sampleRate: options?.preferredSampleRate ?? TARGET_SAMPLE_RATE,
503
504
  echoCancellation: false,
504
505
  noiseSuppression: false,
505
506
  autoGainControl: false
@@ -534,6 +535,8 @@ var MicrophoneMatcher = class {
534
535
  audioContext;
535
536
  stopTracksOnStop;
536
537
  suspendContextOnStop;
538
+ audiomarker;
539
+ captureSampleRate;
537
540
  audioGraphFactory;
538
541
  packSource;
539
542
  options;
@@ -548,8 +551,12 @@ var MicrophoneMatcher = class {
548
551
  pendingMatch = false;
549
552
  captureGeneration = 0;
550
553
  pcmBuffer = new Float32Array(0);
554
+ markerPcmBuffer = new Float32Array(0);
551
555
  resampleBuffer = new Float32Array(0);
552
556
  resampleOffset = 0;
557
+ latestAudioMarker = null;
558
+ latestAudioMarkerDetection = null;
559
+ audioMarkerRequestId = 0;
553
560
  hopSize = 0;
554
561
  diagnostics = {
555
562
  sampleCallbacks: 0,
@@ -564,7 +571,9 @@ var MicrophoneMatcher = class {
564
571
  this.options = options;
565
572
  this.matcherSession = new MatcherSession(options.wasm?.matcher);
566
573
  this.mediaDevices = getBrowserMediaDevices();
567
- this.audioContext = createBrowserAudioContext();
574
+ this.audiomarker = options.audiomarker === true;
575
+ this.captureSampleRate = this.audiomarker ? AUDIO_MARKER_SAMPLE_RATE : TARGET_SAMPLE_RATE;
576
+ this.audioContext = createBrowserAudioContext(this.captureSampleRate);
568
577
  this.stopTracksOnStop = options.stopTracksOnStop ?? true;
569
578
  this.suspendContextOnStop = options.suspendContextOnStop ?? true;
570
579
  this.audioGraphFactory = defaultAudioGraphFactory;
@@ -668,7 +677,9 @@ var MicrophoneMatcher = class {
668
677
  if (this.audioContext.state === "suspended") {
669
678
  await this.audioContext.resume();
670
679
  }
671
- const stream = await requestMicrophoneStream(this.mediaDevices, this.options.mediaConstraints);
680
+ const stream = await requestMicrophoneStream(this.mediaDevices, this.options.mediaConstraints, {
681
+ preferredSampleRate: this.captureSampleRate
682
+ });
672
683
  this.stream = stream;
673
684
  this.events.emit("microphoneready", {
674
685
  stream,
@@ -738,6 +749,9 @@ var MicrophoneMatcher = class {
738
749
  const captureGeneration = this.captureGeneration;
739
750
  this.diagnostics.sampleCallbacks += 1;
740
751
  this.diagnostics.inputSamples += samples.length;
752
+ if (this.audiomarker) {
753
+ this.appendAudioMarkerSamples(samples);
754
+ }
741
755
  const normalizedSamples = this.normalizeInputSamples(samples);
742
756
  if (normalizedSamples.length === 0) {
743
757
  return;
@@ -782,24 +796,28 @@ var MicrophoneMatcher = class {
782
796
  continue;
783
797
  }
784
798
  this.diagnostics.readyQueries += 1;
785
- let distinctBest = null;
799
+ let matchedBest = null;
786
800
  let shouldEmitNoMatch = false;
787
801
  const handleMatch = (event) => {
788
802
  if (!event.best) {
789
803
  return;
790
804
  }
791
- distinctBest = event.best;
805
+ matchedBest = event.best;
792
806
  shouldEmitNoMatch = false;
793
807
  };
794
808
  const handleNoMatch = () => {
795
- distinctBest = null;
809
+ matchedBest = null;
796
810
  shouldEmitNoMatch = true;
797
811
  };
798
812
  this.matcherSession.on("match", handleMatch);
799
813
  this.matcherSession.on("nomatch", handleNoMatch);
800
814
  try {
801
815
  this.diagnostics.matchRequests += 1;
802
- await this.matcherSession.bestMatch(query, DEFAULT_RUNTIME_CONFIG.matchConfig);
816
+ const bestMatchPromise = this.matcherSession.bestMatch(query, this.matchConfig());
817
+ const audioMarkerRequestId = this.beginAudioMarkerDetection(captureGeneration);
818
+ await bestMatchPromise;
819
+ const distinctBest = this.withAudioMarkerForRequest(matchedBest, audioMarkerRequestId);
820
+ matchedBest = distinctBest;
803
821
  } finally {
804
822
  this.matcherSession.off("match", handleMatch);
805
823
  this.matcherSession.off("nomatch", handleNoMatch);
@@ -807,9 +825,9 @@ var MicrophoneMatcher = class {
807
825
  if (captureGeneration !== this.captureGeneration || !this.running) {
808
826
  return;
809
827
  }
810
- if (distinctBest) {
811
- this.events.emit("match", { best: distinctBest });
812
- void this.reportCampaign(distinctBest, captureGeneration);
828
+ if (matchedBest) {
829
+ this.events.emit("match", { best: matchedBest });
830
+ void this.reportCampaign(matchedBest, captureGeneration);
813
831
  } else if (shouldEmitNoMatch) {
814
832
  this.events.emit("nomatch", {});
815
833
  }
@@ -825,11 +843,76 @@ var MicrophoneMatcher = class {
825
843
  this.pendingMatch = false;
826
844
  this.matchInFlight = false;
827
845
  this.pcmBuffer = new Float32Array(0);
846
+ this.markerPcmBuffer = new Float32Array(0);
828
847
  this.resampleBuffer = new Float32Array(0);
829
848
  this.resampleOffset = 0;
849
+ this.latestAudioMarker = null;
850
+ this.latestAudioMarkerDetection = null;
851
+ this.audioMarkerRequestId += 1;
830
852
  this.matchWindowOrThrow().reset();
831
853
  await this.extractor?.reset();
832
854
  }
855
+ appendAudioMarkerSamples(samples) {
856
+ const maxSamples = Math.max(
857
+ 1,
858
+ Math.ceil(this.audioContext.sampleRate * DEFAULT_RUNTIME_CONFIG.matchWindowMs / 1e3)
859
+ );
860
+ const merged = appendFloat32(this.markerPcmBuffer, samples);
861
+ this.markerPcmBuffer = merged.length > maxSamples ? merged.slice(merged.length - maxSamples) : merged;
862
+ }
863
+ beginAudioMarkerDetection(captureGeneration) {
864
+ if (!this.audiomarker || this.markerPcmBuffer.length === 0) {
865
+ return null;
866
+ }
867
+ const requestId = this.audioMarkerRequestId + 1;
868
+ this.audioMarkerRequestId = requestId;
869
+ const markerPcm = float32ToPcm16Le(this.markerPcmBuffer);
870
+ void this.detectAudioMarkerForRequest(captureGeneration, requestId, markerPcm);
871
+ return requestId;
872
+ }
873
+ async detectAudioMarkerForRequest(captureGeneration, requestId, markerPcm) {
874
+ try {
875
+ const detection = await this.matcherSession.detectAudioMarker(
876
+ markerPcm,
877
+ Math.round(this.audioContext.sampleRate)
878
+ );
879
+ if (requestId !== this.audioMarkerRequestId || captureGeneration !== this.captureGeneration || !this.running) {
880
+ return null;
881
+ }
882
+ this.latestAudioMarkerDetection = { requestId, detection };
883
+ this.emitAudioMarkerIfChanged(detection);
884
+ return detection;
885
+ } catch (error) {
886
+ emitError(this.events, "audiomarker", error);
887
+ return null;
888
+ }
889
+ }
890
+ matchConfig() {
891
+ if (!this.audiomarker) {
892
+ return DEFAULT_RUNTIME_CONFIG.matchConfig;
893
+ }
894
+ return {
895
+ ...DEFAULT_RUNTIME_CONFIG.matchConfig,
896
+ audiomarker: true
897
+ };
898
+ }
899
+ withAudioMarkerForRequest(match, requestId) {
900
+ if (!match || requestId === null || this.latestAudioMarkerDetection?.requestId !== requestId) {
901
+ return match;
902
+ }
903
+ return {
904
+ ...match,
905
+ audioMarker: this.latestAudioMarkerDetection.detection
906
+ };
907
+ }
908
+ emitAudioMarkerIfChanged(detection) {
909
+ const marker = detection.code ?? null;
910
+ if (marker === this.latestAudioMarker) {
911
+ return;
912
+ }
913
+ this.latestAudioMarker = marker;
914
+ this.events.emit("audiomarker", { marker, detection });
915
+ }
833
916
  normalizeInputSamples(samples) {
834
917
  if (this.audioContext.sampleRate === TARGET_SAMPLE_RATE) {
835
918
  return samples;
@@ -916,6 +999,16 @@ function appendFloat32(left, right) {
916
999
  merged.set(right, left.length);
917
1000
  return merged;
918
1001
  }
1002
+ function float32ToPcm16Le(samples) {
1003
+ const bytes = new Uint8Array(samples.length * 2);
1004
+ const view = new DataView(bytes.buffer);
1005
+ for (let index = 0; index < samples.length; index += 1) {
1006
+ const sample = Math.max(-1, Math.min(1, samples[index] ?? 0));
1007
+ const value = sample < 0 ? sample * 32768 : sample * 32767;
1008
+ view.setInt16(index * 2, Math.round(value), true);
1009
+ }
1010
+ return bytes;
1011
+ }
919
1012
  async function createMicrophoneMatcher(options = {}) {
920
1013
  const matcher = new MicrophoneMatcher(options);
921
1014
  await matcher.prepare();
@@ -964,6 +1057,7 @@ var FORWARDED_EVENTS = [
964
1057
  "microphoneready",
965
1058
  "start",
966
1059
  "match",
1060
+ "audiomarker",
967
1061
  "campaign",
968
1062
  "nomatch",
969
1063
  "stop",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.1.5",
3
+ "version": "0.5.1",
4
4
  "description": "Web SDK for browser-based audio recognition with SORI API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,8 +34,8 @@
34
34
  "license": "SEE LICENSE IN LICENSE.md",
35
35
  "homepage": "https://docs.soriapi.com/ko/integration/web",
36
36
  "dependencies": {
37
- "@sorisdk/afpgen": "0.1.5",
38
- "@sorisdk/matcher": "0.1.5"
37
+ "@sorisdk/afpgen": "0.5.1",
38
+ "@sorisdk/matcher": "0.5.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",