@mcptoolshop/claude-sfx 0.1.0
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/LICENSE +21 -0
- package/README.es.md +188 -0
- package/README.fr.md +188 -0
- package/README.hi.md +188 -0
- package/README.it.md +188 -0
- package/README.ja.md +188 -0
- package/README.md +188 -0
- package/README.pt-BR.md +188 -0
- package/README.zh.md +188 -0
- package/assets/logo.jpg +0 -0
- package/dist/ambient.d.ts +28 -0
- package/dist/ambient.js +179 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +594 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +104 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.js +87 -0
- package/dist/hook-handler.d.ts +34 -0
- package/dist/hook-handler.js +200 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +192 -0
- package/dist/player.d.ts +16 -0
- package/dist/player.js +109 -0
- package/dist/profiles.d.ts +103 -0
- package/dist/profiles.js +297 -0
- package/dist/synth.d.ts +72 -0
- package/dist/synth.js +254 -0
- package/dist/verbs.d.ts +25 -0
- package/dist/verbs.js +251 -0
- package/package.json +54 -0
- package/profiles/minimal.json +202 -0
- package/profiles/retro.json +200 -0
package/dist/player.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform audio playback.
|
|
3
|
+
* Writes a temp .wav file, plays it with the OS native player, cleans up.
|
|
4
|
+
* Zero dependencies.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { execSync, spawn } from "node:child_process";
|
|
10
|
+
import { encodeWav } from "./synth.js";
|
|
11
|
+
const TEMP_DIR = join(tmpdir(), "claude-sfx");
|
|
12
|
+
function ensureTempDir() {
|
|
13
|
+
if (!existsSync(TEMP_DIR)) {
|
|
14
|
+
mkdirSync(TEMP_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getTempPath() {
|
|
18
|
+
ensureTempDir();
|
|
19
|
+
return join(TEMP_DIR, `sfx-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.wav`);
|
|
20
|
+
}
|
|
21
|
+
/** Detect OS and return the appropriate play command. */
|
|
22
|
+
function getPlayCommand(filePath) {
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
// PowerShell SoundPlayer — synchronous, reliable, built-in
|
|
26
|
+
return {
|
|
27
|
+
command: "powershell",
|
|
28
|
+
args: [
|
|
29
|
+
"-NoProfile",
|
|
30
|
+
"-Command",
|
|
31
|
+
`(New-Object Media.SoundPlayer '${filePath}').PlaySync()`,
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (platform === "darwin") {
|
|
36
|
+
return { command: "afplay", args: [filePath] };
|
|
37
|
+
}
|
|
38
|
+
// Linux: try paplay (PulseAudio), then aplay (ALSA)
|
|
39
|
+
try {
|
|
40
|
+
execSync("which paplay", { stdio: "ignore" });
|
|
41
|
+
return { command: "paplay", args: [filePath] };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
try {
|
|
45
|
+
execSync("which aplay", { stdio: "ignore" });
|
|
46
|
+
return { command: "aplay", args: ["-q", filePath] };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Play a PCM audio buffer through the system speakers. Blocks until done. */
|
|
54
|
+
export function playSync(buffer) {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
const wavData = encodeWav(buffer);
|
|
57
|
+
const tempPath = getTempPath();
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(tempPath, wavData);
|
|
60
|
+
const cmd = getPlayCommand(tempPath);
|
|
61
|
+
if (!cmd) {
|
|
62
|
+
return { played: false, method: "none", durationMs: 0 };
|
|
63
|
+
}
|
|
64
|
+
execSync(`${cmd.command} ${cmd.args.map((a) => `"${a}"`).join(" ")}`, {
|
|
65
|
+
stdio: "ignore",
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
played: true,
|
|
70
|
+
method: cmd.command,
|
|
71
|
+
durationMs: Date.now() - start,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(tempPath);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Temp file cleanup is best-effort
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Play a PCM audio buffer asynchronously (fire and forget). */
|
|
84
|
+
export function playAsync(buffer) {
|
|
85
|
+
const wavData = encodeWav(buffer);
|
|
86
|
+
const tempPath = getTempPath();
|
|
87
|
+
writeFileSync(tempPath, wavData);
|
|
88
|
+
const cmd = getPlayCommand(tempPath);
|
|
89
|
+
if (!cmd)
|
|
90
|
+
return;
|
|
91
|
+
const child = spawn(cmd.command, cmd.args, {
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
detached: true,
|
|
94
|
+
});
|
|
95
|
+
child.on("exit", () => {
|
|
96
|
+
try {
|
|
97
|
+
unlinkSync(tempPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// best-effort
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
child.unref();
|
|
104
|
+
}
|
|
105
|
+
/** Write a WAV file to disk (for export / debugging). */
|
|
106
|
+
export function saveWav(buffer, outputPath) {
|
|
107
|
+
const wavData = encodeWav(buffer);
|
|
108
|
+
writeFileSync(outputPath, wavData);
|
|
109
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile system — configurable sound palettes.
|
|
3
|
+
* Each profile defines synthesis parameters for every verb + session sounds.
|
|
4
|
+
* Profiles are JSON-serializable, so users can create their own.
|
|
5
|
+
*/
|
|
6
|
+
import type { Waveform, Envelope } from "./synth.js";
|
|
7
|
+
/** Synthesis params for a tone-based verb (intake, transform, commit, navigate, execute). */
|
|
8
|
+
export interface ToneVerbConfig {
|
|
9
|
+
type: "tone";
|
|
10
|
+
waveform: Waveform;
|
|
11
|
+
frequency: number;
|
|
12
|
+
frequencyEnd?: number;
|
|
13
|
+
duration: number;
|
|
14
|
+
envelope: Envelope;
|
|
15
|
+
gain: number;
|
|
16
|
+
fmRatio?: number;
|
|
17
|
+
fmDepth?: number;
|
|
18
|
+
harmonicGain?: number;
|
|
19
|
+
/** If true, mix a short noise burst underneath (execute verb). */
|
|
20
|
+
noiseBurst?: {
|
|
21
|
+
duration: number;
|
|
22
|
+
gain: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Synthesis params for a whoosh-based verb (move, sync). */
|
|
26
|
+
export interface WhooshVerbConfig {
|
|
27
|
+
type: "whoosh";
|
|
28
|
+
duration: number;
|
|
29
|
+
/** Freq pair for direction=up. [start, end] */
|
|
30
|
+
freqUp: [number, number];
|
|
31
|
+
/** Freq pair for direction=down. [start, end] */
|
|
32
|
+
freqDown: [number, number];
|
|
33
|
+
bandwidth: number;
|
|
34
|
+
envelope: Envelope;
|
|
35
|
+
gain: number;
|
|
36
|
+
/** Optional tonal anchor mixed underneath (sync verb). */
|
|
37
|
+
tonalAnchor?: {
|
|
38
|
+
freqUp: [number, number];
|
|
39
|
+
freqDown: [number, number];
|
|
40
|
+
envelope: Envelope;
|
|
41
|
+
gain: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export type VerbConfig = ToneVerbConfig | WhooshVerbConfig;
|
|
45
|
+
/** A two-tone chime (session start/end). */
|
|
46
|
+
export interface ChimeConfig {
|
|
47
|
+
tone1: {
|
|
48
|
+
waveform: Waveform;
|
|
49
|
+
frequency: number;
|
|
50
|
+
duration: number;
|
|
51
|
+
envelope: Envelope;
|
|
52
|
+
gain: number;
|
|
53
|
+
harmonicGain?: number;
|
|
54
|
+
};
|
|
55
|
+
tone2: {
|
|
56
|
+
waveform: Waveform;
|
|
57
|
+
frequency: number;
|
|
58
|
+
duration: number;
|
|
59
|
+
envelope: Envelope;
|
|
60
|
+
gain: number;
|
|
61
|
+
harmonicGain?: number;
|
|
62
|
+
};
|
|
63
|
+
/** Offset in seconds before tone2 starts. */
|
|
64
|
+
staggerSeconds: number;
|
|
65
|
+
}
|
|
66
|
+
/** Long-running ambient drone config. */
|
|
67
|
+
export interface AmbientConfig {
|
|
68
|
+
/** Low drone frequency */
|
|
69
|
+
droneFreq: number;
|
|
70
|
+
droneWaveform: Waveform;
|
|
71
|
+
droneGain: number;
|
|
72
|
+
/** Seconds per loop chunk (the drone is generated in chunks). */
|
|
73
|
+
chunkDuration: number;
|
|
74
|
+
/** Resolution stinger: two ascending notes. */
|
|
75
|
+
resolveNote1: number;
|
|
76
|
+
resolveNote2: number;
|
|
77
|
+
resolveWaveform: Waveform;
|
|
78
|
+
resolveDuration: number;
|
|
79
|
+
resolveGain: number;
|
|
80
|
+
}
|
|
81
|
+
/** Complete profile definition. */
|
|
82
|
+
export interface Profile {
|
|
83
|
+
name: string;
|
|
84
|
+
description: string;
|
|
85
|
+
verbs: Record<string, VerbConfig>;
|
|
86
|
+
sessionStart: ChimeConfig;
|
|
87
|
+
sessionEnd: ChimeConfig;
|
|
88
|
+
ambient: AmbientConfig;
|
|
89
|
+
}
|
|
90
|
+
/** Get a built-in profile by name. */
|
|
91
|
+
export declare function getBuiltinProfile(name: string): Profile | undefined;
|
|
92
|
+
/** List all built-in profile names. */
|
|
93
|
+
export declare function listBuiltinProfiles(): string[];
|
|
94
|
+
/** Load a profile from a JSON file path. */
|
|
95
|
+
export declare function loadProfileFromFile(filePath: string): Profile;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a profile by name.
|
|
98
|
+
* 1. Check built-in profiles
|
|
99
|
+
* 2. Check profiles/ directory next to the package
|
|
100
|
+
* 3. Treat as absolute/relative file path
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveProfile(nameOrPath: string): Profile;
|
|
103
|
+
export declare const DEFAULT_PROFILE_NAME = "minimal";
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile system — configurable sound palettes.
|
|
3
|
+
* Each profile defines synthesis parameters for every verb + session sounds.
|
|
4
|
+
* Profiles are JSON-serializable, so users can create their own.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
// --- Built-in profiles ---
|
|
10
|
+
const MINIMAL_PROFILE = {
|
|
11
|
+
name: "minimal",
|
|
12
|
+
description: "Tasteful UI tones — subtle, professional, daily-driver",
|
|
13
|
+
verbs: {
|
|
14
|
+
intake: {
|
|
15
|
+
type: "tone",
|
|
16
|
+
waveform: "sine",
|
|
17
|
+
frequency: 440,
|
|
18
|
+
frequencyEnd: 620,
|
|
19
|
+
duration: 0.15,
|
|
20
|
+
envelope: { attack: 0.02, decay: 0.06, sustain: 0.4, release: 0.04 },
|
|
21
|
+
gain: 0.6,
|
|
22
|
+
},
|
|
23
|
+
transform: {
|
|
24
|
+
type: "tone",
|
|
25
|
+
waveform: "sine",
|
|
26
|
+
frequency: 520,
|
|
27
|
+
duration: 0.1,
|
|
28
|
+
envelope: { attack: 0.005, decay: 0.03, sustain: 0.2, release: 0.03 },
|
|
29
|
+
gain: 0.55,
|
|
30
|
+
fmRatio: 1.5,
|
|
31
|
+
fmDepth: 30,
|
|
32
|
+
},
|
|
33
|
+
commit: {
|
|
34
|
+
type: "tone",
|
|
35
|
+
waveform: "sine",
|
|
36
|
+
frequency: 840,
|
|
37
|
+
duration: 0.08,
|
|
38
|
+
envelope: { attack: 0.005, decay: 0.03, sustain: 0.2, release: 0.03 },
|
|
39
|
+
gain: 0.6,
|
|
40
|
+
},
|
|
41
|
+
navigate: {
|
|
42
|
+
type: "tone",
|
|
43
|
+
waveform: "sine",
|
|
44
|
+
frequency: 1050,
|
|
45
|
+
duration: 0.2,
|
|
46
|
+
envelope: { attack: 0.003, decay: 0.15, sustain: 0.0, release: 0.05 },
|
|
47
|
+
gain: 0.5,
|
|
48
|
+
},
|
|
49
|
+
execute: {
|
|
50
|
+
type: "tone",
|
|
51
|
+
waveform: "sine",
|
|
52
|
+
frequency: 620,
|
|
53
|
+
duration: 0.1,
|
|
54
|
+
envelope: { attack: 0.005, decay: 0.03, sustain: 0.2, release: 0.03 },
|
|
55
|
+
gain: 0.5,
|
|
56
|
+
noiseBurst: { duration: 0.04, gain: 0.25 },
|
|
57
|
+
},
|
|
58
|
+
move: {
|
|
59
|
+
type: "whoosh",
|
|
60
|
+
duration: 0.25,
|
|
61
|
+
freqUp: [600, 2800],
|
|
62
|
+
freqDown: [2800, 600],
|
|
63
|
+
bandwidth: 0.8,
|
|
64
|
+
envelope: { attack: 0.04, decay: 0.08, sustain: 0.5, release: 0.1 },
|
|
65
|
+
gain: 0.6,
|
|
66
|
+
},
|
|
67
|
+
sync: {
|
|
68
|
+
type: "whoosh",
|
|
69
|
+
duration: 0.32,
|
|
70
|
+
freqUp: [400, 3200],
|
|
71
|
+
freqDown: [3200, 400],
|
|
72
|
+
bandwidth: 0.6,
|
|
73
|
+
envelope: { attack: 0.05, decay: 0.1, sustain: 0.45, release: 0.12 },
|
|
74
|
+
gain: 0.55,
|
|
75
|
+
tonalAnchor: {
|
|
76
|
+
freqUp: [350, 700],
|
|
77
|
+
freqDown: [700, 350],
|
|
78
|
+
envelope: { attack: 0.03, decay: 0.1, sustain: 0.15, release: 0.08 },
|
|
79
|
+
gain: 0.2,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
sessionStart: {
|
|
84
|
+
tone1: {
|
|
85
|
+
waveform: "sine",
|
|
86
|
+
frequency: 523,
|
|
87
|
+
duration: 0.18,
|
|
88
|
+
envelope: { attack: 0.01, decay: 0.08, sustain: 0.3, release: 0.06 },
|
|
89
|
+
gain: 0.5,
|
|
90
|
+
harmonicGain: 0.1,
|
|
91
|
+
},
|
|
92
|
+
tone2: {
|
|
93
|
+
waveform: "sine",
|
|
94
|
+
frequency: 659,
|
|
95
|
+
duration: 0.22,
|
|
96
|
+
envelope: { attack: 0.01, decay: 0.1, sustain: 0.3, release: 0.08 },
|
|
97
|
+
gain: 0.45,
|
|
98
|
+
harmonicGain: 0.1,
|
|
99
|
+
},
|
|
100
|
+
staggerSeconds: 0.07,
|
|
101
|
+
},
|
|
102
|
+
sessionEnd: {
|
|
103
|
+
tone1: {
|
|
104
|
+
waveform: "sine",
|
|
105
|
+
frequency: 659,
|
|
106
|
+
duration: 0.15,
|
|
107
|
+
envelope: { attack: 0.01, decay: 0.08, sustain: 0.2, release: 0.04 },
|
|
108
|
+
gain: 0.45,
|
|
109
|
+
},
|
|
110
|
+
tone2: {
|
|
111
|
+
waveform: "sine",
|
|
112
|
+
frequency: 523,
|
|
113
|
+
duration: 0.25,
|
|
114
|
+
envelope: { attack: 0.01, decay: 0.12, sustain: 0.2, release: 0.1 },
|
|
115
|
+
gain: 0.4,
|
|
116
|
+
},
|
|
117
|
+
staggerSeconds: 0.08,
|
|
118
|
+
},
|
|
119
|
+
ambient: {
|
|
120
|
+
droneFreq: 150,
|
|
121
|
+
droneWaveform: "sine",
|
|
122
|
+
droneGain: 0.12,
|
|
123
|
+
chunkDuration: 2.0,
|
|
124
|
+
resolveNote1: 523,
|
|
125
|
+
resolveNote2: 659,
|
|
126
|
+
resolveWaveform: "sine",
|
|
127
|
+
resolveDuration: 0.15,
|
|
128
|
+
resolveGain: 0.45,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const RETRO_PROFILE = {
|
|
132
|
+
name: "retro",
|
|
133
|
+
description: "8-bit micro-chirps — fun but controlled",
|
|
134
|
+
verbs: {
|
|
135
|
+
intake: {
|
|
136
|
+
type: "tone",
|
|
137
|
+
waveform: "square",
|
|
138
|
+
frequency: 480,
|
|
139
|
+
frequencyEnd: 720,
|
|
140
|
+
duration: 0.1,
|
|
141
|
+
envelope: { attack: 0.005, decay: 0.04, sustain: 0.3, release: 0.02 },
|
|
142
|
+
gain: 0.4,
|
|
143
|
+
},
|
|
144
|
+
transform: {
|
|
145
|
+
type: "tone",
|
|
146
|
+
waveform: "square",
|
|
147
|
+
frequency: 560,
|
|
148
|
+
duration: 0.07,
|
|
149
|
+
envelope: { attack: 0.003, decay: 0.03, sustain: 0.15, release: 0.02 },
|
|
150
|
+
gain: 0.38,
|
|
151
|
+
fmRatio: 2,
|
|
152
|
+
fmDepth: 50,
|
|
153
|
+
},
|
|
154
|
+
commit: {
|
|
155
|
+
type: "tone",
|
|
156
|
+
waveform: "square",
|
|
157
|
+
frequency: 880,
|
|
158
|
+
duration: 0.06,
|
|
159
|
+
envelope: { attack: 0.002, decay: 0.02, sustain: 0.1, release: 0.02 },
|
|
160
|
+
gain: 0.42,
|
|
161
|
+
},
|
|
162
|
+
navigate: {
|
|
163
|
+
type: "tone",
|
|
164
|
+
waveform: "square",
|
|
165
|
+
frequency: 1100,
|
|
166
|
+
duration: 0.12,
|
|
167
|
+
envelope: { attack: 0.002, decay: 0.08, sustain: 0.0, release: 0.03 },
|
|
168
|
+
gain: 0.38,
|
|
169
|
+
},
|
|
170
|
+
execute: {
|
|
171
|
+
type: "tone",
|
|
172
|
+
waveform: "square",
|
|
173
|
+
frequency: 660,
|
|
174
|
+
duration: 0.08,
|
|
175
|
+
envelope: { attack: 0.002, decay: 0.03, sustain: 0.15, release: 0.02 },
|
|
176
|
+
gain: 0.38,
|
|
177
|
+
noiseBurst: { duration: 0.03, gain: 0.3 },
|
|
178
|
+
},
|
|
179
|
+
move: {
|
|
180
|
+
type: "whoosh",
|
|
181
|
+
duration: 0.18,
|
|
182
|
+
freqUp: [800, 3500],
|
|
183
|
+
freqDown: [3500, 800],
|
|
184
|
+
bandwidth: 1.0,
|
|
185
|
+
envelope: { attack: 0.02, decay: 0.06, sustain: 0.4, release: 0.06 },
|
|
186
|
+
gain: 0.45,
|
|
187
|
+
},
|
|
188
|
+
sync: {
|
|
189
|
+
type: "whoosh",
|
|
190
|
+
duration: 0.22,
|
|
191
|
+
freqUp: [500, 4000],
|
|
192
|
+
freqDown: [4000, 500],
|
|
193
|
+
bandwidth: 0.8,
|
|
194
|
+
envelope: { attack: 0.03, decay: 0.06, sustain: 0.35, release: 0.08 },
|
|
195
|
+
gain: 0.42,
|
|
196
|
+
tonalAnchor: {
|
|
197
|
+
freqUp: [400, 800],
|
|
198
|
+
freqDown: [800, 400],
|
|
199
|
+
envelope: { attack: 0.02, decay: 0.06, sustain: 0.1, release: 0.05 },
|
|
200
|
+
gain: 0.18,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
sessionStart: {
|
|
205
|
+
tone1: {
|
|
206
|
+
waveform: "square",
|
|
207
|
+
frequency: 523,
|
|
208
|
+
duration: 0.1,
|
|
209
|
+
envelope: { attack: 0.005, decay: 0.04, sustain: 0.2, release: 0.03 },
|
|
210
|
+
gain: 0.4,
|
|
211
|
+
},
|
|
212
|
+
tone2: {
|
|
213
|
+
waveform: "square",
|
|
214
|
+
frequency: 659,
|
|
215
|
+
duration: 0.12,
|
|
216
|
+
envelope: { attack: 0.005, decay: 0.05, sustain: 0.2, release: 0.04 },
|
|
217
|
+
gain: 0.38,
|
|
218
|
+
},
|
|
219
|
+
staggerSeconds: 0.06,
|
|
220
|
+
},
|
|
221
|
+
sessionEnd: {
|
|
222
|
+
tone1: {
|
|
223
|
+
waveform: "square",
|
|
224
|
+
frequency: 659,
|
|
225
|
+
duration: 0.08,
|
|
226
|
+
envelope: { attack: 0.005, decay: 0.04, sustain: 0.15, release: 0.02 },
|
|
227
|
+
gain: 0.38,
|
|
228
|
+
},
|
|
229
|
+
tone2: {
|
|
230
|
+
waveform: "square",
|
|
231
|
+
frequency: 523,
|
|
232
|
+
duration: 0.14,
|
|
233
|
+
envelope: { attack: 0.005, decay: 0.06, sustain: 0.15, release: 0.05 },
|
|
234
|
+
gain: 0.35,
|
|
235
|
+
},
|
|
236
|
+
staggerSeconds: 0.06,
|
|
237
|
+
},
|
|
238
|
+
ambient: {
|
|
239
|
+
droneFreq: 120,
|
|
240
|
+
droneWaveform: "square",
|
|
241
|
+
droneGain: 0.08,
|
|
242
|
+
chunkDuration: 2.0,
|
|
243
|
+
resolveNote1: 523,
|
|
244
|
+
resolveNote2: 784,
|
|
245
|
+
resolveWaveform: "square",
|
|
246
|
+
resolveDuration: 0.1,
|
|
247
|
+
resolveGain: 0.35,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
const BUILTIN_PROFILES = {
|
|
251
|
+
minimal: MINIMAL_PROFILE,
|
|
252
|
+
retro: RETRO_PROFILE,
|
|
253
|
+
};
|
|
254
|
+
// --- Profile loading ---
|
|
255
|
+
/** Get a built-in profile by name. */
|
|
256
|
+
export function getBuiltinProfile(name) {
|
|
257
|
+
return BUILTIN_PROFILES[name];
|
|
258
|
+
}
|
|
259
|
+
/** List all built-in profile names. */
|
|
260
|
+
export function listBuiltinProfiles() {
|
|
261
|
+
return Object.keys(BUILTIN_PROFILES);
|
|
262
|
+
}
|
|
263
|
+
/** Load a profile from a JSON file path. */
|
|
264
|
+
export function loadProfileFromFile(filePath) {
|
|
265
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
266
|
+
const parsed = JSON.parse(raw);
|
|
267
|
+
// Basic validation
|
|
268
|
+
if (!parsed.name || !parsed.verbs || !parsed.sessionStart || !parsed.sessionEnd) {
|
|
269
|
+
throw new Error(`Invalid profile: missing required fields in ${filePath}`);
|
|
270
|
+
}
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a profile by name.
|
|
275
|
+
* 1. Check built-in profiles
|
|
276
|
+
* 2. Check profiles/ directory next to the package
|
|
277
|
+
* 3. Treat as absolute/relative file path
|
|
278
|
+
*/
|
|
279
|
+
export function resolveProfile(nameOrPath) {
|
|
280
|
+
// Built-in?
|
|
281
|
+
const builtin = getBuiltinProfile(nameOrPath);
|
|
282
|
+
if (builtin)
|
|
283
|
+
return builtin;
|
|
284
|
+
// Check profiles/ directory relative to this package
|
|
285
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
286
|
+
const profilesDir = join(__dirname, "..", "profiles");
|
|
287
|
+
const inProfilesDir = join(profilesDir, `${nameOrPath}.json`);
|
|
288
|
+
if (existsSync(inProfilesDir)) {
|
|
289
|
+
return loadProfileFromFile(inProfilesDir);
|
|
290
|
+
}
|
|
291
|
+
// Treat as file path
|
|
292
|
+
if (existsSync(nameOrPath)) {
|
|
293
|
+
return loadProfileFromFile(nameOrPath);
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`Unknown profile "${nameOrPath}". Built-in: ${listBuiltinProfiles().join(", ")}`);
|
|
296
|
+
}
|
|
297
|
+
export const DEFAULT_PROFILE_NAME = "minimal";
|
package/dist/synth.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure PCM synthesis engine. Zero dependencies.
|
|
3
|
+
* Generates audio from math — oscillators, envelopes, FM, and WAV encoding.
|
|
4
|
+
*/
|
|
5
|
+
declare const SAMPLE_RATE = 44100;
|
|
6
|
+
declare const BIT_DEPTH = 16;
|
|
7
|
+
declare const NUM_CHANNELS = 1;
|
|
8
|
+
export type Waveform = "sine" | "square" | "sawtooth" | "triangle" | "noise";
|
|
9
|
+
export interface Envelope {
|
|
10
|
+
attack: number;
|
|
11
|
+
decay: number;
|
|
12
|
+
sustain: number;
|
|
13
|
+
release: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ToneParams {
|
|
16
|
+
/** Base waveform */
|
|
17
|
+
waveform: Waveform;
|
|
18
|
+
/** Starting frequency in Hz */
|
|
19
|
+
frequency: number;
|
|
20
|
+
/** Ending frequency in Hz (for sweeps). If omitted, stays at `frequency`. */
|
|
21
|
+
frequencyEnd?: number;
|
|
22
|
+
/** Duration in seconds */
|
|
23
|
+
duration: number;
|
|
24
|
+
/** Amplitude envelope */
|
|
25
|
+
envelope: Envelope;
|
|
26
|
+
/** Master gain 0–1 */
|
|
27
|
+
gain: number;
|
|
28
|
+
/** FM modulator frequency ratio (relative to carrier). e.g., 2 = double the carrier freq */
|
|
29
|
+
fmRatio?: number;
|
|
30
|
+
/** FM modulation depth in Hz */
|
|
31
|
+
fmDepth?: number;
|
|
32
|
+
/** Tremolo rate in Hz (amplitude modulation) */
|
|
33
|
+
tremoloRate?: number;
|
|
34
|
+
/** Tremolo depth 0–1 (0 = no effect, 1 = full AM) */
|
|
35
|
+
tremoloDepth?: number;
|
|
36
|
+
/** Add an octave harmonic at this gain level (0–1) */
|
|
37
|
+
harmonicGain?: number;
|
|
38
|
+
/** Detune amount in Hz (creates a second oscillator for beating) */
|
|
39
|
+
detune?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Generate a Float64 audio buffer from tone parameters. */
|
|
42
|
+
export declare function generateTone(params: ToneParams): Float64Array;
|
|
43
|
+
/** Mix multiple tone buffers into one (summed and normalized). */
|
|
44
|
+
export declare function mixBuffers(buffers: Float64Array[]): Float64Array;
|
|
45
|
+
/** Concatenate buffers sequentially with an optional gap (in seconds). */
|
|
46
|
+
export declare function concatBuffers(buffers: Float64Array[], gapSeconds?: number): Float64Array;
|
|
47
|
+
/** Encode a Float64 audio buffer as a WAV file (PCM 16-bit mono). */
|
|
48
|
+
export declare function encodeWav(buffer: Float64Array): Buffer;
|
|
49
|
+
/** Apply a hard loudness cap with gentle compression to a buffer. */
|
|
50
|
+
export declare function limitLoudness(buffer: Float64Array, ceiling?: number): Float64Array;
|
|
51
|
+
/** Apply a volume gain (0.0–1.0) to a buffer. */
|
|
52
|
+
export declare function applyVolume(buffer: Float64Array, gain: number): Float64Array;
|
|
53
|
+
export interface WhooshParams {
|
|
54
|
+
/** Duration in seconds */
|
|
55
|
+
duration: number;
|
|
56
|
+
/** Starting center frequency of the bandpass in Hz */
|
|
57
|
+
freqStart: number;
|
|
58
|
+
/** Ending center frequency of the bandpass in Hz */
|
|
59
|
+
freqEnd: number;
|
|
60
|
+
/** Bandwidth (Q) — lower = wider/breathier, higher = narrower/whistly */
|
|
61
|
+
bandwidth: number;
|
|
62
|
+
/** Amplitude envelope */
|
|
63
|
+
envelope: Envelope;
|
|
64
|
+
/** Master gain 0–1 */
|
|
65
|
+
gain: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Generate a whoosh: bandpass-filtered noise with a sweeping center frequency.
|
|
69
|
+
* Uses a state-variable filter (SVF) for stable real-time coefficient changes.
|
|
70
|
+
*/
|
|
71
|
+
export declare function generateWhoosh(params: WhooshParams): Float64Array;
|
|
72
|
+
export { SAMPLE_RATE, BIT_DEPTH, NUM_CHANNELS };
|