refactor: 使用flutter对原先的web项目重构,正在进行中

This commit is contained in:
cancerai committed 2026-10-25 00:06:51 +08:00
1 parent 273224f5cf
commit 18017f819a
205 files changed
+13563

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
// 纯合成音效(对应 web 版 audio.js 的 WebAudio 合成),无外部资源。
// 在 Dart 侧生成 16-bit PCM WAV 字节,通过 audioplayers 播放(自带播放池以支持重叠)。
import 'dart:math';
import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart';
class _Voice {
final Float32List samples; // 已含包络、相对自身 0 起点
final int delaySamples;
_Voice(this.samples, this.delaySamples);
}
class Sfx {
static final Sfx instance = Sfx._();
factory Sfx() => instance;
Sfx._();
static bool enabled = true;
static double volume = 0.5;
/// 音频不可用时永久静默。
///
/// 音效纯属装饰,但 audioplayers 的异常会在**别的 Zone 上下文**里冒出来,
/// 现有的 try/catch 抓不到;一旦冒泡到全局错误钩子,整个界面会被切换成错误详情。
/// 因此只要音频子系统出过一次错,就彻底关掉,保证「音频坏掉 ≠ 游戏坏掉」。
static bool broken = false;
final int rate = 44100;
final List<AudioPlayer> _pool = [];
final Map<String, Uint8List> _cache = {};
bool _init = false;
void init() {
if (_init || broken) return;
_init = true;
try {
for (int i = 0; i < 12; i++) {
final p = AudioPlayer();
p.setVolume(volume);
_pool.add(p);
}
} catch (_) {
broken = true;
}
}
void setVolume(double v) {
volume = v;
for (final p in _pool) p.setVolume(v);
}
/// 首次用户手势时解锁音频(移动端要求用户手势后才能播放)。
/// 音效被关闭时不创建播放器,省资源也避免插件噪声。
void unlock() {
if (!enabled || broken) return;
init();
}
Uint8List _render(List<_Voice> voices) {
int maxLen = 0;
for (final v in voices) {
maxLen = max(maxLen, v.delaySamples + v.samples.length);
}
final mix = Float32List(maxLen);
for (final v in voices) {
for (int i = 0; i < v.samples.length; i++) {
mix[v.delaySamples + i] += v.samples[i];
}
}
// 限幅
for (int i = 0; i < mix.length; i++) {
if (mix[i] > 1) mix[i] = 1;
if (mix[i] < -1) mix[i] = -1;
}
return _encodeWav(mix);
}
Float32List _genTone({required double f, double f2 = 0, required double d, String type = 'square', double vol = 0.25, double delay = 0, double decay = 0}) {
final total = d + decay + 0.02;
final n = (total * rate).ceil();
final out = Float32List(n);
final t0 = (delay * rate).round();
final len = (d * rate).round();
final end = ((d + decay) * rate).round();
final k = decay > 0 ? -log(0.0001) / max(1e-4, (d + decay - 0.008)) : 0.0;
double phase = 0;
for (int i = 0; i < n; i++) {
final t = i / rate;
final fr = f2 > 0 ? f * pow(f2 / f, t / max(1e-6, d)) : f;
phase += 2 * pi * fr / rate;
if (phase > 1e9) phase = phase % (2 * pi);
double e;
if (t < 0.008) {
e = t / 0.008;
} else {
e = exp(-k * (t - 0.008));
}
final w = _wave(type, phase);
out[i] = w * vol * e;
}
return out;
}
Float32List _genNoise({required double d, double vol = 0.25, double delay = 0, double hp = 400}) {
final total = d + 0.02;
final n = (total * rate).ceil();
final out = Float32List(n);
final t0 = (delay * rate).round();
final end = (d * rate).round();
final a = hp / (hp + rate);
double hpState = 0, prevX = 0, prevOut = 0;
for (int i = 0; i < n; i++) {
final t = i / rate;
final x = (Random().nextDouble() * 2 - 1) * (1 - i / max(1, n));
final e = (i < end) ? 1.0 : 0.0;
hpState = 0.99 * hpState + x - prevX;
prevX = x;
prevOut = a * (prevOut + hpState);
out[i] = prevOut * vol * e;
}
return out;
}
static double _wave(String type, double phase) {
switch (type) {
case 'sine':
return sin(phase);
case 'triangle':
final f = (phase / (2 * pi)) % 1;
return 4 * (f - 0.5).abs() - 1;
case 'sawtooth':
final f = (phase / (2 * pi)) % 1;
return 2 * f - 1;
case 'square':
default:
return sin(phase) >= 0 ? 1 : -1;
}
}
Uint8List _encodeWav(Float32List samples) {
final n = samples.length;
final bytes = ByteData(44 + n * 2);
final bc = bytes.buffer.asUint8List();
// RIFF
bc.setRange(0, 4, [0x52, 0x49, 0x46, 0x46]); // RIFF
bytes.setUint32(4, 36 + n * 2, Endian.little);
bc.setRange(8, 12, [0x57, 0x41, 0x56, 0x45]); // WAVE
bc.setRange(12, 16, [0x66, 0x6d, 0x74, 0x20]); // fmt
bytes.setUint32(16, 16, Endian.little);
bytes.setUint16(20, 1, Endian.little); // PCM
bytes.setUint16(22, 1, Endian.little); // mono
bytes.setUint32(24, rate, Endian.little);
bytes.setUint32(28, rate * 2, Endian.little);
bytes.setUint16(32, 2, Endian.little);
bytes.setUint16(34, 16, Endian.little);
bc.setRange(36, 40, [0x64, 0x61, 0x74, 0x61]); // data
bytes.setUint32(40, n * 2, Endian.little);
for (int i = 0; i < n; i++) {
int s = (samples[i] * 32767).round().clamp(-32768, 32767);
bytes.setInt16(44 + i * 2, s, Endian.little);
}
return bc;
}
Uint8List _build(String name) {
late List<_Voice> voices;
switch (name) {
case 'jump':
voices = [_v(_genTone(f: 380, f2: 700, d: 0.09, type: 'square', vol: 0.16))];
break;
case 'djump':
voices = [_v(_genTone(f: 520, f2: 900, d: 0.09, type: 'square', vol: 0.14))];
break;
case 'shoot':
voices = [
_v(_genTone(f: 900, f2: 260, d: 0.07, type: 'square', vol: 0.14)),
_v(_genNoise(d: 0.05, vol: 0.06, hp: 2000), 0),
];
break;
case 'death':
voices = [
_v(_genTone(f: 620, f2: 60, d: 0.55, type: 'sawtooth', vol: 0.26)),
_v(_genNoise(d: 0.35, vol: 0.16, hp: 300), (0.02 * rate).round()),
];
break;
case 'save':
voices = [
_v(_genTone(f: 700, d: 0.08, type: 'triangle', vol: 0.2)),
_v(_genTone(f: 1050, d: 0.13, type: 'triangle', vol: 0.18), (0.07 * rate).round()),
];
break;
case 'goal':
voices = [
for (int i = 0; i < 4; i++)
_v(_genTone(f: [523.0, 659.0, 784.0, 1047.0][i], d: 0.16, type: 'triangle', vol: 0.2), (i * 0.1 * rate).round()),
];
break;
case 'warp':
voices = [_v(_genTone(f: 300, f2: 1400, d: 0.2, type: 'sine', vol: 0.18))];
break;
case 'hit':
voices = [_v(_genTone(f: 200, f2: 90, d: 0.08, type: 'square', vol: 0.16))];
break;
case 'bossHit':
voices = [_v(_genTone(f: 260, f2: 140, d: 0.1, type: 'sawtooth', vol: 0.14))];
break;
case 'bossDie':
voices = [
for (int i = 0; i < 5; i++)
_v(_genTone(f: [400.0, 330.0, 260.0, 180.0, 110.0][i], d: 0.22, type: 'sawtooth', vol: 0.22), (i * 0.11 * rate).round()),
_v(_genNoise(d: 0.8, vol: 0.2, hp: 150), (0.1 * rate).round()),
];
break;
case 'menu':
voices = [_v(_genTone(f: 620, d: 0.05, type: 'square', vol: 0.12))];
break;
case 'vanish':
voices = [_v(_genTone(f: 700, f2: 300, d: 0.12, type: 'sine', vol: 0.1))];
break;
case 'troll':
voices = [_v(_genTone(f: 180, f2: 900, d: 0.3, type: 'sine', vol: 0.18))];
break;
case 'splash':
voices = [_v(_genNoise(d: 0.18, vol: 0.12, hp: 800))];
break;
default:
voices = [_v(_genTone(f: 440, d: 0.05, type: 'square', vol: 0.1))];
}
return _render(voices);
}
_Voice _v(Float32List s, [int delaySamples = 0]) => _Voice(s, delaySamples);
void _play(String name) {
if (!enabled || broken) return;
try {
init();
final bytes = _cache.putIfAbsent(name, () => _build(name));
// 找一个空闲播放器
AudioPlayer? free;
for (final p in _pool) {
if (p.state == PlayerState.stopped || p.state == PlayerState.completed) {
free = p;
break;
}
}
free ??= _pool[_lastIdx = (_lastIdx + 1) % _pool.length];
free.stop().catchError((Object _) {
broken = true;
});
free
.play(BytesSource(bytes, mimeType: 'audio/wav'))
.catchError((Object _) {
broken = true;
});
} catch (_) {
// 任何失败都只静音,不影响玩法。
broken = true;
}
}
int _lastIdx = 0;
void jump() => _play('jump');
void djump() => _play('djump');
void shoot() => _play('shoot');
void death() => _play('death');
void save() => _play('save');
void goal() => _play('goal');
void warp() => _play('warp');
void hit() => _play('hit');
void bossHit() => _play('bossHit');
void bossDie() => _play('bossDie');
void menu() => _play('menu');
void vanish() => _play('vanish');
void troll() => _play('troll');
void splash() => _play('splash');
}