Skip to content

Commit d31bf09

Browse files
authored
Merge pull request #1 from eshaz/multi-threading
Multi Threading
2 parents 782b2c6 + a99bc32 commit d31bf09

File tree

6 files changed

+823
-516
lines changed

6 files changed

+823
-516
lines changed

README.md

+68-32
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
`synaudio` is a JavaScript library that finds the synchronization point between two similar audio clips.
44
* Synchronize two audio clips by finding the sample offset with the [Pearson correlation coefficient](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient).
55
* Correlation algorithm implemented as WebAssembly SIMD.
6-
* Built in Web Worker implementation for easy multithreading.
6+
* Built in Web Worker implementations for concurrency.
77

88
---
99

1010
* [Installing](#installing)
1111
* [Usage](#usage)
1212
* [API](#api)
1313
* [Options](#options)
14-
* [Types](#types)
1514
* [Methods](#methods)
15+
* [Types](#types)
1616

1717
## Installing
1818

@@ -32,7 +32,7 @@
3232
});
3333
```
3434

35-
1. Call `sync` or `syncWorker` method on the instance to find the synchronization point in samples between two audio clips.
35+
1. Call the `sync`, `syncWorker`, or `syncWorkerConcurrent` method on the instance to find the synchronization point in samples between two audio clips.
3636

3737
* See the [API](#api) section below for details on these methods.
3838

@@ -50,12 +50,13 @@
5050
}
5151

5252
const {
53-
sampleOffset, // position relative to `base` where `comparison` matches best
54-
correlation // covariance coefficient of the match [ ranging -1 (worst) to 1 (best) ]
55-
} = await synAudio.syncWorker(
56-
base, // audio data to use a base for the comparison
57-
comparison // audio data to compare against the base
58-
);
53+
sampleOffset, // position relative to `base` where `comparison` matches best
54+
correlation, // covariance coefficient of the match [ ranging -1 (worst) to 1 (best) ]
55+
} = await synAudio.syncWorkerConcurrent(
56+
base, // audio data to use a base for the comparison
57+
comparison, // audio data to compare against the base
58+
4 // number of threads to spawn
59+
);
5960
```
6061

6162
## API
@@ -64,22 +65,74 @@
6465

6566
Class that that finds the synchronization point between two similar audio clips.
6667

67-
### Options
68-
6968
```js
70-
const synAudio = new SynAudio({
71-
correlationSampleSize: 5000,
72-
initialGranularity: 16,
69+
new SynAudio({
70+
correlationSampleSize: 1234,
71+
initialGranularity: 1
7372
});
7473
```
7574

75+
### Options
76+
```ts
77+
declare interface SynAudioOptions {
78+
correlationSampleSize?: number; // default 11025
79+
initialGranularity?: number; // default 16
80+
}
81+
```
7682
* `correlationSampleSize` *optional, defaults to 11025*
7783
* Number of samples to compare while finding the best offset
7884
* Higher numbers will increase accuracy at the cost of slower computation
7985
* `initialGranularity` *optional, defaults to 16*
8086
* Number of samples to jump while performing the first pass search
8187
* Higher numbers will decrease accuracy at the benefit of much faster computation
8288

89+
90+
### Methods
91+
```ts
92+
declare class SynAudio {
93+
constructor(options?: SynAudioOptions);
94+
95+
public async sync(
96+
base: PCMAudio,
97+
comparison: PCMAudio
98+
): Promise<SynAudioResult>;
99+
100+
public async syncWorker(
101+
base: PCMAudio,
102+
comparison: PCMAudio
103+
): Promise<SynAudioResult>;
104+
105+
public async syncWorkerConcurrent(
106+
base: PCMAudio,
107+
comparison: PCMAudio,
108+
threads?: number // default 1
109+
): Promise<SynAudioResult>;
110+
}
111+
```
112+
113+
* `sync(base: PCMAudio, comparison: PCMAudio): SynAudioResult`
114+
* Executes on the main thread.
115+
* Parameters
116+
* `base` Audio data to compare against
117+
* `comparison` Audio data to use as a comparison
118+
* Returns
119+
* `SynAudioResult` containing the `correlation` and `sampleOffset`
120+
* `syncWorker(base: PCMAudio, comparison: PCMAudio): SynAudioResult`
121+
* Execute in a separate thread as a web worker.
122+
* Parameters
123+
* `base` Audio data to compare against
124+
* `comparison` Audio data to use as a comparison
125+
* Returns
126+
* `SynAudioResult` containing the `correlation` and `sampleOffset`
127+
* `syncWorkerConcurrent(base: PCMAudio, comparison: PCMAudio, threads: number): SynAudioResult`
128+
* Splits the incoming data into chunks and spawns multiple workers that execute concurrently.
129+
* Parameters
130+
* `base` Audio data to compare against
131+
* `comparison` Audio data to use as a comparison
132+
* `threads` Number of threads to spawn *optional, defaults to 1*
133+
* Returns
134+
* `SynAudioResult` containing the `correlation` and `sampleOffset`
135+
83136
### Types
84137

85138
```ts
@@ -105,21 +158,4 @@ interface SynAudioResult {
105158
* Correlation coefficient of the `base` and `comparison` audio at the `sampleOffset`
106159
* Ranging from -1 (worst) to 1 (best)
107160
* `sampleOffset`
108-
* Number of samples relative to `base` where `comparison` has the highest correlation
109-
110-
### Methods
111-
112-
* `sync(base: PCMAudio, comparison: PCMAudio): SynAudioResult`
113-
* Executes on the main thread.
114-
* Parameters
115-
* `base` Audio data to compare against
116-
* `comparison` Audio data to use as a comparison
117-
* Returns
118-
* `SynAudioResult` containing the `correlation` and `sampleOffset`
119-
* `syncWorker(base: PCMAudio, comparison: PCMAudio): SynAudioResult`
120-
* Execute in a separate thread as a web worker
121-
* Parameters
122-
* `base` Audio data to compare against
123-
* `comparison` Audio data to use as a comparison
124-
* Returns
125-
* `SynAudioResult` containing the `correlation` and `sampleOffset`
161+
* Number of samples relative to `base` where `comparison` has the highest correlation

index.d.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,24 @@ declare interface SynAudioResult {
99
}
1010

1111
declare interface SynAudioOptions {
12-
correlationSampleSize?: number;
13-
initialGranularity?: number;
12+
correlationSampleSize?: number; // default 11025
13+
initialGranularity?: number; // default 16
1414
}
1515

1616
declare class SynAudio {
1717
constructor(options?: SynAudioOptions);
1818

19-
public async sync(
19+
public sync(base: PCMAudio, comparison: PCMAudio): Promise<SynAudioResult>;
20+
21+
public syncWorker(
2022
base: PCMAudio,
2123
comparison: PCMAudio
2224
): Promise<SynAudioResult>;
2325

24-
public async syncWorker(
26+
public syncWorkerConcurrent(
2527
base: PCMAudio,
26-
comparison: PCMAudio
28+
comparison: PCMAudio,
29+
threads?: number // default 1
2730
): Promise<SynAudioResult>;
2831
}
2932

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
22
"name": "synaudio",
3-
"version": "0.0.1",
3+
"version": "0.1.0",
44
"description": "Library that finds the synchronization point between two similar audio clips.",
5+
"files": [
6+
"index.js",
7+
"index.d.ts",
8+
"src/SynAudio.js"
9+
],
510
"keywords": [
611
"wasm",
712
"simd",

src/SynAudio.js

+87-34
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Worker from "web-worker";
2323
const simd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]))
2424

2525
const wasmModule = new WeakMap();
26+
const webworkerSource = new WeakMap();
2627

2728
/* WASM strings are embeded during the build */
2829
const simdWasm = String.raw`dynEncode0064dÅ×ÑedddereÄnããããããããããdfsegÉÒÚjÑÉÑÓÖÝfdfgfedjleãd¥äìhokfmÇÓÖÖÉÐÅØÉddoÃÃÌÉÅÔÃÆÅ×Égdn wewhvãgàkßjá„e„j„e„j¬†v¥hӅwf¤„e¥d°qd„f¥f¬qd„f¥eυx¥d„eυy„e¥eՅz„e¥àՅo„e¥f؆{„d΅|„e¥hφ}¥fÚ¥eΆf¥bccckՅp„f¥eՅ~„e¥h­…tg¤„e„sЅq¥d…jf¤f¤„tqd„d„d„e„qÎ¥fØÎ­„d„q¥fØÎ„|­Õqd¥d…f„}¥h³h¤„p…n„d…jg¤„j„j„rΆuadfd„jadfdaHeaofd„j„uadft„jadftaHeaoft„j¥„Î…j„f¥l΅f„n¥fφnqdoo„~h¤„d„f¥fØÎ†j„d„f„qÎ¥fØÎadfd„jadfdaHeaofdo„o†j„eªqeo„j¥ãׅf„zh¤„d„j¥fØÎ†n„d„j„qÎ¥fØÎŽfd„nŽfdöœfd„j¥eօjo„f„yªqd„e„jυn„j¥f؆q„r΅u„d…jg¤„j„qΆf„j„uΆŽfd„fŽfdöœfd„f„Žfh„fŽfhöœfh„j¥l΅j„n¥fφnqdoo„r„{΅r„x„s¥eΆs«qdoo„v„wυof¤„i¥f¬qd„h„o„h„o¬†p¥d°qd„i¥eυx„p¥eՅy„p¥àՅh„p¥f؆z„g΅{„p¥hφ|¥fÚ¥eΆf¥bccckՅi„f¥eՅ}¥d…r„p¥h­…~¥d…sg¤„p„sЅq¥d…jf¤f¤„~qd„g„g„p„qÎ¥fØÎ­„g„q¥fØÎ„{­Õqd¥d…f„|¥h³h¤„i…n„g…jg¤„j„j„rΆtadfd„jadfdaHeaofd„j„tadft„jadftaHeaoft„j¥„Î…j„f¥l΅f„n¥fφnqdoo„}h¤„g„f¥fØÎ†j„g„f„qÎ¥fØÎadfd„jadfdaHeaofdo„p„h†jªqeo„j¥eօf„yh¤„g„j¥fØÎ†n„g„j„qÎ¥fØÎŽfd„nŽfdöœfd„f…jo„f„pªqd„p„jυn„j¥f؆q„r΅t„g…jg¤„j„qΆf„j„tΆuŽfd„fŽfdöœfd„f„uŽfh„fŽfhöœfh„j¥l΅j„n¥fφnqdoo„r„z΅r„x„s¥eΆs«qdoo¥d…i„m¥dšfd„l¥dšfd„e„oυpf¤„o¥d°qd„o¥gՅff¤„v„w¥ã×Άe¥g­h¤¥d…hpeo„d…j„o¥àՆh…ng¤„€„jŽfd„jŽfh„jŽfl„jŽfp…€„j¥t΅j„n¥hφnqdoo„fh¤„d„h¥fØÎ…j„f…ng¤„€„jŽfd…€„j¥h΅j„n¥eφnqdoof¤„e¥g­h¤¥d…epeo„g…j„o¥àՆe…ng¤„„jŽfd„jŽfh„jŽfl„jŽfp…„j¥t΅j„n¥hφnqdoo„f©qd„g„e¥fØÎ…jg¤„„jŽfd…„j¥h΅j„f¥eφfqdoo„„o†Œ…Ž„p¥d®h¤„k¥f؅r„Œ§ddä#ö…Š„o¥hυs„Žaw…ˆ„o…„o¥i¬…q„d…e¥d…hg¤„d„h¥fØÎŽfd…‚„d„h„oÎ¥fØÎŽfd…f¤„qh¤apdddddddddddddddd…ƒapdddddddddddddddd…„apdddddddddddddddd……peo„€„aw…‰¥d…n„e…j„g…fapdddddddddddddddd……apdddddddddddddddd…„apdddddddddddddddd…ƒg¤„ƒ„jaddd„‰aIe††„faddd„ˆaIe†‡aJeaHe…ƒ„j¥t΅j„f¥t΅f„…„‡„‡aJeaHe……„„„†„†aJeaHe…„„s„n¥hΆn®qdoo„ƒaƒg„ƒaƒf„ƒaƒd„ƒaƒeööö„Šù„„aƒg„„aƒf„„aƒd„„aƒeööö„Šù„…aƒg„…aƒf„…aƒd„…aƒeööö„Šù†‹„Âh¤„m„hšfd„l„‹œfd„‹…„h…io„€„‚„…€„e„r΅e„p„h„kΆh®qdoo„k¥e؆e„iΆf„p„f„p¬…k„i„eφe¥d„e¥d®…hf¤„o¥d°h¤¨dddddddd…€peo„v„w¥ã×΅ifã„o¥gՆe©h¤¨dddddddd…€„hpeo„d„h¥fØÎ…j¨dddddddd…€„e…fg¤„€„jŽfd…€„j¥h΅j„f¥eφfqdo„e„hÎo…e„i¥g­qd„d„e¥fØÎ…j„h„v΄eτwυfg¤„€„jŽfd„jŽfh„jŽfl„jŽfp…€„j¥t΅j„f¥hφfqdoo„h„k¬h¤„Œ§ddä#ö…Š„o¥hυi„d„h¥fØÎ…e„Žaw…ˆ„o…„o¥i¬…pg¤„d„h¥fØÎŽfd…‚„d„h„oÎ¥fØÎŽfd…Œf¤„ph¤apdddddddddddddddd…ƒapdddddddddddddddd…„apdddddddddddddddd……peo„€„aw…‰¥d…n„e…j„g…fapdddddddddddddddd……apdddddddddddddddd…„apdddddddddddddddd…ƒg¤„ƒ„jaddd„‰aIe††„faddd„ˆaIe†‡aJeaHe…ƒ„j¥t΅j„f¥t΅f„…„‡„‡aJeaHe……„„„†„†aJeaHe…„„i„n¥hΆn®qdoo„ƒaƒg„ƒaƒf„ƒaƒd„ƒaƒeööö„Šù„„aƒg„„aƒf„„aƒd„„aƒeööö„Šù„…aƒg„…aƒf„…aƒd„…aƒeööö„Šù†‹„Âh¤„m„hšfd„l„‹œfd„‹…o„€„‚„Œ…€„e¥h΅e„k„h¥eΆh«qdoood~sØÅÖËÉØÃÊÉÅØÙÖÉ×ek×ÍÑȕ–œ`;
@@ -67,11 +68,6 @@ export default class SynAudio {
6768
const pageSize = 64 * 1024;
6869
const floatByteLength = Float32Array.BYTES_PER_ELEMENT;
6970

70-
if (a.sampleRate !== b.sampleRate)
71-
throw new Error(
72-
"SynAudio: sample rates of both audio data must be the same"
73-
);
74-
7571
const memory = new WebAssembly.Memory({
7672
initial:
7773
((a.samplesDecoded * a.channelData.length +
@@ -142,36 +138,93 @@ export default class SynAudio {
142138
};
143139
}
144140

141+
async syncWorkerConcurrent(a, b, threads = 1) {
142+
const promises = [];
143+
const lengths = [];
144+
145+
// split a buffer into equal chunks for threads
146+
// overlap at the end of the buffer by correlation sample size
147+
let offset = 0;
148+
for (let i = 1; i <= threads; i++) {
149+
const aBufferLength = Math.ceil(a.samplesDecoded / threads);
150+
const correlationSampleOverlap =
151+
i === threads ? 0 : this._correlationSampleSize;
152+
153+
const aSplit = {
154+
channelData: [],
155+
};
156+
157+
for (const channel of a.channelData) {
158+
const cutChannel = channel.subarray(
159+
offset,
160+
offset + aBufferLength + correlationSampleOverlap
161+
);
162+
aSplit.channelData.push(cutChannel);
163+
aSplit.samplesDecoded = cutChannel.length;
164+
}
165+
166+
const actualLength =
167+
aBufferLength < a.samplesDecoded ? aBufferLength : a.samplesDecoded;
168+
lengths.push(actualLength);
169+
offset += actualLength;
170+
171+
promises.push(this.syncWorker(aSplit, b));
172+
}
173+
174+
const results = await Promise.all(promises);
175+
176+
// find the result with the highest correlation and calculate the offset relative to the input data
177+
let bestResultIdx = 0;
178+
let bestCorrelation = -1;
179+
for (let i = 0; i < results.length; i++)
180+
if (results[i].correlation > bestCorrelation) {
181+
bestResultIdx = i;
182+
bestCorrelation = results[i].correlation;
183+
}
184+
185+
return {
186+
correlation: results[bestResultIdx].correlation,
187+
sampleOffset:
188+
results[bestResultIdx].sampleOffset +
189+
lengths.slice(0, bestResultIdx).reduce((acc, len) => acc + len, 0),
190+
};
191+
}
192+
145193
async syncWorker(a, b) {
146-
const webworkerSourceCode =
147-
"'use strict';" +
148-
`(${((SynAudioWorker, correlationSampleSize, initialGranularity) => {
149-
self.onmessage = ({ data: { module, a, b } }) => {
150-
const worker = new SynAudioWorker(
151-
Promise.resolve(module),
152-
correlationSampleSize,
153-
initialGranularity
154-
);
155-
156-
worker.sync(a, b).then((results) => {
157-
self.postMessage(results);
158-
});
159-
};
160-
}).toString()})(${this.SynAudioWorker.toString()}, ${
161-
this._correlationSampleSize
162-
}, ${this._initialGranularity})`;
163-
164-
let type = "text/javascript",
165-
source;
166-
167-
try {
168-
// browser
169-
source = URL.createObjectURL(new Blob([webworkerSourceCode], { type }));
170-
} catch {
171-
// nodejs
172-
source = `data:${type};base64,${Buffer.from(webworkerSourceCode).toString(
173-
"base64"
174-
)}`;
194+
let source = webworkerSource.get(this);
195+
196+
if (!source) {
197+
const webworkerSourceCode =
198+
"'use strict';" +
199+
`(${((SynAudioWorker, correlationSampleSize, initialGranularity) => {
200+
self.onmessage = ({ data: { module, a, b } }) => {
201+
const worker = new SynAudioWorker(
202+
Promise.resolve(module),
203+
correlationSampleSize,
204+
initialGranularity
205+
);
206+
207+
worker.sync(a, b).then((results) => {
208+
self.postMessage(results);
209+
});
210+
};
211+
}).toString()})(${this.SynAudioWorker.toString()}, ${
212+
this._correlationSampleSize
213+
}, ${this._initialGranularity})`;
214+
215+
let type = "text/javascript";
216+
217+
try {
218+
// browser
219+
source = URL.createObjectURL(new Blob([webworkerSourceCode], { type }));
220+
} catch {
221+
// nodejs
222+
source = `data:${type};base64,${Buffer.from(
223+
webworkerSourceCode
224+
).toString("base64")}`;
225+
}
226+
227+
webworkerSource.set(this, source);
175228
}
176229

177230
const worker = new Worker(source, { name: "SynAudio" });

0 commit comments

Comments
 (0)