Regenerate softer orchestral bgm
This commit is contained in:
@@ -15,7 +15,7 @@ Build a small complete tactical RPG loop:
|
|||||||
- Vite, TypeScript, and Phaser project foundation
|
- Vite, TypeScript, and Phaser project foundation
|
||||||
- Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio
|
- Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio
|
||||||
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
|
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
|
||||||
- File-based original orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
|
- File-based original soft stereo orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
|
||||||
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, and grayscale acted-unit feedback
|
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, and grayscale acted-unit feedback
|
||||||
- Flow verification script from title to first battle
|
- Flow verification script from title to first battle
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
const sampleRate = 32000;
|
const sampleRate = 44100;
|
||||||
|
const channels = 2;
|
||||||
const outputDir = 'src/assets/audio/bgm';
|
const outputDir = 'src/assets/audio/bgm';
|
||||||
|
|
||||||
mkdirSync(outputDir, { recursive: true });
|
mkdirSync(outputDir, { recursive: true });
|
||||||
@@ -10,84 +11,88 @@ const tracks = [
|
|||||||
{
|
{
|
||||||
name: 'title-theme',
|
name: 'title-theme',
|
||||||
root: 50,
|
root: 50,
|
||||||
tempo: 64,
|
tempo: 74,
|
||||||
bars: 12,
|
bars: 12,
|
||||||
|
mood: 'open',
|
||||||
progression: [
|
progression: [
|
||||||
[0, 5, 9],
|
[0, 7, 12],
|
||||||
|
[5, 9, 14],
|
||||||
[2, 7, 11],
|
[2, 7, 11],
|
||||||
[-3, 2, 7],
|
[-3, 4, 9]
|
||||||
[0, 5, 12]
|
|
||||||
],
|
],
|
||||||
melody: [12, 14, 16, 19, 21, 19, 16, 14],
|
melody: [12, 16, 19, 21, 19, 16, 14, 12],
|
||||||
warmth: 1,
|
harp: [0, 7, 12, 19, 12, 7, 5, 12]
|
||||||
motion: 0.45
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'story-dark',
|
name: 'story-dark',
|
||||||
root: 45,
|
root: 45,
|
||||||
tempo: 56,
|
tempo: 62,
|
||||||
bars: 10,
|
bars: 10,
|
||||||
|
mood: 'quiet',
|
||||||
progression: [
|
progression: [
|
||||||
[0, 3, 7],
|
[0, 7, 10],
|
||||||
[-5, 0, 5],
|
[-5, 2, 7],
|
||||||
[-2, 2, 7],
|
[-3, 4, 9],
|
||||||
[-7, 0, 3]
|
[-7, 0, 7]
|
||||||
],
|
],
|
||||||
melody: [10, 7, 5, 3, 5, 7],
|
melody: [10, 7, 5, 3, 5, 7],
|
||||||
warmth: 0.72,
|
harp: [0, 7, 10, 14, 10, 7]
|
||||||
motion: 0.18
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'oath-theme',
|
name: 'oath-theme',
|
||||||
root: 52,
|
root: 52,
|
||||||
tempo: 60,
|
tempo: 68,
|
||||||
bars: 12,
|
bars: 12,
|
||||||
|
mood: 'warm',
|
||||||
progression: [
|
progression: [
|
||||||
[0, 5, 9],
|
[0, 7, 12],
|
||||||
[4, 9, 12],
|
[4, 9, 16],
|
||||||
[5, 9, 14],
|
[5, 12, 17],
|
||||||
[2, 7, 11]
|
[2, 9, 14]
|
||||||
],
|
],
|
||||||
melody: [12, 16, 19, 21, 24, 21, 19, 16],
|
melody: [12, 16, 19, 21, 24, 21, 19, 16],
|
||||||
warmth: 1,
|
harp: [0, 7, 12, 16, 19, 16, 12, 7]
|
||||||
motion: 0.32
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'militia-theme',
|
name: 'militia-theme',
|
||||||
root: 48,
|
root: 48,
|
||||||
tempo: 72,
|
tempo: 84,
|
||||||
bars: 12,
|
bars: 12,
|
||||||
|
mood: 'march',
|
||||||
progression: [
|
progression: [
|
||||||
[0, 5, 9],
|
[0, 7, 12],
|
||||||
[5, 9, 12],
|
[5, 12, 17],
|
||||||
[7, 11, 14],
|
[7, 14, 19],
|
||||||
[2, 7, 11]
|
[2, 9, 14]
|
||||||
],
|
],
|
||||||
melody: [12, 14, 16, 19, 16, 14, 12, 14],
|
melody: [12, 14, 16, 19, 16, 14, 12, 14],
|
||||||
warmth: 0.9,
|
harp: [0, 7, 12, 7, 5, 12, 7, 12]
|
||||||
motion: 0.5
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'battle-prep',
|
name: 'battle-prep',
|
||||||
root: 43,
|
root: 43,
|
||||||
tempo: 68,
|
tempo: 78,
|
||||||
bars: 12,
|
bars: 12,
|
||||||
|
mood: 'tense',
|
||||||
progression: [
|
progression: [
|
||||||
[0, 3, 7],
|
[0, 7, 10],
|
||||||
[2, 7, 10],
|
[2, 9, 14],
|
||||||
[-2, 3, 7],
|
[-2, 5, 10],
|
||||||
[-5, 0, 5]
|
[-5, 2, 7]
|
||||||
],
|
],
|
||||||
melody: [12, 15, 17, 19, 17, 15, 12, 10],
|
melody: [12, 15, 17, 19, 17, 15, 12, 10],
|
||||||
warmth: 0.82,
|
harp: [0, 7, 10, 15, 10, 7, 2, 10]
|
||||||
motion: 0.42
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
const buffer = buildTrack(track);
|
const buffer = buildTrack(track);
|
||||||
normalize(buffer, 0.48);
|
removeLowRumble(buffer, 72);
|
||||||
|
applySmallRoom(buffer);
|
||||||
|
normalize(buffer, 0.34);
|
||||||
applySoftLimiter(buffer);
|
applySoftLimiter(buffer);
|
||||||
|
smoothEdges(buffer, Math.floor(sampleRate * 1.6));
|
||||||
|
|
||||||
const fileName = join(outputDir, `${track.name}.wav`);
|
const fileName = join(outputDir, `${track.name}.wav`);
|
||||||
writeFileSync(fileName, encodeWav(buffer));
|
writeFileSync(fileName, encodeWav(buffer));
|
||||||
console.log(`Generated ${fileName}`);
|
console.log(`Generated ${fileName}`);
|
||||||
@@ -97,113 +102,264 @@ function buildTrack(track) {
|
|||||||
const beat = 60 / track.tempo;
|
const beat = 60 / track.tempo;
|
||||||
const bar = beat * 4;
|
const bar = beat * 4;
|
||||||
const duration = bar * track.bars;
|
const duration = bar * track.bars;
|
||||||
const buffer = new Float32Array(Math.ceil(duration * sampleRate));
|
const buffer = createBuffer(duration);
|
||||||
|
|
||||||
for (let barIndex = 0; barIndex < track.bars; barIndex += 1) {
|
for (let barIndex = 0; barIndex < track.bars; barIndex += 1) {
|
||||||
const start = barIndex * bar;
|
const start = barIndex * bar;
|
||||||
const chord = track.progression[barIndex % track.progression.length];
|
const chord = track.progression[barIndex % track.progression.length];
|
||||||
const lift = 0.82 + Math.sin((barIndex / track.bars) * Math.PI) * 0.18;
|
const lift = 0.86 + Math.sin((barIndex / Math.max(1, track.bars - 1)) * Math.PI) * 0.14;
|
||||||
const bass = track.root + chord[0] - 12;
|
|
||||||
|
|
||||||
chord.forEach((step, index) => {
|
addStringBed(buffer, start, bar * 0.96, track.root, chord, lift, track.mood);
|
||||||
const midi = track.root + step + 12;
|
addCelloPulse(buffer, start, beat, track.root + chord[0], track.mood);
|
||||||
addPad(buffer, start + index * 0.04, bar * 1.08, midiToFrequency(midi), 0.07 * track.warmth * lift);
|
addHarpFigure(buffer, start, beat, track.root, chord, track.harp, track.mood);
|
||||||
addPad(buffer, start + 0.06 + index * 0.03, bar * 1.02, midiToFrequency(midi + 12), 0.025 * track.warmth * lift);
|
|
||||||
});
|
|
||||||
|
|
||||||
addLowPad(buffer, start, bar * 1.04, midiToFrequency(bass), 0.055 * lift);
|
if (track.mood === 'march' || track.mood === 'tense') {
|
||||||
addLowPad(buffer, start + beat * 2, bar * 0.54, midiToFrequency(bass + 7), 0.035 * lift);
|
addSoftDrum(buffer, start, beat, track.mood);
|
||||||
|
|
||||||
if (track.motion > 0.25) {
|
|
||||||
addGentlePulse(buffer, start, beat, track.root, chord, track.motion * 0.026);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addSoftMelody(buffer, track, beat, bar);
|
addLeadLine(buffer, track, beat, bar);
|
||||||
smoothEdges(buffer, Math.floor(sampleRate * 1.2));
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSoftMelody(buffer, track, beat, bar) {
|
function createBuffer(duration) {
|
||||||
for (let i = 0; i < track.bars * 2; i += 1) {
|
const length = Math.ceil(duration * sampleRate);
|
||||||
const barIndex = Math.floor(i / 2);
|
return {
|
||||||
const start = barIndex * bar + (i % 2 === 0 ? beat * 0.38 : beat * 2.38);
|
left: new Float32Array(length),
|
||||||
|
right: new Float32Array(length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStringBed(buffer, start, duration, root, chord, lift, mood) {
|
||||||
|
const baseGain = mood === 'quiet' ? 0.019 : mood === 'tense' ? 0.021 : 0.026;
|
||||||
|
const notes = [
|
||||||
|
{ midi: root + chord[0] + 12, pan: -0.22, gain: baseGain * 0.72 },
|
||||||
|
{ midi: root + chord[1] + 12, pan: 0.24, gain: baseGain * 0.64 },
|
||||||
|
{ midi: root + chord[2] + 12, pan: -0.04, gain: baseGain * 0.56 },
|
||||||
|
{ midi: root + chord[1] + 24, pan: 0.18, gain: baseGain * 0.28 }
|
||||||
|
];
|
||||||
|
|
||||||
|
notes.forEach((note, index) => {
|
||||||
|
addTone(buffer, {
|
||||||
|
start: start + index * 0.045,
|
||||||
|
duration,
|
||||||
|
frequency: midiToFrequency(note.midi),
|
||||||
|
gain: note.gain * lift,
|
||||||
|
pan: note.pan,
|
||||||
|
attack: mood === 'tense' ? 1.1 : 1.45,
|
||||||
|
release: 1.25,
|
||||||
|
wave: softStringWave
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCelloPulse(buffer, start, beat, midi, mood) {
|
||||||
|
const pulses = mood === 'quiet' ? [0] : [0, 2];
|
||||||
|
const gain = mood === 'tense' ? 0.024 : mood === 'march' ? 0.026 : 0.018;
|
||||||
|
|
||||||
|
pulses.forEach((beatOffset) => {
|
||||||
|
addTone(buffer, {
|
||||||
|
start: start + beat * beatOffset,
|
||||||
|
duration: beat * 1.35,
|
||||||
|
frequency: midiToFrequency(Math.max(43, midi + 12)),
|
||||||
|
gain,
|
||||||
|
pan: -0.08,
|
||||||
|
attack: 0.08,
|
||||||
|
release: 0.55,
|
||||||
|
wave: mellowCelloWave
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHarpFigure(buffer, start, beat, root, chord, pattern, mood) {
|
||||||
|
const gain = mood === 'quiet' ? 0.011 : mood === 'tense' ? 0.014 : 0.016;
|
||||||
|
const stepDuration = beat * 0.46;
|
||||||
|
const stepGap = mood === 'quiet' ? beat : beat * 0.5;
|
||||||
|
const steps = mood === 'quiet' ? 4 : 8;
|
||||||
|
|
||||||
|
for (let i = 0; i < steps; i += 1) {
|
||||||
|
const offset = pattern[i % pattern.length];
|
||||||
|
const chordTone = chord[i % chord.length];
|
||||||
|
addPluck(buffer, {
|
||||||
|
start: start + i * stepGap + beat * 0.08,
|
||||||
|
duration: stepDuration,
|
||||||
|
frequency: midiToFrequency(root + offset + 12 + (i % 2 === 0 ? 0 : chordTone % 5)),
|
||||||
|
gain,
|
||||||
|
pan: i % 2 === 0 ? -0.32 : 0.32
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLeadLine(buffer, track, beat, bar) {
|
||||||
|
const noteEvery = track.mood === 'quiet' ? 2 : 1;
|
||||||
|
const gain = track.mood === 'tense' ? 0.018 : track.mood === 'quiet' ? 0.016 : 0.021;
|
||||||
|
|
||||||
|
for (let i = 0; i < track.bars * noteEvery; i += 1) {
|
||||||
|
const barIndex = Math.floor(i / noteEvery);
|
||||||
|
const offsetInBar = noteEvery === 1 ? beat * 1.42 : beat * 1.85;
|
||||||
|
const start = barIndex * bar + offsetInBar + (i % noteEvery) * beat * 1.34;
|
||||||
const midi = track.root + track.melody[i % track.melody.length] + 12;
|
const midi = track.root + track.melody[i % track.melody.length] + 12;
|
||||||
addWoodwind(buffer, start, beat * 1.12, midiToFrequency(midi), 0.034);
|
|
||||||
|
addTone(buffer, {
|
||||||
|
start,
|
||||||
|
duration: beat * 1.05,
|
||||||
|
frequency: midiToFrequency(midi),
|
||||||
|
gain,
|
||||||
|
pan: 0.12,
|
||||||
|
attack: 0.13,
|
||||||
|
release: 0.5,
|
||||||
|
wave: woodwindWave
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGentlePulse(buffer, start, beat, root, chord, gain) {
|
function addSoftDrum(buffer, start, beat, mood) {
|
||||||
const pattern = [0, 1, 2, 1, 0, 2, 1, 2];
|
const accents = mood === 'tense' ? [0, 2] : [0, 1.5, 2.5];
|
||||||
|
const gain = mood === 'tense' ? 0.034 : 0.026;
|
||||||
|
|
||||||
for (let i = 0; i < pattern.length; i += 1) {
|
accents.forEach((beatOffset, index) => {
|
||||||
const midi = root + chord[pattern[i] % chord.length] + 12;
|
addTone(buffer, {
|
||||||
addPluckedString(buffer, start + i * beat * 0.5, beat * 0.42, midiToFrequency(midi), gain);
|
start: start + beat * beatOffset,
|
||||||
}
|
duration: beat * 0.5,
|
||||||
|
frequency: index === 0 ? 92 : 122,
|
||||||
|
gain: gain * (index === 0 ? 1 : 0.62),
|
||||||
|
pan: 0,
|
||||||
|
attack: 0.006,
|
||||||
|
release: 0.16,
|
||||||
|
wave: drumWave,
|
||||||
|
pluck: true,
|
||||||
|
decay: 8.5
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPad(buffer, start, duration, frequency, gain) {
|
function addTone(buffer, options) {
|
||||||
addInstrument(buffer, start, duration, frequency, gain, (phase, t) => {
|
const {
|
||||||
const slow = Math.sin(2 * Math.PI * 0.18 * t) * 0.03;
|
start,
|
||||||
return (
|
duration,
|
||||||
Math.sin(phase + slow) * 0.72 +
|
frequency,
|
||||||
Math.sin(phase * 2.001) * 0.12 +
|
gain,
|
||||||
Math.sin(phase * 0.5) * 0.08
|
pan,
|
||||||
);
|
attack,
|
||||||
}, 0.78, 1.05);
|
release,
|
||||||
}
|
wave,
|
||||||
|
pluck = false,
|
||||||
function addLowPad(buffer, start, duration, frequency, gain) {
|
decay = 5
|
||||||
addInstrument(buffer, start, duration, frequency, gain, (phase) => {
|
} = options;
|
||||||
return Math.sin(phase) * 0.84 + Math.sin(phase * 2) * 0.1;
|
|
||||||
}, 0.46, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWoodwind(buffer, start, duration, frequency, gain) {
|
|
||||||
addInstrument(buffer, start, duration, frequency, gain, (phase, t) => {
|
|
||||||
const vibrato = Math.sin(2 * Math.PI * 4.6 * t) * 0.012;
|
|
||||||
return Math.sin(phase + vibrato) * 0.82 + Math.sin(phase * 2) * 0.06;
|
|
||||||
}, 0.12, 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPluckedString(buffer, start, duration, frequency, gain) {
|
|
||||||
addInstrument(buffer, start, duration, frequency, gain, (phase) => {
|
|
||||||
return Math.sin(phase) * 0.75 + Math.sin(phase * 2) * 0.1;
|
|
||||||
}, 0.02, 0.18, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addInstrument(buffer, start, duration, frequency, gain, wave, attack, release, pluck = false) {
|
|
||||||
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
||||||
const count = Math.max(1, Math.floor(duration * sampleRate));
|
const count = Math.max(1, Math.floor(duration * sampleRate));
|
||||||
|
const leftGain = Math.cos((pan + 1) * Math.PI * 0.25);
|
||||||
|
const rightGain = Math.sin((pan + 1) * Math.PI * 0.25);
|
||||||
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
const index = startIndex + i;
|
const index = startIndex + i;
|
||||||
if (index >= buffer.length) {
|
if (index >= buffer.left.length) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = i / sampleRate;
|
const t = i / sampleRate;
|
||||||
const env = pluck
|
const env = pluck
|
||||||
? Math.exp(-t * 5.8) * Math.min(1, t / attack)
|
? Math.exp(-t * decay) * Math.min(1, t / Math.max(0.001, attack))
|
||||||
: envelope(t, duration, attack, release);
|
: envelope(t, duration, attack, release);
|
||||||
const phase = 2 * Math.PI * frequency * t;
|
const phase = 2 * Math.PI * frequency * t;
|
||||||
buffer[index] += wave(phase, t) * gain * env;
|
const sample = wave(phase, t) * gain * env;
|
||||||
|
buffer.left[index] += sample * leftGain;
|
||||||
|
buffer.right[index] += sample * rightGain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPluck(buffer, options) {
|
||||||
|
addTone(buffer, {
|
||||||
|
...options,
|
||||||
|
attack: 0.01,
|
||||||
|
release: 0.22,
|
||||||
|
wave: harpWave,
|
||||||
|
pluck: true,
|
||||||
|
decay: 4.6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function softStringWave(phase, t) {
|
||||||
|
const bow = 1 + Math.sin(2 * Math.PI * 0.7 * t) * 0.018;
|
||||||
|
return (
|
||||||
|
Math.sin(phase * bow) * 0.7 +
|
||||||
|
Math.sin(phase * 2) * 0.13 +
|
||||||
|
Math.sin(phase * 3) * 0.045
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mellowCelloWave(phase) {
|
||||||
|
return Math.sin(phase) * 0.78 + Math.sin(phase * 2) * 0.09 + Math.sin(phase * 0.5) * 0.04;
|
||||||
|
}
|
||||||
|
|
||||||
|
function woodwindWave(phase, t) {
|
||||||
|
const vibrato = Math.sin(2 * Math.PI * 4.2 * t) * 0.006;
|
||||||
|
return Math.sin(phase + vibrato) * 0.86 + Math.sin(phase * 2) * 0.045;
|
||||||
|
}
|
||||||
|
|
||||||
|
function harpWave(phase) {
|
||||||
|
return Math.sin(phase) * 0.82 + Math.sin(phase * 2) * 0.12 + Math.sin(phase * 3) * 0.035;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drumWave(phase, t) {
|
||||||
|
const drop = 1 - Math.min(1, t * 5.2) * 0.42;
|
||||||
|
return Math.sin(phase * drop) * 0.92 + Math.sin(phase * 2.02) * 0.06;
|
||||||
|
}
|
||||||
|
|
||||||
function envelope(t, duration, attack, release) {
|
function envelope(t, duration, attack, release) {
|
||||||
const attackLevel = Math.min(1, t / attack);
|
const attackLevel = Math.min(1, t / Math.max(0.001, attack));
|
||||||
const releaseLevel = Math.min(1, (duration - t) / release);
|
const releaseLevel = Math.min(1, (duration - t) / Math.max(0.001, release));
|
||||||
const curved = Math.sin(Math.min(1, attackLevel) * Math.PI * 0.5);
|
const attackCurve = Math.sin(attackLevel * Math.PI * 0.5);
|
||||||
return Math.max(0, Math.min(curved, releaseLevel));
|
return Math.max(0, Math.min(attackCurve, releaseLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLowRumble(buffer, cutoff) {
|
||||||
|
highPass(buffer.left, cutoff);
|
||||||
|
highPass(buffer.right, cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
function highPass(samples, cutoff) {
|
||||||
|
const rc = 1 / (2 * Math.PI * cutoff);
|
||||||
|
const dt = 1 / sampleRate;
|
||||||
|
const alpha = rc / (rc + dt);
|
||||||
|
let previousOutput = 0;
|
||||||
|
let previousInput = samples[0] ?? 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < samples.length; i += 1) {
|
||||||
|
const input = samples[i];
|
||||||
|
const output = alpha * (previousOutput + input - previousInput);
|
||||||
|
samples[i] = output;
|
||||||
|
previousOutput = output;
|
||||||
|
previousInput = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySmallRoom(buffer) {
|
||||||
|
const originalLeft = Float32Array.from(buffer.left);
|
||||||
|
const originalRight = Float32Array.from(buffer.right);
|
||||||
|
const delays = [
|
||||||
|
{ samples: Math.floor(sampleRate * 0.075), gain: 0.09 },
|
||||||
|
{ samples: Math.floor(sampleRate * 0.132), gain: 0.055 }
|
||||||
|
];
|
||||||
|
|
||||||
|
delays.forEach(({ samples, gain }) => {
|
||||||
|
for (let i = samples; i < buffer.left.length; i += 1) {
|
||||||
|
buffer.left[i] += originalRight[i - samples] * gain;
|
||||||
|
buffer.right[i] += originalLeft[i - samples] * gain;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function smoothEdges(buffer, samples) {
|
function smoothEdges(buffer, samples) {
|
||||||
for (let i = 0; i < Math.min(samples, buffer.length); i += 1) {
|
const limit = Math.min(samples, buffer.left.length);
|
||||||
const fadeIn = Math.sin((i / samples) * Math.PI * 0.5);
|
|
||||||
const fadeOut = Math.sin((i / samples) * Math.PI * 0.5);
|
for (let i = 0; i < limit; i += 1) {
|
||||||
buffer[i] *= fadeIn;
|
const fadeIn = Math.sin((i / limit) * Math.PI * 0.5);
|
||||||
buffer[buffer.length - 1 - i] *= fadeOut;
|
const fadeOut = Math.sin((i / limit) * Math.PI * 0.5);
|
||||||
|
buffer.left[i] *= fadeIn;
|
||||||
|
buffer.right[i] *= fadeIn;
|
||||||
|
buffer.left[buffer.left.length - 1 - i] *= fadeOut;
|
||||||
|
buffer.right[buffer.right.length - 1 - i] *= fadeOut;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +369,8 @@ function midiToFrequency(midi) {
|
|||||||
|
|
||||||
function normalize(buffer, target) {
|
function normalize(buffer, target) {
|
||||||
let peak = 0;
|
let peak = 0;
|
||||||
for (const value of buffer) {
|
for (let i = 0; i < buffer.left.length; i += 1) {
|
||||||
peak = Math.max(peak, Math.abs(value));
|
peak = Math.max(peak, Math.abs(buffer.left[i]), Math.abs(buffer.right[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (peak === 0) {
|
if (peak === 0) {
|
||||||
@@ -222,20 +378,23 @@ function normalize(buffer, target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scale = target / peak;
|
const scale = target / peak;
|
||||||
for (let i = 0; i < buffer.length; i += 1) {
|
for (let i = 0; i < buffer.left.length; i += 1) {
|
||||||
buffer[i] *= scale;
|
buffer.left[i] *= scale;
|
||||||
|
buffer.right[i] *= scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySoftLimiter(buffer) {
|
function applySoftLimiter(buffer) {
|
||||||
for (let i = 0; i < buffer.length; i += 1) {
|
for (let i = 0; i < buffer.left.length; i += 1) {
|
||||||
buffer[i] = Math.tanh(buffer[i] * 1.15) / Math.tanh(1.15);
|
buffer.left[i] = Math.tanh(buffer.left[i] * 1.08) / Math.tanh(1.08);
|
||||||
|
buffer.right[i] = Math.tanh(buffer.right[i] * 1.08) / Math.tanh(1.08);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeWav(buffer) {
|
function encodeWav(buffer) {
|
||||||
const bytesPerSample = 2;
|
const bytesPerSample = 2;
|
||||||
const dataSize = buffer.length * bytesPerSample;
|
const frameCount = buffer.left.length;
|
||||||
|
const dataSize = frameCount * channels * bytesPerSample;
|
||||||
const wav = Buffer.alloc(44 + dataSize);
|
const wav = Buffer.alloc(44 + dataSize);
|
||||||
|
|
||||||
wav.write('RIFF', 0);
|
wav.write('RIFF', 0);
|
||||||
@@ -244,17 +403,20 @@ function encodeWav(buffer) {
|
|||||||
wav.write('fmt ', 12);
|
wav.write('fmt ', 12);
|
||||||
wav.writeUInt32LE(16, 16);
|
wav.writeUInt32LE(16, 16);
|
||||||
wav.writeUInt16LE(1, 20);
|
wav.writeUInt16LE(1, 20);
|
||||||
wav.writeUInt16LE(1, 22);
|
wav.writeUInt16LE(channels, 22);
|
||||||
wav.writeUInt32LE(sampleRate, 24);
|
wav.writeUInt32LE(sampleRate, 24);
|
||||||
wav.writeUInt32LE(sampleRate * bytesPerSample, 28);
|
wav.writeUInt32LE(sampleRate * channels * bytesPerSample, 28);
|
||||||
wav.writeUInt16LE(bytesPerSample, 32);
|
wav.writeUInt16LE(channels * bytesPerSample, 32);
|
||||||
wav.writeUInt16LE(16, 34);
|
wav.writeUInt16LE(16, 34);
|
||||||
wav.write('data', 36);
|
wav.write('data', 36);
|
||||||
wav.writeUInt32LE(dataSize, 40);
|
wav.writeUInt32LE(dataSize, 40);
|
||||||
|
|
||||||
for (let i = 0; i < buffer.length; i += 1) {
|
for (let i = 0; i < frameCount; i += 1) {
|
||||||
const value = Math.max(-1, Math.min(1, buffer[i]));
|
const left = Math.max(-1, Math.min(1, buffer.left[i]));
|
||||||
wav.writeInt16LE(Math.round(value * 32767), 44 + i * bytesPerSample);
|
const right = Math.max(-1, Math.min(1, buffer.right[i]));
|
||||||
|
const offset = 44 + i * channels * bytesPerSample;
|
||||||
|
wav.writeInt16LE(Math.round(left * 32767), offset);
|
||||||
|
wav.writeInt16LE(Math.round(right * 32767), offset + bytesPerSample);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wav;
|
return wav;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,7 @@ class SoundDirector {
|
|||||||
private currentMusicKey?: string;
|
private currentMusicKey?: string;
|
||||||
private pendingMusicKey?: string;
|
private pendingMusicKey?: string;
|
||||||
private musicFadeTimer?: number;
|
private musicFadeTimer?: number;
|
||||||
private readonly musicVolume = 0.2;
|
private readonly musicVolume = 0.14;
|
||||||
|
|
||||||
registerMusicTracks(tracks: AudioTrackMap) {
|
registerMusicTracks(tracks: AudioTrackMap) {
|
||||||
this.musicTracks = tracks;
|
this.musicTracks = tracks;
|
||||||
@@ -29,7 +29,7 @@ class SoundDirector {
|
|||||||
if (AudioContextCtor) {
|
if (AudioContextCtor) {
|
||||||
this.context = new AudioContextCtor();
|
this.context = new AudioContextCtor();
|
||||||
this.master = this.context.createGain();
|
this.master = this.context.createGain();
|
||||||
this.master.gain.value = this.muted ? 0 : 0.34;
|
this.master.gain.value = this.muted ? 0 : 0.3;
|
||||||
this.master.connect(this.context.destination);
|
this.master.connect(this.context.destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class SoundDirector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.master.gain.cancelScheduledValues(this.context.currentTime);
|
this.master.gain.cancelScheduledValues(this.context.currentTime);
|
||||||
this.master.gain.linearRampToValueAtTime(muted ? 0 : 0.34, this.context.currentTime + 0.18);
|
this.master.gain.linearRampToValueAtTime(muted ? 0 : 0.3, this.context.currentTime + 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
isMuted() {
|
isMuted() {
|
||||||
|
|||||||
Reference in New Issue
Block a user