Files
ChronicleOfTheImmortalClan/src/renderer/ui/sound.ts
T
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
2026-08-23 13:23:25 +08:00

121 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
import { RngHub } from '../game/engine/kernel/rng'
let ctx: AudioContext | null = null
let enabled = true
export function setSoundEnabled(on: boolean): void {
enabled = on
}
export function isSoundEnabled(): boolean {
return enabled
}
function ac(): AudioContext | null {
if (!enabled) return null
try {
if (!ctx) {
const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext
ctx = new AC()
}
if (ctx.state === 'suspended') void ctx.resume()
return ctx
} catch {
return null
}
}
function tone(freq: number, dur: number, gain: number, type: OscillatorType = 'sine', when = 0, slideTo?: number): void {
const a = ac()
if (!a) return
const t0 = a.currentTime + when
const o = a.createOscillator()
const g = a.createGain()
o.type = type
o.frequency.setValueAtTime(freq, t0)
if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur)
g.gain.setValueAtTime(0, t0)
g.gain.linearRampToValueAtTime(gain, t0 + 0.012)
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
o.connect(g)
g.connect(a.destination)
o.start(t0)
o.stop(t0 + dur + 0.05)
}
function noise(dur: number, gain: number, when = 0, freq = 3200): void {
const a = ac()
if (!a) return
const t0 = a.currentTime + when
const len = Math.floor(a.sampleRate * dur)
const buf = a.createBuffer(1, len, a.sampleRate)
const data = buf.getChannelData(0)
for (let i = 0; i < len; i++) data[i] = (RngHub.audioNoise01() * 2 - 1) * (1 - i / len)
const src = a.createBufferSource()
src.buffer = buf
const f = a.createBiquadFilter()
f.type = 'highpass'
f.frequency.value = freq
const g = a.createGain()
g.gain.setValueAtTime(gain, t0)
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
src.connect(f)
f.connect(g)
g.connect(a.destination)
src.start(t0)
}
/** 月度推进:极轻的硖墨声 */
export function sTick(): void {
noise(0.05, 0.015, 0, 5000)
}
/** 开卷 / 纸笺 */
export function sPaper(): void {
noise(0.12, 0.03, 0, 2400)
tone(620, 0.1, 0.015, 'sine', 0.02)
}
/** 好消息:铜钟一声 */
export function sGood(): void {
tone(660, 0.5, 0.06, 'sine')
tone(990, 0.42, 0.03, 'sine', 0.05)
tone(1320, 0.3, 0.018, 'sine', 0.09)
}
/** 坏消息:折磬低响 */
export function sBad(): void {
tone(196, 0.4, 0.05, 'triangle', 0, 130)
tone(131, 0.5, 0.04, 'triangle', 0.04, 82)
}
/** 战报:鼓点急雨 */
export function sWar(): void {
tone(120, 0.16, 0.07, 'sine', 0, 60)
tone(120, 0.16, 0.06, 'sine', 0.16, 60)
tone(160, 0.3, 0.06, 'sine', 0.32, 80)
noise(0.28, 0.02, 0.36, 1500)
}
/** 钟鸣:大事件(碑立/飞升) */
export function sBell(): void {
tone(392, 1.4, 0.06, 'sine')
tone(494, 1.2, 0.035, 'sine', 0.08)
tone(587, 1.0, 0.028, 'sine', 0.16)
}
/** 突破:磬缶清越 */
export function sGong(): void {
tone(440, 0.9, 0.05, 'sine')
tone(660, 0.7, 0.03, 'sine', 0.06)
}
let lastClick = 0
/** 按钮:纸面轻叩(80ms 节流防连点爆 buffer */
export function sClick(): void {
const now = performance.now()
if (now - lastClick < 80) return
lastClick = now
noise(0.045, 0.03, 0, 4200)
}