Files
my_site/vue/public/race/audio.js
yoyuzh d669738967 init
2026-02-27 14:29:05 +08:00

179 lines
6.4 KiB
JavaScript

'use strict';
///////////////////////////////////////////////////////////////////////////////
// Audio settings
let soundEnable = 1;
let soundVolume = .3;
///////////////////////////////////////////////////////////////////////////////
class Sound
{
constructor(zzfxSound)
{
if (!soundEnable) return;
// generate zzfx sound now for fast playback
this.randomness = zzfxSound[1] || 0;
this.samples = zzfxG(...zzfxSound);
}
play(volume=1, pitch=1)
{
if (!soundEnable) return;
// play the sound
const playbackRate = pitch + this.randomness*rand(-pitch,pitch);
return playSamples(this.samples, volume, playbackRate);
}
playNote(semitoneOffset, pos, volume)
{ return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
}
///////////////////////////////////////////////////////////////////////////////
let audioContext;
function playSamples(samples, volume, rate)
{
const sampleRate=zzfxR;
if (!soundEnable || isTouchDevice && !audioContext)
return;
if (!audioContext)
audioContext = new AudioContext; // create audio context
// prevent sounds from building up if they can't be played
if (audioContext.state != 'running')
{
// fix stalled audio
audioContext.resume();
return; // prevent suspended sounds from building up
}
// create buffer and source
const buffer = audioContext.createBuffer(1, samples.length, sampleRate),
source = audioContext.createBufferSource();
// copy samples to buffer and setup source
buffer.getChannelData(0).set(samples);
source.buffer = buffer;
source.playbackRate.value = rate;
// create and connect gain node (createGain is more widely spported then GainNode construtor)
const gainNode = audioContext.createGain();
gainNode.gain.value = soundVolume*volume;
gainNode.connect(audioContext.destination);
// connect source to stereo panner and gain
//source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
source.connect(gainNode);
// play and return sound
source.start();
return source;
}
///////////////////////////////////////////////////////////////////////////////
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
const zzfxR = 44100;
function zzfxG
(
// parameters
volume = 1, randomness, frequency = 220, attack = 0, sustain = 0,
release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
)
{
// init parameters
let PI2 = PI*2, sampleRate = zzfxR,
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
startFrequency = frequency *= PI2 / sampleRate, // no randomness
// rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
// biquad LP/HP filter
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
b0 = (1 + sign(filter) * cos) / 2 / a0,
b1 = -(sign(filter) + cos) / a0, b2 = b0,
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
// scale by sample rate
attack = attack * sampleRate + 9; // minimum attack to prevent pop
decay *= sampleRate;
sustain *= sampleRate;
release *= sampleRate;
delay *= sampleRate;
deltaSlide *= 500 * PI2 / sampleRate**3;
modulation *= PI2 / sampleRate;
pitchJump *= PI2 / sampleRate;
pitchJumpTime *= sampleRate;
repeatTime = repeatTime * sampleRate | 0;
ASSERT(shape != 3 && shape != 2); // need save space
// generate waveform
for(length = attack + decay + sustain + release + delay | 0;
i < length; b[i++] = s * volume) // sample
{
if (!(++c%(bitCrush*100|0))) // bit crush
{
s = shape? shape>1?
//shape>2? shape>3? // wave shape
//Math.sin(t**3) : // 4 noise
//clamp(Math.tan(t),1,-1): // 3 tan
1-(2*t/PI2%2+2)%2: // 2 saw
1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
Math.sin(t); // 0 sin
s = (repeatTime ?
1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
: 1) *
sign(s)*(abs(s)**shapeCurve) * // curve
(i < attack ? i/attack : // attack
i < attack + decay ? // decay
1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
i < attack + decay + sustain ? // sustain
sustainVolume : // sustain volume
i < length - delay ? // release
(length - i - delay)/release * // release falloff
sustainVolume : // release volume
0); // post release
s = delay ? s/2 + (delay > i ? 0 : // delay
(i<length-delay? 1 : (length-i)/delay) * // release delay
b[i-delay|0]/2/volume) : s; // sample delay
if (filter) // apply filter
s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
}
f = (frequency += slide += deltaSlide) *// frequency
Math.cos(modulation*tm++); // modulation
t += f + f*noise*Math.sin(i**5); // noise
if (j && ++j > pitchJumpTime) // pitch jump
{
frequency += pitchJump; // apply pitch jump
startFrequency += pitchJump; // also apply to start
j = 0; // stop pitch jump time
}
if (repeatTime && !(++r % repeatTime)) // repeat
{
frequency = startFrequency; // reset frequency
slide = startSlide; // reset slide
j = j || 1; // reset pitch jump time
}
}
return b;
}