klaudio 0.3.0 → 0.4.1
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/README.md +8 -6
- package/package.json +1 -1
- package/src/cache.js +1 -1
- package/src/cli.js +4 -4
- package/src/extractor.js +20 -10
- package/src/installer.js +7 -7
- package/src/player.js +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ npx klaudio
|
|
|
11
11
|
The interactive installer walks you through:
|
|
12
12
|
|
|
13
13
|
1. **Choose scope** — install globally (`~/.claude`) or per-project (`.claude/`)
|
|
14
|
-
2. **Pick a source** — use a built-in preset, scan your
|
|
15
|
-
3. **Preview & assign** — listen to sounds and assign them to events
|
|
14
|
+
2. **Pick a source** — use a built-in preset, scan your Steam & Epic Games library for sounds, or provide custom files
|
|
15
|
+
3. **Preview & assign** — listen to sounds and assign them to events (tab to switch between events)
|
|
16
16
|
4. **Install** — writes Claude Code hooks to your `settings.json`
|
|
17
17
|
|
|
18
18
|
## Sound Sources
|
|
@@ -23,13 +23,14 @@ Ready-made sound packs (Retro 8-bit, Minimal Zen, Sci-Fi Terminal, Victory Fanfa
|
|
|
23
23
|
|
|
24
24
|
### Game Sound Scanner
|
|
25
25
|
|
|
26
|
-
Scans your Steam and Epic Games libraries for audio files:
|
|
26
|
+
Scans your local Steam and Epic Games libraries for audio files:
|
|
27
27
|
|
|
28
28
|
- Finds loose audio files (`.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`)
|
|
29
29
|
- Extracts packed audio (Wwise `.wem`, FMOD `.bank`, `.fsb`) using [vgmstream](https://vgmstream.org/) (downloaded automatically)
|
|
30
|
+
- Extracts Unity game audio from `.resource` files (PCM decoded directly, Vorbis converted via vgmstream)
|
|
30
31
|
- Parses Wwise metadata (`SoundbanksInfo.json`) for descriptive filenames
|
|
31
32
|
- Categorizes sounds (voice, ambient, music, SFX, UI, creature) for easy browsing
|
|
32
|
-
- Caches extracted sounds in `~/.
|
|
33
|
+
- Caches extracted sounds in `~/.klaudio/cache/` for instant reuse
|
|
33
34
|
|
|
34
35
|
### Custom Files
|
|
35
36
|
|
|
@@ -38,18 +39,19 @@ Point to your own `.wav`/`.mp3` files.
|
|
|
38
39
|
## Features
|
|
39
40
|
|
|
40
41
|
- **Auto-preview** — sounds play automatically as you browse the list (toggle with `p`)
|
|
42
|
+
- **Multi-game selection** — pick sounds from different games, tab between events
|
|
41
43
|
- **Category filtering** — drill into voice, ambient, SFX, etc. when a game has enough variety
|
|
42
44
|
- **Type-to-filter** — start typing to narrow down long lists
|
|
43
45
|
- **10-second clamp** — long sounds are processed with ffmpeg: silence stripped, fade out baked in
|
|
44
46
|
- **Background scanning** — game list updates live as directories are scanned
|
|
45
|
-
- **
|
|
47
|
+
- **Pre-loads existing config** — re-running the installer shows your current sound selections
|
|
46
48
|
|
|
47
49
|
## Events
|
|
48
50
|
|
|
49
51
|
| Event | Triggers when |
|
|
50
52
|
|---|---|
|
|
51
|
-
| Task Complete | Claude finishes a response |
|
|
52
53
|
| Notification | Claude needs your attention |
|
|
54
|
+
| Task Complete | Claude finishes a response |
|
|
53
55
|
|
|
54
56
|
## Uninstall
|
|
55
57
|
|
package/package.json
CHANGED
package/src/cache.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readdir, mkdir, readFile, writeFile, stat, copyFile } from "node:fs/pro
|
|
|
2
2
|
import { join, extname, basename, dirname } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
|
-
const CACHE_DIR = join(homedir(), ".
|
|
5
|
+
const CACHE_DIR = join(homedir(), ".klaudio", "cache");
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Category keywords to match against folder names, filenames, and Wwise event names.
|
package/src/cli.js
CHANGED
|
@@ -803,7 +803,7 @@ const ExtractingScreen = ({ game, onDone, onBack }) => {
|
|
|
803
803
|
return;
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
const outputDir = join(tmpdir(), "
|
|
806
|
+
const outputDir = join(tmpdir(), "klaudio-extract", game.name.replace(/[^a-zA-Z0-9]/g, "_"));
|
|
807
807
|
const allOutputs = [];
|
|
808
808
|
|
|
809
809
|
// Unity .resource files — extract FSB5 banks directly (no vgmstream needed for PCM16)
|
|
@@ -995,7 +995,7 @@ const DoneScreen = ({ result }) => {
|
|
|
995
995
|
),
|
|
996
996
|
),
|
|
997
997
|
h(Box, { marginTop: 1 },
|
|
998
|
-
h(Text, { dimColor: true }, " To remove: npx
|
|
998
|
+
h(Text, { dimColor: true }, " To remove: npx klaudio --uninstall"),
|
|
999
999
|
),
|
|
1000
1000
|
);
|
|
1001
1001
|
};
|
|
@@ -1037,13 +1037,13 @@ const UninstallApp = () => {
|
|
|
1037
1037
|
if (phase === "done") {
|
|
1038
1038
|
return h(Box, { flexDirection: "column" },
|
|
1039
1039
|
h(Header, null),
|
|
1040
|
-
h(Text, { color: "green", marginLeft: 2 }, " ✓
|
|
1040
|
+
h(Text, { color: "green", marginLeft: 2 }, " ✓ Klaudio hooks removed."),
|
|
1041
1041
|
);
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
1044
|
return h(Box, { flexDirection: "column" },
|
|
1045
1045
|
h(Header, null),
|
|
1046
|
-
h(Text, { color: "yellow", marginLeft: 2 }, " No
|
|
1046
|
+
h(Text, { color: "yellow", marginLeft: 2 }, " No Klaudio configuration found."),
|
|
1047
1047
|
);
|
|
1048
1048
|
};
|
|
1049
1049
|
|
package/src/extractor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join, extname, basename, resolve as resolvePath } from "node:path";
|
|
|
5
5
|
import { platform, homedir, tmpdir } from "node:os";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
|
|
8
|
-
const TOOLS_DIR = join(homedir(), ".
|
|
8
|
+
const TOOLS_DIR = join(homedir(), ".klaudio", "tools");
|
|
9
9
|
|
|
10
10
|
// Packed audio formats that vgmstream-cli can convert to WAV
|
|
11
11
|
const PACKED_EXTENSIONS = new Set([".wem", ".bnk", ".bank", ".fsb", ".pck"]);
|
|
@@ -65,29 +65,31 @@ export async function getVgmstreamPath(onProgress) {
|
|
|
65
65
|
await mkdir(TOOLS_DIR, { recursive: true });
|
|
66
66
|
|
|
67
67
|
// Download the appropriate release
|
|
68
|
-
const
|
|
68
|
+
const isWindows = os === "win32";
|
|
69
|
+
const releaseUrl = isWindows
|
|
69
70
|
? "https://github.com/vgmstream/vgmstream-releases/releases/download/nightly/vgmstream-win64.zip"
|
|
70
71
|
: os === "darwin"
|
|
71
|
-
? "https://github.com/vgmstream/vgmstream-releases/releases/download/nightly/vgmstream-
|
|
72
|
-
: "https://github.com/vgmstream/vgmstream-releases/releases/download/nightly/vgmstream-linux.
|
|
72
|
+
? "https://github.com/vgmstream/vgmstream-releases/releases/download/nightly/vgmstream-mac-cli.tar.gz"
|
|
73
|
+
: "https://github.com/vgmstream/vgmstream-releases/releases/download/nightly/vgmstream-linux-cli.tar.gz";
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
+
const archiveExt = isWindows ? ".zip" : ".tar.gz";
|
|
76
|
+
const archivePath = join(tmpdir(), `vgmstream${archiveExt}`);
|
|
75
77
|
|
|
76
78
|
// Download using Node.js fetch
|
|
77
79
|
const response = await fetch(releaseUrl, { redirect: "follow" });
|
|
78
80
|
if (!response.ok) throw new Error(`Failed to download vgmstream: ${response.status}`);
|
|
79
81
|
|
|
80
|
-
const fileStream = createWriteStream(
|
|
82
|
+
const fileStream = createWriteStream(archivePath);
|
|
81
83
|
await pipeline(response.body, fileStream);
|
|
82
84
|
|
|
83
85
|
if (onProgress) onProgress("Extracting vgmstream-cli...");
|
|
84
86
|
|
|
85
|
-
// Extract
|
|
86
|
-
if (
|
|
87
|
+
// Extract: PowerShell for Windows, tar for macOS/Linux
|
|
88
|
+
if (isWindows) {
|
|
87
89
|
await new Promise((resolve, reject) => {
|
|
88
90
|
execFile("powershell.exe", [
|
|
89
91
|
"-NoProfile", "-Command",
|
|
90
|
-
`Expand-Archive -Path '${
|
|
92
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${TOOLS_DIR}' -Force`,
|
|
91
93
|
], { windowsHide: true }, (err) => {
|
|
92
94
|
if (err) reject(err);
|
|
93
95
|
else resolve();
|
|
@@ -95,13 +97,21 @@ export async function getVgmstreamPath(onProgress) {
|
|
|
95
97
|
});
|
|
96
98
|
} else {
|
|
97
99
|
await new Promise((resolve, reject) => {
|
|
98
|
-
execFile("
|
|
100
|
+
execFile("tar", ["xzf", archivePath, "-C", TOOLS_DIR], (err) => {
|
|
99
101
|
if (err) reject(err);
|
|
100
102
|
else resolve();
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
// Make executable
|
|
104
106
|
try { await chmod(toolPath, 0o755); } catch { /* ignore */ }
|
|
107
|
+
// Remove macOS quarantine attribute so Gatekeeper doesn't block execution
|
|
108
|
+
if (os === "darwin") {
|
|
109
|
+
try {
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
execFile("xattr", ["-d", "com.apple.quarantine", toolPath], () => resolve());
|
|
112
|
+
});
|
|
113
|
+
} catch { /* ignore — attribute may not exist */ }
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// Verify it exists
|
package/src/installer.js
CHANGED
|
@@ -62,19 +62,19 @@ export async function install({ scope, sounds }) {
|
|
|
62
62
|
const hookEvent = event.hookEvent;
|
|
63
63
|
const playCommand = getHookPlayCommand(soundPath);
|
|
64
64
|
|
|
65
|
-
// Check if there's already a
|
|
65
|
+
// Check if there's already a klaudio hook for this event
|
|
66
66
|
if (!settings.hooks[hookEvent]) {
|
|
67
67
|
settings.hooks[hookEvent] = [];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Remove any existing
|
|
70
|
+
// Remove any existing klaudio entries
|
|
71
71
|
settings.hooks[hookEvent] = settings.hooks[hookEvent].filter(
|
|
72
|
-
(entry) => !entry.
|
|
72
|
+
(entry) => !entry._klaudio
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
// Add our hook
|
|
76
76
|
settings.hooks[hookEvent].push({
|
|
77
|
-
|
|
77
|
+
_klaudio: true,
|
|
78
78
|
matcher: "",
|
|
79
79
|
hooks: [
|
|
80
80
|
{
|
|
@@ -112,7 +112,7 @@ export async function getExistingSounds(scope) {
|
|
|
112
112
|
for (const [eventId, event] of Object.entries(EVENTS)) {
|
|
113
113
|
const hookEntries = settings.hooks[event.hookEvent];
|
|
114
114
|
if (!hookEntries) continue;
|
|
115
|
-
const entry = hookEntries.find((e) => e.
|
|
115
|
+
const entry = hookEntries.find((e) => e._klaudio);
|
|
116
116
|
if (!entry?.hooks?.[0]?.command) continue;
|
|
117
117
|
|
|
118
118
|
// Extract file path from the play command
|
|
@@ -129,7 +129,7 @@ export async function getExistingSounds(scope) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Uninstall
|
|
132
|
+
* Uninstall klaudio hooks from settings.
|
|
133
133
|
*/
|
|
134
134
|
export async function uninstall(scope) {
|
|
135
135
|
const claudeDir = getTargetDir(scope);
|
|
@@ -142,7 +142,7 @@ export async function uninstall(scope) {
|
|
|
142
142
|
if (settings.hooks) {
|
|
143
143
|
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
144
144
|
settings.hooks[event] = entries.filter(
|
|
145
|
-
(entry) => !entry.
|
|
145
|
+
(entry) => !entry._klaudio
|
|
146
146
|
);
|
|
147
147
|
if (settings.hooks[event].length === 0) {
|
|
148
148
|
delete settings.hooks[event];
|
package/src/player.js
CHANGED
|
@@ -301,7 +301,7 @@ export async function processSound(filePath) {
|
|
|
301
301
|
|
|
302
302
|
// Build a deterministic output path based on input file hash
|
|
303
303
|
const hash = createHash("md5").update(absPath).digest("hex").slice(0, 12);
|
|
304
|
-
const outDir = join(tmpdir(), "
|
|
304
|
+
const outDir = join(tmpdir(), "klaudio-processed");
|
|
305
305
|
const outName = `${basename(absPath, extname(absPath))}_${hash}.wav`;
|
|
306
306
|
const outPath = join(outDir, outName);
|
|
307
307
|
|