vibeaudio 0.6.1 โ†’ 0.6.3

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 CHANGED
@@ -23,6 +23,7 @@ AI coding agents take 15โ€“45 seconds to reason, read files and write code. Star
23
23
  * ๐Ÿงฎ **Pure synthesis, zero MP3s.** Every note, chord and pad is generated in code โ€” no audio assets, no npm dependencies, no `node-gyp`.
24
24
  * ๐ŸŽผ **A different arrangement per project.** Your working directory seeds the progression, bass line and melody, so each repo has its own sound and keeps it.
25
25
  * ๐Ÿ“ˆ **Escalating layers.** Tier 1 (0โ€“15s) gentle intro โ†’ Tier 2 (15โ€“45s) main groove โ†’ Tier 3 (45s+) deep focus. You can hear how deep into the task the agent is.
26
+ * ๐ŸŽ›๏ธ **Music that follows the work** ([reactive mode](#reactive-mode-opt-in), opt-in). Calm while the agent reads, fuller while it edits, busiest when it runs commands or hands work to sub-agents โ€” you can hear *what* it's doing, not just for how long.
26
27
  * ๐Ÿ”” **Outcome-aware chimes.** Ascending on success, a soft descending minor chord on failure, and **silence on `Ctrl+C`** โ€” an abort is never reported as done.
27
28
  * โœ‹ **A "your turn" chime.** When Claude Code stops to ask permission (or an MCP server asks for input), the music pauses and a rising two-note chime asks for you; it picks back up once you've answered.
28
29
  * ๐Ÿ”Œ **Universal drop-in.** Hooks for **Claude Code, Codex, Cursor, Grok, Gemini CLI, Copilot CLI and Qwen Code**; MCP for **Claude Desktop and Antigravity**; the wrapper (`vibe <command>`) for anything else.
@@ -94,6 +95,14 @@ That puts `vibe` and `vibeaudio` on your `PATH`. Re-run the same command to upda
94
95
  vibe --install-hooks
95
96
  ```
96
97
 
98
+ Want the music to follow what the agent is doing โ€” calm while it reads, busiest while it runs commands? Add `--reactive`:
99
+
100
+ ```bash
101
+ vibe --reactive --install-hooks
102
+ ```
103
+
104
+ Prefer a guided setup? Run `vibe` with nothing after it: a menu asks for your agent, genre and volume (press `p` to hear a genre first) and installs the hooks for you.
105
+
97
106
  Now run your agent normally, with no prefix. Music starts when you submit a prompt and stops with a chime when the agent finishes. [Details below.](#-agent-hooks-no-wrapper-needed)
98
107
 
99
108
  **Running one-shot commands?** Wrap them:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeaudio",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Procedural focus music while your AI coding tools (Claude Code, Codex, Cursor, Grok, Gemini, Copilot) think \u2014 a different arrangement per project.",
5
5
  "bin": {
6
6
  "vibeaudio": "bin/vibeaudio.js",
@@ -10,6 +10,21 @@
10
10
  * report and the user's to wonder about, so everything is wrapped and the exit
11
11
  * code is always 0 - a greeting is not worth a failed install.
12
12
  */
13
+ // npm 7+ hides lifecycle-script stdout unless the script fails - measured on
14
+ // npm 10, a global install printed only "added 1 package". The controlling
15
+ // terminal is outside npm's pipe, so write there; stdout remains for when
16
+ // there is no terminal (CI, Windows), where npm may or may not show it.
17
+ function say(text) {
18
+ const fs = require("fs");
19
+ try {
20
+ const fd = fs.openSync("/dev/tty", "w");
21
+ fs.writeSync(fd, text + "\n");
22
+ fs.closeSync(fd);
23
+ } catch (e) {
24
+ console.log(text);
25
+ }
26
+ }
27
+
13
28
  try {
14
29
  // Local installs are a dependency of something else; their user is not here
15
30
  // and not the one who would run `vibe`. CI and Docker set this too.
@@ -18,14 +33,16 @@ try {
18
33
  // install is already the narrow case - a dependency install sets this false.
19
34
  if (process.env.npm_config_global === "true") {
20
35
  const c = (code, text) => `\x1b[${code}m${text}\x1b[0m`;
21
- console.log(`
22
- ${c("1;36", "๐ŸŽง VibeAudio installed.")} Two ways to start:
36
+ say(`
37
+ ${c("1;36", "๐ŸŽง VibeAudio installed.")} Start here:
23
38
 
24
- ${c("1", "vibe --install-hooks")} ${c("90", "music follows your agent's thinking โ€” best for Claude Code, Codex, Cursorโ€ฆ")}
25
- ${c("1", "vibe npm test")} ${c("90", "wrap any command that exits when it's done")}
39
+ ${c("1", "vibe")} ${c("90", "guided setup: pick your agent, genre and volume")}
40
+
41
+ Or skip the menu:
26
42
 
27
- ${c("90", "vibe to pick a tool and a sound from a menu")}
28
- ${c("90", "vibe --preview jazz to hear one first")}
43
+ ${c("1", "vibe --install-hooks")} ${c("90", "music follows your agent's thinking โ€” Claude Code, Codex, Cursorโ€ฆ")}
44
+ ${c("1", "vibe npm test")} ${c("90", "wrap any command that exits when it's done")}
45
+ ${c("1", "vibe --preview jazz")} ${c("90", "hear a genre first")}
29
46
  `);
30
47
  }
31
48
  } catch (e) {
@@ -64,16 +64,26 @@ function generateChiptuneLoop(durationSec = 7.5, tier = 2, seed = 0) {
64
64
  const startIdx = Math.floor(m.start * secPerBeat * SAMPLE_RATE);
65
65
  const numSamples = Math.floor(m.dur * secPerBeat * SAMPLE_RATE);
66
66
 
67
+ // Accumulated, for the same reason as the arpeggio below, plus one of its
68
+ // own: `softPulse((f + vib) * t)` is not vibrato but a phase that jumps
69
+ // when the vibrato switches on at t > 0.15 - by (vib * 0.15) cycles, once
70
+ // per note. Accumulating makes `vib` an actual frequency deviation of
71
+ // +/-3.5Hz, which is what it was meant to be, and the switch-on changes
72
+ // only the rate.
73
+ let leadPhase = 0.0;
74
+
67
75
  for (let i = 0; i < numSamples; i++) {
68
76
  const idx = startIdx + i;
69
77
  if (idx >= totalSamples) break;
70
78
  const t = i / SAMPLE_RATE;
71
79
  const env = Math.min(1.0, t * 75.0) * Math.max(0.0, 1.0 - (t / (m.dur * secPerBeat)));
72
80
  const vib = t > 0.15 ? Math.sin(2 * Math.PI * 5.5 * t) * 3.5 : 0.0;
73
- const sample = softPulse((f + vib) * t) * env * 0.22;
81
+ const sample = softPulse(leadPhase) * env * 0.22;
74
82
 
75
83
  left[idx] += sample;
76
84
  right[idx] += sample;
85
+
86
+ leadPhase += (f + vib) / SAMPLE_RATE;
77
87
  }
78
88
  }
79
89
 
@@ -81,6 +91,22 @@ function generateChiptuneLoop(durationSec = 7.5, tier = 2, seed = 0) {
81
91
  const chords = variant.chords;
82
92
  const arpSpeed = 0.075;
83
93
 
94
+ // The phase is accumulated, not computed as `f * t`.
95
+ //
96
+ // This arpeggio steps to a new note every 75ms and has no envelope at all -
97
+ // it is meant to run continuously. With `softPulse(f * t)`, changing f while
98
+ // t keeps running jumps the phase by (f2 - f1) * t: hundreds of cycles a few
99
+ // seconds in, which lands the waveform on an arbitrary value. That is a
100
+ // click, 13 times a second, with no envelope to hide it - reported as static
101
+ // on 8bit, and at tier 1 every single step over 0.05 full-scale landed
102
+ // exactly on one of these boundaries.
103
+ //
104
+ // Accumulating instead means a new note changes the *rate* the phase
105
+ // advances, never its value, so the waveform stays continuous across the
106
+ // step. It is also how a real oscillator behaves.
107
+ let arpPhase = 0.0;
108
+ let shimmerPhase = 0.0;
109
+
84
110
  for (let i = 0; i < totalSamples; i++) {
85
111
  const t = i / SAMPLE_RATE;
86
112
  const beat = Math.floor(t / secPerBeat);
@@ -88,17 +114,20 @@ function generateChiptuneLoop(durationSec = 7.5, tier = 2, seed = 0) {
88
114
  const chord = chords[chordIdx];
89
115
  const noteIdx = Math.floor(t / arpSpeed) % chord.length;
90
116
  const f = noteToFreq(chord[noteIdx]);
91
- const sample = softPulse(f * t) * (tier === 1 ? 0.09 : 0.075);
117
+ const sample = softPulse(arpPhase) * (tier === 1 ? 0.09 : 0.075);
92
118
 
93
119
  left[i] += sample * 0.85;
94
120
  right[i] += sample * 0.85;
95
121
 
96
122
  // Tier 3: octave-up shimmer arpeggio doubling
97
123
  if (tier >= 3) {
98
- const shimmer = softPulse(f * 2.0 * t) * 0.03;
124
+ const shimmer = softPulse(shimmerPhase) * 0.03;
99
125
  left[i] += shimmer * 1.1;
100
126
  right[i] += shimmer * 0.9;
101
127
  }
128
+
129
+ arpPhase += f / SAMPLE_RATE;
130
+ shimmerPhase += (f * 2.0) / SAMPLE_RATE;
102
131
  }
103
132
 
104
133
  // 3. NES Triangle Bass