Optimizations, increased variability

This commit is contained in:
2026-03-02 13:42:14 +02:00
parent 7aacb29dbd
commit 4fbe4eb23e
6 changed files with 204 additions and 179 deletions

View File

@@ -2,7 +2,7 @@ import * as Tone from 'tone';
export function createReverb(wetValue: number): Tone.Reverb {
const reverb = new Tone.Reverb({
decay: 16,
decay: 4,
preDelay: 0.5
});
reverb.wet.value = wetValue;

View File

@@ -4,7 +4,7 @@ export function createPadSynth(isDay: boolean): Tone.PolySynth {
return new Tone.PolySynth(Tone.Synth, {
oscillator: {
type: 'fatsine', // Fat oscillator creates multiple detuned voices for lush sound
count: 3, // Number of detuned oscillators
count: 2, // Number of detuned oscillators
spread: 30 // Amount of detune in cents for width and movement
},
envelope: {

View File

@@ -130,7 +130,7 @@
}
p.background(0, 30); // Fade effect
if (isPlaying && analyser) {
if (isPlaying && analyser && p.frameCount % 2 === 0) {
// Get FFT data (frequency analysis)
audioData = analyser.getValue() as Float32Array;

View File

@@ -114,7 +114,7 @@
// Create effects with much more spacious reverb for air quality
reverb = new Tone.Reverb({
decay: 30, // Very long decay for spacious sound
decay: 5, // Reduced from 30 for better performance
preDelay: 0.1
});
reverb.wet.value = 0.8; // Higher wet signal for more reverb

View File

@@ -46,7 +46,7 @@
let pingSynth: Tone.Synth | null = null;
let bassSynth: Tone.Synth | null = null;
let arpSequence: Tone.Sequence | null = null;
let pingSequence: Tone.Sequence | null = null;
let pingSequence: Tone.Loop | null = null;
let bassSequence: Tone.Sequence | null = null;
let reverb: Tone.Reverb | null = null;
let delay: Tone.FeedbackDelay | null = null;
@@ -54,6 +54,11 @@
let sequence: Tone.Sequence | null = null;
let gain: Tone.Gain | null = null;
let analyser: Tone.Analyser | null = null;
let driftLoop: Tone.Loop | null = null;
let dropoutLoop: Tone.Loop | null = null;
let arpActive = true;
let pingActive = true;
let bassActive = true;
// Select chord progression based on weather mood
const currentProgression: ChordProgression = $derived.by(() => {
@@ -142,6 +147,27 @@
return 1 - comfortScore; // Invert: higher = more extreme
});
// Pad volume: louder/present in clear sky, quieter/receded in overcast
const padVolume = $derived.by(() => {
const clouds = cloudCover ?? 50;
return -8 - (Math.min(100, Math.max(0, clouds)) / 100) * 14;
});
// Pad spread: tight detune in clear sky, wide/diffuse in overcast
const padSpread = $derived.by(() => {
const clouds = cloudCover ?? 50;
return 10 + (Math.min(100, Math.max(0, clouds)) / 100) * 50;
});
// Arp pattern shape: changes based on weather conditions
const arpPatternType = $derived.by(() => {
if (precipitation > 5 || weatherExtremity > 0.8) return 'random';
if (temperature2m < 5) return 'sparse';
if (relativeHumidity2m > 80 && cloudCover > 60) return 'probabilistic';
if (temperature2m > 25) return 'descending';
return 'ascending';
});
// Ping volume: quieter in pleasant weather, louder in extreme weather
const pingVolume = $derived.by(() => {
// Map extremity: 0 (pleasant) = -22dB (present), 1 (extreme) = -8dB (prominent)
@@ -239,6 +265,9 @@
sequence = null;
}
// Shuffle chord order — different harmonic arrangement each playback
const shuffledProgression = [...currentProgression].sort(() => Math.random() - 0.5);
// Set all audio parameters once (static until page refresh)
Tone.getTransport().bpm.value = bpm;
@@ -268,13 +297,18 @@
pingSynth.volume.value = pingVolume;
}
if (synth) {
synth.volume.value = padVolume;
synth.set({ oscillator: { spread: padSpread } });
}
sequence = new Tone.Sequence(
(time: number, chord) => {
if (synth && chord) {
synth!.triggerAttackRelease(chord.notes, '4n', time);
}
},
currentProgression,
shuffledProgression,
'4n'
);
@@ -286,56 +320,103 @@
arpSequence = null;
}
// Arp burst/silence — phrase-level gaps independent of BPM
const arpT = temperature2m ?? 20;
const arpBurstMax = arpT <= 10 ? 6 : arpT <= 20 ? 8 : 12; // seconds
const arpSilenceMax = arpT <= 10 ? 5 : arpT <= 20 ? 4 : 3; // seconds
let inArpBurst = true;
let arpStateUntil = Tone.now() + Math.random() * arpBurstMax + 2;
arpSequence = new Tone.Sequence(
(time: number, chord) => {
const now = Tone.now();
if (now >= arpStateUntil) {
inArpBurst = !inArpBurst;
arpStateUntil = now + (inArpBurst
? Math.random() * arpBurstMax + 2
: Math.random() * arpSilenceMax + 1);
}
if (!inArpBurst) return;
if (arpSynth && chord && chord.notes) {
// Capture synth reference for TypeScript
const synth = arpSynth;
// Calculate time between notes based on 16th notes
const sixteenthNote = Tone.Time('16n').toSeconds();
// Play arpeggio pattern through the chord notes
chord.notes.forEach((note: string, index: number) => {
let notes = [...chord.notes];
if (arpPatternType === 'sparse') {
// Root (index 0) and fifth (index 2) only — hollow, minimal
notes = [chord.notes[0], chord.notes[2]].filter(Boolean);
} else if (arpPatternType === 'descending') {
notes = [...chord.notes].reverse();
} else if (arpPatternType === 'random') {
// Fisher-Yates shuffle — chaotic, unsettled
for (let i = notes.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[notes[i], notes[j]] = [notes[j], notes[i]];
}
}
notes.forEach((note: string, index: number) => {
if (arpPatternType === 'probabilistic' && Math.random() > 0.5) return;
synth.triggerAttackRelease(note, '16n', time + index * sixteenthNote);
});
}
},
currentProgression,
shuffledProgression,
arpInterval
);
arpSequence.start(0);
// Create ping sequence (reverse arpeggio - evenly spaced through chord duration)
// Create ping — free melodic random walk over progression's scale tones
if (pingSequence) {
pingSequence.dispose();
pingSequence = null;
}
// Build a flat array of notes: 4 notes per chord, in reverse order, transposed up 2 octaves
const pingNotes: string[] = [];
// Build sorted note pool from all unique pitch classes in the progression
const notePool: string[] = [];
const seenNames = new Set<string>();
currentProgression.forEach((chord) => {
if (chord && chord.notes) {
// Get 4 notes in reverse order (last to first, cycling if needed)
const reversedNotes = [...chord.notes].reverse();
for (let i = 0; i < 4; i++) {
const note = reversedNotes[i % reversedNotes.length];
if (chord?.notes) {
chord.notes.forEach((note: string) => {
const noteName = note.slice(0, -1);
const octave = parseInt(note.slice(-1));
const highNote = noteName + (octave + 2);
pingNotes.push(highNote);
}
if (!seenNames.has(noteName)) {
seenNames.add(noteName);
if (octave + 1 <= 6) notePool.push(noteName + (octave + 1));
if (octave + 2 <= 6) notePool.push(noteName + (octave + 2));
}
});
}
});
// Captured once at sequence start — same pattern as all other params
// Burst/silence state tracked via Tone.now() inside the callback itself —
// completely independent of pingInterval and BPM.
const temp = temperature2m ?? 20;
const burstMax = temp <= 10 ? 5 : temp <= 20 ? 7 : 10; // seconds
const silenceMax = temp <= 10 ? 4 : temp <= 20 ? 3 : 2; // seconds — shorter for more presence
pingSequence = new Tone.Sequence(
(time: number, note: string) => {
if (pingSynth && note) {
pingSynth.triggerAttackRelease(note, '32n', time);
}
},
pingNotes,
pingInterval // Temperature-reactive: 1n (cold) to 16n (hot)
);
let inBurst = Math.random() < 0.5;
let stateUntil = Tone.now() + (inBurst
? Math.random() * burstMax + 1
: Math.random() * silenceMax + 1);
pingSequence = new Tone.Loop((time: number) => {
const now = Tone.now();
if (now >= stateUntil) {
inBurst = !inBurst;
stateUntil = now + (inBurst
? Math.random() * burstMax + 1
: Math.random() * silenceMax + 1);
}
if (!inBurst) return;
if (pingSynth && notePool.length > 0) {
const note = notePool[Math.floor(Math.random() * notePool.length)];
pingSynth.volume.value = pingVolume + (Math.random() * 12 - 6);
pingSynth.triggerAttackRelease(note, '32n', time);
}
}, pingInterval);
pingSequence.start(0);
@@ -347,27 +428,58 @@
bassSequence = new Tone.Sequence(
(time: number, chord) => {
if (bassSynth && chord && chord.notes && chord.notes.length > 0) {
// Get root note (first note of chord) and transpose down 2 octaves
const rootNote = chord.notes[0];
const noteName = rootNote.slice(0, -1); // e.g., 'C' from 'C4'
const octave = parseInt(rootNote.slice(-1)); // e.g., 4 from 'C4'
const bassNote = noteName + (octave - 2); // e.g., 'C2'
if (!bassSynth || !chord?.notes?.length) return;
// Randomize note duration: half to full chord duration (2n to 4n)
const randomDuration = 0.5 + Math.random() * 0.5;
const noteDuration = Tone.Time('4n').toSeconds() * randomDuration;
// 20% chance to skip — rhythmic breathing without harmonic risk
if (Math.random() < 0.2) return;
// Trigger bass note at the scheduled time
bassSynth.triggerAttackRelease(bassNote, noteDuration, time);
}
const noteName = chord.notes[0].slice(0, -1);
const octave = parseInt(chord.notes[0].slice(-1));
const bassNote = noteName + (octave - 2);
// Random duration: 50100% of a quarter note
const noteDuration = Tone.Time('4n').toSeconds() * (0.5 + Math.random() * 0.5);
bassSynth.triggerAttackRelease(bassNote, noteDuration, time);
},
currentProgression,
shuffledProgression,
'4n'
);
bassSequence.start(0);
// Slow parameter drift — filter and reverb breathe around weather baselines
if (driftLoop) { driftLoop.dispose(); driftLoop = null; }
driftLoop = new Tone.Loop(() => {
const filterRange = filterCutoff * 0.4;
const newFreq = filterCutoff + (Math.random() * 2 - 1) * filterRange;
filter?.frequency.rampTo(Math.max(200, Math.min(12000, newFreq)), 20);
const newWet = reverbWet + (Math.random() * 2 - 1) * 0.2;
reverb?.wet.rampTo(Math.max(0.1, Math.min(1.0, newWet)), 25);
}, 20); // every 20 seconds — independent of BPM
driftLoop.start('+10'); // first fire 10 seconds after playback starts
// Arp dropout — asymmetric: low chance to go silent, high chance to recover
// Ping has its own burst/silence mechanism — excluded here to avoid conflict
// Bass stays as harmonic foundation
arpActive = true;
if (dropoutLoop) { dropoutLoop.dispose(); dropoutLoop = null; }
dropoutLoop = new Tone.Loop(() => {
if (arpActive) {
// Only 15% chance to go silent — arp stays on most of the time
if (Math.random() < 0.15) {
arpActive = false;
arpSynth?.volume.rampTo(-80, 2);
}
} else {
// 75% chance to come back — silences are short
if (Math.random() < 0.75) {
arpActive = true;
arpSynth?.volume.rampTo(arpVolume, 1.5);
}
}
}, 25);
dropoutLoop.start('+30'); // wait 30s before first dropout decision
Tone.getTransport().start();
// Request wake lock to prevent screen sleep
@@ -401,6 +513,9 @@
bassSequence.dispose();
bassSequence = null;
}
if (driftLoop) { driftLoop.stop(); driftLoop.dispose(); driftLoop = null; }
if (dropoutLoop) { dropoutLoop.stop(); dropoutLoop.dispose(); dropoutLoop = null; }
arpActive = true;
Tone.getTransport().stop();
Tone.getTransport().cancel();
isPlaying = false;
@@ -492,6 +607,8 @@
if (analyser) {
analyser.dispose();
}
if (driftLoop) { driftLoop.dispose(); }
if (dropoutLoop) { dropoutLoop.dispose(); }
});
</script>