返回 DeepSeek-Reasonix
generative-music.ts
根目录 / desktop / frontend / src / lib / generative-music.ts
1 /** Token-driven generative ambient music; opt-in via the "generativeMusicPreset" localStorage key. */
2
3 export type GenerativePreset = "off" | "classic" | "ethereal" | "digital" | "retro";
4
5 type NonEmptyPreset = "classic" | "ethereal" | "digital" | "retro";
6
7 interface PresetConfig {
8 oscillatorType: OscillatorType;
9 attack: number; // 秒
10 decay: number; // 秒
11 sustain: number; // 0-1 电平
12 release: number; // 秒
13 filterFreq: number; // Hz
14 reverbDecay: number; // 秒(脉冲响应长度)
15 reverbWet: number; // 0-1 混合比
16 masterVolume: number; // 0-1
17 }
18
19 const PRESETS: Record<NonEmptyPreset, PresetConfig> = {
20 classic: {
21 oscillatorType: "triangle",
22 attack: 0.01,
23 decay: 0.2,
24 sustain: 0.2,
25 release: 0.3,
26 filterFreq: 2000,
27 reverbDecay: 0.3,
28 reverbWet: 0.2,
29 masterVolume: 0.08,
30 },
31 ethereal: {
32 oscillatorType: "sine",
33 attack: 0.1,
34 decay: 0.4,
35 sustain: 0.4,
36 release: 0.8,
37 filterFreq: 3000,
38 reverbDecay: 0.8,
39 reverbWet: 0.35,
40 masterVolume: 0.07,
41 },
42 digital: {
43 oscillatorType: "square",
44 attack: 0.005,
45 decay: 0.1,
46 sustain: 0.1,
47 release: 0.1,
48 filterFreq: 5000,
49 reverbDecay: 0.05,
50 reverbWet: 0.05,
51 masterVolume: 0.06,
52 },
53 retro: {
54 oscillatorType: "sawtooth",
55 attack: 0.02,
56 decay: 0.3,
57 sustain: 0.3,
58 release: 0.4,
59 filterFreq: 1500,
60 reverbDecay: 0.35,
61 reverbWet: 0.25,
62 masterVolume: 0.08,
63 },
64 };
65
66 // C 大调五声音阶,无半音 → 任意顺序都和谐
67 const SCALE_FREQS = [261.63, 293.66, 349.23, 392.0, 440.0, 523.25, 587.33]; // C4–D5
68 const OCTAVE_FREQS = [130.81, 146.83, 174.61, 196.0, 220.0, 261.63, 293.66]; // C3–D3
69
70 const PRESET_KEY = "generativeMusicPreset";
71
72 function readPresetPref(): GenerativePreset {
73 if (typeof localStorage === "undefined") return "off";
74 const val = localStorage.getItem(PRESET_KEY);
75 if (val === "off" || val === "classic" || val === "ethereal" || val === "digital" || val === "retro") return val;
76 return "off";
77 }
78
79 function writePresetPref(pref: GenerativePreset): void {
80 if (typeof localStorage !== "undefined") {
81 localStorage.setItem(PRESET_KEY, pref);
82 }
83 }
84
85 export function getGenerativePreset(): GenerativePreset {
86 return readPresetPref();
87 }
88
89 export function setGenerativePreset(pref: GenerativePreset): void {
90 writePresetPref(pref);
91 }
92
93 export function isGenerativeMusicEnabled(): boolean {
94 return readPresetPref() !== "off";
95 }
96
97 class GenerativeMusicEngine {
98 private ctx: AudioContext | null = null;
99 private masterGain: GainNode | null = null;
100 private filterNode: BiquadFilterNode | null = null;
101 private reverbNode: ConvolverNode | null = null;
102 private reverbGain: GainNode | null = null; // wet
103 private preset: NonEmptyPreset = "ethereal";
104 private running = false;
105 private lastNoteTime = 0;
106 private tokenToggle = 0;
107
108 start(preset?: NonEmptyPreset): void {
109 if (this.running) return;
110 const p = preset ?? readPresetPref();
111 if (p === "off") return;
112 this.preset = p;
113 this.running = true;
114
115 try {
116 this.ctx = new AudioContext();
117 this.buildSignalChain();
118 } catch (e) {
119 console.warn("generative-music: failed to create AudioContext", e);
120 this.running = false;
121 }
122 }
123
124 stop(): void {
125 if (!this.running) return;
126 this.running = false;
127
128 if (this.masterGain && this.ctx) {
129 try {
130 const now = this.ctx.currentTime;
131 this.masterGain.gain.cancelScheduledValues(now);
132 this.masterGain.gain.setValueAtTime(this.masterGain.gain.value, now);
133 this.masterGain.gain.linearRampToValueAtTime(0, now + 0.3);
134 } catch (e) {
135 console.warn("generative-music: gain ramp failed", e);
136 }
137 }
138
139 const ctx = this.ctx;
140 setTimeout(() => {
141 ctx?.close();
142 }, 500);
143 this.ctx = null;
144 this.masterGain = null;
145 this.filterNode = null;
146 this.reverbNode = null;
147 this.reverbGain = null;
148 }
149
150 setPreset(preset: NonEmptyPreset): void {
151 this.preset = preset;
152 writePresetPref(preset);
153 if (this.ctx && this.masterGain) {
154 this.buildSignalChain();
155 }
156 }
157
158 get isRunning(): boolean {
159 return this.running;
160 }
161
162 /** 双重节流:每 4 个 token + 最小 100ms 间隔,适配 DeepSeek 高频输出。 */
163 playTokenNote(): void {
164 if (!this.running || !this.ctx) return;
165
166 this.tokenToggle++;
167 if (this.tokenToggle % 4 !== 0) return;
168
169 const now = performance.now();
170 if (now - this.lastNoteTime < 100) return;
171 this.lastNoteTime = now;
172
173 const time = this.ctx.currentTime;
174
175 const useLow = Math.random() < 0.15;
176 const freqs = useLow ? OCTAVE_FREQS : SCALE_FREQS;
177 const freq = freqs[Math.floor(Math.random() * freqs.length)];
178
179 const detune = 1 + (Math.random() - 0.5) * 0.1;
180 this.playNote(time, freq * detune);
181 }
182
183 private buildSignalChain(): void {
184 if (!this.ctx) return;
185 const cfg = PRESETS[this.preset];
186
187 if (this.masterGain) this.masterGain.disconnect();
188 this.masterGain = this.ctx.createGain();
189 this.masterGain.gain.value = cfg.masterVolume;
190 this.masterGain.connect(this.ctx.destination);
191
192 if (this.filterNode) this.filterNode.disconnect();
193 this.filterNode = this.ctx.createBiquadFilter();
194 this.filterNode.type = "lowpass";
195 this.filterNode.frequency.value = cfg.filterFreq;
196 this.filterNode.Q.value = 1;
197 this.filterNode.connect(this.masterGain);
198
199 if (this.reverbNode) this.reverbNode.disconnect();
200 if (this.reverbGain) this.reverbGain.disconnect();
201
202 this.reverbNode = this.ctx.createConvolver();
203 this.reverbNode.buffer = this.createReverbImpulse(this.ctx, cfg.reverbDecay);
204 this.reverbGain = this.ctx.createGain();
205 this.reverbGain.gain.value = cfg.reverbWet;
206 this.filterNode.connect(this.reverbNode);
207 this.reverbNode.connect(this.reverbGain);
208 this.reverbGain.connect(this.masterGain);
209 }
210
211 private createReverbImpulse(ctx: AudioContext, duration: number): AudioBuffer {
212 const sampleRate = ctx.sampleRate;
213 const length = Math.max(Math.round(sampleRate * duration), 1);
214 const buffer = ctx.createBuffer(2, length, sampleRate);
215 for (let ch = 0; ch < 2; ch++) {
216 const data = buffer.getChannelData(ch);
217 for (let i = 0; i < length; i++) {
218 const t = i / length;
219 data[i] = (Math.random() * 2 - 1) * Math.pow(1 - t, 1.5) * (1 + Math.sin(t * Math.PI * 3) * 0.3);
220 }
221 }
222 return buffer;
223 }
224
225 private playNote(time: number, freq: number): void {
226 if (!this.ctx) return;
227 const cfg = PRESETS[this.preset];
228 const now = this.ctx.currentTime;
229 const t = Math.max(time, now + 0.005);
230
231 const osc = this.ctx.createOscillator();
232 osc.type = cfg.oscillatorType;
233 osc.frequency.setValueAtTime(freq, t);
234
235 const envGain = this.ctx.createGain();
236 const env = envGain.gain;
237 const noteEnd = t + cfg.attack + cfg.decay + cfg.release + 0.01;
238 env.setValueAtTime(0, t);
239 env.linearRampToValueAtTime(1, t + cfg.attack);
240 env.linearRampToValueAtTime(cfg.sustain, t + cfg.attack + cfg.decay);
241 env.setValueAtTime(cfg.sustain, t + cfg.attack + cfg.decay + 0.01);
242 env.linearRampToValueAtTime(0.001, t + cfg.attack + cfg.decay + 0.01 + cfg.release);
243
244 osc.connect(envGain);
245 if (this.filterNode) {
246 envGain.connect(this.filterNode);
247 } else {
248 envGain.connect(this.ctx.destination);
249 }
250
251 osc.start(t);
252 osc.stop(noteEnd);
253 }
254
255 playPreview(preset: NonEmptyPreset): void {
256 const ctx = new AudioContext();
257 const cfg = PRESETS[preset];
258
259 const filter = ctx.createBiquadFilter();
260 filter.type = "lowpass";
261 filter.frequency.value = cfg.filterFreq;
262
263 const reverb = ctx.createConvolver();
264 reverb.buffer = this.createReverbImpulse(ctx, cfg.reverbDecay);
265 const reverbGain = ctx.createGain();
266 reverbGain.gain.value = cfg.reverbWet;
267
268 const master = ctx.createGain();
269 master.gain.value = cfg.masterVolume * 2;
270 master.connect(ctx.destination);
271
272 filter.connect(master);
273 filter.connect(reverb);
274 reverb.connect(reverbGain);
275 reverbGain.connect(master);
276
277 const notes = [SCALE_FREQS[0], SCALE_FREQS[2], SCALE_FREQS[4], SCALE_FREQS[6]];
278 notes.forEach((freq, i) => {
279 const t = ctx.currentTime + i * 0.05;
280
281 const osc = ctx.createOscillator();
282 osc.type = cfg.oscillatorType;
283 osc.frequency.setValueAtTime(freq, t);
284
285 const env = ctx.createGain();
286 env.gain.setValueAtTime(0, t);
287 env.gain.linearRampToValueAtTime(1, t + cfg.attack);
288 env.gain.linearRampToValueAtTime(cfg.sustain, t + cfg.attack + cfg.decay);
289 env.gain.setValueAtTime(cfg.sustain, t + 0.05);
290 env.gain.linearRampToValueAtTime(0.001, t + cfg.attack + cfg.decay + cfg.release);
291
292 osc.connect(env);
293 env.connect(filter);
294 osc.start(t);
295 osc.stop(t + cfg.attack + cfg.decay + cfg.release + 0.05);
296 });
297
298 setTimeout(() => ctx.close(), 3000);
299 }
300 }
301
302 export const generativeMusic = new GenerativeMusicEngine();
303
303 lines TYPESCRIPT