added generative audio, tailwind, improvements, adjustments
This commit is contained in:
@@ -1,238 +1,466 @@
|
||||
<script lang="ts">
|
||||
import * as Tone from 'tone';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as Tone from 'tone';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import AudioVisualization from '$lib/components/AudioVisualization.svelte';
|
||||
import { createPadSynth } from '$lib/audio/instruments/padSynth';
|
||||
import { createArpSynth } from '$lib/audio/instruments/arpSynth';
|
||||
import { createBassSynth } from '$lib/audio/instruments/bassSynth';
|
||||
import { createPingSynth } from '$lib/audio/instruments/pingSynth';
|
||||
import {
|
||||
createReverb,
|
||||
createDelay,
|
||||
createFilter,
|
||||
createGain,
|
||||
createAnalyser
|
||||
} from '$lib/audio/audio-effects';
|
||||
import { selectChordProgression, calculateComfortScore } from '$lib/audio/weather-mood';
|
||||
import type { ChordProgression } from '$lib/audio/chord-progressions';
|
||||
|
||||
// Component props with default values
|
||||
let {temperature2m = 20, relativeHumidity2m = 50, cloudCover = 30, volume = -10, windSpeed10m = 0, isDay} = $props();
|
||||
// Component props with default values
|
||||
let {
|
||||
temperature2m = 20,
|
||||
relativeHumidity2m = 50,
|
||||
cloudCover = 30,
|
||||
precipitation = 0,
|
||||
volume = -10,
|
||||
windSpeed10m = 0,
|
||||
isDay
|
||||
} = $props();
|
||||
|
||||
// Component state using runes
|
||||
let isPlaying = $state(false);
|
||||
let currentChordIndex = $state(0);
|
||||
let isInitialized = $state(false);
|
||||
// Component state using runes
|
||||
let isPlaying = $state(false);
|
||||
let isInitialized = $state(false);
|
||||
|
||||
// Audio components
|
||||
let synth: Tone.PolySynth | null = null;
|
||||
let reverb: Tone.Reverb | null = null;
|
||||
let delay: Tone.FeedbackDelay | null = null;
|
||||
let phaser: Tone.Phaser | null = null;
|
||||
let sequence: Tone.Sequence | null = null;
|
||||
let gain: Tone.Gain | null = null;
|
||||
// Audio components
|
||||
let synth: Tone.PolySynth | null = null;
|
||||
let arpSynth: Tone.Synth | null = null;
|
||||
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 bassSequence: Tone.Sequence | null = null;
|
||||
let reverb: Tone.Reverb | null = null;
|
||||
let delay: Tone.FeedbackDelay | null = null;
|
||||
let filter: Tone.Filter | null = null;
|
||||
let phaser: Tone.Phaser | null = null;
|
||||
let sequence: Tone.Sequence | null = null;
|
||||
let gain: Tone.Gain | null = null;
|
||||
let analyser: Tone.Analyser | null = null;
|
||||
|
||||
// Select chord progression based on weather mood
|
||||
const currentProgression: ChordProgression = $derived.by(() => {
|
||||
return selectChordProgression({
|
||||
temperature2m,
|
||||
relativeHumidity2m,
|
||||
cloudCover,
|
||||
windSpeed10m,
|
||||
precipitation,
|
||||
isDay
|
||||
});
|
||||
});
|
||||
|
||||
//TODO - ADD DIFFERENT PROGRESSIONS
|
||||
const chordProgressions = [
|
||||
[
|
||||
{ time: "0:0:0", notes: ['C4', 'E4', 'G4', 'B4'] },
|
||||
{ time: "0:1:0", notes: ['A3', 'C4', 'E4', 'G4'] },
|
||||
{ time: "0:2:0", notes: ['F3', 'A3', 'C4', 'E4'] },
|
||||
{ time: "0:3:0", notes: ['G3', 'B3', 'D4', 'F4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:1:0", notes: ['G3', 'B3', 'D4', 'F4'] },
|
||||
{ time: "0:2:0", notes: ['C4', 'E4', 'G4', 'B4'] },
|
||||
{ time: "0:3:0", notes: ['A3', 'C4', 'E4', 'G4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['E4', 'G4', 'B4', 'D5'] },
|
||||
{ time: "0:1:0", notes: ['A3', 'C4', 'E4', 'G4'] },
|
||||
{ time: "0:2:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:3:0", notes: ['G3', 'B3', 'D4', 'F4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['F3', 'A3', 'C4', 'E4'] },
|
||||
{ time: "0:1:0", notes: ['E4', 'G4', 'B4', 'D5'] },
|
||||
{ time: "0:2:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:3:0", notes: ['C4', 'E4', 'G4', 'B4'] }
|
||||
],
|
||||
];
|
||||
// Derived reactive values using runes with safe fallbacks
|
||||
const bpm = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// BPM starts at 10 for 0°C and increases with temperature
|
||||
// Day: more energetic (2x scaling), Night: calmer (1x scaling)
|
||||
const tempAboveZero = Math.max(0, temp);
|
||||
const scaledBpm = isDay ? 10 + tempAboveZero * 2 : 10 + tempAboveZero;
|
||||
return Math.max(10, Math.min(200, scaledBpm));
|
||||
});
|
||||
|
||||
let currentProgression = $state(chordProgressions[0]);
|
||||
const reverbWet = $derived.by(() => {
|
||||
const humidity = relativeHumidity2m ?? 50;
|
||||
// Ensure minimum 0.3 wet signal so reverb is always audible
|
||||
return Math.max(0.3, Math.min(1, humidity / 100));
|
||||
});
|
||||
|
||||
// Derived reactive values using runes
|
||||
const bpm = $derived((isDay? temperature2m * 2 : temperature2m));
|
||||
const reverbWet = $derived(relativeHumidity2m/100);
|
||||
const delayWet = $derived(Math.round(windSpeed10m)/10);
|
||||
const phaserBase = $derived((1 / cloudCover) * 100);
|
||||
// Delay time: 8th note for calm, quarter note for windy
|
||||
const delayTime = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
return speed > 5 ? '4n' : '8n';
|
||||
});
|
||||
|
||||
// Initialize audio components
|
||||
const initializeAudio = async (): Promise<void> => {
|
||||
try {
|
||||
// Create dreamy synth
|
||||
synth = new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: {
|
||||
type: isDay? 'triangle' : 'sine',
|
||||
},
|
||||
envelope: {
|
||||
attack: 1.5,
|
||||
decay: 1,
|
||||
sustain: 0.7,
|
||||
release: 1.0,
|
||||
},
|
||||
volume: -20,
|
||||
});
|
||||
// Delay feedback: stronger with more wind
|
||||
const delayFeedback = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
// Map 0-20 m/s wind to 0.2-0.7 feedback range
|
||||
return Math.max(0.2, Math.min(0.7, (speed / 20) * 0.5 + 0.2));
|
||||
});
|
||||
|
||||
// Create reverb with long, dreamy tail
|
||||
reverb = new Tone.Reverb({
|
||||
decay: 16,
|
||||
wet: reverbWet,
|
||||
preDelay: 0.5,
|
||||
});
|
||||
// Filter cutoff: more clouds = darker/lower frequency
|
||||
const filterCutoff = $derived.by(() => {
|
||||
const cover = cloudCover ?? 30;
|
||||
// Map cloud cover: 0% clouds = 8000Hz (bright), 100% clouds = 400Hz (dark)
|
||||
return Math.max(400, Math.min(8000, 8000 - (cover / 100) * 7600));
|
||||
});
|
||||
|
||||
// Filter resonance: more wind = more resonant
|
||||
const filterResonance = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
// Map 0-30 m/s wind to 1-18 resonance (Q factor)
|
||||
return Math.max(1, Math.min(18, 1 + (speed / 30) * 17));
|
||||
});
|
||||
|
||||
delay = new Tone.FeedbackDelay({
|
||||
delayTime: '0.5',
|
||||
feedback: delayWet
|
||||
})
|
||||
// Arpeggio interval: slower in cold, faster in heat
|
||||
const arpInterval = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: <0°C = 1n (whole note), 30°C+ = 8n (eighth note)
|
||||
if (temp < 0) return '1n';
|
||||
if (temp < 10) return '2n';
|
||||
if (temp < 20) return '4n';
|
||||
return '8n';
|
||||
});
|
||||
|
||||
// Create a phaser
|
||||
phaser = new Tone.Phaser({
|
||||
frequency : phaserBase,
|
||||
octaves : 5,
|
||||
baseFrequency : 350
|
||||
})
|
||||
|
||||
// Create gain node
|
||||
gain = new Tone.Gain(Tone.dbToGain(volume));
|
||||
// Arpeggio volume: quieter in cold, louder in heat
|
||||
const arpVolume = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: <0°C = -22dB, 30°C+ = -10dB
|
||||
const normalizedTemp = Math.max(0, Math.min(30, temp));
|
||||
return -22 + (normalizedTemp / 30) * 12;
|
||||
});
|
||||
|
||||
// Connect audio chain
|
||||
synth.connect(phaser).connect(delay).connect(reverb).connect(gain).toDestination();
|
||||
// Weather extremity: 0 = pleasant, 1 = extreme conditions
|
||||
const weatherExtremity = $derived.by(() => {
|
||||
const comfortScore = calculateComfortScore({
|
||||
temperature2m,
|
||||
relativeHumidity2m,
|
||||
cloudCover,
|
||||
windSpeed10m,
|
||||
precipitation,
|
||||
isDay
|
||||
});
|
||||
return 1 - comfortScore; // Invert: higher = more extreme
|
||||
});
|
||||
|
||||
// Generate reverb impulse
|
||||
await reverb.generate();
|
||||
// Ping volume: quieter in pleasant weather, louder in extreme weather
|
||||
const pingVolume = $derived.by(() => {
|
||||
// Map extremity: 0 (pleasant) = -22dB (present), 1 (extreme) = -8dB (prominent)
|
||||
return -22 + weatherExtremity * 14;
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
console.log('Audio initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize audio:', error);
|
||||
}
|
||||
};
|
||||
// Ping interval: slower in cold, faster in heat
|
||||
const pingInterval = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: 0°C = 1n (whole note, slow), 30°C = 16n (16th note, fast)
|
||||
if (temp <= 0) return '1n';
|
||||
if (temp <= 10) return '2n';
|
||||
if (temp <= 20) return '4n';
|
||||
if (temp <= 25) return '8n';
|
||||
return '16n';
|
||||
});
|
||||
|
||||
// Start the chord sequence
|
||||
const startSequence = async (): Promise<void> => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await initializeAudio();
|
||||
}
|
||||
// Initialize audio components
|
||||
const initializeAudio = async (): Promise<void> => {
|
||||
try {
|
||||
// Create instruments
|
||||
synth = createPadSynth(isDay);
|
||||
arpSynth = createArpSynth(arpVolume);
|
||||
pingSynth = createPingSynth(pingVolume);
|
||||
bassSynth = createBassSynth();
|
||||
|
||||
await Tone.start();
|
||||
// Create effects
|
||||
reverb = createReverb(reverbWet);
|
||||
delay = createDelay(delayTime, delayFeedback);
|
||||
filter = createFilter(filterCutoff, filterResonance);
|
||||
gain = createGain(volume);
|
||||
analyser = createAnalyser();
|
||||
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
// Connect audio chain using .chain() for clarity
|
||||
synth.chain(filter, delay, reverb, gain, analyser, Tone.Destination);
|
||||
arpSynth.chain(filter, delay, reverb, gain);
|
||||
pingSynth.chain(filter, delay, reverb, gain);
|
||||
bassSynth.chain(delay, reverb, gain);
|
||||
|
||||
// Set transport BPM
|
||||
Tone.getTransport().bpm.value = bpm;
|
||||
// Generate reverb impulse
|
||||
await reverb.generate();
|
||||
|
||||
let progressionChangeCounter = 0;
|
||||
isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize audio:', error);
|
||||
}
|
||||
};
|
||||
|
||||
sequence = new Tone.Sequence((time: number, chord) => {
|
||||
if (synth && chord) {
|
||||
synth!.triggerAttackRelease(chord.notes, '4n', time);
|
||||
}
|
||||
// Start the chord sequence
|
||||
const startSequence = async (): Promise<void> => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await initializeAudio();
|
||||
}
|
||||
|
||||
progressionChangeCounter++;
|
||||
|
||||
//Change progression every full cycle (4 chords) for variation
|
||||
if (progressionChangeCounter >= currentProgression.length) {
|
||||
currentChordIndex = (currentChordIndex + 1) % chordProgressions.length;
|
||||
currentProgression = chordProgressions[currentChordIndex];
|
||||
sequence!.events = currentProgression;
|
||||
progressionChangeCounter = 0;
|
||||
}
|
||||
}, currentProgression, "4n");
|
||||
await Tone.start();
|
||||
|
||||
sequence.start(0);
|
||||
Tone.getTransport().start();
|
||||
isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting sequence:', error);
|
||||
}
|
||||
};
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
|
||||
// Stop the sequence
|
||||
const stopSequence = (): void => {
|
||||
if (sequence) {
|
||||
sequence.stop();
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
Tone.getTransport().stop();
|
||||
Tone.getTransport().cancel();
|
||||
isPlaying = false;
|
||||
};
|
||||
// Set transport BPM
|
||||
Tone.getTransport().bpm.value = bpm;
|
||||
|
||||
// Toggle playback
|
||||
const togglePlayback = async (): Promise<void> => {
|
||||
if (isPlaying) {
|
||||
stopSequence();
|
||||
} else {
|
||||
await startSequence();
|
||||
}
|
||||
};
|
||||
sequence = new Tone.Sequence(
|
||||
(time: number, chord) => {
|
||||
if (synth && chord) {
|
||||
synth!.triggerAttackRelease(chord.notes, '4n', time);
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
'4n'
|
||||
);
|
||||
|
||||
// Reactive updates for environmental parameters using effects
|
||||
$effect(() => {
|
||||
if (reverb && isInitialized) {
|
||||
reverb.wet.rampTo(reverbWet, 0.5);
|
||||
}
|
||||
});
|
||||
sequence.start(0);
|
||||
|
||||
$effect(() => {
|
||||
if (phaser && isInitialized) {
|
||||
phaser.frequency.rampTo(phaserBase, 0.5);
|
||||
}
|
||||
});
|
||||
// Create arpeggio sequence
|
||||
if (arpSequence) {
|
||||
arpSequence.dispose();
|
||||
arpSequence = null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (gain && isInitialized) {
|
||||
gain.gain.rampTo(Tone.dbToGain(volume), 0.1);
|
||||
}
|
||||
});
|
||||
arpSequence = new Tone.Sequence(
|
||||
(time: number, chord) => {
|
||||
if (arpSynth && chord && chord.notes) {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
arpInterval
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (isPlaying && isInitialized) {
|
||||
Tone.getTransport().bpm.rampTo(bpm, 1.0);
|
||||
}
|
||||
});
|
||||
arpSequence.start(0);
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
initializeAudio();
|
||||
});
|
||||
// Create ping sequence (reverse arpeggio - evenly spaced through chord duration)
|
||||
if (pingSequence) {
|
||||
pingSequence.dispose();
|
||||
pingSequence = null;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
}
|
||||
if (synth) {
|
||||
synth.dispose();
|
||||
}
|
||||
if (reverb) {
|
||||
reverb.dispose();
|
||||
}
|
||||
if (phaser) {
|
||||
phaser.dispose();
|
||||
}
|
||||
if (gain) {
|
||||
gain.dispose();
|
||||
}
|
||||
});
|
||||
// Build a flat array of notes: 4 notes per chord, in reverse order, transposed up 2 octaves
|
||||
const pingNotes: 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];
|
||||
const noteName = note.slice(0, -1);
|
||||
const octave = parseInt(note.slice(-1));
|
||||
const highNote = noteName + (octave + 2);
|
||||
pingNotes.push(highNote);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
pingSequence.start(0);
|
||||
|
||||
// Create bass sequence
|
||||
if (bassSequence) {
|
||||
bassSequence.dispose();
|
||||
bassSequence = null;
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
// 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);
|
||||
|
||||
// Trigger bass note
|
||||
bassSynth.triggerAttackRelease(bassNote, '4n', time);
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
'4n'
|
||||
);
|
||||
|
||||
bassSequence.start(0);
|
||||
|
||||
Tone.getTransport().start();
|
||||
isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting sequence:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Stop the sequence
|
||||
const stopSequence = (): void => {
|
||||
if (sequence) {
|
||||
sequence.stop();
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
if (arpSequence) {
|
||||
arpSequence.stop();
|
||||
arpSequence.dispose();
|
||||
arpSequence = null;
|
||||
}
|
||||
if (pingSequence) {
|
||||
pingSequence.stop();
|
||||
pingSequence.dispose();
|
||||
pingSequence = null;
|
||||
}
|
||||
if (bassSequence) {
|
||||
bassSequence.stop();
|
||||
bassSequence.dispose();
|
||||
bassSequence = null;
|
||||
}
|
||||
Tone.getTransport().stop();
|
||||
Tone.getTransport().cancel();
|
||||
isPlaying = false;
|
||||
};
|
||||
|
||||
// Toggle playback
|
||||
const togglePlayback = async (): Promise<void> => {
|
||||
if (isPlaying) {
|
||||
stopSequence();
|
||||
} else {
|
||||
await startSequence();
|
||||
}
|
||||
};
|
||||
|
||||
// Reactive updates for environmental parameters using effects
|
||||
$effect(() => {
|
||||
if (reverb && isInitialized) {
|
||||
reverb.wet.rampTo(reverbWet, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (delay && isInitialized) {
|
||||
delay.delayTime.value = delayTime;
|
||||
delay.feedback.rampTo(delayFeedback, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (filter && isInitialized) {
|
||||
filter.frequency.rampTo(filterCutoff, 1.0);
|
||||
filter.Q.rampTo(filterResonance, 1.0);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (gain && isInitialized) {
|
||||
gain.gain.rampTo(Tone.dbToGain(volume), 0.1);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isPlaying && isInitialized) {
|
||||
Tone.getTransport().bpm.rampTo(bpm, 1.0);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (arpSynth && isInitialized) {
|
||||
arpSynth.volume.rampTo(arpVolume, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (pingSynth && isInitialized) {
|
||||
pingSynth.volume.rampTo(pingVolume, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
initializeAudio();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
}
|
||||
if (arpSequence) {
|
||||
arpSequence.dispose();
|
||||
}
|
||||
if (pingSequence) {
|
||||
pingSequence.dispose();
|
||||
}
|
||||
if (bassSequence) {
|
||||
bassSequence.dispose();
|
||||
}
|
||||
if (synth) {
|
||||
synth.dispose();
|
||||
}
|
||||
if (arpSynth) {
|
||||
arpSynth.dispose();
|
||||
}
|
||||
if (pingSynth) {
|
||||
pingSynth.dispose();
|
||||
}
|
||||
if (bassSynth) {
|
||||
bassSynth.dispose();
|
||||
}
|
||||
if (reverb) {
|
||||
reverb.dispose();
|
||||
}
|
||||
if (delay) {
|
||||
delay.dispose();
|
||||
}
|
||||
if (filter) {
|
||||
filter.dispose();
|
||||
}
|
||||
if (gain) {
|
||||
gain.dispose();
|
||||
}
|
||||
if (analyser) {
|
||||
analyser.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 p-4 max-w-6xl mx-auto">
|
||||
<div class="flex flex-col gap-4">
|
||||
<button
|
||||
class="px-6 py-3 text-base cursor-crosshair transition-all duration-300 rounded-md border border-white/20 hover:border-white/40 disabled:opacity-50 disabled:cursor-not-allowed {isPlaying
|
||||
? 'bg-white text-black'
|
||||
: 'bg-transparent text-white'}"
|
||||
onclick={togglePlayback}
|
||||
disabled={!isInitialized}
|
||||
>
|
||||
{isPlaying ? 'Stop' : 'Play'} Weather Ambient
|
||||
</button>
|
||||
|
||||
<!-- TODO: ADD VIZUALZ https://www.npmjs.com/package/p5-svelte -->
|
||||
<!-- https://jsfiddle.net/aqilahmisuary/ztf5a72h/#base -->
|
||||
<div class="controls-container">
|
||||
<!-- Playback Control -->
|
||||
<div class="playback-section">
|
||||
<button
|
||||
class="play-button {isPlaying ? 'playing' : ''}"
|
||||
onclick = {togglePlayback}
|
||||
disabled={!isInitialized}
|
||||
>
|
||||
{isPlaying ? 'Stop' : 'Play'} ✨
|
||||
</button>
|
||||
</div>
|
||||
{#if isPlaying}
|
||||
<div class="flex flex-col gap-2 text-sm opacity-80">
|
||||
<p class="m-0">Temperature: {temperature2m.toFixed(1)}°C</p>
|
||||
<p class="m-0">Humidity: {relativeHumidity2m}%</p>
|
||||
<p class="m-0">Cloud Cover: {cloudCover}%</p>
|
||||
<p class="m-0">Wind Speed: {windSpeed10m.toFixed(1)} m/s</p>
|
||||
<p class="opacity-40 my-1">---</p>
|
||||
<p class="m-0">BPM: {bpm}</p>
|
||||
<p class="m-0">Weather Extremity: {(weatherExtremity * 100).toFixed(0)}%</p>
|
||||
<p class="m-0">Reverb: {reverbWet.toFixed(2)}</p>
|
||||
<p class="m-0">Delay: {delayTime} @ {delayFeedback.toFixed(2)} feedback</p>
|
||||
<p class="m-0">Filter: {Math.round(filterCutoff)}Hz Q:{filterResonance.toFixed(1)}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<AudioVisualization {isPlaying} {analyser} width={400} height={400} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user