// FreeCut 程序化配乐生成器 —— 全部在浏览器端合成,无版权风险,人人免费使用 // 每种情绪生成一段可循环的纯音乐,作为内置配乐库 const MusicEngine = (() => { const cache = {}; let _ctx = null; function ctx() { if (!_ctx) _ctx = new (window.AudioContext || window.webkitAudioContext)(); return _ctx; } // 确定性伪随机(保证同一情绪每次生成一样,可循环) function makeRng(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; } // 定义:BPM / 根音(midi) / 调式 / 和弦进行(每小节1个和弦根音, 类型) / 琶音间隔 / 节奏感 const MOODS = { warm: { name: '温馨治愈', bpm: 74, root: 60, scale: 'major', prog: [[60,'maj7'],[65,'maj7'],[67,'maj7'],[62,'maj7']], arp: 'full', feel: 'pad' }, fresh: { name: '清新活力', bpm: 100, root: 62, scale: 'major', prog: [[62,'maj7'],[60,'maj7'],[67,'maj7'],[65,'maj7']], arp: 'up', feel: 'pluck' }, epic: { name: '激昂大片', bpm: 120, root: 57, scale: 'minor', prog: [[57,'min'],[55,'min'],[60,'min'],[55,'min']], arp: 'drive', feel: 'epic' }, night: { name: '深夜思绪', bpm: 70, root: 57, scale: 'minor', prog: [[57,'min7'],[62,'maj7'],[55,'min7'],[60,'maj7']], arp: 'soft', feel: 'lofi' }, tech: { name: '科技未来', bpm: 110, root: 64, scale: 'minor', prog: [[64,'min'],[61,'min'],[66,'min'],[59,'min']], arp: 'seq', feel: 'synth' }, happy: { name: '欢乐节奏', bpm: 118, root: 60, scale: 'major', prog: [[60,'maj'],[65,'maj'],[60,'maj'],[67,'maj']], arp: 'bounce', feel: 'pop' }, }; const MAJOR = [0, 2, 4, 5, 7, 9, 11, 12]; const MINOR = [0, 2, 3, 5, 7, 8, 10, 12]; const CHORD = { 'maj': [0, 4, 7], 'maj7': [0, 4, 7, 11], 'min': [0, 3, 7], 'min7': [0, 3, 7, 10], 'sus': [0, 5, 7], 'dim': [0, 3, 6], }; function scaleOf(mood) { return mood.scale === 'minor' ? MINOR : MAJOR; } function generate(key, bars = 8) { const mood = MOODS[key]; if (!mood) return null; const ac = ctx(); const rng = makeRng(hashStr(key)); const spb = 60 / mood.bpm; // 每拍秒数 const secsPerBar = spb * 4; const total = secsPerBar * bars; const sr = ac.sampleRate; const buf = ac.createBuffer(2, Math.ceil(total * sr), sr); const sc = scaleOf(mood); const rootTone = mood.root; // 和弦根音序列 const prog = mood.prog; const chords = []; for (let i = 0; i < bars; i++) { const [r, type] = prog[i % prog.length]; const tones = CHORD[type].map(iv => r + 12 + iv); // 中音区 chords.push(tones); } // 主旋律音阶池 const pool = sc.map(iv => rootTone + 12 + iv).concat(sc.map(iv => rootTone + 24 + iv)); const pool2 = sc.map(iv => rootTone + 12 + iv); // 旋律事件(每小节2-4个音) const melody = []; for (let b = 0; b < bars; b++) { const n = mood.feel === 'epic' ? 2 : mood.feel === 'lofi' ? 2 : 3 + (rng() > 0.5 ? 1 : 0); for (let i = 0; i < n; i++) { const beatPos = (i / n) * 4; const t = (b + beatPos / 4) * secsPerBar; let note; if (mood.feel === 'epic') { note = chords[b][0] + 12; // 根音高八度,宏大 } else if (mood.feel === 'lofi') { note = pool[Math.floor(rng() * pool.length)]; // 慵懒随机 } else { note = pool2[Math.floor(rng() * pool2.length)]; } const dur = (spb * 4) / n * (0.75 + rng() * 0.4); melody.push({ t, note, dur, vel: 0.5 + rng() * 0.3 }); } } // ---------- 渲染 ---------- const L = buf.getChannelData(0), R = buf.getChannelData(1); const N = L.length; const add = (ch, from, dur, fn) => { const s0 = Math.max(0, Math.floor(from * sr)); const n = Math.min(N - s0, Math.ceil(dur * sr)); for (let i = 0; i < n; i++) { const t = i / sr; const v = fn(t); ch[s0 + i] += v; } }; // 底鼓/军鼓简单节奏(确定性) for (let b = 0; b < bars; b++) { const t0 = b * secsPerBar; // 底鼓:每小节 4 拍 for (let k = 0; k < 4; k++) { const t = t0 + k * spb; add(L, t, 0.25, tt => Math.sin(2 * Math.PI * (90 - tt * 160) * tt) * Math.exp(-tt * 14) * 0.5); add(R, t, 0.25, tt => Math.sin(2 * Math.PI * (90 - tt * 160) * tt) * Math.exp(-tt * 14) * 0.5); } // 军鼓/拍手:2、4拍,激昂时用 if (mood.feel === 'epic' || mood.feel === 'pop' || mood.feel === 'drive') { for (const k of [1, 3]) { const t = t0 + k * spb; const noise = makeNoise(rng()); add(L, t, 0.18, tt => noise(Math.floor(tt * 20000)) * Math.exp(-tt * 30) * 0.35); add(R, t, 0.18, tt => noise(Math.floor(tt * 20000)) * Math.exp(-tt * 30) * 0.35); } } } // 贝斯 for (let b = 0; b < bars; b++) { const chRoot = mood.prog[b % mood.prog.length][0]; for (let k = 0; k < 4; k++) { const t = b * secsPerBar + k * spb; const f = midiToFreq(chRoot - 12); add(L, t, spb * 0.9, tt => Math.sin(2 * Math.PI * f * tt) * Math.exp(-tt * 3.2) * 0.30); add(R, t, spb * 0.9, tt => Math.sin(2 * Math.PI * f * tt) * Math.exp(-tt * 3.2) * 0.30); } } // 和弦铺底 for (let b = 0; b < bars; b++) { const tones = chords[b]; const t = b * secsPerBar; for (const n of tones) { const f = midiToFreq(n); add(L, t, secsPerBar, tt => detuneSaw(tt, f) * env(tt, secsPerBar, 0.8, 0.5) * 0.055); add(R, t, secsPerBar, tt => detuneSaw(tt, f * 1.003) * env(tt, secsPerBar, 0.8, 0.5) * 0.055); } } // 主旋律 for (const m of melody) { const f = midiToFreq(m.note); if (mood.feel === 'epic') { add(L, m.t, m.dur, tt => detuneSaw(tt, f) * env(tt, m.dur, 0.15, 0.6) * m.vel * 0.22); add(R, m.t, m.dur, tt => detuneSaw(tt, f * 1.002) * env(tt, m.dur, 0.15, 0.6) * m.vel * 0.22); } else if (mood.feel === 'lofi') { add(L, m.t, m.dur, tt => softTone(tt, f) * env(tt, m.dur, 0.2, 0.8) * m.vel * 0.35); add(R, m.t, m.dur, tt => softTone(tt, f) * env(tt, m.dur, 0.2, 0.8) * m.vel * 0.35); } else { // 琶音拨弦 add(L, m.t, m.dur, tt => pluck(tt, f) * m.vel * 0.30); add(R, m.t, m.dur, tt => pluck(tt, f * 1.001) * m.vel * 0.30); } } // 归一化 let peak = 0; for (let i = 0; i < N; i++) peak = Math.max(peak, Math.abs(L[i]), Math.abs(R[i])); if (peak > 0.95) { const g = 0.95 / peak; for (let i = 0; i < N; i++) { L[i] *= g; R[i] *= g; } } cache[key] = buf; return buf; } function hashStr(s) { let h = 5381; for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0; return h; } function midiToFreq(m) { return 440 * Math.pow(2, (m - 69) / 12); } function env(t, dur, a, r) { const rel = Math.max(0.05, dur * r); if (t < a) return t / a; if (t > dur) return 0; return Math.max(0, 1 - (t - a) / rel); } function detuneSaw(t, f) { const p = (t * f) % 1; return 2 * p - 1; } function softTone(t, f) { const s = Math.sin(2 * Math.PI * f * t); return 0.6 * s + 0.4 * Math.sin(2 * Math.PI * f * 2 * t) * 0.5; } function pluck(t, f) { const d = Math.sin(2 * Math.PI * f * t) * Math.exp(-t * 6); const h = Math.sin(2 * Math.PI * f * 2.01 * t) * Math.exp(-t * 16) * 0.4; return d + h; } function makeNoise(rng) { const buf = new Float32Array(20000); for (let i = 0; i < buf.length; i++) buf[i] = rng() * 2 - 1; return idx => buf[Math.abs(idx) % buf.length]; } // 获取(生成并缓存)配乐 AudioBuffer function getBuffer(key) { if (cache[key]) return Promise.resolve(cache[key]); // 稍等一帧,避免阻塞 return new Promise(res => setTimeout(() => res(generate(key)), 30)); } function list() { return Object.entries(MOODS).map(([k, v]) => ({ key: k, name: v.name, bpm: v.bpm })); } return { list, getBuffer, generate, ctx }; })();