Skip to content

Commit faee341

Browse files
committed
Refactor silence removal functionality in audio processing
- Simplified the `removeSilences` method in `MediaClip` by removing unnecessary options, defaulting to a standard behavior. - Updated the `removeSilences` method signature in `MediaTrack` to use the new `SilenceRemoveOptions` type for consistency. - Added export for `media.types` to enhance type management in the media module. - Removed outdated mock tests related to silence removal to streamline the test suite. These changes enhance the clarity and maintainability of the silence removal logic in the audio processing workflow.
1 parent 437f2cb commit faee341

File tree

6 files changed

+26
-107
lines changed

6 files changed

+26
-107
lines changed

playground/main.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ const audioSource = await core.AudioSource.from('/harvard.MP3');
4747
await audioTrack.add(
4848
await new core.AudioClip(audioSource)
4949
);
50-
await audioTrack.removeSilences({
51-
minDuration: 300,
52-
windowSize: 1,
53-
});
50+
await audioTrack.removeSilences();
5451

5552
image.animate()
5653
.rotation(-16).to(14, 5).to(-7, 10).to(24, 7).to(-3, 9).to(19, 7).to(-14, 12).to(5, 9).to(-30, 13)

src/clips/media/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77

88
export * from './media';
99
export * from './media.interfaces';
10+
export * from './media.types';

src/clips/media/media.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { Timestamp, Transcript } from '../../models';
9-
import { AudioSource, SilenceDetectionOptions } from '../../sources';
9+
import { AudioSource } from '../../sources';
1010
import { RangeDeserializer } from './media.deserializer';
1111
import { serializable } from '../../services';
1212
import { replaceKeyframes } from '../clip/clip.utils';
@@ -16,6 +16,7 @@ import { Clip } from '../clip';
1616
import type { CaptionPresetStrategy, CaptionTrack } from '../../tracks';
1717
import type { float, frame } from '../../types';
1818
import type { MediaClipProps } from './media.interfaces';
19+
import type { SilenceRemoveOptions } from './media.types';
1920

2021
export class MediaClip<Props extends MediaClipProps = MediaClipProps> extends Clip<MediaClipProps> {
2122
public source = new AudioSource();
@@ -319,7 +320,7 @@ export class MediaClip<Props extends MediaClipProps = MediaClipProps> extends Cl
319320
*
320321
* @param options - Options for silence detection
321322
*/
322-
public async removeSilences(options: SilenceDetectionOptions = {}): Promise<MediaClip<Props>[]> {
323+
public async removeSilences(options: SilenceRemoveOptions = {}): Promise<MediaClip<Props>[]> {
323324
const silences = (await this.source.silences(options))
324325
.filter((silence) => inRange(silence, this.range))
325326
.sort((a, b) => a.start.millis - b.start.millis);
@@ -328,6 +329,8 @@ export class MediaClip<Props extends MediaClipProps = MediaClipProps> extends Cl
328329
return [this];
329330
}
330331

332+
// default padding between clips
333+
const padding = options.padding ?? 500;
331334
const result: MediaClip<Props>[] = [this];
332335

333336
for (const silence of silences) {
@@ -336,17 +339,22 @@ export class MediaClip<Props extends MediaClipProps = MediaClipProps> extends Cl
336339
if (!item) break;
337340
if (!inRange(silence, item.range)) continue;
338341

342+
// start with padding
343+
const start = new Timestamp(
344+
Math.min(silence.start.millis + padding, silence.stop.millis)
345+
);
346+
339347
if (silence.start.millis > item.range[0].millis && silence.stop.millis < item.range[1].millis) {
340348
const copy = item.copy();
341349

342-
item.range[1] = silence.start;
350+
item.range[1] = start;
343351
copy.range[0] = silence.stop;
344352

345353
result.push(copy);
346354
} else if (silence.start.millis <= item.range[0].millis) {
347355
item.range[0] = silence.stop;
348356
} else if (silence.stop.millis >= item.range[1].millis) {
349-
item.range[1] = silence.start;
357+
item.range[1] = start;
350358
}
351359
}
352360

src/clips/media/media.types.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { SilenceDetectionOptions } from "../../sources";
2+
3+
export type SilenceRemoveOptions = {
4+
/**
5+
* Adds padding in milliseconds after each detected non-silent segment.
6+
* This helps prevent cutting off audio too abruptly.
7+
* @default 500
8+
*/
9+
padding?: number;
10+
} & SilenceDetectionOptions;

src/tracks/media/media.spec.ts

-96
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,6 @@ import { Composition } from '../../composition';
1010
import { MediaClip } from '../../clips';
1111
import { Timestamp } from '../../models';
1212
import { MediaTrack } from './media';
13-
import { AudioSource } from '../../sources';
14-
15-
class MockMediaClip extends MediaClip {
16-
constructor(range: [Timestamp, Timestamp], silences: { start: Timestamp, stop: Timestamp }[], element: HTMLMediaElement) {
17-
super();
18-
this.duration.millis = range[1].millis - range[0].millis;
19-
this.range = range;
20-
this.source = {
21-
silences: async () => silences,
22-
} as any as AudioSource;
23-
this.element = element;
24-
}
25-
}
2613

2714
describe('The Media Track Object', () => {
2815
let comp: Composition;
@@ -36,89 +23,6 @@ describe('The Media Track Object', () => {
3623
track.on('update', updateMock);
3724
});
3825

39-
it('ignores no silences', async () => {
40-
const clip = new MockMediaClip([new Timestamp(10000), new Timestamp(20000)], [], new Audio());
41-
clip.duration.frames = 30;
42-
await track.add(clip);
43-
await track.removeSilences();
44-
expect(track.clips.length).toBe(1);
45-
});
46-
47-
it('ignores not applicable silences', async () => {
48-
const clip = new MockMediaClip([new Timestamp(10000), new Timestamp(20000)], [
49-
{
50-
start: new Timestamp(0),
51-
stop: new Timestamp(500),
52-
},
53-
{
54-
start: new Timestamp(30000),
55-
stop: new Timestamp(30500),
56-
},
57-
], new Audio());
58-
await track.add(clip);
59-
expect(clip.source).toBeDefined();
60-
await track.removeSilences();
61-
expect(track.clips.length).toBe(1);
62-
expect(track.clips.at(0)).toBe(clip);
63-
expect(track.clips.at(0)?.state).toBe('ATTACHED');
64-
});
65-
66-
it('removes silences', async () => {
67-
const clip = new MockMediaClip([new Timestamp(10000), new Timestamp(20000)], [
68-
{
69-
start: new Timestamp(0),
70-
stop: new Timestamp(10050),
71-
},
72-
{
73-
start: new Timestamp(11000),
74-
stop: new Timestamp(15000),
75-
},
76-
{
77-
start: new Timestamp(19000),
78-
stop: new Timestamp(30500),
79-
},
80-
], new Audio());
81-
await track.add(clip);
82-
expect(clip.source).toBeDefined();
83-
await track.removeSilences();
84-
expect(track.clips.length).toBe(2);
85-
expect(track.clips.at(0)?.range[0].millis).toBe(10050);
86-
expect(track.clips.at(0)?.range[1].millis).toBe(11000);
87-
expect(track.clips.at(1)?.range[0].millis).toBe(15000);
88-
expect(track.clips.at(1)?.range[1].millis).toBe(19000);
89-
expect(track.clips.at(0)?.state).toBe('ATTACHED');
90-
expect(track.clips.at(1)?.state).toBe('ATTACHED');
91-
});
92-
93-
it('removes silences twice', async () => {
94-
const clip = new MockMediaClip([new Timestamp(10000), new Timestamp(20000)], [
95-
{
96-
start: new Timestamp(0),
97-
stop: new Timestamp(500),
98-
},
99-
{
100-
start: new Timestamp(11000),
101-
stop: new Timestamp(15000),
102-
},
103-
{
104-
start: new Timestamp(19000),
105-
stop: new Timestamp(30500),
106-
},
107-
], new Audio());
108-
await track.add(clip);
109-
expect(clip.source).toBeDefined();
110-
await track.removeSilences();
111-
await track.removeSilences();
112-
expect(track.clips.length).toBe(2);
113-
expect(track.clips.at(0)?.id).not.toBe(clip.id);
114-
expect(track.clips.at(0)?.range[0].millis).toBe(10000);
115-
expect(track.clips.at(0)?.range[1].millis).toBe(11000);
116-
expect(track.clips.at(1)?.range[0].millis).toBe(15000);
117-
expect(track.clips.at(1)?.range[1].millis).toBe(19000);
118-
expect(track.clips.at(0)?.state).toBe('ATTACHED');
119-
expect(track.clips.at(1)?.state).toBe('ATTACHED');
120-
});
121-
12226
it('should propagate a seek call', async () => {
12327
const clip = new MediaClip();
12428
clip.element = new Audio();

src/tracks/media/media.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77

88
import { Track } from '../track';
99
import { Timestamp } from '../../models';
10-
import { SilenceDetectionOptions } from '../../sources';
1110

12-
import type { MediaClip } from '../../clips';
11+
import type { MediaClip, SilenceRemoveOptions } from '../../clips';
1312

1413
export class MediaTrack<Clip extends MediaClip> extends Track<MediaClip> {
1514
public clips: Clip[] = [];
@@ -24,7 +23,7 @@ export class MediaTrack<Clip extends MediaClip> extends Track<MediaClip> {
2423
*
2524
* @param options - Options for silence detection
2625
*/
27-
public async removeSilences(options: SilenceDetectionOptions = {}) {
26+
public async removeSilences(options: SilenceRemoveOptions = {}) {
2827
const clips: MediaClip[] = [];
2928

3029
for (const clip of this.clips.map((clip) => clip.detach())) {

0 commit comments

Comments
 (0)