Skip to content

Instantly share code, notes, and snippets.

@rsbohn
Last active July 31, 2025 13:34
Show Gist options
  • Select an option

  • Save rsbohn/a6c2c51d397c60b4d15d011ec270f05d to your computer and use it in GitHub Desktop.

Select an option

Save rsbohn/a6c2c51d397c60b4d15d011ec270f05d to your computer and use it in GitHub Desktop.
A WebAudio Experiment
<!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>
<!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