diff --git a/src/app.html b/src/app.html
index 420dee5..2e4b786 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,7 +3,10 @@
-
+ Hear On Out - Listen to the Weather
+
+
+
diff --git a/src/lib/audio/audio-effects.ts b/src/lib/audio/audio-effects.ts
index f21af0f..531841f 100644
--- a/src/lib/audio/audio-effects.ts
+++ b/src/lib/audio/audio-effects.ts
@@ -31,5 +31,5 @@ export function createGain(volume: number): Tone.Gain {
}
export function createAnalyser(): Tone.Analyser {
- return new Tone.Analyser('fft', 512);
+ return new Tone.Analyser('fft', 256); // Reduced from 512 for better performance
}
diff --git a/src/lib/components/AudioVisualization.svelte b/src/lib/components/AudioVisualization.svelte
index 45c860f..beee6ba 100644
--- a/src/lib/components/AudioVisualization.svelte
+++ b/src/lib/components/AudioVisualization.svelte
@@ -11,9 +11,14 @@
}>();
let particles: Particle[] = [];
- const numParticles = 60;
+ const numParticles = 40; // Reduced from 60 for better performance with two visualizations
let audioData: Float32Array | null = null;
+ // Cache frequently used calculations
+ let bassRange = 0;
+ let midRange = 0;
+ let trebleStart = 0;
+
class Particle {
x: number;
y: number;
@@ -42,11 +47,13 @@
this.x += this.vx;
this.y += this.vy;
- // Subtle audio reactive displacement
- const displacement = audioLevel * 20;
- const angle = p.noise(this.x * 0.01, this.y * 0.01, p.frameCount * 0.01) * p.TWO_PI;
- this.x += p.cos(angle) * displacement * 0.05;
- this.y += p.sin(angle) * displacement * 0.05;
+ // Subtle audio reactive displacement (only every 3rd frame for performance)
+ if (p.frameCount % 3 === 0) {
+ const displacement = audioLevel * 20;
+ const angle = p.noise(this.x * 0.01, this.y * 0.01, p.frameCount * 0.01) * p.TWO_PI;
+ this.x += p.cos(angle) * displacement * 0.05;
+ this.y += p.sin(angle) * displacement * 0.05;
+ }
// Subtle audio reactive size
this.size = p.map(bass + mid, 0, 2, 3, 10);
@@ -79,6 +86,7 @@
const sketch = (p: p5) => {
p.setup = () => {
p.createCanvas(width, height);
+ p.frameRate(30); // Reduce from 60fps to 30fps for better performance
p.background(0);
// Initialize particles
@@ -89,12 +97,23 @@
};
p.draw = () => {
+ // Pause visualization when document is hidden (tab not focused)
+ if (document.hidden) {
+ return;
+ }
p.background(0, 30); // Fade effect
if (isPlaying && analyser) {
// Get FFT data (frequency analysis)
audioData = analyser.getValue() as Float32Array;
+ // Initialize range values on first run
+ if (bassRange === 0) {
+ bassRange = Math.floor(audioData.length * 0.15); // Low frequencies
+ midRange = Math.floor(audioData.length * 0.4); // Mid frequencies
+ trebleStart = midRange;
+ }
+
// Calculate audio metrics from FFT data
// FFT values are in decibels (negative values, typically -100 to 0)
let sum = 0;
@@ -102,9 +121,6 @@
let mid = 0;
let treble = 0;
- const bassRange = Math.floor(audioData.length * 0.15); // Low frequencies
- const midRange = Math.floor(audioData.length * 0.4); // Mid frequencies
-
for (let i = 0; i < audioData.length; i++) {
// Convert from decibels to linear scale (0-1)
// FFT returns values from -100 to 0 dB
@@ -124,7 +140,7 @@
let audioLevel = (sum / audioData.length) * 2;
bass = (bass / bassRange) * 2.5;
mid = (mid / (midRange - bassRange)) * 2;
- treble = (treble / (audioData.length - midRange)) * 1.5;
+ treble = (treble / (audioData.length - trebleStart)) * 1.5;
// Clamp values
audioLevel = p.constrain(audioLevel, 0, 1);
@@ -139,10 +155,15 @@
for (let i = 0; i < particles.length; i++) {
particles[i].update(p, audioLevel, bass, mid);
particles[i].display(p, audioLevel);
+ }
- // Connect nearby particles (non-reactive distance)
- for (let j = i + 1; j < particles.length; j++) {
- particles[i].connect(p, particles[j], connectionDist);
+ // Draw connections less frequently (every other frame) for better performance
+ if (p.frameCount % 2 === 0) {
+ for (let i = 0; i < particles.length; i++) {
+ // Only check next 5 particles instead of all, reduces O(n²) significantly
+ for (let j = i + 1; j < Math.min(i + 6, particles.length); j++) {
+ particles[i].connect(p, particles[j], connectionDist);
+ }
}
}
} else if (!isPlaying) {
diff --git a/src/lib/generators/air-quality/AirQualityGen.svelte b/src/lib/generators/air-quality/AirQualityGen.svelte
index 8d1925f..6912260 100644
--- a/src/lib/generators/air-quality/AirQualityGen.svelte
+++ b/src/lib/generators/air-quality/AirQualityGen.svelte
@@ -12,7 +12,7 @@
pm25 = 0,
relativeHumidity2m = 50,
windSpeed10m = 0,
- volume = -15
+ volume = -12
} = $props();
// Component state
@@ -65,6 +65,9 @@
// Initialize audio components
const initializeAudio = async (): Promise => {
try {
+ // Optimize audio scheduling for better stability
+ Tone.getContext().lookAhead = 0.1; // Keep default 100ms lookahead
+
// Create instruments
noiseSynth = createNoiseSynth(volume);
@@ -105,20 +108,22 @@
loop = null;
}
- // Create a loop that triggers at random intervals
+ // Create a loop that triggers at intervals with variation
+ const updateLoopInterval = () => {
+ if (loop) {
+ const randomFactor = 0.8 + Math.random() * 0.4; // 0.8x to 1.2x variation
+ loop.interval = burstInterval * randomFactor;
+ }
+ };
+
loop = new Tone.Loop((time) => {
if (noiseSynth) {
- // Trigger noise burst
+ // Trigger noise burst at the scheduled time
noiseSynth.triggerAttackRelease('16n', time);
-
- // Schedule next burst with randomization
- const baseInterval = burstInterval;
- const randomFactor = 0.5 + Math.random(); // 0.5x to 1.5x variation
- const nextBurstTime = baseInterval * randomFactor;
-
- if (loop) {
- loop.interval = nextBurstTime;
- }
+ // Schedule interval update for after this callback completes
+ Tone.Draw.schedule(() => {
+ updateLoopInterval();
+ }, time);
}
}, burstInterval);
@@ -158,9 +163,14 @@
// Reactive updates for environmental parameters
// Note: Reverb wet is fixed at 0.8 for spacious sound, not reactive to humidity
+ // Memoize delay time conversion to avoid repeated calculations
+ const delayTimeSeconds = $derived.by(() => {
+ return Tone.Time(delayTime).toSeconds();
+ });
+
$effect(() => {
if (delay && isInitialized) {
- delay.delayTime.value = delayTime;
+ delay.delayTime.rampTo(delayTimeSeconds, 0.5);
delay.feedback.rampTo(delayFeedback, 0.5);
}
});
diff --git a/src/lib/generators/weather/WeatherGen.svelte b/src/lib/generators/weather/WeatherGen.svelte
index f572ed4..d9460e3 100644
--- a/src/lib/generators/weather/WeatherGen.svelte
+++ b/src/lib/generators/weather/WeatherGen.svelte
@@ -151,6 +151,9 @@
// Initialize audio components
const initializeAudio = async (): Promise => {
try {
+ // Optimize audio scheduling for better stability
+ Tone.getContext().lookAhead = 0.1; // Keep default 100ms lookahead
+
// Create instruments
synth = createPadSynth(isDay);
arpSynth = createArpSynth(arpVolume);
@@ -219,10 +222,13 @@
arpSequence = new Tone.Sequence(
(time: number, chord) => {
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) => {
- const noteTime = time + index * 0.15; // 150ms between notes
- arpSynth!.triggerAttackRelease(note, '16n', noteTime);
+ synth.triggerAttackRelease(note, '16n', time + index * sixteenthNote);
});
}
},
@@ -281,13 +287,12 @@
const octave = parseInt(rootNote.slice(-1)); // e.g., 4 from 'C4'
const bassNote = noteName + (octave - 2); // e.g., 'C2'
- // Randomize release time: half to full chord duration
- // Quarter note = 1 beat, so random between 0.5 and 1.0 beats
- const randomRelease = 0.5 + Math.random() * 0.5;
- bassSynth.envelope.release = randomRelease * (60 / bpm);
+ // 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;
- // Trigger bass note
- bassSynth.triggerAttackRelease(bassNote, '4n', time);
+ // Trigger bass note at the scheduled time
+ bassSynth.triggerAttackRelease(bassNote, noteDuration, time);
}
},
currentProgression,
@@ -346,9 +351,14 @@
}
});
+ // Memoize delay time conversion to avoid repeated calculations
+ const delayTimeSeconds = $derived.by(() => {
+ return Tone.Time(delayTime).toSeconds();
+ });
+
$effect(() => {
if (delay && isInitialized) {
- delay.delayTime.value = delayTime;
+ delay.delayTime.rampTo(delayTimeSeconds, 0.5);
delay.feedback.rampTo(delayFeedback, 0.5);
}
});
diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png
new file mode 100644
index 0000000..811bb7d
Binary files /dev/null and b/static/favicon-16x16.png differ
diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png
new file mode 100644
index 0000000..977ff24
Binary files /dev/null and b/static/favicon-32x32.png differ
diff --git a/static/favicon-96x96.png b/static/favicon-96x96.png
new file mode 100644
index 0000000..e3367c2
Binary files /dev/null and b/static/favicon-96x96.png differ
diff --git a/static/favicon.png b/static/favicon.png
index 825b9e6..977ff24 100644
Binary files a/static/favicon.png and b/static/favicon.png differ