@tensamin/audio 0.1.1 → 0.1.2

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.
Files changed (56) hide show
  1. package/README.md +24 -2
  2. package/dist/chunk-FS635GMR.mjs +47 -0
  3. package/dist/chunk-HFSKQ33X.mjs +38 -0
  4. package/{src/vad/vad-state.ts → dist/chunk-JJASCVEW.mjs} +21 -33
  5. package/dist/chunk-OZ7KMC4S.mjs +46 -0
  6. package/dist/chunk-QU7E5HBA.mjs +106 -0
  7. package/dist/chunk-SDTOKWM2.mjs +39 -0
  8. package/{src/vad/vad-node.ts → dist/chunk-UMU2KIB6.mjs} +10 -20
  9. package/dist/chunk-WBQAMGXK.mjs +0 -0
  10. package/dist/context/audio-context.d.mts +32 -0
  11. package/dist/context/audio-context.d.ts +32 -0
  12. package/dist/context/audio-context.js +75 -0
  13. package/dist/context/audio-context.mjs +16 -0
  14. package/dist/extensibility/plugins.d.mts +9 -0
  15. package/dist/extensibility/plugins.d.ts +9 -0
  16. package/dist/extensibility/plugins.js +180 -0
  17. package/dist/extensibility/plugins.mjs +14 -0
  18. package/dist/index.d.mts +10 -216
  19. package/dist/index.d.ts +10 -216
  20. package/dist/index.js +11 -10
  21. package/dist/index.mjs +29 -352
  22. package/dist/livekit/integration.d.mts +11 -0
  23. package/dist/livekit/integration.d.ts +11 -0
  24. package/dist/livekit/integration.js +368 -0
  25. package/dist/livekit/integration.mjs +12 -0
  26. package/dist/noise-suppression/rnnoise-node.d.mts +10 -0
  27. package/dist/noise-suppression/rnnoise-node.d.ts +10 -0
  28. package/dist/noise-suppression/rnnoise-node.js +73 -0
  29. package/dist/noise-suppression/rnnoise-node.mjs +6 -0
  30. package/dist/pipeline/audio-pipeline.d.mts +6 -0
  31. package/dist/pipeline/audio-pipeline.d.ts +6 -0
  32. package/dist/pipeline/audio-pipeline.js +335 -0
  33. package/dist/pipeline/audio-pipeline.mjs +11 -0
  34. package/dist/types.d.mts +155 -0
  35. package/dist/types.d.ts +155 -0
  36. package/dist/types.js +18 -0
  37. package/dist/types.mjs +1 -0
  38. package/dist/vad/vad-node.d.mts +9 -0
  39. package/dist/vad/vad-node.d.ts +9 -0
  40. package/dist/vad/vad-node.js +92 -0
  41. package/dist/vad/vad-node.mjs +6 -0
  42. package/dist/vad/vad-state.d.mts +15 -0
  43. package/dist/vad/vad-state.d.ts +15 -0
  44. package/dist/vad/vad-state.js +83 -0
  45. package/dist/vad/vad-state.mjs +6 -0
  46. package/package.json +8 -5
  47. package/.github/workflows/publish.yml +0 -29
  48. package/bun.lock +0 -258
  49. package/src/context/audio-context.ts +0 -69
  50. package/src/extensibility/plugins.ts +0 -45
  51. package/src/index.ts +0 -8
  52. package/src/livekit/integration.ts +0 -61
  53. package/src/noise-suppression/rnnoise-node.ts +0 -62
  54. package/src/pipeline/audio-pipeline.ts +0 -154
  55. package/src/types.ts +0 -167
  56. package/tsconfig.json +0 -46
@@ -1,45 +0,0 @@
1
- import type { NoiseSuppressionPlugin, VADPlugin } from "../types.js";
2
- import { RNNoisePlugin } from "../noise-suppression/rnnoise-node.js";
3
- import { EnergyVADPlugin } from "../vad/vad-node.js";
4
-
5
- const nsPlugins = new Map<string, NoiseSuppressionPlugin>();
6
- const vadPlugins = new Map<string, VADPlugin>();
7
-
8
- // Register defaults
9
- const defaultNs = new RNNoisePlugin();
10
- nsPlugins.set(defaultNs.name, defaultNs);
11
-
12
- const defaultVad = new EnergyVADPlugin();
13
- vadPlugins.set(defaultVad.name, defaultVad);
14
-
15
- export function registerNoiseSuppressionPlugin(plugin: NoiseSuppressionPlugin) {
16
- nsPlugins.set(plugin.name, plugin);
17
- }
18
-
19
- export function registerVADPlugin(plugin: VADPlugin) {
20
- vadPlugins.set(plugin.name, plugin);
21
- }
22
-
23
- export function getNoiseSuppressionPlugin(
24
- name?: string,
25
- ): NoiseSuppressionPlugin {
26
- if (!name) return defaultNs;
27
- const plugin = nsPlugins.get(name);
28
- if (!plugin) {
29
- console.warn(
30
- `Noise suppression plugin '${name}' not found, falling back to default.`,
31
- );
32
- return defaultNs;
33
- }
34
- return plugin;
35
- }
36
-
37
- export function getVADPlugin(name?: string): VADPlugin {
38
- if (!name) return defaultVad;
39
- const plugin = vadPlugins.get(name);
40
- if (!plugin) {
41
- console.warn(`VAD plugin '${name}' not found, falling back to default.`);
42
- return defaultVad;
43
- }
44
- return plugin;
45
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from "./types.js";
2
- export * from "./context/audio-context.js";
3
- export * from "./pipeline/audio-pipeline.js";
4
- export * from "./livekit/integration.js";
5
- export * from "./extensibility/plugins.js";
6
- export * from "./noise-suppression/rnnoise-node.js";
7
- export * from "./vad/vad-node.js";
8
- export * from "./vad/vad-state.js";
@@ -1,61 +0,0 @@
1
- import type { LocalAudioTrack } from "livekit-client";
2
- import { createAudioPipeline } from "../pipeline/audio-pipeline.js";
3
- import type { AudioPipelineHandle, AudioProcessingConfig } from "../types.js";
4
-
5
- /**
6
- * Attaches the audio processing pipeline to a LiveKit LocalAudioTrack.
7
- * This replaces the underlying MediaStreamTrack with the processed one.
8
- */
9
- export async function attachProcessingToTrack(
10
- track: LocalAudioTrack,
11
- config: AudioProcessingConfig = {},
12
- ): Promise<AudioPipelineHandle> {
13
- // 1. Get the original track
14
- const originalTrack = track.mediaStreamTrack;
15
-
16
- // 2. Create pipeline
17
- const pipeline = await createAudioPipeline(originalTrack, config);
18
-
19
- // 3. Replace the track in LiveKit
20
- // Use replaceTrack which is the public API to swap the underlying MediaStreamTrack.
21
- await track.replaceTrack(pipeline.processedTrack);
22
-
23
- // 4. Handle intelligent muting if enabled
24
- if (config.livekit?.manageTrackMute) {
25
- let isVadMuted = false;
26
-
27
- pipeline.events.on("vadChange", async (state) => {
28
- if (state.isSpeaking) {
29
- if (isVadMuted) {
30
- // Only unmute if we were the ones who muted it
31
- // And check if the track is not globally muted by user?
32
- // This is tricky. If user muted manually, track.isMuted is true.
33
- // We should probably check a separate flag or assume VAD overrides only when "active".
34
- // For safety, we only unmute if we muted.
35
- await track.unmute();
36
- isVadMuted = false;
37
- }
38
- } else {
39
- // Silence
40
- if (!track.isMuted) {
41
- await track.mute();
42
- isVadMuted = true;
43
- }
44
- }
45
- });
46
- }
47
-
48
- // 5. Handle cleanup
49
- const originalDispose = pipeline.dispose;
50
- pipeline.dispose = () => {
51
- // Restore original track?
52
- // Or just stop.
53
- // If we dispose, we should probably try to restore the original track if it's still alive.
54
- if (originalTrack.readyState === "live") {
55
- track.replaceTrack(originalTrack).catch(console.error);
56
- }
57
- originalDispose();
58
- };
59
-
60
- return pipeline;
61
- }
@@ -1,62 +0,0 @@
1
- import {
2
- RnnoiseWorkletNode,
3
- loadRnnoise,
4
- } from "@sapphi-red/web-noise-suppressor";
5
- import type {
6
- AudioProcessingConfig,
7
- NoiseSuppressionPlugin,
8
- } from "../types.js";
9
-
10
- // Default URLs (can be overridden by config)
11
- // These defaults assume the assets are served from the same origin or a known CDN.
12
- // In a real package, we might want to bundle them or require the user to provide them.
13
- const DEFAULT_WASM_URL =
14
- "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/rnnoise.wasm";
15
- const DEFAULT_SIMD_WASM_URL =
16
- "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/rnnoise_simd.wasm";
17
- const DEFAULT_WORKLET_URL =
18
- "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/noise-suppressor-worklet.min.js";
19
-
20
- export class RNNoisePlugin implements NoiseSuppressionPlugin {
21
- name = "rnnoise-ns";
22
- private wasmBuffer: ArrayBuffer | null = null;
23
-
24
- async createNode(
25
- context: AudioContext,
26
- config: AudioProcessingConfig["noiseSuppression"],
27
- ): Promise<AudioNode> {
28
- if (!config?.enabled) {
29
- // Return a passthrough gain node if disabled but requested (though pipeline usually handles this)
30
- const pass = context.createGain();
31
- return pass;
32
- }
33
-
34
- // 1. Load WASM if not loaded
35
- // We use the library's loader which handles SIMD detection if we provide both URLs.
36
- // But wait, loadRnnoise returns ArrayBuffer.
37
- if (!this.wasmBuffer) {
38
- this.wasmBuffer = await loadRnnoise({
39
- url: config.wasmUrl || DEFAULT_WASM_URL,
40
- simdUrl: DEFAULT_SIMD_WASM_URL, // We should probably allow config for this too, but for now default is fine.
41
- });
42
- }
43
-
44
- // 2. Load Worklet
45
- const workletUrl = config.workletUrl || DEFAULT_WORKLET_URL;
46
-
47
- try {
48
- await context.audioWorklet.addModule(workletUrl);
49
- } catch (e) {
50
- console.warn("Failed to add RNNoise worklet module:", e);
51
- // Proceeding, assuming it might be already loaded.
52
- }
53
-
54
- // 3. Create Node
55
- const node = new RnnoiseWorkletNode(context, {
56
- wasmBinary: this.wasmBuffer,
57
- maxChannels: 1, // Mono for now
58
- });
59
-
60
- return node;
61
- }
62
- }
@@ -1,154 +0,0 @@
1
- import mitt from "mitt";
2
- import {
3
- getAudioContext,
4
- registerPipeline,
5
- unregisterPipeline,
6
- } from "../context/audio-context.js";
7
- import {
8
- getNoiseSuppressionPlugin,
9
- getVADPlugin,
10
- } from "../extensibility/plugins.js";
11
- import { VADStateMachine } from "../vad/vad-state.js";
12
- import type {
13
- AudioPipelineEvents,
14
- AudioPipelineHandle,
15
- AudioProcessingConfig,
16
- VADState,
17
- } from "../types.js";
18
-
19
- export async function createAudioPipeline(
20
- sourceTrack: MediaStreamTrack,
21
- config: AudioProcessingConfig = {},
22
- ): Promise<AudioPipelineHandle> {
23
- const context = getAudioContext();
24
- registerPipeline();
25
-
26
- // Defaults
27
- const fullConfig: AudioProcessingConfig = {
28
- noiseSuppression: { enabled: true, ...config.noiseSuppression },
29
- vad: { enabled: true, ...config.vad },
30
- output: {
31
- speechGain: 1.0,
32
- silenceGain: 0.0,
33
- gainRampTime: 0.02,
34
- ...config.output,
35
- },
36
- livekit: { manageTrackMute: false, ...config.livekit },
37
- };
38
-
39
- // 1. Source
40
- const sourceStream = new MediaStream([sourceTrack]);
41
- const sourceNode = context.createMediaStreamSource(sourceStream);
42
-
43
- // 2. Noise Suppression
44
- const nsPlugin = getNoiseSuppressionPlugin(
45
- fullConfig.noiseSuppression?.pluginName,
46
- );
47
- const nsNode = await nsPlugin.createNode(
48
- context,
49
- fullConfig.noiseSuppression,
50
- );
51
-
52
- // 3. VAD
53
- const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
54
- const vadStateMachine = new VADStateMachine(fullConfig.vad);
55
- const emitter = mitt<AudioPipelineEvents>();
56
-
57
- const vadNode = await vadPlugin.createNode(
58
- context,
59
- fullConfig.vad,
60
- (prob) => {
61
- const timestamp = context.currentTime * 1000;
62
- const newState = vadStateMachine.processFrame(prob, timestamp);
63
-
64
- // Emit if state changed or periodically?
65
- // For now, emit on change.
66
- if (
67
- newState.state !== lastVadState.state ||
68
- Math.abs(newState.probability - lastVadState.probability) > 0.1
69
- ) {
70
- emitter.emit("vadChange", newState);
71
- lastVadState = newState;
72
- updateGain(newState);
73
- }
74
- },
75
- );
76
-
77
- let lastVadState: VADState = {
78
- isSpeaking: false,
79
- probability: 0,
80
- state: "silent",
81
- };
82
-
83
- // 4. Pipeline Wiring
84
- // Source -> NS -> Splitter
85
- // Splitter -> VAD
86
- // Splitter -> Delay -> Gain -> Destination
87
-
88
- const splitter = context.createGain(); // Using Gain as splitter (fan-out)
89
-
90
- sourceNode.connect(nsNode);
91
- nsNode.connect(splitter);
92
-
93
- // Path 1: VAD
94
- splitter.connect(vadNode);
95
- // vadNode usually doesn't output audio, or we don't connect it to destination.
96
-
97
- // Path 2: Audio Output
98
- const delayNode = context.createDelay(1.0); // Max 1 sec
99
- const preRollSeconds = (fullConfig.vad?.preRollMs ?? 200) / 1000;
100
- delayNode.delayTime.value = preRollSeconds;
101
-
102
- const gainNode = context.createGain();
103
- gainNode.gain.value = fullConfig.output?.silenceGain ?? 0;
104
-
105
- const destination = context.createMediaStreamDestination();
106
-
107
- splitter.connect(delayNode);
108
- delayNode.connect(gainNode);
109
- gainNode.connect(destination);
110
-
111
- // Helper to update gain
112
- function updateGain(state: VADState) {
113
- const { speechGain, silenceGain, gainRampTime } = fullConfig.output!;
114
- const targetGain = state.isSpeaking
115
- ? (speechGain ?? 1.0)
116
- : (silenceGain ?? 0.0);
117
-
118
- // Ramp to target
119
- const now = context.currentTime;
120
- gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
121
- }
122
-
123
- // Handle disposal
124
- function dispose() {
125
- sourceNode.disconnect();
126
- nsNode.disconnect();
127
- splitter.disconnect();
128
- vadNode.disconnect();
129
- delayNode.disconnect();
130
- gainNode.disconnect();
131
-
132
- // Stop tracks? No, we don't own the source track.
133
- // But we own the destination track.
134
- destination.stream.getTracks().forEach((t) => t.stop());
135
-
136
- unregisterPipeline();
137
- }
138
-
139
- return {
140
- processedTrack: destination.stream.getAudioTracks()[0]!,
141
- events: emitter,
142
- get state() {
143
- return lastVadState;
144
- },
145
- setConfig: (newConfig) => {
146
- // TODO: Implement runtime config updates
147
- // For now, just update the VAD state machine config
148
- if (newConfig.vad) {
149
- vadStateMachine.updateConfig(newConfig.vad);
150
- }
151
- },
152
- dispose,
153
- };
154
- }
package/src/types.ts DELETED
@@ -1,167 +0,0 @@
1
- import type { LocalAudioTrack, TrackPublication } from "livekit-client";
2
- import type { Emitter } from "mitt";
3
-
4
- /**
5
- * Configuration for the audio processing pipeline.
6
- */
7
- export interface AudioProcessingConfig {
8
- /**
9
- * Noise suppression configuration.
10
- */
11
- noiseSuppression?: {
12
- enabled: boolean;
13
- /**
14
- * Path or URL to the RNNoise WASM binary.
15
- * If not provided, the default from @sapphi-red/web-noise-suppressor will be used (if bundler supports it).
16
- */
17
- wasmUrl?: string;
18
- /**
19
- * Path or URL to the RNNoise worklet script.
20
- */
21
- workletUrl?: string;
22
- /**
23
- * Plugin name to use. Defaults to 'rnnoise-ns'.
24
- */
25
- pluginName?: string;
26
- };
27
-
28
- /**
29
- * Voice Activity Detection (VAD) configuration.
30
- */
31
- vad?: {
32
- enabled: boolean;
33
- /**
34
- * Plugin name to use. Defaults to 'rnnoise-vad' or 'energy-vad'.
35
- */
36
- pluginName?: string;
37
- /**
38
- * Probability threshold for speech onset (0-1).
39
- * Default: 0.5
40
- */
41
- startThreshold?: number;
42
- /**
43
- * Probability threshold for speech offset (0-1).
44
- * Default: 0.4
45
- */
46
- stopThreshold?: number;
47
- /**
48
- * Time in ms to wait after speech stops before considering it silent.
49
- * Default: 300ms
50
- */
51
- hangoverMs?: number;
52
- /**
53
- * Time in ms of audio to buffer before speech onset to avoid cutting the start.
54
- * Default: 200ms
55
- */
56
- preRollMs?: number;
57
- };
58
-
59
- /**
60
- * Output gain and muting configuration.
61
- */
62
- output?: {
63
- /**
64
- * Gain to apply when speaking (0-1+). Default: 1.0
65
- */
66
- speechGain?: number;
67
- /**
68
- * Gain to apply when silent (0-1). Default: 0.0 (mute)
69
- */
70
- silenceGain?: number;
71
- /**
72
- * Time in seconds to ramp gain changes. Default: 0.02
73
- */
74
- gainRampTime?: number;
75
- };
76
-
77
- /**
78
- * LiveKit integration configuration.
79
- */
80
- livekit?: {
81
- /**
82
- * Whether to call track.mute()/unmute() on the LocalAudioTrack based on VAD.
83
- * This saves bandwidth but has more signaling overhead.
84
- * Default: false (uses gain gating only)
85
- */
86
- manageTrackMute?: boolean;
87
- };
88
- }
89
-
90
- /**
91
- * Represents the state of Voice Activity Detection.
92
- */
93
- export interface VADState {
94
- /**
95
- * Whether speech is currently detected (after hysteresis).
96
- */
97
- isSpeaking: boolean;
98
- /**
99
- * Raw probability of speech from the VAD model (0-1).
100
- */
101
- probability: number;
102
- /**
103
- * Current state enum.
104
- */
105
- state: "silent" | "speech_starting" | "speaking" | "speech_ending";
106
- }
107
-
108
- /**
109
- * Events emitted by the audio pipeline.
110
- */
111
- export type AudioPipelineEvents = {
112
- vadChange: VADState;
113
- error: Error;
114
- };
115
-
116
- /**
117
- * Handle to a running audio processing pipeline.
118
- */
119
- export interface AudioPipelineHandle {
120
- /**
121
- * The processed MediaStreamTrack.
122
- */
123
- readonly processedTrack: MediaStreamTrack;
124
-
125
- /**
126
- * Event emitter for VAD state and errors.
127
- */
128
- readonly events: Emitter<AudioPipelineEvents>;
129
-
130
- /**
131
- * Current VAD state.
132
- */
133
- readonly state: VADState;
134
-
135
- /**
136
- * Update configuration at runtime.
137
- */
138
- setConfig(config: Partial<AudioProcessingConfig>): void;
139
-
140
- /**
141
- * Stop processing and release resources.
142
- */
143
- dispose(): void;
144
- }
145
-
146
- /**
147
- * Interface for a Noise Suppression Plugin.
148
- */
149
- export interface NoiseSuppressionPlugin {
150
- name: string;
151
- createNode(
152
- context: AudioContext,
153
- config: AudioProcessingConfig["noiseSuppression"],
154
- ): Promise<AudioNode>;
155
- }
156
-
157
- /**
158
- * Interface for a VAD Plugin.
159
- */
160
- export interface VADPlugin {
161
- name: string;
162
- createNode(
163
- context: AudioContext,
164
- config: AudioProcessingConfig["vad"],
165
- onDecision: (probability: number) => void,
166
- ): Promise<AudioNode>;
167
- }
package/tsconfig.json DELETED
@@ -1,46 +0,0 @@
1
- {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
- "compilerOptions": {
4
- // File Layout
5
- "rootDir": "./src",
6
- "outDir": "./dist",
7
-
8
- // Environment Settings
9
- // See also https://aka.ms/tsconfig/module
10
- "moduleResolution": "bundler",
11
- "module": "esnext",
12
- "target": "esnext",
13
- "types": [],
14
- // For nodejs:
15
- // "lib": ["esnext"],
16
- // "types": ["node"],
17
- // and npm install -D @types/node
18
-
19
- // Other Outputs
20
- "sourceMap": true,
21
- "declaration": true,
22
- "declarationMap": true,
23
-
24
- // Stricter Typechecking Options
25
- "noUncheckedIndexedAccess": true,
26
- "exactOptionalPropertyTypes": true,
27
-
28
- // Style Options
29
- // "noImplicitReturns": true,
30
- // "noImplicitOverride": true,
31
- // "noUnusedLocals": true,
32
- // "noUnusedParameters": true,
33
- // "noFallthroughCasesInSwitch": true,
34
- // "noPropertyAccessFromIndexSignature": true,
35
-
36
- // Recommended Options
37
- "strict": true,
38
- "jsx": "react-jsx",
39
- "verbatimModuleSyntax": true,
40
- "isolatedModules": true,
41
- "noUncheckedSideEffectImports": true,
42
- "moduleDetection": "force",
43
- "skipLibCheck": true,
44
- "noEmit": true
45
- }
46
- }