initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -0,0 +1,372 @@
|
||||
export class SoundSystem {
|
||||
constructor({ muted = false } = {}) {
|
||||
this.muted = muted;
|
||||
this.context = null;
|
||||
this.master = null;
|
||||
this.delaySend = null;
|
||||
this.musicOn = false;
|
||||
this.musicTimer = null;
|
||||
this._musicStep = 0;
|
||||
this._nextNoteTime = 0;
|
||||
this._baseStepDur = 60 / 132 / 4;
|
||||
this._stepDur = this._baseStepDur;
|
||||
this._intensity = 0;
|
||||
this._bossMode = false;
|
||||
this._stepsPerBar = 16;
|
||||
this._progression = [
|
||||
{ root: 73.42, chord: [146.83, 174.61, 220] },
|
||||
{ root: 58.27, chord: [116.54, 146.83, 174.61] },
|
||||
{ root: 87.31, chord: [174.61, 220, 261.63] },
|
||||
{ root: 65.41, chord: [130.81, 164.81, 196] },
|
||||
];
|
||||
this._bossProgression = [
|
||||
{ root: 41.2, chord: [82.41, 98, 123.47] },
|
||||
{ root: 65.41, chord: [130.81, 164.81, 196] },
|
||||
{ root: 55, chord: [110, 130.81, 164.81] },
|
||||
{ root: 73.42, chord: [146.83, 174.61, 220] },
|
||||
];
|
||||
this._arpPattern = [0, 1, 2, 1, 0, 2, 1, 2, 0, 1, 2, 1, 0, 2, 1, 2];
|
||||
this._leadPattern = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[587.33, 0, 698.46, 0, 880, 0, 783.99, 0, 698.46, 0, 659.26, 0, 587.33, 0, 523.25, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 440, 0, 523.25, 0, 587.33, 0, 0, 0],
|
||||
];
|
||||
this._leadPatternB = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[659.26, 0, 880, 0, 987.77, 0, 880, 0, 783.99, 0, 698.46, 0, 659.26, 0, 0, 0],
|
||||
[0, 0, 0, 0, 587.33, 0, 659.26, 0, 698.46, 0, 659.26, 0, 587.33, 0, 0, 0],
|
||||
];
|
||||
this._bassPattern = [1, 0, 1, 0, 1, 0, 1, 0];
|
||||
this._barPlan = { fill: false, breakdown: false, lift: false, leadVariant: 0, bar: -1 };
|
||||
|
||||
const unlock = () => {
|
||||
if (!this.context) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContext) return;
|
||||
this.context = new AudioContext();
|
||||
this.master = this.context.createGain();
|
||||
this.master.gain.value = this.muted ? 0 : 0.5;
|
||||
this.master.connect(this.context.destination);
|
||||
|
||||
const delay = this.context.createDelay(0.6);
|
||||
delay.delayTime.value = 0.31;
|
||||
const feedback = this.context.createGain();
|
||||
feedback.gain.value = 0.35;
|
||||
const wet = this.context.createGain();
|
||||
wet.gain.value = 0.22;
|
||||
delay.connect(feedback);
|
||||
feedback.connect(delay);
|
||||
delay.connect(wet);
|
||||
wet.connect(this.master);
|
||||
this.delaySend = delay;
|
||||
}
|
||||
if (this.context.state === 'suspended') {
|
||||
this.context.resume().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', unlock, { once: false });
|
||||
window.addEventListener('pointerdown', unlock, { once: false });
|
||||
}
|
||||
|
||||
startMusic() {
|
||||
if (this.musicOn) return;
|
||||
this.musicOn = true;
|
||||
this._musicStep = 0;
|
||||
this._nextNoteTime = this.context ? this.context.currentTime + 0.1 : 0;
|
||||
if (this.musicTimer) clearInterval(this.musicTimer);
|
||||
this.musicTimer = setInterval(() => this._scheduleMusic(), 100);
|
||||
}
|
||||
|
||||
stopMusic() {
|
||||
this.musicOn = false;
|
||||
if (this.musicTimer) {
|
||||
clearInterval(this.musicTimer);
|
||||
this.musicTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
setIntensity(intensity) {
|
||||
const clamped = Math.max(0, Math.min(1, intensity));
|
||||
this._intensity = clamped;
|
||||
this._stepDur = this._baseStepDur * (1 - 0.35 * clamped);
|
||||
}
|
||||
|
||||
setBossMode(on) {
|
||||
this._bossMode = on;
|
||||
}
|
||||
|
||||
_scheduleMusic() {
|
||||
if (!this.context || !this.musicOn) return;
|
||||
const aheadTime = this.context.currentTime + 0.5;
|
||||
const totalSteps = this._stepsPerBar * this._progression.length;
|
||||
while (this._nextNoteTime < aheadTime) {
|
||||
try {
|
||||
this._scheduleStep(this._musicStep, this._nextNoteTime);
|
||||
} catch (error) {
|
||||
console.error('Music schedule error', error);
|
||||
}
|
||||
this._nextNoteTime += this._stepDur;
|
||||
this._musicStep = (this._musicStep + 1) % totalSteps;
|
||||
}
|
||||
}
|
||||
|
||||
_rollBarPlan(bar) {
|
||||
const plan = { fill: false, breakdown: false, lift: false, leadVariant: 0, bar };
|
||||
if (bar === 1) {
|
||||
const roll = Math.random();
|
||||
if (roll < 0.2) {
|
||||
plan.fill = true;
|
||||
} else if (roll < 0.32) {
|
||||
plan.breakdown = true;
|
||||
}
|
||||
} else if (bar === 2 || bar === 3) {
|
||||
plan.leadVariant = Math.random() < 0.35 ? 1 : 0;
|
||||
plan.lift = Math.random() < 0.25;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
_scheduleStep(step, time) {
|
||||
const delay = time - this.context.currentTime;
|
||||
const bar = Math.floor(step / this._stepsPerBar) % this._progression.length;
|
||||
const beat = step % this._stepsPerBar;
|
||||
const barDur = this._stepDur * this._stepsPerBar;
|
||||
const intensity = this._intensity;
|
||||
const drumGain = 1 + intensity * 0.6;
|
||||
|
||||
if (beat === 0 || this._barPlan.bar !== bar) {
|
||||
this._barPlan = this._rollBarPlan(bar);
|
||||
}
|
||||
const plan = this._barPlan;
|
||||
const fill = plan.fill;
|
||||
const breakdown = plan.breakdown;
|
||||
const boss = this._bossMode;
|
||||
|
||||
let { root, chord } = boss
|
||||
? this._bossProgression[bar]
|
||||
: this._progression[bar];
|
||||
if (plan.lift && bar === 2 && !boss) {
|
||||
chord = [174.61, 207.65, 261.63];
|
||||
}
|
||||
|
||||
if (boss && beat === 0) {
|
||||
this.tone(27.5, 'sine', barDur, 0.06, 27.5, delay);
|
||||
}
|
||||
|
||||
if (beat === 0) {
|
||||
for (const note of chord) {
|
||||
this.tone(note, 'sawtooth', barDur, 0.02 + intensity * 0.01, note, delay);
|
||||
}
|
||||
}
|
||||
|
||||
const kickFreq = boss ? 44 : 55;
|
||||
const snareFreq = boss ? 700 : 1800;
|
||||
|
||||
if (!fill && !breakdown) {
|
||||
if (beat % 4 === 2 && this._bassPattern[Math.floor(beat / 4)]) {
|
||||
this.tone(root, 'square', this._stepDur * 1.5, 0.055, root, delay);
|
||||
}
|
||||
if (intensity > 0.4 && beat % 2 === 0) {
|
||||
this.tone(root * 2, 'square', this._stepDur * 0.9, 0.02, root * 2, delay);
|
||||
}
|
||||
if (beat % 4 === 0) {
|
||||
this.tone(kickFreq, 'sine', 0.14, 0.1 * drumGain, Math.max(28, kickFreq - 18), delay);
|
||||
}
|
||||
if (beat === 4 || beat === 12) {
|
||||
this.noise(0.1, 0.07 * drumGain, snareFreq, delay);
|
||||
}
|
||||
if (beat % 4 === 2) {
|
||||
this.noise(0.03, 0.02 * drumGain, 9000, delay);
|
||||
}
|
||||
if (beat % 4 === 3 || beat % 4 === 1) {
|
||||
this.noise(0.02, 0.012 * drumGain, 10000, delay);
|
||||
}
|
||||
|
||||
if (beat % 4 === 1 || beat % 4 === 3) {
|
||||
const arp = chord[this._arpPattern[beat]];
|
||||
this.tone(arp, 'triangle', 0.1, 0.018, arp, delay, this.delaySend);
|
||||
this.tone(arp * 2, 'triangle', 0.12, 0.012, arp * 2, delay);
|
||||
}
|
||||
} else if (fill) {
|
||||
if (beat % 2 === 0) {
|
||||
this.tone(55, 'sine', 0.1, 0.06, 34, delay);
|
||||
}
|
||||
if (beat >= 12) {
|
||||
this.noise(0.06, 0.045, 1500, delay);
|
||||
}
|
||||
if (beat % 2 === 1) {
|
||||
this.noise(0.03, 0.018, 8500, delay);
|
||||
}
|
||||
if (beat % 3 === 0) {
|
||||
this.noise(0.05, 0.012, 400 + beat * 160, delay);
|
||||
}
|
||||
const index = beat >= 12 ? (beat - 12) % 3 : Math.floor(beat / 2) % 3;
|
||||
const note = chord[index] * (beat >= 12 ? 4 : 2);
|
||||
this.tone(note, 'triangle', 0.09, 0.02, note, delay);
|
||||
} else if (breakdown) {
|
||||
if (beat === 0 || beat === 8) {
|
||||
this.tone(55, 'sine', 0.18, 0.08, 32, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fill && !breakdown) {
|
||||
const lead = (plan.leadVariant === 1 ? this._leadPatternB : this._leadPattern)[bar][beat];
|
||||
if (lead > 0) {
|
||||
this.tone(lead, 'sawtooth', 0.22, 0.03, lead, delay, this.delaySend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setMuted(muted) {
|
||||
this.muted = muted;
|
||||
if (this.master && this.context) {
|
||||
this.master.gain.setTargetAtTime(muted ? 0 : 0.5, this.context.currentTime, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
toggleMuted() {
|
||||
this.setMuted(!this.muted);
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
tone(freq, type, duration, gain, glideTo = freq, delay = 0, bus = this.master) {
|
||||
if (!this.context || this.muted) return;
|
||||
if (!Number.isFinite(freq) || !Number.isFinite(glideTo)) return;
|
||||
const { context } = this;
|
||||
const start = context.currentTime + delay;
|
||||
const oscillator = context.createOscillator();
|
||||
const shape = context.createGain();
|
||||
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(Math.max(1, freq), start);
|
||||
oscillator.frequency.exponentialRampToValueAtTime(Math.max(1, glideTo), start + duration);
|
||||
|
||||
shape.gain.setValueAtTime(0.0001, start);
|
||||
shape.gain.exponentialRampToValueAtTime(gain, start + 0.008);
|
||||
shape.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||
|
||||
oscillator.connect(shape);
|
||||
shape.connect(bus || this.master);
|
||||
oscillator.start(start);
|
||||
oscillator.stop(start + duration + 0.02);
|
||||
}
|
||||
|
||||
noise(duration, gain, filterFreq, delay = 0, bus = this.master) {
|
||||
if (!this.context || this.muted) return;
|
||||
const { context } = this;
|
||||
const start = context.currentTime + delay;
|
||||
const bufferSize = Math.max(1, Math.ceil(context.sampleRate * duration));
|
||||
const buffer = context.createBuffer(1, bufferSize, context.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i += 1) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = filterFreq;
|
||||
filter.Q.value = 1.2;
|
||||
|
||||
const shape = context.createGain();
|
||||
shape.gain.setValueAtTime(0.0001, start);
|
||||
shape.gain.exponentialRampToValueAtTime(gain, start + 0.005);
|
||||
shape.gain.exponentialRampToValueAtTime(0.0001, start + duration);
|
||||
|
||||
source.connect(filter);
|
||||
filter.connect(shape);
|
||||
shape.connect(bus || this.master);
|
||||
source.start(start);
|
||||
source.stop(start + duration + 0.02);
|
||||
}
|
||||
|
||||
shoot() {
|
||||
this.tone(240, 'square', 0.1, 0.075, 85);
|
||||
this.noise(0.05, 0.028, 3200);
|
||||
}
|
||||
|
||||
hit() {
|
||||
this.noise(0.12, 0.16, 1500);
|
||||
this.tone(180, 'triangle', 0.1, 0.12, 120);
|
||||
}
|
||||
|
||||
death(big = false) {
|
||||
if (big) {
|
||||
this.noise(0.35, 0.22, 700);
|
||||
this.tone(120, 'sawtooth', 0.3, 0.2, 45);
|
||||
this.tone(90, 'sine', 0.4, 0.24, 30);
|
||||
return;
|
||||
}
|
||||
this.noise(0.2, 0.14, 500);
|
||||
this.tone(140, 'triangle', 0.22, 0.16, 55);
|
||||
}
|
||||
|
||||
damage() {
|
||||
this.tone(160, 'sawtooth', 0.2, 0.18, 80);
|
||||
this.noise(0.15, 0.1, 900);
|
||||
}
|
||||
|
||||
explosion() {
|
||||
this.noise(0.7, 0.3, 400);
|
||||
this.tone(200, 'sawtooth', 0.6, 0.22, 30);
|
||||
this.tone(60, 'sine', 0.8, 0.3, 20);
|
||||
}
|
||||
|
||||
pickup() {
|
||||
this.tone(660, 'sine', 0.12, 0.14, 660);
|
||||
this.tone(990, 'sine', 0.18, 0.14, 990, 0.09);
|
||||
}
|
||||
|
||||
stingerStart() {
|
||||
this.tone(392, 'triangle', 0.12, 0.1);
|
||||
this.tone(523, 'triangle', 0.12, 0.1, undefined, 0.1);
|
||||
this.tone(659, 'triangle', 0.3, 0.12, undefined, 0.2);
|
||||
}
|
||||
|
||||
bossEntrance() {
|
||||
this.tone(27.5, 'sine', 1.6, 0.22, 27.5);
|
||||
this.tone(110, 'sawtooth', 0.5, 0.1, 55);
|
||||
this.noise(0.5, 0.12, 250);
|
||||
this.tone(82.41, 'square', 0.25, 0.08, 82.41, 0.7);
|
||||
this.tone(61.74, 'square', 0.4, 0.08, 61.74, 0.95);
|
||||
}
|
||||
|
||||
playGameOverMusic() {
|
||||
this.stopMusic();
|
||||
this._playGameOverSeq();
|
||||
}
|
||||
|
||||
_playGameOverSeq() {
|
||||
if (!this.context) return;
|
||||
const now = this.context.currentTime;
|
||||
const t = (offset) => Math.max(0, offset - now);
|
||||
|
||||
this.tone(55, 'sine', 8, 0.05, 51, t(0));
|
||||
this.tone(55, 'sine', 8, 0.03, 55, t(4));
|
||||
|
||||
const chords = [
|
||||
[110, 164.81, 220],
|
||||
[98, 146.83, 196],
|
||||
[87.31, 130.81, 174.61],
|
||||
[82.41, 123.47, 164.81],
|
||||
];
|
||||
chords.forEach((chord, i) => {
|
||||
const start = 0.2 + i * 1.5;
|
||||
for (const note of chord) {
|
||||
this.tone(note, 'sawtooth', 1.5, 0.018, note, t(start));
|
||||
}
|
||||
if (i === 0 || i === 2) {
|
||||
this.tone(55, 'sine', 0.14, 0.09, 32, t(start));
|
||||
}
|
||||
});
|
||||
|
||||
const melody = [880, 783.99, 698.46, 659.26, 587.33, 523.25, 493.88, 440];
|
||||
melody.forEach((note, i) => {
|
||||
this.tone(note, 'triangle', 0.7, 0.032, note, t(0.5 + i * 0.55), this.delaySend);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user