@kernel.chat/kbot 3.95.0 → 3.97.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 +3 -3
- package/dist/agent.js +30 -0
- package/dist/cli.js +1 -1
- package/dist/coordinator.d.ts +164 -0
- package/dist/coordinator.js +839 -0
- package/dist/doctor.js +5 -4
- package/dist/share.js +1 -1
- package/dist/streaming.js +1 -1
- package/dist/tools/audio-engine.d.ts +76 -0
- package/dist/tools/audio-engine.js +583 -24
- package/dist/tools/audit.js +2 -2
- package/dist/tools/containers.js +75 -14
- package/dist/tools/index.js +6 -0
- package/dist/tools/sprite-engine.d.ts +18 -0
- package/dist/tools/sprite-engine.js +435 -1
- package/dist/tools/stream-brain.js +1 -1
- package/dist/tools/stream-character.js +4 -4
- package/dist/tools/stream-chat-ai.d.ts +56 -0
- package/dist/tools/stream-chat-ai.js +625 -0
- package/dist/tools/stream-commands.d.ts +91 -0
- package/dist/tools/stream-commands.js +911 -0
- package/dist/tools/stream-intelligence.js +2 -2
- package/dist/tools/stream-overlay.d.ts +53 -0
- package/dist/tools/stream-overlay.js +494 -0
- package/dist/tools/stream-renderer.js +706 -107
- package/dist/tools/stream-vod.d.ts +60 -0
- package/dist/tools/stream-vod.js +449 -0
- package/dist/tools/stream-weather.d.ts +79 -0
- package/dist/tools/stream-weather.js +811 -0
- package/dist/tools/tile-world.d.ts +6 -0
- package/dist/tools/tile-world.js +3 -3
- package/dist/ui.js +23 -21
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.js +111 -0
- package/package.json +5 -4
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
// kbot Audio Engine — Procedural sound generation for the stream
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// v1: Audio Description System
|
|
6
|
-
// The stream currently pipes silent audio via ffmpeg's anullsrc filter.
|
|
7
|
-
// Piping actual PCM audio requires significant architecture changes (separate
|
|
8
|
-
// pipe fd, real-time audio synthesis, mixing). For v1, this engine generates
|
|
9
|
-
// textual audio atmosphere descriptions that the narrative engine and renderer
|
|
10
|
-
// can display as italic stage directions (like a screenplay).
|
|
11
|
-
//
|
|
12
|
-
// v2 roadmap: Generate actual PCM Float32 audio and pipe to ffmpeg via pipe:3.
|
|
13
|
-
//
|
|
14
|
-
// FUTURE: Generate actual PCM audio
|
|
15
|
-
// export function generatePCMAudio(engine: AudioEngine, sampleRate: number, samples: number): Float32Array
|
|
16
|
-
// This would generate procedural audio using:
|
|
17
|
-
// - Oscillators (sine, square, sawtooth for chiptune)
|
|
18
|
-
// - Noise generators (white noise for wind/rain)
|
|
19
|
-
// - Envelope generators (ADSR for notes)
|
|
20
|
-
// - Simple reverb (delay line)
|
|
21
|
-
// - Mix to mono PCM, pipe to ffmpeg via separate audio input
|
|
22
|
-
// ffmpeg would need: -f f32le -ar 44100 -ac 1 -i pipe:3 (additional pipe for audio)
|
|
2
|
+
// Tools: audio_status, audio_mood, audio_pcm_status
|
|
3
|
+
// v1: Text description system (fallback). v2: Real PCM Float32 synthesis.
|
|
4
|
+
// Output: Float32Array piped to ffmpeg via -f f32le -ar 44100 -ac 1 -i pipe:3
|
|
23
5
|
import { registerTool } from './index.js';
|
|
6
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
7
|
+
const DEFAULT_SAMPLE_RATE = 44100;
|
|
8
|
+
const TWO_PI = Math.PI * 2;
|
|
9
|
+
/** MIDI note to frequency (A4 = 440 Hz) */
|
|
10
|
+
function midiToFreq(note) {
|
|
11
|
+
return 440 * Math.pow(2, (note - 69) / 12);
|
|
12
|
+
}
|
|
13
|
+
// Scale intervals (semitones from root)
|
|
14
|
+
const SCALES = {
|
|
15
|
+
pentatonic: [0, 2, 4, 7, 9],
|
|
16
|
+
minor: [0, 2, 3, 5, 7, 8, 10],
|
|
17
|
+
major: [0, 2, 4, 5, 7, 9, 11],
|
|
18
|
+
};
|
|
19
|
+
/** Pick a scale based on mood */
|
|
20
|
+
function moodToScale(mood) {
|
|
21
|
+
switch (mood) {
|
|
22
|
+
case 'calm': return 'pentatonic';
|
|
23
|
+
case 'tense': return 'minor';
|
|
24
|
+
case 'epic': return 'minor';
|
|
25
|
+
case 'dreamy': return 'pentatonic';
|
|
26
|
+
case 'playful': return 'major';
|
|
27
|
+
default: return 'pentatonic';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Base MIDI note for mood (root note + octave) */
|
|
31
|
+
function moodToRoot(mood) {
|
|
32
|
+
switch (mood) {
|
|
33
|
+
case 'calm': return 60; // C4
|
|
34
|
+
case 'tense': return 57; // A3
|
|
35
|
+
case 'epic': return 48; // C3
|
|
36
|
+
case 'dreamy': return 64; // E4
|
|
37
|
+
case 'playful': return 60; // C4
|
|
38
|
+
default: return 60;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function createDefaultChannel(waveform, vol, filterFreq) {
|
|
42
|
+
return {
|
|
43
|
+
pattern: new Array(16).fill(0),
|
|
44
|
+
waveform,
|
|
45
|
+
envelope: { attack: 0.01, decay: 0.1, sustain: 0.5, release: 0.15 },
|
|
46
|
+
volume: vol,
|
|
47
|
+
filterType: 'lowpass',
|
|
48
|
+
filterFreq,
|
|
49
|
+
filterQ: 1.0,
|
|
50
|
+
phase: 0,
|
|
51
|
+
envStage: 'off',
|
|
52
|
+
envLevel: 0,
|
|
53
|
+
envTime: 0,
|
|
54
|
+
currentNote: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function createDelayLine(sampleRate) {
|
|
58
|
+
const delaySamples = Math.floor(sampleRate * 0.3); // 300ms delay
|
|
59
|
+
return {
|
|
60
|
+
buffer: new Float32Array(delaySamples),
|
|
61
|
+
writeIndex: 0,
|
|
62
|
+
delaySamples,
|
|
63
|
+
feedback: 0.3,
|
|
64
|
+
mix: 0.2,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
24
67
|
// ─── Factory ─────────────────────────────────────────────────────
|
|
25
68
|
export function createAudioEngine() {
|
|
26
|
-
|
|
69
|
+
const sampleRate = DEFAULT_SAMPLE_RATE;
|
|
70
|
+
const bpm = 70;
|
|
71
|
+
const samplesPerStep = Math.floor((sampleRate * 60) / (bpm * 4)); // 16th notes
|
|
72
|
+
const engine = {
|
|
27
73
|
currentAmbience: 'peaceful',
|
|
28
74
|
musicState: {
|
|
29
|
-
bpm
|
|
75
|
+
bpm,
|
|
30
76
|
key: 'C major',
|
|
31
77
|
mood: 'calm',
|
|
32
78
|
playing: true,
|
|
@@ -38,7 +84,459 @@ export function createAudioEngine() {
|
|
|
38
84
|
lastSoundFrame: 0,
|
|
39
85
|
ambienceInterval: 720, // 120 seconds at 6fps
|
|
40
86
|
totalDescriptions: 0,
|
|
87
|
+
// v2 PCM
|
|
88
|
+
pcmEnabled: false,
|
|
89
|
+
sfxQueue: [],
|
|
90
|
+
sequencer: {
|
|
91
|
+
step: 0,
|
|
92
|
+
sampleCounter: 0,
|
|
93
|
+
samplesPerStep,
|
|
94
|
+
channels: {
|
|
95
|
+
melody: createDefaultChannel('square', 0.25, 4000),
|
|
96
|
+
bass: createDefaultChannel('sawtooth', 0.3, 800),
|
|
97
|
+
arp: createDefaultChannel('square', 0.15, 6000),
|
|
98
|
+
drums: createDefaultChannel('noise_white', 0.35, 2000),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
masterVolume: 0.6,
|
|
102
|
+
delayLine: createDelayLine(sampleRate),
|
|
103
|
+
totalSamplesGenerated: 0,
|
|
104
|
+
musicEnabled: true,
|
|
41
105
|
};
|
|
106
|
+
// Generate initial patterns based on default mood
|
|
107
|
+
regeneratePatterns(engine);
|
|
108
|
+
return engine;
|
|
109
|
+
}
|
|
110
|
+
// ─── PCM Oscillators ─────────────────────────────────────────────
|
|
111
|
+
function oscillator(waveform, phase) {
|
|
112
|
+
switch (waveform) {
|
|
113
|
+
case 'sine':
|
|
114
|
+
return Math.sin(phase);
|
|
115
|
+
case 'square':
|
|
116
|
+
return Math.sin(phase) >= 0 ? 0.8 : -0.8;
|
|
117
|
+
case 'sawtooth':
|
|
118
|
+
return ((phase % TWO_PI) / Math.PI) - 1.0;
|
|
119
|
+
case 'triangle': {
|
|
120
|
+
const t = (phase % TWO_PI) / TWO_PI;
|
|
121
|
+
return t < 0.5 ? (4 * t - 1) : (3 - 4 * t);
|
|
122
|
+
}
|
|
123
|
+
case 'noise_white':
|
|
124
|
+
return (Math.random() * 2) - 1;
|
|
125
|
+
case 'noise_pink': {
|
|
126
|
+
// Voss-McCartney approximation (simplified)
|
|
127
|
+
const w = (Math.random() * 2 - 1) * 0.5;
|
|
128
|
+
return w + (Math.random() * 2 - 1) * 0.3 + (Math.random() * 2 - 1) * 0.2;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ─── ADSR Envelope ───────────────────────────────────────────────
|
|
135
|
+
function tickEnvelope(ch, dt) {
|
|
136
|
+
const env = ch.envelope;
|
|
137
|
+
ch.envTime += dt;
|
|
138
|
+
switch (ch.envStage) {
|
|
139
|
+
case 'attack':
|
|
140
|
+
ch.envLevel = env.attack > 0 ? Math.min(1, ch.envTime / env.attack) : 1;
|
|
141
|
+
if (ch.envTime >= env.attack) {
|
|
142
|
+
ch.envStage = 'decay';
|
|
143
|
+
ch.envTime = 0;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case 'decay':
|
|
147
|
+
ch.envLevel = 1 - (1 - env.sustain) * Math.min(1, ch.envTime / env.decay);
|
|
148
|
+
if (ch.envTime >= env.decay) {
|
|
149
|
+
ch.envStage = 'sustain';
|
|
150
|
+
ch.envTime = 0;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 'sustain':
|
|
154
|
+
ch.envLevel = env.sustain;
|
|
155
|
+
break;
|
|
156
|
+
case 'release':
|
|
157
|
+
ch.envLevel = env.sustain * Math.max(0, 1 - ch.envTime / env.release);
|
|
158
|
+
if (ch.envTime >= env.release) {
|
|
159
|
+
ch.envStage = 'off';
|
|
160
|
+
ch.envLevel = 0;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case 'off':
|
|
164
|
+
ch.envLevel = 0;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function triggerNote(ch, note) {
|
|
169
|
+
ch.currentNote = note;
|
|
170
|
+
ch.envStage = 'attack';
|
|
171
|
+
ch.envTime = 0;
|
|
172
|
+
ch.envLevel = 0;
|
|
173
|
+
}
|
|
174
|
+
function releaseNote(ch) {
|
|
175
|
+
if (ch.envStage !== 'off') {
|
|
176
|
+
ch.envStage = 'release';
|
|
177
|
+
ch.envTime = 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const filterStates = new WeakMap();
|
|
181
|
+
function getFilterState(ch) {
|
|
182
|
+
let s = filterStates.get(ch);
|
|
183
|
+
if (!s) {
|
|
184
|
+
s = { y1: 0, y2: 0, x1: 0, x2: 0 };
|
|
185
|
+
filterStates.set(ch, s);
|
|
186
|
+
}
|
|
187
|
+
return s;
|
|
188
|
+
}
|
|
189
|
+
function applyFilter(ch, input, sampleRate) {
|
|
190
|
+
const s = getFilterState(ch);
|
|
191
|
+
const freq = Math.min(ch.filterFreq, sampleRate * 0.45);
|
|
192
|
+
const w0 = TWO_PI * freq / sampleRate;
|
|
193
|
+
const sinW0 = Math.sin(w0);
|
|
194
|
+
const cosW0 = Math.cos(w0);
|
|
195
|
+
const alpha = sinW0 / (2 * ch.filterQ);
|
|
196
|
+
let b0, b1, b2, a0, a1, a2;
|
|
197
|
+
switch (ch.filterType) {
|
|
198
|
+
case 'lowpass':
|
|
199
|
+
b0 = (1 - cosW0) / 2;
|
|
200
|
+
b1 = 1 - cosW0;
|
|
201
|
+
b2 = (1 - cosW0) / 2;
|
|
202
|
+
a0 = 1 + alpha;
|
|
203
|
+
a1 = -2 * cosW0;
|
|
204
|
+
a2 = 1 - alpha;
|
|
205
|
+
break;
|
|
206
|
+
case 'highpass':
|
|
207
|
+
b0 = (1 + cosW0) / 2;
|
|
208
|
+
b1 = -(1 + cosW0);
|
|
209
|
+
b2 = (1 + cosW0) / 2;
|
|
210
|
+
a0 = 1 + alpha;
|
|
211
|
+
a1 = -2 * cosW0;
|
|
212
|
+
a2 = 1 - alpha;
|
|
213
|
+
break;
|
|
214
|
+
case 'bandpass':
|
|
215
|
+
b0 = alpha;
|
|
216
|
+
b1 = 0;
|
|
217
|
+
b2 = -alpha;
|
|
218
|
+
a0 = 1 + alpha;
|
|
219
|
+
a1 = -2 * cosW0;
|
|
220
|
+
a2 = 1 - alpha;
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
return input;
|
|
224
|
+
}
|
|
225
|
+
const output = (b0 / a0) * input + (b1 / a0) * s.x1 + (b2 / a0) * s.x2
|
|
226
|
+
- (a1 / a0) * s.y1 - (a2 / a0) * s.y2;
|
|
227
|
+
s.x2 = s.x1;
|
|
228
|
+
s.x1 = input;
|
|
229
|
+
s.y2 = s.y1;
|
|
230
|
+
s.y1 = output;
|
|
231
|
+
return output;
|
|
232
|
+
}
|
|
233
|
+
// ─── Delay Line (reverb/echo) ───────────────────────────────────
|
|
234
|
+
function processDelay(dl, input) {
|
|
235
|
+
const readIndex = (dl.writeIndex - dl.delaySamples + dl.buffer.length) % dl.buffer.length;
|
|
236
|
+
const delayed = dl.buffer[readIndex];
|
|
237
|
+
dl.buffer[dl.writeIndex] = input + delayed * dl.feedback;
|
|
238
|
+
dl.writeIndex = (dl.writeIndex + 1) % dl.buffer.length;
|
|
239
|
+
return input * (1 - dl.mix) + delayed * dl.mix;
|
|
240
|
+
}
|
|
241
|
+
// ─── Pattern Generation ─────────────────────────────────────────
|
|
242
|
+
function generatePattern(scale, root, octaveRange, density, seed) {
|
|
243
|
+
const intervals = SCALES[scale];
|
|
244
|
+
const pattern = new Array(16).fill(0);
|
|
245
|
+
// Deterministic-ish from seed
|
|
246
|
+
let r = seed;
|
|
247
|
+
const next = () => { r = (r * 1103515245 + 12345) & 0x7fffffff; return r / 0x7fffffff; };
|
|
248
|
+
for (let i = 0; i < 16; i++) {
|
|
249
|
+
if (next() < density) {
|
|
250
|
+
const octave = Math.floor(next() * octaveRange);
|
|
251
|
+
const idx = Math.floor(next() * intervals.length);
|
|
252
|
+
pattern[i] = root + intervals[idx] + octave * 12;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return pattern;
|
|
256
|
+
}
|
|
257
|
+
function generateDrumPattern(mood, seed) {
|
|
258
|
+
const pattern = new Array(16).fill(0);
|
|
259
|
+
let r = seed;
|
|
260
|
+
const next = () => { r = (r * 1103515245 + 12345) & 0x7fffffff; return r / 0x7fffffff; };
|
|
261
|
+
// Kick on beats (MIDI 36), snare on 2 & 4 (38), hat (42)
|
|
262
|
+
for (let i = 0; i < 16; i++) {
|
|
263
|
+
if (i % 4 === 0)
|
|
264
|
+
pattern[i] = 36; // kick
|
|
265
|
+
else if (i % 4 === 2 && mood !== 'calm')
|
|
266
|
+
pattern[i] = 38; // snare
|
|
267
|
+
else if (i % 2 === 0 && next() < 0.4)
|
|
268
|
+
pattern[i] = 42; // hat
|
|
269
|
+
else if (mood === 'epic' && next() < 0.3)
|
|
270
|
+
pattern[i] = 36; // extra kicks for epic
|
|
271
|
+
}
|
|
272
|
+
return pattern;
|
|
273
|
+
}
|
|
274
|
+
/** Regenerate all 4 channel patterns based on the current mood */
|
|
275
|
+
function regeneratePatterns(engine) {
|
|
276
|
+
const mood = engine.musicState.mood;
|
|
277
|
+
const scale = moodToScale(mood);
|
|
278
|
+
const root = moodToRoot(mood);
|
|
279
|
+
const seed = Date.now() & 0xffffff;
|
|
280
|
+
const seq = engine.sequencer;
|
|
281
|
+
const ch = seq.channels;
|
|
282
|
+
// Mood shapes pattern density and channel config
|
|
283
|
+
switch (mood) {
|
|
284
|
+
case 'calm':
|
|
285
|
+
ch.melody.pattern = generatePattern(scale, root + 12, 1, 0.3, seed);
|
|
286
|
+
ch.melody.envelope = { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.3 };
|
|
287
|
+
ch.bass.pattern = generatePattern(scale, root - 12, 1, 0.2, seed + 1);
|
|
288
|
+
ch.bass.envelope = { attack: 0.02, decay: 0.15, sustain: 0.6, release: 0.2 };
|
|
289
|
+
ch.arp.pattern = generatePattern(scale, root, 2, 0.5, seed + 2);
|
|
290
|
+
ch.arp.envelope = { attack: 0.005, decay: 0.08, sustain: 0.2, release: 0.1 };
|
|
291
|
+
ch.drums.pattern = generateDrumPattern(mood, seed + 3);
|
|
292
|
+
ch.drums.volume = 0.15;
|
|
293
|
+
ch.melody.filterFreq = 3000;
|
|
294
|
+
break;
|
|
295
|
+
case 'tense':
|
|
296
|
+
ch.melody.pattern = generatePattern(scale, root + 12, 1, 0.4, seed);
|
|
297
|
+
ch.melody.envelope = { attack: 0.01, decay: 0.1, sustain: 0.6, release: 0.1 };
|
|
298
|
+
ch.bass.pattern = generatePattern(scale, root - 12, 1, 0.5, seed + 1);
|
|
299
|
+
ch.bass.envelope = { attack: 0.005, decay: 0.08, sustain: 0.7, release: 0.1 };
|
|
300
|
+
ch.arp.pattern = generatePattern(scale, root, 2, 0.6, seed + 2);
|
|
301
|
+
ch.arp.envelope = { attack: 0.003, decay: 0.05, sustain: 0.3, release: 0.08 };
|
|
302
|
+
ch.drums.pattern = generateDrumPattern(mood, seed + 3);
|
|
303
|
+
ch.drums.volume = 0.3;
|
|
304
|
+
ch.melody.filterFreq = 2500; // darker
|
|
305
|
+
break;
|
|
306
|
+
case 'epic':
|
|
307
|
+
ch.melody.pattern = generatePattern(scale, root + 12, 2, 0.55, seed);
|
|
308
|
+
ch.melody.envelope = { attack: 0.01, decay: 0.15, sustain: 0.7, release: 0.15 };
|
|
309
|
+
ch.melody.volume = 0.3;
|
|
310
|
+
ch.bass.pattern = generatePattern(scale, root - 12, 1, 0.6, seed + 1);
|
|
311
|
+
ch.bass.envelope = { attack: 0.005, decay: 0.1, sustain: 0.8, release: 0.1 };
|
|
312
|
+
ch.bass.volume = 0.35;
|
|
313
|
+
ch.arp.pattern = generatePattern(scale, root, 2, 0.7, seed + 2);
|
|
314
|
+
ch.arp.envelope = { attack: 0.003, decay: 0.06, sustain: 0.4, release: 0.08 };
|
|
315
|
+
ch.drums.pattern = generateDrumPattern(mood, seed + 3);
|
|
316
|
+
ch.drums.volume = 0.35;
|
|
317
|
+
ch.melody.filterFreq = 5000;
|
|
318
|
+
break;
|
|
319
|
+
case 'dreamy':
|
|
320
|
+
ch.melody.pattern = generatePattern(scale, root + 12, 1, 0.25, seed);
|
|
321
|
+
ch.melody.envelope = { attack: 0.1, decay: 0.3, sustain: 0.5, release: 0.5 };
|
|
322
|
+
ch.melody.waveform = 'sine';
|
|
323
|
+
ch.bass.pattern = generatePattern(scale, root - 12, 1, 0.15, seed + 1);
|
|
324
|
+
ch.bass.envelope = { attack: 0.08, decay: 0.2, sustain: 0.4, release: 0.4 };
|
|
325
|
+
ch.arp.pattern = generatePattern(scale, root, 2, 0.35, seed + 2);
|
|
326
|
+
ch.arp.envelope = { attack: 0.05, decay: 0.15, sustain: 0.3, release: 0.3 };
|
|
327
|
+
ch.drums.pattern = generateDrumPattern(mood, seed + 3);
|
|
328
|
+
ch.drums.volume = 0.1;
|
|
329
|
+
engine.delayLine.mix = 0.4; // more reverb
|
|
330
|
+
engine.delayLine.feedback = 0.45;
|
|
331
|
+
ch.melody.filterFreq = 2000;
|
|
332
|
+
break;
|
|
333
|
+
case 'playful':
|
|
334
|
+
ch.melody.pattern = generatePattern(scale, root + 12, 2, 0.5, seed);
|
|
335
|
+
ch.melody.envelope = { attack: 0.005, decay: 0.08, sustain: 0.4, release: 0.1 };
|
|
336
|
+
ch.bass.pattern = generatePattern(scale, root - 12, 1, 0.4, seed + 1);
|
|
337
|
+
ch.bass.envelope = { attack: 0.005, decay: 0.1, sustain: 0.5, release: 0.1 };
|
|
338
|
+
ch.arp.pattern = generatePattern(scale, root, 2, 0.65, seed + 2);
|
|
339
|
+
ch.arp.envelope = { attack: 0.003, decay: 0.05, sustain: 0.3, release: 0.08 };
|
|
340
|
+
ch.drums.pattern = generateDrumPattern(mood, seed + 3);
|
|
341
|
+
ch.drums.volume = 0.25;
|
|
342
|
+
ch.melody.filterFreq = 6000;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
// Update tempo
|
|
346
|
+
seq.samplesPerStep = Math.floor((DEFAULT_SAMPLE_RATE * 60) / (engine.musicState.bpm * 4));
|
|
347
|
+
}
|
|
348
|
+
// ─── Render One Channel Sample ───────────────────────────────────
|
|
349
|
+
function renderChannel(ch, sampleRate) {
|
|
350
|
+
if (ch.envStage === 'off')
|
|
351
|
+
return 0;
|
|
352
|
+
const freq = ch.waveform.startsWith('noise') ? 0 : midiToFreq(ch.currentNote);
|
|
353
|
+
let sample;
|
|
354
|
+
if (ch.waveform.startsWith('noise')) {
|
|
355
|
+
sample = oscillator(ch.waveform, 0);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
sample = oscillator(ch.waveform, ch.phase);
|
|
359
|
+
ch.phase += TWO_PI * freq / sampleRate;
|
|
360
|
+
if (ch.phase > TWO_PI)
|
|
361
|
+
ch.phase -= TWO_PI;
|
|
362
|
+
}
|
|
363
|
+
// Apply envelope
|
|
364
|
+
sample *= ch.envLevel;
|
|
365
|
+
// Apply filter
|
|
366
|
+
sample = applyFilter(ch, sample, sampleRate);
|
|
367
|
+
// Apply channel volume
|
|
368
|
+
sample *= ch.volume;
|
|
369
|
+
return sample;
|
|
370
|
+
}
|
|
371
|
+
// ─── SFX Rendering ──────────────────────────────────────────────
|
|
372
|
+
const SFX_DURATION_SAMPLES = {
|
|
373
|
+
chat: Math.floor(DEFAULT_SAMPLE_RATE * 0.08),
|
|
374
|
+
follow: Math.floor(DEFAULT_SAMPLE_RATE * 0.4),
|
|
375
|
+
achievement: Math.floor(DEFAULT_SAMPLE_RATE * 0.6),
|
|
376
|
+
boss: Math.floor(DEFAULT_SAMPLE_RATE * 0.8),
|
|
377
|
+
raid: Math.floor(DEFAULT_SAMPLE_RATE * 0.7),
|
|
378
|
+
build: Math.floor(DEFAULT_SAMPLE_RATE * 0.12),
|
|
379
|
+
discovery: Math.floor(DEFAULT_SAMPLE_RATE * 0.5),
|
|
380
|
+
};
|
|
381
|
+
function renderSFXSample(sfx, sampleRate) {
|
|
382
|
+
const totalSamples = SFX_DURATION_SAMPLES[sfx.type];
|
|
383
|
+
const elapsed = totalSamples - sfx.samplesRemaining;
|
|
384
|
+
const t = elapsed / sampleRate; // time in seconds
|
|
385
|
+
const progress = elapsed / totalSamples; // 0-1
|
|
386
|
+
// Simple amplitude envelope (quick attack, exponential decay)
|
|
387
|
+
const env = Math.exp(-progress * 5) * (1 - Math.exp(-elapsed * 0.01));
|
|
388
|
+
switch (sfx.type) {
|
|
389
|
+
case 'chat': {
|
|
390
|
+
// Soft blip — high sine with fast decay
|
|
391
|
+
const freq = 1200 + 400 * (1 - progress);
|
|
392
|
+
sfx.phase += TWO_PI * freq / sampleRate;
|
|
393
|
+
return Math.sin(sfx.phase) * env * 0.3;
|
|
394
|
+
}
|
|
395
|
+
case 'follow': {
|
|
396
|
+
// Ascending chime — 3 tones rising
|
|
397
|
+
const stage = Math.floor(progress * 3);
|
|
398
|
+
const freqs = [523, 659, 784]; // C5, E5, G5
|
|
399
|
+
const freq = freqs[Math.min(stage, 2)];
|
|
400
|
+
sfx.phase += TWO_PI * freq / sampleRate;
|
|
401
|
+
return Math.sin(sfx.phase) * env * 0.4;
|
|
402
|
+
}
|
|
403
|
+
case 'achievement': {
|
|
404
|
+
// Fanfare — 3 ascending notes with harmonics
|
|
405
|
+
const stage = Math.floor(progress * 3);
|
|
406
|
+
const freqs = [440, 554, 659]; // A4, C#5, E5
|
|
407
|
+
const freq = freqs[Math.min(stage, 2)];
|
|
408
|
+
sfx.phase += TWO_PI * freq / sampleRate;
|
|
409
|
+
const fundamental = Math.sin(sfx.phase);
|
|
410
|
+
const harmonic = Math.sin(sfx.phase * 2) * 0.3;
|
|
411
|
+
return (fundamental + harmonic) * env * 0.4;
|
|
412
|
+
}
|
|
413
|
+
case 'boss': {
|
|
414
|
+
// Low rumble — detuned bass sine + noise
|
|
415
|
+
sfx.phase += TWO_PI * 55 / sampleRate; // A1
|
|
416
|
+
const bass = Math.sin(sfx.phase) + Math.sin(sfx.phase * 1.01) * 0.5;
|
|
417
|
+
const noise = (Math.random() * 2 - 1) * 0.15;
|
|
418
|
+
return (bass + noise) * env * 0.5;
|
|
419
|
+
}
|
|
420
|
+
case 'raid': {
|
|
421
|
+
// Drum roll — rapid noise bursts
|
|
422
|
+
const rollFreq = 15 + progress * 10; // accelerating
|
|
423
|
+
const burstEnv = Math.abs(Math.sin(TWO_PI * rollFreq * t));
|
|
424
|
+
const noise = (Math.random() * 2 - 1);
|
|
425
|
+
return noise * burstEnv * env * 0.4;
|
|
426
|
+
}
|
|
427
|
+
case 'build': {
|
|
428
|
+
// Thunk — short noise burst with lowpass feel
|
|
429
|
+
const noise = (Math.random() * 2 - 1);
|
|
430
|
+
const thunkEnv = Math.exp(-progress * 20);
|
|
431
|
+
sfx.phase += TWO_PI * 150 / sampleRate;
|
|
432
|
+
return (noise * 0.3 + Math.sin(sfx.phase) * 0.7) * thunkEnv * 0.4;
|
|
433
|
+
}
|
|
434
|
+
case 'discovery': {
|
|
435
|
+
// Shimmer — detuned sine sweep
|
|
436
|
+
const freq = 400 + 1200 * progress;
|
|
437
|
+
sfx.phase += TWO_PI * freq / sampleRate;
|
|
438
|
+
const s1 = Math.sin(sfx.phase);
|
|
439
|
+
const s2 = Math.sin(sfx.phase * 1.005); // slight detune
|
|
440
|
+
const s3 = Math.sin(sfx.phase * 0.995);
|
|
441
|
+
return (s1 + s2 + s3) / 3 * env * 0.35;
|
|
442
|
+
}
|
|
443
|
+
default:
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// ─── Public PCM API ─────────────────────────────────────────────
|
|
448
|
+
/**
|
|
449
|
+
* Trigger a sound effect. The SFX will be mixed into the next generateAudioBuffer call.
|
|
450
|
+
*/
|
|
451
|
+
export function triggerSFX(engine, sfx) {
|
|
452
|
+
engine.sfxQueue.push({
|
|
453
|
+
type: sfx,
|
|
454
|
+
triggeredAt: engine.totalSamplesGenerated,
|
|
455
|
+
samplesRemaining: SFX_DURATION_SAMPLES[sfx],
|
|
456
|
+
phase: 0,
|
|
457
|
+
});
|
|
458
|
+
if (engine.sfxQueue.length > 8) { // cap concurrent SFX
|
|
459
|
+
engine.sfxQueue = engine.sfxQueue.slice(-8);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Enable or disable background music generation.
|
|
464
|
+
*/
|
|
465
|
+
export function setMusicEnabled(engine, enabled) {
|
|
466
|
+
engine.musicEnabled = enabled;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Generate a buffer of PCM Float32 audio samples.
|
|
470
|
+
* Mixes the 4-channel chiptune sequencer + any active SFX.
|
|
471
|
+
* Output: mono Float32Array, ready to pipe to ffmpeg via -f f32le -ar 44100 -ac 1 -i pipe:3
|
|
472
|
+
*
|
|
473
|
+
* @param engine The audio engine state
|
|
474
|
+
* @param sampleCount Number of samples to generate
|
|
475
|
+
* @param sampleRate Sample rate (default 44100)
|
|
476
|
+
* @returns Float32Array of PCM samples in [-1, 1]
|
|
477
|
+
*/
|
|
478
|
+
export function generateAudioBuffer(engine, sampleCount, sampleRate = DEFAULT_SAMPLE_RATE) {
|
|
479
|
+
const buffer = new Float32Array(sampleCount);
|
|
480
|
+
if (!engine.pcmEnabled) {
|
|
481
|
+
// PCM disabled — return silence
|
|
482
|
+
return buffer;
|
|
483
|
+
}
|
|
484
|
+
const seq = engine.sequencer;
|
|
485
|
+
const dt = 1 / sampleRate;
|
|
486
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
487
|
+
let sample = 0;
|
|
488
|
+
// ── Music (4-channel sequencer) ──
|
|
489
|
+
if (engine.musicEnabled && engine.musicState.playing) {
|
|
490
|
+
// Step advance
|
|
491
|
+
seq.sampleCounter++;
|
|
492
|
+
if (seq.sampleCounter >= seq.samplesPerStep) {
|
|
493
|
+
seq.sampleCounter = 0;
|
|
494
|
+
seq.step = (seq.step + 1) % 16;
|
|
495
|
+
// Trigger or release notes on each channel
|
|
496
|
+
const channels = seq.channels;
|
|
497
|
+
for (const key of ['melody', 'bass', 'arp', 'drums']) {
|
|
498
|
+
const ch = channels[key];
|
|
499
|
+
const note = ch.pattern[seq.step];
|
|
500
|
+
if (note > 0) {
|
|
501
|
+
triggerNote(ch, note);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
releaseNote(ch);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Render each channel
|
|
509
|
+
for (const key of ['melody', 'bass', 'arp', 'drums']) {
|
|
510
|
+
const ch = seq.channels[key];
|
|
511
|
+
tickEnvelope(ch, dt);
|
|
512
|
+
sample += renderChannel(ch, sampleRate);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// ── SFX layer ──
|
|
516
|
+
for (let s = engine.sfxQueue.length - 1; s >= 0; s--) {
|
|
517
|
+
const sfx = engine.sfxQueue[s];
|
|
518
|
+
if (sfx.samplesRemaining > 0) {
|
|
519
|
+
sample += renderSFXSample(sfx, sampleRate);
|
|
520
|
+
sfx.samplesRemaining--;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
engine.sfxQueue.splice(s, 1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// ── Master processing ──
|
|
527
|
+
// Delay (reverb/echo)
|
|
528
|
+
sample = processDelay(engine.delayLine, sample);
|
|
529
|
+
// Master volume
|
|
530
|
+
sample *= engine.masterVolume;
|
|
531
|
+
// Soft clip to prevent harsh distortion
|
|
532
|
+
if (sample > 1)
|
|
533
|
+
sample = 1 - 1 / (sample + 1);
|
|
534
|
+
else if (sample < -1)
|
|
535
|
+
sample = -1 + 1 / (-sample + 1);
|
|
536
|
+
buffer[i] = sample;
|
|
537
|
+
}
|
|
538
|
+
engine.totalSamplesGenerated += sampleCount;
|
|
539
|
+
return buffer;
|
|
42
540
|
}
|
|
43
541
|
// ─── Ambience Descriptions ───────────────────────────────────────
|
|
44
542
|
const AMBIENCE_DESCRIPTIONS = {
|
|
@@ -311,6 +809,10 @@ export function tickAudio(engine, biome, weather, mood, timeOfDay, frame) {
|
|
|
311
809
|
const newMood = resolveMusicMood(mood);
|
|
312
810
|
const moodChanged = newMood !== engine.musicState.mood;
|
|
313
811
|
engine.musicState.mood = newMood;
|
|
812
|
+
// When mood changes and PCM is active, regenerate sequencer patterns
|
|
813
|
+
if (moodChanged && engine.pcmEnabled) {
|
|
814
|
+
regeneratePatterns(engine);
|
|
815
|
+
}
|
|
314
816
|
// Priority 1: Queued sound events (immediate)
|
|
315
817
|
const soundDescriptions = drainSoundQueue(engine);
|
|
316
818
|
if (soundDescriptions.length > 0) {
|
|
@@ -371,7 +873,7 @@ export function registerAudioEngineTools() {
|
|
|
371
873
|
'notification', 'achievement', 'weather',
|
|
372
874
|
'footstep', 'build', 'discovery',
|
|
373
875
|
],
|
|
374
|
-
note: 'v1
|
|
876
|
+
note: 'v1: text descriptions (fallback). v2: real PCM synthesis via generateAudioBuffer().',
|
|
375
877
|
}, null, 2);
|
|
376
878
|
},
|
|
377
879
|
});
|
|
@@ -422,5 +924,62 @@ export function registerAudioEngineTools() {
|
|
|
422
924
|
}, null, 2);
|
|
423
925
|
},
|
|
424
926
|
});
|
|
927
|
+
registerTool({
|
|
928
|
+
name: 'audio_pcm_status',
|
|
929
|
+
description: 'Get the PCM audio synthesis engine state — sequencer position, channel patterns, ' +
|
|
930
|
+
'active SFX, delay settings, total samples generated. Shows the v2 real-time audio status.',
|
|
931
|
+
parameters: {},
|
|
932
|
+
tier: 'free',
|
|
933
|
+
execute: async () => {
|
|
934
|
+
const engine = createAudioEngine();
|
|
935
|
+
engine.pcmEnabled = true; // show what PCM state looks like
|
|
936
|
+
const seq = engine.sequencer;
|
|
937
|
+
const channelSummary = (name, ch) => ({
|
|
938
|
+
name,
|
|
939
|
+
waveform: ch.waveform,
|
|
940
|
+
volume: ch.volume,
|
|
941
|
+
filterType: ch.filterType,
|
|
942
|
+
filterFreq: ch.filterFreq,
|
|
943
|
+
envelope: ch.envelope,
|
|
944
|
+
activeNotes: ch.pattern.filter(n => n > 0).length,
|
|
945
|
+
pattern: ch.pattern.map(n => n > 0 ? n : '.').join(' '),
|
|
946
|
+
});
|
|
947
|
+
return JSON.stringify({
|
|
948
|
+
pcmEnabled: engine.pcmEnabled,
|
|
949
|
+
musicEnabled: engine.musicEnabled,
|
|
950
|
+
masterVolume: engine.masterVolume,
|
|
951
|
+
sampleRate: DEFAULT_SAMPLE_RATE,
|
|
952
|
+
format: 'Float32 mono',
|
|
953
|
+
ffmpegPipe: '-f f32le -ar 44100 -ac 1 -i pipe:3',
|
|
954
|
+
sequencer: {
|
|
955
|
+
step: seq.step,
|
|
956
|
+
samplesPerStep: seq.samplesPerStep,
|
|
957
|
+
bpm: engine.musicState.bpm,
|
|
958
|
+
mood: engine.musicState.mood,
|
|
959
|
+
channels: [
|
|
960
|
+
channelSummary('melody', seq.channels.melody),
|
|
961
|
+
channelSummary('bass', seq.channels.bass),
|
|
962
|
+
channelSummary('arp', seq.channels.arp),
|
|
963
|
+
channelSummary('drums', seq.channels.drums),
|
|
964
|
+
],
|
|
965
|
+
},
|
|
966
|
+
delay: {
|
|
967
|
+
delaySamples: engine.delayLine.delaySamples,
|
|
968
|
+
delayMs: Math.round(engine.delayLine.delaySamples / DEFAULT_SAMPLE_RATE * 1000),
|
|
969
|
+
feedback: engine.delayLine.feedback,
|
|
970
|
+
mix: engine.delayLine.mix,
|
|
971
|
+
},
|
|
972
|
+
sfxQueue: engine.sfxQueue.length,
|
|
973
|
+
supportedSFX: ['chat', 'follow', 'achievement', 'boss', 'raid', 'build', 'discovery'],
|
|
974
|
+
totalSamplesGenerated: engine.totalSamplesGenerated,
|
|
975
|
+
synthesis: {
|
|
976
|
+
oscillators: ['sine', 'square', 'sawtooth', 'triangle', 'noise_white', 'noise_pink'],
|
|
977
|
+
filters: ['lowpass', 'highpass', 'bandpass'],
|
|
978
|
+
envelope: 'ADSR (attack, decay, sustain, release)',
|
|
979
|
+
effects: 'delay line (reverb/echo), soft clipper',
|
|
980
|
+
},
|
|
981
|
+
}, null, 2);
|
|
982
|
+
},
|
|
983
|
+
});
|
|
425
984
|
}
|
|
426
985
|
//# sourceMappingURL=audio-engine.js.map
|
package/dist/tools/audit.js
CHANGED
|
@@ -415,7 +415,7 @@ function formatAuditReport(result) {
|
|
|
415
415
|
// Badge
|
|
416
416
|
const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
|
|
417
417
|
const badgeUrl = `https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor}`;
|
|
418
|
-
lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents,
|
|
418
|
+
lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 787+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
|
|
419
419
|
return lines.join('\n');
|
|
420
420
|
}
|
|
421
421
|
/** Generate a compact one-line summary for social sharing */
|
|
@@ -546,7 +546,7 @@ function formatAuditTerminal(result) {
|
|
|
546
546
|
// Install CTA
|
|
547
547
|
lines.push(chalk.hex(DIM)(' Audited by ') +
|
|
548
548
|
chalk.hex(VIOLET).bold('kbot') +
|
|
549
|
-
chalk.hex(DIM)(' \u2014 35 specialist agents,
|
|
549
|
+
chalk.hex(DIM)(' \u2014 35 specialist agents, 787+ tools, 20 AI providers'));
|
|
550
550
|
lines.push(chalk.hex(DIM)(' Install: ') +
|
|
551
551
|
chalk.hex(WHITE)('npm install -g @kernel.chat/kbot') +
|
|
552
552
|
chalk.hex(DIM)(' | Audit any repo: ') +
|