@sorisdk/web-audio 0.4.0 → 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.
- package/dist/generated/window_bg.wasm +0 -0
- package/dist/index.d.ts +27 -5
- package/dist/index.js +108 -14
- package/package.json +3 -3
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
805
|
+
matchedBest = event.best;
|
|
792
806
|
shouldEmitNoMatch = false;
|
|
793
807
|
};
|
|
794
808
|
const handleNoMatch = () => {
|
|
795
|
-
|
|
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
|
-
|
|
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 (
|
|
811
|
-
this.events.emit("match", { best:
|
|
812
|
-
void this.reportCampaign(
|
|
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.
|
|
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.
|
|
38
|
-
"@sorisdk/matcher": "0.
|
|
37
|
+
"@sorisdk/afpgen": "0.5.1",
|
|
38
|
+
"@sorisdk/matcher": "0.5.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsup": "^8.5.1",
|