Last active
July 31, 2025 13:34
-
-
Save rsbohn/a6c2c51d397c60b4d15d011ec270f05d to your computer and use it in GitHub Desktop.
A WebAudio Experiment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>How Soon Is Now? - Interactive Synth</title> | |
| <style> | |
| body { font-family: sans-serif; background-color: #282828; color: #e0e0e0; display: flex; flex-direction: column; align-items: center; } | |
| h1 { font-weight: 300; } | |
| #controls { background-color: #3c3c3c; border-radius: 8px; padding: 20px; margin: 20px; width: 350px; } | |
| .control-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } | |
| label { font-size: 1em; } | |
| input[type="range"] { width: 150px; } | |
| span, select, button { font-size: 1em; } | |
| button { padding: 10px 20px; font-size: 1.2em; cursor: pointer; border-radius: 5px; border: none; background-color: #4CAF50; color: white; } | |
| button:hover { background-color: #45a049; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Interactive Tremolo Synth</h1> | |
| <button id="playButton">Play</button> | |
| <div id="controls"> | |
| <div class="control-group"> | |
| <label for="bpm">Tempo (BPM)</label> | |
| <input type="range" id="bpm" min="40" max="140" value="95" step="1"> | |
| <span id="bpmValue">95</span> | |
| </div> | |
| <div class="control-group"> | |
| <label for="pitch">Pitch (Hz)</label> | |
| <input type="range" id="pitch" min="100" max="400" value="196" step="1"> | |
| <span id="pitchValue">196.00</span> | |
| </div> | |
| <div class="control-group"> | |
| <label for="oscType">Waveform</label> | |
| <select id="oscType"> | |
| <option value="sawtooth" selected>Sawtooth</option> | |
| <option value="square">Square</option> | |
| <option value="sine">Sine</option> | |
| <option value="triangle">Triangle</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label for="lfoType">LFO Wave</label> | |
| <select id="lfoType"> | |
| <option value="triangle" selected>Triangle</option> | |
| <option value="square">Square</option> | |
| </select> | |
| </div> | |
| </div> | |
| <script> | |
| // --- UI Elements --- | |
| const playButton = document.getElementById('playButton'); | |
| const bpmSlider = document.getElementById('bpm'); | |
| const bpmValue = document.getElementById('bpmValue'); | |
| const pitchSlider = document.getElementById('pitch'); | |
| const pitchValue = document.getElementById('pitchValue'); | |
| const oscTypeSelect = document.getElementById('oscType'); | |
| const lfoTypeSelect = document.getElementById('lfoType'); | |
| // --- Audio Variables --- | |
| let audioContext; | |
| let isPlaying = false; | |
| let oscillator; | |
| let gainNode; | |
| let lfo1, lfo2; | |
| let lfo1Gain, lfo2Gain; | |
| let BPM = 95; | |
| function setupAudio() { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Main Oscillator | |
| oscillator = audioContext.createOscillator(); | |
| oscillator.type = oscTypeSelect.value; | |
| oscillator.frequency.setValueAtTime(parseFloat(pitchSlider.value), audioContext.currentTime); | |
| // Main Gain Node (the volume control) | |
| gainNode = audioContext.createGain(); | |
| gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
| // --- LFOs (The "Tremolo" effect generators) --- | |
| const sixteenthNoteTime = 60 / BPM / 4; | |
| const eighthNoteTripletTime = 60 / BPM / 3; | |
| // LFO 1 (16th notes) | |
| lfo1 = audioContext.createOscillator(); | |
| lfo1.type = lfoTypeSelect.value; | |
| lfo1.frequency.setValueAtTime(1 / sixteenthNoteTime, audioContext.currentTime); | |
| lfo1Gain = audioContext.createGain(); | |
| lfo1Gain.gain.setValueAtTime(0.5, audioContext.currentTime); | |
| // LFO 2 (8th note triplets) | |
| lfo2 = audioContext.createOscillator(); | |
| lfo2.type = lfoTypeSelect.value; | |
| lfo2.frequency.setValueAtTime(1 / eighthNoteTripletTime, audioContext.currentTime); | |
| lfo2Gain = audioContext.createGain(); | |
| lfo2Gain.gain.setValueAtTime(0.5, audioContext.currentTime); | |
| // --- Connections --- | |
| oscillator.connect(gainNode); | |
| lfo1.connect(lfo1Gain); | |
| lfo2.connect(lfo2Gain); | |
| // The LFOs control the gain of the main gain node, creating the tremolo effect | |
| lfo1Gain.connect(gainNode.gain); | |
| lfo2Gain.connect(gainNode.gain); | |
| gainNode.connect(audioContext.destination); | |
| lfo1.start(); | |
| lfo2.start(); | |
| oscillator.start(); | |
| } | |
| function togglePlayback() { | |
| if (!isPlaying) { | |
| if (!audioContext) { | |
| setupAudio(); | |
| } | |
| audioContext.resume(); | |
| isPlaying = true; | |
| playButton.textContent = 'Stop'; | |
| } else { | |
| audioContext.suspend(); | |
| isPlaying = false; | |
| playButton.textContent = 'Play'; | |
| } | |
| } | |
| // --- Event Listeners for Controls --- | |
| playButton.addEventListener('click', togglePlayback); | |
| bpmSlider.addEventListener('input', (event) => { | |
| BPM = event.target.value; | |
| bpmValue.textContent = BPM; | |
| if (isPlaying) { | |
| const sixteenthNoteTime = 60 / BPM / 4; | |
| const eighthNoteTripletTime = 60 / BPM / 3; | |
| lfo1.frequency.setValueAtTime(1 / sixteenthNoteTime, audioContext.currentTime); | |
| lfo2.frequency.setValueAtTime(1 / eighthNoteTripletTime, audioContext.currentTime); | |
| } | |
| }); | |
| pitchSlider.addEventListener('input', (event) => { | |
| const newPitch = parseFloat(event.target.value); | |
| pitchValue.textContent = newPitch.toFixed(2); | |
| if (isPlaying) { | |
| oscillator.frequency.setValueAtTime(newPitch, audioContext.currentTime); | |
| } | |
| }); | |
| oscTypeSelect.addEventListener('change', (event) => { | |
| if (isPlaying) { | |
| oscillator.type = event.target.value; | |
| } | |
| }); | |
| lfoTypeSelect.addEventListener('change', (event) => { | |
| if (isPlaying) { | |
| lfo1.type = event.target.value; | |
| lfo2.type = event.target.value; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>How Soon Is Now? - Web Audio</title> | |
| </head> | |
| <body> | |
| <h1>How Soon Is Now?</h1> | |
| <p>The first few seconds of The Smiths' "How Soon is Now" recreated with the Web Audio API and playing on a continuous loop.</p> | |
| <button id="playButton">Play</button> | |
| <script> | |
| const playButton = document.getElementById('playButton'); | |
| let audioContext; | |
| let isPlaying = false; | |
| let oscillator; | |
| let gainNode; | |
| let lfo1; | |
| let lfo2; | |
| let lfo1Gain; | |
| let lfo2Gain; | |
| const BPM = 95; | |
| const sixteenthNoteTime = 60 / BPM / 4; | |
| const eighthNoteTripletTime = 60 / BPM / 3; | |
| function setupAudio() { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Main Oscillator | |
| oscillator = audioContext.createOscillator(); | |
| oscillator.type = 'sawtooth'; | |
| oscillator.frequency.setValueAtTime(196.00, audioContext.currentTime); // G3 | |
| // Main Gain Node | |
| gainNode = audioContext.createGain(); | |
| gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
| // LFO 1 (16th notes) | |
| lfo1 = audioContext.createOscillator(); | |
| lfo1.type = 'triangle'; | |
| lfo1.frequency.setValueAtTime(1 / sixteenthNoteTime, audioContext.currentTime); | |
| lfo1Gain = audioContext.createGain(); | |
| lfo1Gain.gain.setValueAtTime(0.5, audioContext.currentTime); | |
| // LFO 2 (8th note triplets) | |
| lfo2 = audioContext.createOscillator(); | |
| lfo2.type = 'triangle'; | |
| lfo2.frequency.setValueAtTime(1 / eighthNoteTripletTime, audioContext.currentTime); | |
| lfo2Gain = audioContext.createGain(); | |
| lfo2Gain.gain.setValueAtTime(0.5, audioContext.currentTime); | |
| // Connections | |
| oscillator.connect(gainNode); | |
| lfo1.connect(lfo1Gain); | |
| lfo2.connect(lfo2Gain); | |
| lfo1Gain.connect(gainNode.gain); | |
| lfo2Gain.connect(gainNode.gain); | |
| gainNode.connect(audioContext.destination); | |
| lfo1.start(); | |
| lfo2.start(); | |
| oscillator.start(); | |
| } | |
| function togglePlayback() { | |
| if (!isPlaying) { | |
| if (!audioContext) { | |
| setupAudio(); | |
| } | |
| audioContext.resume(); | |
| isPlaying = true; | |
| playButton.textContent = 'Stop'; | |
| } else { | |
| audioContext.suspend(); | |
| isPlaying = false; | |
| playButton.textContent = 'Play'; | |
| } | |
| } | |
| playButton.addEventListener('click', togglePlayback); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment