Soften generated audio
This commit is contained in:
@@ -9,99 +9,85 @@ mkdirSync(outputDir, { recursive: true });
|
|||||||
const tracks = [
|
const tracks = [
|
||||||
{
|
{
|
||||||
name: 'title-theme',
|
name: 'title-theme',
|
||||||
root: 45,
|
root: 50,
|
||||||
tempo: 76,
|
tempo: 64,
|
||||||
bars: 16,
|
bars: 12,
|
||||||
progression: [
|
progression: [
|
||||||
[0, 7, 12, 16],
|
[0, 5, 9],
|
||||||
[5, 12, 17, 21],
|
[2, 7, 11],
|
||||||
[-3, 4, 9, 12],
|
[-3, 2, 7],
|
||||||
[7, 14, 19, 23]
|
[0, 5, 12]
|
||||||
],
|
],
|
||||||
melody: [12, 14, 16, 19, 21, 19, 16, 14, 12, 16, 19, 24, 23, 19, 16, 14],
|
melody: [12, 14, 16, 19, 21, 19, 16, 14],
|
||||||
brass: 0.9,
|
warmth: 1,
|
||||||
choir: 0.3,
|
motion: 0.45
|
||||||
percussion: 0.55,
|
|
||||||
motion: 0.65,
|
|
||||||
color: 'heroic'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'story-dark',
|
name: 'story-dark',
|
||||||
root: 43,
|
root: 45,
|
||||||
tempo: 62,
|
tempo: 56,
|
||||||
bars: 12,
|
bars: 10,
|
||||||
progression: [
|
progression: [
|
||||||
[0, 7, 10, 15],
|
[0, 3, 7],
|
||||||
[-5, 2, 7, 10],
|
[-5, 0, 5],
|
||||||
[-2, 5, 9, 12],
|
[-2, 2, 7],
|
||||||
[-7, 0, 5, 10]
|
[-7, 0, 3]
|
||||||
],
|
],
|
||||||
melody: [10, 7, 5, 3, 0, 3, 5, 7],
|
melody: [10, 7, 5, 3, 5, 7],
|
||||||
brass: 0.34,
|
warmth: 0.72,
|
||||||
choir: 0.42,
|
motion: 0.18
|
||||||
percussion: 0.22,
|
|
||||||
motion: 0.22,
|
|
||||||
color: 'dark'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'oath-theme',
|
name: 'oath-theme',
|
||||||
root: 48,
|
root: 52,
|
||||||
tempo: 68,
|
tempo: 60,
|
||||||
bars: 16,
|
bars: 12,
|
||||||
progression: [
|
progression: [
|
||||||
[0, 7, 12, 16],
|
[0, 5, 9],
|
||||||
[9, 16, 21, 24],
|
[4, 9, 12],
|
||||||
[5, 12, 17, 21],
|
[5, 9, 14],
|
||||||
[7, 14, 19, 23]
|
[2, 7, 11]
|
||||||
],
|
],
|
||||||
melody: [12, 16, 19, 21, 24, 21, 19, 16, 17, 21, 24, 28, 26, 24, 21, 19],
|
melody: [12, 16, 19, 21, 24, 21, 19, 16],
|
||||||
brass: 0.72,
|
warmth: 1,
|
||||||
choir: 0.55,
|
motion: 0.32
|
||||||
percussion: 0.34,
|
|
||||||
motion: 0.4,
|
|
||||||
color: 'gold'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'militia-theme',
|
name: 'militia-theme',
|
||||||
root: 41,
|
root: 48,
|
||||||
tempo: 88,
|
tempo: 72,
|
||||||
bars: 16,
|
bars: 12,
|
||||||
progression: [
|
progression: [
|
||||||
[0, 7, 12, 15],
|
[0, 5, 9],
|
||||||
[5, 12, 17, 20],
|
[5, 9, 12],
|
||||||
[7, 14, 19, 22],
|
[7, 11, 14],
|
||||||
[-2, 5, 10, 14]
|
[2, 7, 11]
|
||||||
],
|
],
|
||||||
melody: [12, 14, 15, 19, 22, 19, 15, 14, 12, 15, 19, 22, 24, 22, 19, 15],
|
melody: [12, 14, 16, 19, 16, 14, 12, 14],
|
||||||
brass: 0.68,
|
warmth: 0.9,
|
||||||
choir: 0.18,
|
motion: 0.5
|
||||||
percussion: 0.78,
|
|
||||||
motion: 0.82,
|
|
||||||
color: 'march'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'battle-prep',
|
name: 'battle-prep',
|
||||||
root: 40,
|
root: 43,
|
||||||
tempo: 96,
|
tempo: 68,
|
||||||
bars: 16,
|
bars: 12,
|
||||||
progression: [
|
progression: [
|
||||||
[0, 7, 10, 15],
|
[0, 3, 7],
|
||||||
[3, 10, 15, 19],
|
[2, 7, 10],
|
||||||
[-2, 5, 10, 14],
|
[-2, 3, 7],
|
||||||
[-5, 2, 7, 10]
|
[-5, 0, 5]
|
||||||
],
|
],
|
||||||
melody: [12, 15, 17, 19, 22, 19, 17, 15, 12, 10, 12, 15, 17, 19, 22, 24],
|
melody: [12, 15, 17, 19, 17, 15, 12, 10],
|
||||||
brass: 0.86,
|
warmth: 0.82,
|
||||||
choir: 0.2,
|
motion: 0.42
|
||||||
percussion: 0.92,
|
|
||||||
motion: 0.9,
|
|
||||||
color: 'steel'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
const buffer = buildTrack(track);
|
const buffer = buildTrack(track);
|
||||||
normalize(buffer, 0.92);
|
normalize(buffer, 0.48);
|
||||||
|
applySoftLimiter(buffer);
|
||||||
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}`);
|
||||||
@@ -116,282 +102,108 @@ function buildTrack(track) {
|
|||||||
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.78 + (barIndex / Math.max(1, track.bars - 1)) * 0.34;
|
const lift = 0.82 + Math.sin((barIndex / track.bars) * Math.PI) * 0.18;
|
||||||
const chordNotes = chord.map((step) => track.root + step + 12);
|
|
||||||
const upperNotes = chord.map((step) => track.root + step + 24);
|
|
||||||
const bass = track.root + chord[0] - 12;
|
const bass = track.root + chord[0] - 12;
|
||||||
|
|
||||||
addStringSection(buffer, start, bar * 1.04, chordNotes, 0.085 * lift, track.color);
|
chord.forEach((step, index) => {
|
||||||
addChoirSection(buffer, start + beat * 0.45, bar * 0.96, upperNotes, track.choir * 0.04, track.color);
|
const midi = track.root + step + 12;
|
||||||
addLowStrings(buffer, start, bar * 1.02, bass, 0.085 * lift);
|
addPad(buffer, start + index * 0.04, bar * 1.08, midiToFrequency(midi), 0.07 * track.warmth * lift);
|
||||||
|
addPad(buffer, start + 0.06 + index * 0.03, bar * 1.02, midiToFrequency(midi + 12), 0.025 * track.warmth * lift);
|
||||||
|
});
|
||||||
|
|
||||||
if (track.brass > 0.3) {
|
addLowPad(buffer, start, bar * 1.04, midiToFrequency(bass), 0.055 * lift);
|
||||||
addBrassChord(buffer, start + beat * 1.85, beat * 2.0, upperNotes, track.brass * 0.058 * lift, track.color);
|
addLowPad(buffer, start + beat * 2, bar * 0.54, midiToFrequency(bass + 7), 0.035 * lift);
|
||||||
}
|
|
||||||
|
|
||||||
if (track.motion > 0.35) {
|
if (track.motion > 0.25) {
|
||||||
addOstinato(buffer, start, beat, track.root, chord, track.motion * 0.052, track.color);
|
addGentlePulse(buffer, start, beat, track.root, chord, track.motion * 0.026);
|
||||||
}
|
|
||||||
|
|
||||||
if (track.percussion > 0.18) {
|
|
||||||
addTimpani(buffer, start, beat * 0.48, midiToFrequency(bass), track.percussion * 0.11);
|
|
||||||
if (barIndex % 2 === 1 || track.percussion > 0.7) {
|
|
||||||
addTimpani(buffer, start + beat * 2, beat * 0.38, midiToFrequency(bass + 7), track.percussion * 0.07);
|
|
||||||
}
|
|
||||||
if (track.percussion > 0.65) {
|
|
||||||
addWarDrum(buffer, start + beat, beat * 0.22, track.percussion * 0.08);
|
|
||||||
addWarDrum(buffer, start + beat * 3, beat * 0.22, track.percussion * 0.07);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (barIndex % 4 === 3) {
|
addSoftMelody(buffer, track, beat, bar);
|
||||||
addCymbalSwell(buffer, start + bar - beat * 1.35, beat * 1.2, 0.032 + track.percussion * 0.018);
|
smoothEdges(buffer, Math.floor(sampleRate * 1.2));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addMelody(buffer, track, beat, bar);
|
|
||||||
addRoomTone(buffer, 0.012);
|
|
||||||
smoothEdges(buffer, sampleRate * 0.08);
|
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMelody(buffer, track, beat, bar) {
|
function addSoftMelody(buffer, track, beat, bar) {
|
||||||
const noteLength = beat * (track.color === 'dark' ? 1.35 : 1.05);
|
|
||||||
for (let i = 0; i < track.bars * 2; i += 1) {
|
for (let i = 0; i < track.bars * 2; i += 1) {
|
||||||
const barIndex = Math.floor(i / 2);
|
const barIndex = Math.floor(i / 2);
|
||||||
const local = i % 2 === 0 ? beat * 0.18 : beat * 2.18;
|
const start = barIndex * bar + (i % 2 === 0 ? beat * 0.38 : beat * 2.38);
|
||||||
const start = barIndex * bar + local;
|
const midi = track.root + track.melody[i % track.melody.length] + 12;
|
||||||
const step = track.melody[i % track.melody.length];
|
addWoodwind(buffer, start, beat * 1.12, midiToFrequency(midi), 0.034);
|
||||||
const midi = track.root + step + 12;
|
|
||||||
const gain = track.color === 'dark' ? 0.045 : 0.062;
|
|
||||||
const profile = track.color === 'gold' ? 'flute' : track.color === 'steel' ? 'hornLead' : 'oboe';
|
|
||||||
addInstrument(buffer, start, noteLength, midiToFrequency(midi), gain, profile, {
|
|
||||||
attack: 0.05,
|
|
||||||
release: 0.28,
|
|
||||||
vibrato: 0.007
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStringSection(buffer, start, duration, midiNotes, gain, color) {
|
function addGentlePulse(buffer, start, beat, root, chord, gain) {
|
||||||
midiNotes.forEach((midi, index) => {
|
const pattern = [0, 1, 2, 1, 0, 2, 1, 2];
|
||||||
addInstrument(buffer, start + index * 0.018, duration, midiToFrequency(midi), gain, 'strings', {
|
|
||||||
attack: 0.5,
|
|
||||||
release: 0.72,
|
|
||||||
vibrato: color === 'dark' ? 0.004 : 0.006
|
|
||||||
});
|
|
||||||
addInstrument(buffer, start + 0.04 + index * 0.014, duration * 0.96, midiToFrequency(midi + 12), gain * 0.38, 'stringsAir', {
|
|
||||||
attack: 0.62,
|
|
||||||
release: 0.8,
|
|
||||||
vibrato: 0.008
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLowStrings(buffer, start, duration, midi, gain) {
|
|
||||||
addInstrument(buffer, start, duration, midiToFrequency(midi), gain, 'lowStrings', {
|
|
||||||
attack: 0.18,
|
|
||||||
release: 0.55,
|
|
||||||
vibrato: 0.003
|
|
||||||
});
|
|
||||||
addInstrument(buffer, start + 0.09, duration * 0.92, midiToFrequency(midi + 12), gain * 0.42, 'lowStrings', {
|
|
||||||
attack: 0.2,
|
|
||||||
release: 0.5,
|
|
||||||
vibrato: 0.004
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addChoirSection(buffer, start, duration, midiNotes, gain, color) {
|
|
||||||
if (gain <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
midiNotes.forEach((midi, index) => {
|
|
||||||
addInstrument(buffer, start + index * 0.03, duration, midiToFrequency(midi), gain, color === 'dark' ? 'darkChoir' : 'choir', {
|
|
||||||
attack: 0.75,
|
|
||||||
release: 0.9,
|
|
||||||
vibrato: 0.002
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBrassChord(buffer, start, duration, midiNotes, gain, color) {
|
|
||||||
midiNotes.slice(0, 3).forEach((midi, index) => {
|
|
||||||
addInstrument(buffer, start + index * 0.012, duration, midiToFrequency(midi), gain, color === 'steel' ? 'battleBrass' : 'brass', {
|
|
||||||
attack: 0.2,
|
|
||||||
release: 0.45,
|
|
||||||
vibrato: 0.004
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOstinato(buffer, barStart, beat, root, chord, gain, color) {
|
|
||||||
const pattern = [0, 2, 1, 3, 2, 1, 0, 2];
|
|
||||||
const profile = color === 'march' || color === 'steel' ? 'spiccato' : 'pizzicato';
|
|
||||||
|
|
||||||
for (let i = 0; i < pattern.length; i += 1) {
|
for (let i = 0; i < pattern.length; i += 1) {
|
||||||
const step = chord[pattern[i] % chord.length];
|
const midi = root + chord[pattern[i] % chord.length] + 12;
|
||||||
const midi = root + step + (i % 2 === 0 ? 12 : 24);
|
addPluckedString(buffer, start + i * beat * 0.5, beat * 0.42, midiToFrequency(midi), gain);
|
||||||
addInstrument(buffer, barStart + i * beat * 0.5, beat * 0.42, midiToFrequency(midi), gain, profile, {
|
|
||||||
attack: 0.012,
|
|
||||||
release: 0.12,
|
|
||||||
vibrato: 0
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTimpani(buffer, start, duration, baseFrequency, gain) {
|
function addPad(buffer, start, duration, frequency, gain) {
|
||||||
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
addInstrument(buffer, start, duration, frequency, gain, (phase, t) => {
|
||||||
const count = Math.max(1, Math.floor(duration * sampleRate));
|
const slow = Math.sin(2 * Math.PI * 0.18 * t) * 0.03;
|
||||||
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
const index = startIndex + i;
|
|
||||||
if (index >= buffer.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = i / sampleRate;
|
|
||||||
const env = Math.exp(-t * 8.5) * Math.min(1, t / 0.018);
|
|
||||||
const frequency = baseFrequency * (1.18 - Math.min(0.28, t * 1.8));
|
|
||||||
const drum = Math.sin(2 * Math.PI * frequency * t) + Math.sin(2 * Math.PI * frequency * 0.5 * t) * 0.45;
|
|
||||||
buffer[index] += drum * gain * env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWarDrum(buffer, start, duration, gain) {
|
|
||||||
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
|
||||||
const count = Math.max(1, Math.floor(duration * sampleRate));
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
const index = startIndex + i;
|
|
||||||
if (index >= buffer.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = i / sampleRate;
|
|
||||||
const env = Math.exp(-t * 18) * Math.min(1, t / 0.01);
|
|
||||||
const tone = Math.sin(2 * Math.PI * (84 - t * 40) * t);
|
|
||||||
const grit = noise(index) * 0.35;
|
|
||||||
buffer[index] += (tone + grit) * gain * env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCymbalSwell(buffer, start, duration, gain) {
|
|
||||||
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
|
||||||
const count = Math.max(1, Math.floor(duration * sampleRate));
|
|
||||||
let filtered = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
const index = startIndex + i;
|
|
||||||
if (index >= buffer.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = noise(index * 3 + 17);
|
|
||||||
filtered = filtered * 0.62 + raw * 0.38;
|
|
||||||
const bright = raw - filtered;
|
|
||||||
const t = i / count;
|
|
||||||
const env = Math.sin(Math.PI * t) ** 0.45;
|
|
||||||
buffer[index] += bright * gain * env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRoomTone(buffer, gain) {
|
|
||||||
let low = 0;
|
|
||||||
for (let i = 0; i < buffer.length; i += 1) {
|
|
||||||
low = low * 0.996 + noise(i + 991) * 0.004;
|
|
||||||
buffer[i] += low * gain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addInstrument(buffer, start, duration, frequency, gain, profile, options) {
|
|
||||||
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
|
||||||
const count = Math.max(1, Math.floor(duration * sampleRate));
|
|
||||||
const attack = options.attack ?? 0.04;
|
|
||||||
const release = options.release ?? 0.2;
|
|
||||||
const vibratoDepth = options.vibrato ?? 0.004;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
const index = startIndex + i;
|
|
||||||
if (index >= buffer.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = i / sampleRate;
|
|
||||||
const env = envelope(t, duration, attack, release);
|
|
||||||
const vibrato = 1 + Math.sin(2 * Math.PI * 5.1 * t) * vibratoDepth;
|
|
||||||
const wave = instrumentWave(profile, frequency * vibrato, t, index);
|
|
||||||
buffer[index] += wave * gain * env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function instrumentWave(profile, frequency, t, index) {
|
|
||||||
const phase = 2 * Math.PI * frequency * t;
|
|
||||||
|
|
||||||
if (profile === 'strings') {
|
|
||||||
return (
|
return (
|
||||||
Math.sin(phase) * 0.78 +
|
Math.sin(phase + slow) * 0.72 +
|
||||||
Math.sin(phase * 2.01) * 0.25 +
|
Math.sin(phase * 2.001) * 0.12 +
|
||||||
Math.sin(phase * 3.02) * 0.12 +
|
Math.sin(phase * 0.5) * 0.08
|
||||||
Math.sin(phase * 0.997) * 0.18
|
|
||||||
);
|
);
|
||||||
|
}, 0.78, 1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'stringsAir') {
|
function addLowPad(buffer, start, duration, frequency, gain) {
|
||||||
return Math.sin(phase) * 0.64 + Math.sin(phase * 2) * 0.16 + noise(index) * 0.035;
|
addInstrument(buffer, start, duration, frequency, gain, (phase) => {
|
||||||
|
return Math.sin(phase) * 0.84 + Math.sin(phase * 2) * 0.1;
|
||||||
|
}, 0.46, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'lowStrings') {
|
function addWoodwind(buffer, start, duration, frequency, gain) {
|
||||||
return Math.sin(phase) * 0.82 + Math.sin(phase * 2) * 0.2 + Math.sin(phase * 3) * 0.08;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'brass' || profile === 'battleBrass') {
|
function addPluckedString(buffer, start, duration, frequency, gain) {
|
||||||
const bite = profile === 'battleBrass' ? 0.24 : 0.15;
|
addInstrument(buffer, start, duration, frequency, gain, (phase) => {
|
||||||
return Math.tanh(
|
return Math.sin(phase) * 0.75 + Math.sin(phase * 2) * 0.1;
|
||||||
Math.sin(phase) * 1.1 +
|
}, 0.02, 0.18, true);
|
||||||
Math.sin(phase * 2) * 0.42 +
|
|
||||||
Math.sin(phase * 3) * bite +
|
|
||||||
Math.sin(phase * 4) * 0.08
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'choir' || profile === 'darkChoir') {
|
function addInstrument(buffer, start, duration, frequency, gain, wave, attack, release, pluck = false) {
|
||||||
const hollow = profile === 'darkChoir' ? 0.32 : 0.22;
|
const startIndex = Math.max(0, Math.floor(start * sampleRate));
|
||||||
return Math.sin(phase) * 0.62 + Math.sin(phase * 1.5) * hollow + Math.sin(phase * 2) * 0.12;
|
const count = Math.max(1, Math.floor(duration * sampleRate));
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const index = startIndex + i;
|
||||||
|
if (index >= buffer.length) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'flute') {
|
const t = i / sampleRate;
|
||||||
return Math.sin(phase) * 0.78 + Math.sin(phase * 2) * 0.08 + noise(index) * 0.018;
|
const env = pluck
|
||||||
|
? Math.exp(-t * 5.8) * Math.min(1, t / attack)
|
||||||
|
: envelope(t, duration, attack, release);
|
||||||
|
const phase = 2 * Math.PI * frequency * t;
|
||||||
|
buffer[index] += wave(phase, t) * gain * env;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile === 'hornLead') {
|
|
||||||
return Math.sin(phase) * 0.72 + Math.sin(phase * 2) * 0.25 + Math.sin(phase * 3) * 0.11;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile === 'oboe') {
|
|
||||||
return Math.sin(phase) * 0.66 + Math.sin(phase * 2) * 0.28 + Math.sin(phase * 3) * 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile === 'spiccato') {
|
|
||||||
return Math.sin(phase) * 0.72 + Math.sin(phase * 2) * 0.2 + noise(index) * 0.025;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.sin(phase) * 0.7 + Math.sin(phase * 2) * 0.16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function envelope(t, duration, attack, release) {
|
function envelope(t, duration, attack, release) {
|
||||||
const attackLevel = attack <= 0 ? 1 : Math.min(1, t / attack);
|
const attackLevel = Math.min(1, t / attack);
|
||||||
const releaseLevel = release <= 0 ? 1 : Math.min(1, (duration - t) / release);
|
const releaseLevel = Math.min(1, (duration - t) / release);
|
||||||
return Math.max(0, Math.min(1, attackLevel, releaseLevel));
|
const curved = Math.sin(Math.min(1, attackLevel) * Math.PI * 0.5);
|
||||||
|
return Math.max(0, Math.min(curved, releaseLevel));
|
||||||
}
|
}
|
||||||
|
|
||||||
function smoothEdges(buffer, samples) {
|
function smoothEdges(buffer, samples) {
|
||||||
for (let i = 0; i < Math.min(samples, buffer.length); i += 1) {
|
for (let i = 0; i < Math.min(samples, buffer.length); i += 1) {
|
||||||
const edge = i / samples;
|
const fadeIn = Math.sin((i / samples) * Math.PI * 0.5);
|
||||||
const fade = Math.sin(edge * Math.PI * 0.5);
|
const fadeOut = Math.sin((i / samples) * Math.PI * 0.5);
|
||||||
buffer[i] *= fade;
|
buffer[i] *= fadeIn;
|
||||||
buffer[buffer.length - 1 - i] *= fade;
|
buffer[buffer.length - 1 - i] *= fadeOut;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,14 +211,6 @@ function midiToFrequency(midi) {
|
|||||||
return 440 * 2 ** ((midi - 69) / 12);
|
return 440 * 2 ** ((midi - 69) / 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
function noise(index) {
|
|
||||||
let value = (index * 1664525 + 1013904223) >>> 0;
|
|
||||||
value ^= value << 13;
|
|
||||||
value ^= value >>> 17;
|
|
||||||
value ^= value << 5;
|
|
||||||
return (value >>> 0) / 0xffffffff * 2 - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(buffer, target) {
|
function normalize(buffer, target) {
|
||||||
let peak = 0;
|
let peak = 0;
|
||||||
for (const value of buffer) {
|
for (const value of buffer) {
|
||||||
@@ -423,6 +227,12 @@ function normalize(buffer, target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySoftLimiter(buffer) {
|
||||||
|
for (let i = 0; i < buffer.length; i += 1) {
|
||||||
|
buffer[i] = Math.tanh(buffer[i] * 1.15) / Math.tanh(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function encodeWav(buffer) {
|
function encodeWav(buffer) {
|
||||||
const bytesPerSample = 2;
|
const bytesPerSample = 2;
|
||||||
const dataSize = buffer.length * bytesPerSample;
|
const dataSize = buffer.length * bytesPerSample;
|
||||||
|
|||||||
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.56;
|
private readonly musicVolume = 0.2;
|
||||||
|
|
||||||
registerMusicTracks(tracks: AudioTrackMap) {
|
registerMusicTracks(tracks: AudioTrackMap) {
|
||||||
this.musicTracks = tracks;
|
this.musicTracks = tracks;
|
||||||
@@ -66,12 +66,12 @@ class SoundDirector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playHover() {
|
playHover() {
|
||||||
this.playTone(660, 0.035, 0.04, 'sine');
|
this.playTone(540, 0.04, 0.018, 'sine');
|
||||||
}
|
}
|
||||||
|
|
||||||
playSelect() {
|
playSelect() {
|
||||||
this.playTone(330, 0.08, 0.12, 'triangle');
|
this.playTone(300, 0.07, 0.04, 'sine');
|
||||||
window.setTimeout(() => this.playTone(495, 0.1, 0.08, 'triangle'), 65);
|
window.setTimeout(() => this.playTone(450, 0.08, 0.028, 'sine'), 70);
|
||||||
}
|
}
|
||||||
|
|
||||||
playMusic(key?: string) {
|
playMusic(key?: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user