feat(audio): 重写BGM为轻快chiptune风格,BPM128 C大调,4层结构(鼓+低音+旋律+装饰)

This commit is contained in:
Cloud Bot
2026-03-24 08:04:26 +00:00
parent 08a3612ba1
commit e922c8fdf6

View File

@@ -230,8 +230,9 @@ export class AudioEngine {
// ─── 背景音乐 ────────────────────────────────────────────────────────────────
/**
* 启动背景音乐(办公室 ambient低频嗡鸣 + 轻微节奏脉冲)
* 完全程序化合成,循环播放
* 启动背景音乐
* 风格:轻快 chiptune / 像素塔防BPM=128C大调4层结构
* 完全程序化合成,无音频文件依赖
*/
startBGM(): void {
if (!this.ac || this.bgmRunning) return
@@ -249,41 +250,170 @@ export class AudioEngine {
private _scheduleBGM(): void {
if (!this.ac || !this.bgmRunning) return
const t = this.ac.currentTime
const ac = this.ac
const t = ac.currentTime
// 底层办公室嗡鸣(电脑风扇感)
const droneGain = this.ac.createGain()
droneGain.gain.value = 0.04
droneGain.connect(this.masterGain)
const drone = this.ac.createOscillator()
drone.type = 'sawtooth'
drone.frequency.setValueAtTime(55, t)
drone.frequency.linearRampToValueAtTime(58, t + 8)
drone.frequency.linearRampToValueAtTime(55, t + 16)
drone.connect(droneGain)
drone.start(t)
drone.stop(t + 16)
this.bgmNodes.push(drone)
const BPM = 128
const beat = 60 / BPM // 0.469s / 拍
const bar = beat * 4 // 1.875s / 小节
const BARS = 8 // 循环 8 小节
const loopLen = bar * BARS // ≈ 15s
// 低频键盘噪声节拍(每 0.8s 一拍)
for (let i = 0; i < 20; i++) {
const beat = t + i * 0.8
const ng = this.ac.createGain()
ng.gain.setValueAtTime(0.03, beat)
ng.gain.exponentialRampToValueAtTime(0.001, beat + 0.15)
ng.connect(this.masterGain)
const ns = this.ac.createOscillator()
ns.type = 'square'
ns.frequency.value = i % 4 === 0 ? 130 : i % 2 === 0 ? 110 : 87
ns.connect(ng)
ns.start(beat)
ns.stop(beat + 0.12)
this.bgmNodes.push(ns)
// ── 辅助:创建带 Gain 的振荡器,自动连接到 masterGain ──────────────────
const addOsc = (
type: OscillatorType,
freq: number,
start: number,
end: number,
vol: number,
freqEnd?: number
): void => {
const g = ac.createGain()
g.gain.setValueAtTime(vol, start)
g.gain.exponentialRampToValueAtTime(0.0001, end)
g.connect(this.masterGain)
const o = ac.createOscillator()
o.type = type
o.frequency.setValueAtTime(freq, start)
if (freqEnd !== undefined) {
o.frequency.linearRampToValueAtTime(freqEnd, end)
}
o.connect(g)
o.start(start)
o.stop(end)
this.bgmNodes.push(o)
}
// 16s 后循环
// ── 辅助创建打击音noise burst ────────────────────────────────────
const addDrum = (start: number, dur: number, vol: number, hp = 0): void => {
const bufLen = Math.ceil(ac.sampleRate * dur)
const buf = ac.createBuffer(1, bufLen, ac.sampleRate)
const d = buf.getChannelData(0)
for (let i = 0; i < bufLen; i++) d[i] = Math.random() * 2 - 1
const src = ac.createBufferSource()
src.buffer = buf
const g = ac.createGain()
g.gain.setValueAtTime(vol, start)
g.gain.exponentialRampToValueAtTime(0.0001, start + dur)
if (hp > 0) {
const f = ac.createBiquadFilter()
f.type = 'highpass'
f.frequency.value = hp
src.connect(f); f.connect(g)
} else {
src.connect(g)
}
g.connect(this.masterGain)
src.start(start)
src.stop(start + dur)
}
// ── 音符频率表C4 = 261.63 Hz ──────────────────────────────────────
// C4 D4 E4 F4 G4 A4 B4 C5 D5 E5 G5
const N: Record<string, number> = {
C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23,
G4: 392.00, A4: 440.00, B4: 493.88,
C5: 523.25, D5: 587.33, E5: 659.25, G5: 783.99,
// 低八度
C3: 130.81, E3: 164.81, F3: 174.61, G3: 196.00, A3: 220.00,
}
// ═══════════════════════════════════════════════════════════════
// 层 1底鼓 + 军鼓kick on 1,3snare on 2,4
// ═══════════════════════════════════════════════════════════════
for (let b = 0; b < BARS * 4; b++) {
const st = t + b * beat
const pos = b % 4 // 每小节内位置 0-3
if (pos === 0 || pos === 2) {
// Kick低频 sine 扫频100→30Hz+ noise burst
addOsc('sine', 100, st, st + 0.18, 0.45, 30)
addDrum(st, 0.12, 0.12, 0)
}
if (pos === 1 || pos === 3) {
// Snare中频 noise + 轻微 sine
addDrum(st, 0.1, 0.18, 800)
addOsc('sine', 220, st, st + 0.07, 0.08)
}
// Hi-hat 每半拍8 分音符)
addDrum(st, 0.05, 0.07, 6000)
addDrum(st + beat / 2, 0.04, 0.05, 6000)
}
// ═══════════════════════════════════════════════════════════════
// 层 2和弦低音线 (square, 低八度)
// 进行C - F - G - C - Am - F - G - C
// ═══════════════════════════════════════════════════════════════
const bassChord = [
N.C3, N.F3, N.G3, N.C3,
N.A3, N.F3, N.G3, N.C3,
]
for (let b = 0; b < BARS; b++) {
const st = t + b * bar
const freq = bassChord[b]
// 每小节 4 拍低音 arpeggio根音每拍重复
for (let i = 0; i < 4; i++) {
addOsc('square', freq, st + i * beat, st + i * beat + beat * 0.8, 0.1)
}
// 五度音(和声感)
addOsc('triangle', freq * 1.5, st, st + bar * 0.9, 0.035)
}
// ═══════════════════════════════════════════════════════════════
// 层 3主旋律triangle亮而柔和8小节 melody
// 轻快的 C 大调旋律8小节/32拍
// ═══════════════════════════════════════════════════════════════
// 每项:[频率, 持续拍数]
const melody: [number, number][] = [
// 小节 1-2上行跳跃
[N.E4, 0.5], [N.G4, 0.5], [N.C5, 1], [N.B4, 0.5], [N.A4, 0.5],
[N.G4, 1], [N.A4, 0.5], [N.G4, 0.5],
// 小节 3-4中段装饰
[N.E4, 0.5], [N.F4, 0.5], [N.G4, 1], [N.E4, 0.5], [N.D4, 0.5],
[N.C4, 1], [N.D4, 0.5], [N.E4, 0.5],
// 小节 5-6高潮段
[N.G4, 0.5], [N.A4, 0.5], [N.C5, 1], [N.D5, 0.5], [N.C5, 0.5],
[N.B4, 1], [N.A4, 0.5], [N.G4, 0.5],
// 小节 7-8收尾回落
[N.E4, 0.5], [N.D4, 0.5], [N.C4, 0.5], [N.E4, 0.5],
[N.G4, 0.5], [N.F4, 0.5], [N.E4, 0.5], [N.D4, 0.5],
[N.C4, 1.5], [0, 0.5],
]
let melodyT = t
for (const [freq, dur] of melody) {
const durSec = dur * beat
if (freq > 0) {
addOsc('triangle', freq, melodyT, melodyT + durSec * 0.85, 0.18)
// 加一层八度泛音(更亮更饱满)
addOsc('sine', freq * 2, melodyT, melodyT + durSec * 0.6, 0.06)
}
melodyT += durSec
}
// ═══════════════════════════════════════════════════════════════
// 层 4装饰音高八度 sine 短促音符,活跃感)
// 每2小节出现一次 arpegio 装饰
// ═══════════════════════════════════════════════════════════════
const decoPattern = [
// 小节2结尾装饰
{ st: t + bar * 1.75, notes: [N.C5, N.E5, N.G5] },
// 小节4结尾装饰
{ st: t + bar * 3.75, notes: [N.G4, N.C5, N.E5] },
// 小节6结尾装饰
{ st: t + bar * 5.75, notes: [N.C5, N.D5, N.E5] },
]
for (const { st, notes } of decoPattern) {
notes.forEach((freq, i) => {
addOsc('sine', freq, st + i * 0.08, st + i * 0.08 + 0.12, 0.1)
})
}
// 循环调度
setTimeout(() => {
if (this.bgmRunning) this._scheduleBGM()
}, 15500)
if (this.bgmRunning) {
this.bgmNodes = []
this._scheduleBGM()
}
}, (loopLen - 0.3) * 1000)
}
}