From 608af228c5721de7cae23f033191f52134cb26e9 Mon Sep 17 00:00:00 2001 From: jawhinge Date: Mon, 29 Dec 2025 13:42:20 +0200 Subject: [PATCH] add favicon, major performance optimizations --- src/app.html | 5 +- src/lib/audio/audio-effects.ts | 2 +- src/lib/components/AudioVisualization.svelte | 47 +++++++++++++----- .../air-quality/AirQualityGen.svelte | 36 +++++++++----- src/lib/generators/weather/WeatherGen.svelte | 28 +++++++---- static/favicon-16x16.png | Bin 0 -> 277 bytes static/favicon-32x32.png | Bin 0 -> 505 bytes static/favicon-96x96.png | Bin 0 -> 1517 bytes static/favicon.png | Bin 1571 -> 505 bytes 9 files changed, 81 insertions(+), 37 deletions(-) create mode 100644 static/favicon-16x16.png create mode 100644 static/favicon-32x32.png create mode 100644 static/favicon-96x96.png 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 0000000000000000000000000000000000000000..811bb7da30b01d998f20f4a0f170e8b30735aedf GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkQ1G~? zi(^Q|oUIqndo?+Vw0)GH=J+N3vg#K7jj`I?)15B3dUS`0?i53c} zr@SCy)BD9MZghP+mz&nb$oJ`yP2SyU#(Q+tO?|XlKKo@=vi}x6wDPuVT*d?8{*U)x z?>TP0u6|yWyYSmH(+$?guKT$#{!W{;QfJi)25XDhH#eLRj#rltkELwRz7tX#uLTIU|ziaL*@D<=W>aS$EV99Q8v?x0tG z9K}Vv!VC0Qay*AOQRgD)#Fdy2iw>!mIIdWdL3J;L%%d%uZkyuShp*A>3j}OeJX<2x z^74g8INbmN$$94xO<4y?ruyhe8=rw&|>i{~At zaW@A`PW?M(OkWzcy*PlO8a5qP%kNk;U*1_Y8urf^Z3>XHfUP)OhusdVXHU|M6)E9q v!FI|2%!LMI;CwX!J5vmmtd{&;H!;g!={uyb8AA1c00000NkvXXu0mjflG@fn literal 0 HcmV?d00001 diff --git a/static/favicon-96x96.png b/static/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..e3367c258f52862ab3fe49f0979940e81f4e9f7d GIT binary patch literal 1517 zcmVRTRK~j+2_HQ%Q}15m{u6MVgV>LK)FYi;-Cw z5fPE4W=XV=74%QEClL`SQd!Zykwnr$3q)lYEzm+`97g^IJ^}6(!Q-JluD9tH)wiu}v2LM+A??;rk zT@2mcgnqRI2LV5O6!krDeo93h4!jUyh*yEJRGg`vVPh+>92k>fJtvAmwGr0U`Bs&Z z@xa$gAptO_LG`%__<4^Jo&iQsF-CZlu}Q}7M$!g6wP>)xcI@UH4tVV9)Zuab1vdUe5^VYjP( zT}c6PPx@b>M!KP>ES#eZa6bj)VAaSKl!q?Wx0fgwZc@5VHPX2q%EBboH@CAd1QUQ; zfS)`|&xs^akwbS(b(658y?KHkBzVH>mpfGH^XG zvzZNgfD2B4 zp(=B3=9?;#U3Rp%NnFh0G2m^WC&CE-0E@G3z$2=XttDIKB;9zP4!ja!h}+1BLp;%} zg;^C|5>5iX^%%j85UF`w73#9ijGdY2Ssl8()#FLQWaWw-kY%Xh$K?>cr;PB6Y~&Jg zxhiaR(t;{PrzcJR=A?x^r3yMN+qRaLxk_(6;U*;El$#v$S-`rYt-R#bUZAv%!eRr%H#hPT(A+e*8qt$_@Van1*%e z((Mz6;R+K4Y9UrD9lD3QZpJAWgxhKx@mQ-25ZhC#PmQ>CadTvMFv^qh{0*>Y(gI7)9Pyil_2tepr((9JP&dOLm`$5Yb&o}lCut#*Q#DIG zA-5ih4Hq_FzDcx6Mt9ivPFSv)nml>|9Qm4A?}UHoXezepCPu0 z*K~yhq^~HUES1xrF8i-PLJyne`h$k%$@~~mdDUe@vpfc4<>W|j0XjWc0O^*KGl`d* zNtqzQ`#04InJUNt@Q&n3?M6W+r5V6^GG4mu;Tsw#{UK%aH@L1LSt#+{Sw&HLo&+HS zxPKc>CNKl!OJD}bm%t2=FM$~#Ujj2gz654~dc*ZJ}hhHPpx3$)nE$wQ*K*q z*xd&v)D)+)Ot0^)^|@FRcF0D#J1A2+QfJi)25XDhH#eLRj#rltkELwRz7tX#uLTIU|ziaL*@D<=W>aS$EV99Q8v?x0tG z9K}Vv!VC0Qay*AOQRgD)#Fdy2iw>!mIIdWdL3J;L%%d%uZkyuShp*A>3j}OeJX<2x z^74g8INbmN$$94xO<4y?ruyhe8=rw&|>i{~At zaW@A`PW?M(OkWzcy*PlO8a5qP%kNk;U*1_Y8urf^Z3>XHfUP)OhusdVXHU|M6)E9q v!FI|2%!LMI;CwX!J5vmmtd{&;H!;g!={uyb8AA1c00000NkvXXu0mjflG@fn literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH