refactor: 使用flutter对原先的web项目重构,正在进行中
This commit is contained in:
1 parent
273224f5cf
commit
18017f819a
205 files changed
+13563
No files matched your search
@@ -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');
|
||||
}
|
||||
Reference in new issue
Block a user