Skip to content

Commit 12a6081

Browse files
authored
Merge pull request #30 from diffusionstudio/kostantin/feature/fast-sampler
Kostantin/feature/fast sampler
2 parents 59982cd + 891a663 commit 12a6081

File tree

6 files changed

+136
-9
lines changed

6 files changed

+136
-9
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@diffusionstudio/core",
33
"private": false,
4-
"version": "1.0.1",
4+
"version": "1.1.0",
55
"type": "module",
66
"description": "Build bleeding edge video processing applications",
77
"files": [

src/composition/composition.ts

-2
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,6 @@ export class Composition extends EventEmitterMixin<CompositionEvents, typeof Ser
438438
output.getChannelData(i).set(outputData);
439439
}
440440
} catch (_) { }
441-
442-
clip.source.audioBuffer = undefined;
443441
}
444442

445443
return output;

src/sources/audio.spec.ts

+51-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class MockOfflineAudioContext {
1616
const audioBuffer = {
1717
duration: 5, // Mock duration
1818
sampleRate: this.sampleRate,
19-
getChannelData: () => new Float32Array(5000), // Return a dummy Float32Array
19+
length: 5000,
20+
getChannelData: () => new Float32Array(5000).fill(0.5), // Return a dummy Float32Array
2021
} as any as AudioBuffer;
2122
return Promise.resolve(audioBuffer);
2223
}
@@ -33,11 +34,59 @@ describe('AudioSource', () => {
3334
});
3435

3536
it('should decode an audio buffer correctly', async () => {
36-
const buffer = await audioSource.decode(2, 44100);
37+
const buffer = await audioSource.decode(2, 44100, true);
3738
expect(buffer.duration).toBe(5); // Mock duration
3839
expect(buffer.sampleRate).toBe(44100);
3940
expect(audioSource.audioBuffer).toBe(buffer);
4041
expect(audioSource.duration.seconds).toBe(5); // Ensure duration is set
42+
43+
audioSource.audioBuffer = undefined;
44+
await audioSource.decode(2, 44100, false);
45+
expect(audioSource.audioBuffer).toBe(undefined);
46+
});
47+
48+
it('should (fast) sample an audio buffer correctly', async () => {
49+
const samples = await audioSource.fastsampler({ length: 20 });
50+
expect(samples.length).toBe(20);
51+
52+
for (const sample of samples) {
53+
expect(sample).toBe(0.5);
54+
}
55+
});
56+
57+
it('should (fast) sample an audio buffer correctly with start', async () => {
58+
const samples = await audioSource.fastsampler({
59+
length: 20,
60+
start: 10,
61+
});
62+
expect(samples.length).toBe(20);
63+
64+
for (const sample of samples) {
65+
expect(sample).toBe(0.5);
66+
}
67+
});
68+
69+
it('should (fast) sample an audio buffer correctly with stop', async () => {
70+
const samples = await audioSource.fastsampler({
71+
length: 20,
72+
stop: 1000,
73+
start: 20,
74+
});
75+
expect(samples.length).toBe(20);
76+
77+
for (const sample of samples) {
78+
expect(sample).toBe(0.5);
79+
}
80+
});
81+
82+
it('should (fast) sample an audio buffer correctly with logarithmic scale', async () => {
83+
const samples = await audioSource.fastsampler({ logarithmic: true });
84+
expect(samples.length).toBe(60);
85+
86+
for (const sample of samples) {
87+
expect(sample).toBeGreaterThanOrEqual(0.5);
88+
expect(sample).toBeLessThanOrEqual(1);
89+
}
4190
});
4291

4392
it('should create a thumbnail with correct DOM elements', async () => {

src/sources/audio.ts

+60-4
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,48 @@ import { Source } from './source';
99

1010
import type { ClipType } from '../clips';
1111
import type { ArgumentTypes } from '../types';
12+
import type { FastSamplerOptions } from './audio.types';
1213

1314
export class AudioSource extends Source {
15+
private decoding = false;
16+
1417
public readonly type: ClipType = 'audio';
1518
public audioBuffer?: AudioBuffer;
1619

1720
public async decode(
1821
numberOfChannels: number = 2,
1922
sampleRate: number = 48000,
23+
cache = false,
2024
): Promise<AudioBuffer> {
25+
// make sure audio is not decoded multiple times
26+
if (this.decoding && cache) {
27+
await new Promise(this.resolve('update'));
28+
29+
if (this.audioBuffer) {
30+
return this.audioBuffer;
31+
}
32+
}
33+
34+
this.decoding = true;
2135
const buffer = await this.arrayBuffer();
2236

2337
const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);
2438

25-
this.audioBuffer = await ctx.decodeAudioData(buffer);
26-
this.duration.seconds = this.audioBuffer.duration;
39+
const audioBuffer = await ctx.decodeAudioData(buffer);
40+
this.duration.seconds = audioBuffer.duration;
41+
if (cache) this.audioBuffer = audioBuffer;
2742

43+
this.decoding = false;
2844
this.trigger('update', undefined);
2945

30-
return this.audioBuffer;
46+
return audioBuffer;
3147
}
3248

49+
/**
50+
* @deprecated Use fastsampler instead.
51+
*/
3352
public async samples(numberOfSampes = 60, windowSize = 50, min = 0): Promise<number[]> {
34-
const buffer = this.audioBuffer ?? (await this.decode(1, 16e3));
53+
const buffer = this.audioBuffer ?? (await this.decode(1, 3000, true));
3554

3655
const window = Math.round(buffer.sampleRate / windowSize);
3756
const length = buffer.sampleRate * buffer.duration - window;
@@ -50,6 +69,43 @@ export class AudioSource extends Source {
5069
return res.map((v) => Math.round((v / Math.max(...res)) * (100 - min)) + min);
5170
}
5271

72+
/**
73+
* Fast sampler that uses a window size to calculate the max value of the samples in the window.
74+
* @param options - Sampling options.
75+
* @returns An array of the max values of the samples in the window.
76+
*/
77+
public async fastsampler({ length = 60, start = 0, stop, logarithmic = false }: FastSamplerOptions): Promise<Float32Array> {
78+
if (typeof start === 'object') start = start.millis;
79+
if (typeof stop === 'object') stop = stop.millis;
80+
81+
const sampleRate = 3000;
82+
const audioBuffer = this.audioBuffer ?? (await this.decode(1, sampleRate, true));
83+
const channelData = audioBuffer.getChannelData(0);
84+
85+
const firstSample = Math.floor(Math.max(start * sampleRate / 1000, 0));
86+
const lastSample = stop
87+
? Math.floor(Math.min(stop * sampleRate / 1000, audioBuffer.length))
88+
: audioBuffer.length;
89+
90+
const windowSize = Math.floor((lastSample - firstSample) / length);
91+
const result = new Float32Array(length);
92+
93+
for (let i = 0; i < length; i++) {
94+
const start = firstSample + i * windowSize;
95+
const end = start + windowSize;
96+
let min = Infinity;
97+
let max = -Infinity;
98+
99+
for (let j = start; j < end; j++) {
100+
const sample = channelData[j];
101+
if (sample < min) min = sample;
102+
if (sample > max) max = sample;
103+
}
104+
result[i] = logarithmic ? Math.log2(1 + max) : max;
105+
}
106+
return result;
107+
}
108+
53109
public async thumbnail(...args: ArgumentTypes<this['samples']>): Promise<HTMLElement> {
54110
const samples = await this.samples(...args);
55111

src/sources/audio.types.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Timestamp } from '../models';
2+
3+
/**
4+
* Fast sampler options.
5+
*/
6+
export type FastSamplerOptions = {
7+
/**
8+
* The number of samples to return.
9+
*/
10+
length?: number;
11+
/**
12+
* The start time in **milliseconds** relative to the beginning of the clip.
13+
*/
14+
start?: Timestamp | number;
15+
/**
16+
* The stop time in **milliseconds** relative to the beginning of the clip.
17+
*/
18+
stop?: Timestamp | number;
19+
/**
20+
* Whether to use a logarithmic scale.
21+
*/
22+
logarithmic?: boolean;
23+
};

src/sources/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
export * from './source';
99
export * from './html';
1010
export * from './audio';
11+
export * from './audio.types';
1112
export * from './image';
1213
export * from './video';

0 commit comments

Comments
 (0)