summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--index.html9
-rw-r--r--soundscape/index.html275
-rw-r--r--soundscape/soundscape.js515
4 files changed, 801 insertions, 0 deletions
diff --git a/README.md b/README.md
index 2d2f218..41d502e 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ A collection of cute & creative web experiments and visual experiences.
- [Digital Zen Garden](/zengarden/) - A meditative drag-and-drop experience where users can arrange elements with subtle animations.
- [BTC Ticker](/btctick/) - Live Bitcoin price visualization with a cool animated chart showing price history.
- [Interactive Particle System](/particles/) - A full-screen interactive particle system where particles respond to cursor movement, clicks, and device tilt.
+- [Ambient Soundscape Generator](/soundscape/) - Create beautiful, generative ambient soundscapes with customizable musical scales and parameters that play continuously in the background.
## 🚀 Local Development
@@ -23,6 +24,7 @@ To view the experiments locally:
- Digital Zen Garden: `zengarden/index.html`
- BTC Ticker: `btctick/index.html`
- Interactive Particle System: `particles/index.html`
+ - Ambient Soundscape Generator: `soundscape/index.html`
## 💻 Technologies
diff --git a/index.html b/index.html
index ebfbc82..440b5b7 100644
--- a/index.html
+++ b/index.html
@@ -33,6 +33,7 @@
.container {
max-width: 800px;
padding: 2rem;
+ margin: 2rem;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 20px;
box-shadow: 0 10px 25px rgba(90, 61, 92, 0.2);
@@ -148,6 +149,14 @@
device tilt.
</div>
</a>
+
+ <a href="soundscape/" class="experiment-card">
+ <div class="experiment-title">Ambient Soundscape Generator</div>
+ <div class="experiment-description">
+ Create beautiful, generative ambient soundscapes with customizable musical scales, modality, and
+ parameters that play continuously in the background.
+ </div>
+ </a>
</div>
<footer>
diff --git a/soundscape/index.html b/soundscape/index.html
new file mode 100644
index 0000000..17da4e7
--- /dev/null
+++ b/soundscape/index.html
@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Ambient Soundscape Generator | Vibes</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=VT323&family=Fredoka:wght@400;600&display=swap"
+ rel="stylesheet">
+ <style>
+ body {
+ font-family: 'Fredoka', sans-serif;
+ background-color: #d4f0f7;
+ color: #2a5674;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ text-align: center;
+ background-image:
+ linear-gradient(45deg, #d4f0f7 25%, transparent 25%),
+ linear-gradient(-45deg, #d4f0f7 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #d4f0f7 75%),
+ linear-gradient(-45deg, transparent 75%, #d4f0f7 75%);
+ background-size: 20px 20px;
+ background-color: #e8f7fc;
+ }
+
+ .container {
+ max-width: 800px;
+ padding: 2rem;
+ margin: 2rem;
+ background-color: rgba(255, 255, 255, 0.85);
+ border-radius: 20px;
+ box-shadow: 0 10px 25px rgba(42, 86, 116, 0.2);
+ border: 5px solid #a5c8e8;
+ image-rendering: pixelated;
+ }
+
+ h1 {
+ font-family: 'VT323', monospace;
+ font-size: 3.5rem;
+ margin-bottom: 1rem;
+ background: linear-gradient(45deg, #6ec4ff, #737ff5);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-shadow: 3px 3px 0px rgba(110, 196, 255, 0.3);
+ letter-spacing: 2px;
+ }
+
+ p {
+ font-size: 1.2rem;
+ line-height: 1.6;
+ margin-bottom: 2rem;
+ color: #4a7295;
+ }
+
+ .controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+ }
+
+ .control-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ text-align: left;
+ }
+
+ label {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #2a5674;
+ }
+
+ select,
+ input[type="range"] {
+ padding: 0.5rem;
+ border-radius: 10px;
+ border: 3px solid #a5c8e8;
+ background-color: #f0f9ff;
+ font-family: 'Fredoka', sans-serif;
+ font-size: 1rem;
+ color: #2a5674;
+ transition: all 0.3s ease;
+ }
+
+ select:hover,
+ input[type="range"]:hover {
+ border-color: #6ec4ff;
+ }
+
+ .button-group {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ margin-top: 1rem;
+ }
+
+ button {
+ padding: 0.8rem 1.5rem;
+ border-radius: 15px;
+ border: none;
+ background: #a5c8e8;
+ color: #2a5674;
+ font-family: 'Fredoka', sans-serif;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 3px 3px 0px #6ec4ff;
+ border: 3px dashed #737ff5;
+ }
+
+ button:hover {
+ transform: translateY(-3px);
+ box-shadow: 5px 5px 0px #6ec4ff;
+ background: #c1d9f0;
+ }
+
+ button.active {
+ background: #6ec4ff;
+ color: white;
+ }
+
+ .visualizer {
+ width: 100%;
+ height: 150px;
+ background-color: rgba(165, 200, 232, 0.3);
+ border-radius: 15px;
+ margin-top: 2rem;
+ overflow: hidden;
+ position: relative;
+ }
+
+ canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+
+ footer {
+ margin-top: 2rem;
+ color: #4a7295;
+ font-size: 0.9rem;
+ font-family: 'VT323', monospace;
+ letter-spacing: 1px;
+ }
+
+ .home-link {
+ display: inline-block;
+ margin-top: 1rem;
+ color: #2a5674;
+ text-decoration: none;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ }
+
+ .home-link:hover {
+ color: #6ec4ff;
+ transform: translateY(-2px);
+ }
+
+ .note-indicators {
+ display: flex;
+ justify-content: space-around;
+ margin-top: 1rem;
+ }
+
+ .note {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: #a5c8e8;
+ transition: all 0.3s ease;
+ }
+
+ .note.active {
+ background-color: #6ec4ff;
+ transform: scale(1.3);
+ box-shadow: 0 0 10px rgba(110, 196, 255, 0.8);
+ }
+ </style>
+</head>
+
+<body>
+ <div class="container">
+ <h1>Ambient Soundscape Generator</h1>
+ <p>Create beautiful, generative ambient soundscapes that play continuously in the background. Adjust the musical
+ scale, modality, and other parameters to craft your perfect sonic atmosphere.</p>
+
+ <div class="controls">
+ <div class="control-group">
+ <label for="scale">Musical Scale:</label>
+ <select id="scale">
+ <option value="major">Major</option>
+ <option value="minor">Minor</option>
+ <option value="pentatonic">Pentatonic</option>
+ <option value="blues">Blues</option>
+ <option value="chromatic">Chromatic</option>
+ <option value="dorian">Dorian</option>
+ <option value="phrygian">Phrygian</option>
+ <option value="lydian">Lydian</option>
+ <option value="mixolydian">Mixolydian</option>
+ <option value="locrian">Locrian</option>
+ </select>
+ </div>
+
+ <div class="control-group">
+ <label for="root">Root Note:</label>
+ <select id="root">
+ <option value="C">C</option>
+ <option value="C#">C#</option>
+ <option value="D">D</option>
+ <option value="D#">D#</option>
+ <option value="E">E</option>
+ <option value="F">F</option>
+ <option value="F#">F#</option>
+ <option value="G">G</option>
+ <option value="G#">G#</option>
+ <option value="A">A</option>
+ <option value="A#">A#</option>
+ <option value="B">B</option>
+ </select>
+ </div>
+
+ <div class="control-group">
+ <label for="tempo">Tempo:</label>
+ <input type="range" id="tempo" min="40" max="120" value="60">
+ <div id="tempo-value">60 BPM</div>
+ </div>
+
+ <div class="control-group">
+ <label for="density">Note Density:</label>
+ <input type="range" id="density" min="1" max="10" value="5">
+ <div id="density-value">Medium</div>
+ </div>
+
+ <div class="control-group">
+ <label for="reverb">Reverb Amount:</label>
+ <input type="range" id="reverb" min="0" max="1" step="0.1" value="0.5">
+ <div id="reverb-value">Medium</div>
+ </div>
+ </div>
+
+ <div class="button-group">
+ <button id="play-button">Play</button>
+ <button id="stop-button">Stop</button>
+ <button id="randomize-button">Randomize</button>
+ </div>
+
+ <div class="visualizer">
+ <canvas id="visualizer-canvas"></canvas>
+ </div>
+
+ <div class="note-indicators" id="note-indicators">
+ <!-- Note indicators will be added here dynamically -->
+ </div>
+
+ <footer>
+ Part of the <a href="../" class="home-link">Vibes</a> collection of web experiments
+ </footer>
+ </div>
+
+ <script src="soundscape.js"></script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/soundscape/soundscape.js b/soundscape/soundscape.js
new file mode 100644
index 0000000..72f02fe
--- /dev/null
+++ b/soundscape/soundscape.js
@@ -0,0 +1,515 @@
+/**
+ * Ambient Soundscape Generator
+ * A generative music system that creates pleasant, continuous soundscapes
+ * using Web Audio API.
+ */
+
+// Initialize audio context
+let audioContext;
+let masterGainNode;
+let reverbNode;
+let analyser;
+let playing = false;
+let noteScheduler;
+let visualizerInterval;
+
+// Musical parameters
+const scales = {
+ major: [0, 2, 4, 5, 7, 9, 11],
+ minor: [0, 2, 3, 5, 7, 8, 10],
+ pentatonic: [0, 2, 4, 7, 9],
+ blues: [0, 3, 5, 6, 7, 10],
+ chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ dorian: [0, 2, 3, 5, 7, 9, 10],
+ phrygian: [0, 1, 3, 5, 7, 8, 10],
+ lydian: [0, 2, 4, 6, 7, 9, 11],
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
+ locrian: [0, 1, 3, 5, 6, 8, 10]
+};
+
+const rootNotes = {
+ 'C': 60,
+ 'C#': 61,
+ 'D': 62,
+ 'D#': 63,
+ 'E': 64,
+ 'F': 65,
+ 'F#': 66,
+ 'G': 67,
+ 'G#': 68,
+ 'A': 69,
+ 'A#': 70,
+ 'B': 71
+};
+
+// DOM elements
+const playButton = document.getElementById('play-button');
+const stopButton = document.getElementById('stop-button');
+const randomizeButton = document.getElementById('randomize-button');
+const scaleSelect = document.getElementById('scale');
+const rootSelect = document.getElementById('root');
+const tempoSlider = document.getElementById('tempo');
+const tempoValue = document.getElementById('tempo-value');
+const densitySlider = document.getElementById('density');
+const densityValue = document.getElementById('density-value');
+const reverbSlider = document.getElementById('reverb');
+const reverbValue = document.getElementById('reverb-value');
+const visualizerCanvas = document.getElementById('visualizer-canvas');
+const noteIndicatorsContainer = document.getElementById('note-indicators');
+
+// Canvas context
+const canvasCtx = visualizerCanvas.getContext('2d');
+let canvasWidth, canvasHeight;
+
+// Active notes for visualization
+const activeNotes = new Set();
+const noteIndicators = [];
+
+// Initialize the application
+function init() {
+ // Set up event listeners
+ playButton.addEventListener('click', startSoundscape);
+ stopButton.addEventListener('click', stopSoundscape);
+ randomizeButton.addEventListener('click', randomizeSettings);
+
+ tempoSlider.addEventListener('input', updateTempoValue);
+ densitySlider.addEventListener('input', updateDensityValue);
+ reverbSlider.addEventListener('input', updateReverbValue);
+
+ // Initialize UI values
+ updateTempoValue();
+ updateDensityValue();
+ updateReverbValue();
+
+ // Create note indicators
+ createNoteIndicators();
+
+ // Set canvas dimensions
+ resizeCanvas();
+ window.addEventListener('resize', resizeCanvas);
+
+ // Initialize with random settings
+ randomizeSettings();
+}
+
+// Create note indicators based on the selected scale
+function createNoteIndicators() {
+ noteIndicatorsContainer.innerHTML = '';
+ noteIndicators.length = 0;
+
+ const currentScale = scales[scaleSelect.value];
+ const numNotes = currentScale.length;
+
+ for (let i = 0; i < numNotes; i++) {
+ const noteElement = document.createElement('div');
+ noteElement.className = 'note';
+ noteIndicatorsContainer.appendChild(noteElement);
+ noteIndicators.push(noteElement);
+ }
+}
+
+// Update note indicators when scale changes
+function updateNoteIndicators() {
+ createNoteIndicators();
+}
+
+// Start the soundscape
+function startSoundscape() {
+ if (playing) return;
+
+ // Initialize audio context if it doesn't exist
+ if (!audioContext) {
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
+
+ // Create master gain node
+ masterGainNode = audioContext.createGain();
+ masterGainNode.gain.value = 0.5;
+
+ // Create analyzer for visualization
+ analyser = audioContext.createAnalyser();
+ analyser.fftSize = 2048;
+
+ // Create reverb
+ setupReverb();
+ }
+
+ // Resume audio context if suspended
+ if (audioContext.state === 'suspended') {
+ audioContext.resume();
+ }
+
+ // Update UI
+ playButton.classList.add('active');
+ stopButton.classList.remove('active');
+
+ // Start scheduling notes
+ playing = true;
+ scheduleNotes();
+
+ // Start visualizer
+ startVisualizer();
+}
+
+// Set up the reverb effect
+function setupReverb() {
+ // Create a convolver node for reverb
+ const convolver = audioContext.createConvolver();
+
+ // Create impulse response
+ const rate = audioContext.sampleRate;
+ const length = rate * 3; // 3 seconds
+ const impulse = audioContext.createBuffer(2, length, rate);
+ const impulseL = impulse.getChannelData(0);
+ const impulseR = impulse.getChannelData(1);
+
+ // Fill the buffer with white noise with an exponential decay
+ for (let i = 0; i < length; i++) {
+ const decay = Math.pow(0.5, i / (rate * 0.5));
+ impulseL[i] = (Math.random() * 2 - 1) * decay;
+ impulseR[i] = (Math.random() * 2 - 1) * decay;
+ }
+
+ convolver.buffer = impulse;
+
+ // Create wet/dry mix
+ const reverbAmount = parseFloat(reverbSlider.value);
+ const wetGain = audioContext.createGain();
+ wetGain.gain.value = reverbAmount;
+
+ const dryGain = audioContext.createGain();
+ dryGain.gain.value = 1 - reverbAmount;
+
+ // Create a gain node to control the final output
+ const reverbMix = audioContext.createGain();
+
+ // Connect the nodes
+ masterGainNode.connect(wetGain);
+ masterGainNode.connect(dryGain);
+
+ wetGain.connect(convolver);
+ convolver.connect(reverbMix);
+ dryGain.connect(reverbMix);
+
+ reverbMix.connect(analyser);
+ analyser.connect(audioContext.destination);
+
+ // Store the nodes for later updates
+ reverbNode = {
+ wetGain: wetGain,
+ dryGain: dryGain,
+ convolver: convolver,
+ reverbMix: reverbMix
+ };
+}
+
+// Update reverb when slider changes
+function updateReverbValue() {
+ const reverb = reverbSlider.value;
+ let reverbText;
+
+ if (reverb <= 0.3) {
+ reverbText = 'Dry';
+ } else if (reverb <= 0.7) {
+ reverbText = 'Medium';
+ } else {
+ reverbText = 'Wet';
+ }
+
+ reverbValue.textContent = reverbText;
+
+ // Update reverb if already playing
+ if (playing && reverbNode) {
+ const reverbAmount = parseFloat(reverb);
+ reverbNode.wetGain.gain.value = reverbAmount;
+ reverbNode.dryGain.gain.value = 1 - reverbAmount;
+ }
+}
+
+// Stop the soundscape
+function stopSoundscape() {
+ if (!playing) return;
+
+ // Clear schedulers
+ clearTimeout(noteScheduler);
+ clearInterval(visualizerInterval);
+
+ // Update UI
+ playButton.classList.remove('active');
+ stopButton.classList.add('active');
+
+ // Stop all sound
+ if (audioContext && masterGainNode) {
+ masterGainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
+ setTimeout(() => {
+ if (audioContext) {
+ masterGainNode.disconnect();
+ if (reverbNode) {
+ reverbNode.wetGain.disconnect();
+ reverbNode.dryGain.disconnect();
+ reverbNode.convolver.disconnect();
+ reverbNode.reverbMix.disconnect();
+ }
+ analyser.disconnect();
+ audioContext.close();
+ audioContext = null;
+ reverbNode = null;
+ }
+ }, 1000);
+ }
+
+ playing = false;
+ activeNotes.clear();
+ updateNoteIndicatorsDisplay();
+}
+
+// Schedule notes based on current settings
+function scheduleNotes() {
+ if (!playing) return;
+
+ const tempo = parseInt(tempoSlider.value);
+ const density = parseInt(densitySlider.value);
+ const rootNote = rootNotes[rootSelect.value];
+ const currentScale = scales[scaleSelect.value];
+
+ // Calculate time between notes based on tempo and density
+ const noteDuration = 60 / tempo * 1000; // in milliseconds
+ const noteInterval = noteDuration / density;
+
+ // Schedule a batch of notes
+ for (let i = 0; i < density; i++) {
+ setTimeout(() => {
+ if (playing && Math.random() < 0.7) { // 70% chance of playing a note
+ playRandomNote(rootNote, currentScale);
+ }
+ }, i * noteInterval);
+ }
+
+ // Schedule the next batch
+ noteScheduler = setTimeout(scheduleNotes, noteDuration);
+}
+
+// Play a random note from the selected scale
+function playRandomNote(rootNote, scale) {
+ if (!audioContext) return;
+
+ // Choose a random octave (-1, 0, or 1 relative to root)
+ const octaveOffset = Math.floor(Math.random() * 3) - 1;
+
+ // Choose a random note from the scale
+ const scaleIndex = Math.floor(Math.random() * scale.length);
+ const noteOffset = scale[scaleIndex];
+
+ // Calculate the final MIDI note number
+ const midiNote = rootNote + noteOffset + (octaveOffset * 12);
+
+ // Convert MIDI note to frequency
+ const frequency = 440 * Math.pow(2, (midiNote - 69) / 12);
+
+ // Create oscillator
+ const oscillator = audioContext.createOscillator();
+ oscillator.type = chooseRandomWaveform();
+ oscillator.frequency.value = frequency;
+
+ // Create envelope
+ const envelope = audioContext.createGain();
+ envelope.gain.value = 0;
+
+ // Random duration between 2 and 8 seconds
+ const duration = 2 + Math.random() * 6;
+
+ // Random pan position
+ const panner = audioContext.createStereoPanner();
+ panner.pan.value = Math.random() * 2 - 1; // -1 to 1
+
+ // Connect nodes
+ oscillator.connect(envelope);
+ envelope.connect(panner);
+ panner.connect(masterGainNode);
+
+ // Apply envelope
+ const now = audioContext.currentTime;
+ const attackTime = 0.2 + Math.random() * 0.3;
+ const releaseTime = 0.5 + Math.random() * 1.5;
+
+ envelope.gain.setValueAtTime(0, now);
+ envelope.gain.linearRampToValueAtTime(0.2 + Math.random() * 0.1, now + attackTime);
+ envelope.gain.setValueAtTime(0.2 + Math.random() * 0.1, now + duration - releaseTime);
+ envelope.gain.linearRampToValueAtTime(0, now + duration);
+
+ // Start and stop the oscillator
+ oscillator.start(now);
+ oscillator.stop(now + duration);
+
+ // Clean up when done
+ oscillator.onended = () => {
+ oscillator.disconnect();
+ envelope.disconnect();
+ panner.disconnect();
+ };
+
+ // Update active notes for visualization
+ activeNotes.add(scaleIndex);
+ updateNoteIndicatorsDisplay();
+
+ // Remove from active notes after the note is done
+ setTimeout(() => {
+ activeNotes.delete(scaleIndex);
+ updateNoteIndicatorsDisplay();
+ }, duration * 1000);
+}
+
+// Choose a random waveform type
+function chooseRandomWaveform() {
+ const waveforms = ['sine', 'triangle', 'sine', 'sine']; // More sine for smoother sound
+ return waveforms[Math.floor(Math.random() * waveforms.length)];
+}
+
+// Start the visualizer
+function startVisualizer() {
+ if (visualizerInterval) {
+ clearInterval(visualizerInterval);
+ }
+
+ visualizerInterval = setInterval(drawVisualizer, 30); // ~30fps
+}
+
+// Draw the visualizer
+function drawVisualizer() {
+ if (!analyser) return;
+
+ const bufferLength = analyser.frequencyBinCount;
+ const dataArray = new Uint8Array(bufferLength);
+ analyser.getByteTimeDomainData(dataArray);
+
+ // Clear the canvas
+ canvasCtx.fillStyle = 'rgba(165, 200, 232, 0.2)';
+ canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // Draw the waveform
+ canvasCtx.lineWidth = 2;
+ canvasCtx.strokeStyle = '#6ec4ff';
+ canvasCtx.beginPath();
+
+ const sliceWidth = canvasWidth / bufferLength;
+ let x = 0;
+
+ for (let i = 0; i < bufferLength; i++) {
+ const v = dataArray[i] / 128.0;
+ const y = v * canvasHeight / 2;
+
+ if (i === 0) {
+ canvasCtx.moveTo(x, y);
+ } else {
+ canvasCtx.lineTo(x, y);
+ }
+
+ x += sliceWidth;
+ }
+
+ canvasCtx.lineTo(canvasWidth, canvasHeight / 2);
+ canvasCtx.stroke();
+
+ // Draw circles for active frequencies
+ const freqData = new Uint8Array(analyser.frequencyBinCount);
+ analyser.getByteFrequencyData(freqData);
+
+ for (let i = 0; i < 20; i++) {
+ const freqIndex = Math.floor(i * bufferLength / 100);
+ const freqValue = freqData[freqIndex];
+ const radius = freqValue / 10;
+
+ if (radius > 1) {
+ canvasCtx.beginPath();
+ const x = Math.random() * canvasWidth;
+ const y = Math.random() * canvasHeight;
+ const gradient = canvasCtx.createRadialGradient(x, y, 0, x, y, radius);
+ gradient.addColorStop(0, 'rgba(110, 196, 255, 0.8)');
+ gradient.addColorStop(1, 'rgba(110, 196, 255, 0)');
+ canvasCtx.fillStyle = gradient;
+ canvasCtx.arc(x, y, radius, 0, Math.PI * 2);
+ canvasCtx.fill();
+ }
+ }
+}
+
+// Resize the canvas to match its container
+function resizeCanvas() {
+ const container = visualizerCanvas.parentElement;
+ canvasWidth = container.clientWidth;
+ canvasHeight = container.clientHeight;
+
+ visualizerCanvas.width = canvasWidth;
+ visualizerCanvas.height = canvasHeight;
+}
+
+// Update the note indicators display
+function updateNoteIndicatorsDisplay() {
+ for (let i = 0; i < noteIndicators.length; i++) {
+ if (activeNotes.has(i)) {
+ noteIndicators[i].classList.add('active');
+ } else {
+ noteIndicators[i].classList.remove('active');
+ }
+ }
+}
+
+// Randomize all settings
+function randomizeSettings() {
+ // Randomize scale
+ const scaleKeys = Object.keys(scales);
+ scaleSelect.value = scaleKeys[Math.floor(Math.random() * scaleKeys.length)];
+
+ // Randomize root note
+ const rootKeys = Object.keys(rootNotes);
+ rootSelect.value = rootKeys[Math.floor(Math.random() * rootKeys.length)];
+
+ // Randomize tempo (40-120 BPM)
+ tempoSlider.value = 40 + Math.floor(Math.random() * 80);
+ updateTempoValue();
+
+ // Randomize density (1-10)
+ densitySlider.value = 1 + Math.floor(Math.random() * 10);
+ updateDensityValue();
+
+ // Randomize reverb (0.2-0.8)
+ reverbSlider.value = (0.2 + Math.random() * 0.6).toFixed(1);
+ updateReverbValue();
+
+ // Update note indicators
+ updateNoteIndicators();
+
+ // Update reverb if already playing
+ if (playing && reverbNode) {
+ const reverbAmount = parseFloat(reverbSlider.value);
+ reverbNode.wetGain.gain.value = reverbAmount;
+ reverbNode.dryGain.gain.value = 1 - reverbAmount;
+ }
+}
+
+// Update tempo display
+function updateTempoValue() {
+ const tempo = tempoSlider.value;
+ tempoValue.textContent = `${tempo} BPM`;
+}
+
+// Update density display
+function updateDensityValue() {
+ const density = densitySlider.value;
+ let densityText;
+
+ if (density <= 3) {
+ densityText = 'Sparse';
+ } else if (density <= 7) {
+ densityText = 'Medium';
+ } else {
+ densityText = 'Dense';
+ }
+
+ densityValue.textContent = densityText;
+}
+
+// Event listener for scale changes
+scaleSelect.addEventListener('change', updateNoteIndicators);
+
+// Initialize the application when the page loads
+window.addEventListener('load', init);