Tanpura PWA body { font-family: Arial, sans-serif; margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background-color: #f0f0f0; touch-action: manipulation; } h1 { margin-bottom: 10px; text-align: center; } .controls { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 400px; } .button-group, .slider-group { display: flex; flex-wrap: wrap; justify-content: center; margin-bottom: 10px; } button { margin: 5px; padding: 10px; flex: 1 1 40%; max-width: 120px; background-color: #e0e0e0; border: none; border-radius: 5px; cursor: pointer; text-align: center; /* Center button text */ } /* Enlarge pitch selection button text */ #btnA, #btnBb, #btnEb, #btnC { font-size: 2em; } button.active { background-color: #2196f3; color: #ffffff; } /* Center the slider groups */ .slider-group { width: 100%; max-width: 400px; margin: 0 auto; } .slider-container { display: flex; align-items: center; width: 100%; margin: 5px 0; } .slider-container span { margin-left: 10px; min-width: 30px; } label { flex: 1; text-align: left; /* Align labels to left */ margin-right: 10px; } /* Responsive styles for mobile devices */ @media (max-width: 480px) { .slider-container { flex-direction: column; align-items: flex-start; } .slider-container span { margin-top: 5px; } } Tanpura PWA A B♭ E♭ C Octave Down Octave Up Master Volume: 50 Pa: 20 sa: 20 sa: 20 Sa: 20 const audioContext = new (window.AudioContext || window.webkitAudioContext)(); // LOCKED PITCH REFERENCES - DO NOT MODIFY THESE. // These frequencies are locked; any modifications require explicit approval. const frequencies = Object.freeze({ 'A': Object.freeze([330, 440, 440, 220]), 'Bb': Object.freeze([349.62, 466.16, 466.16, 233.08]), 'Eb': Object.freeze([233.35, 311.13, 311.13, 155.57]), 'C': Object.freeze([196.22, 261.63, 261.63, 130.82]) }); let activePitch = null; let oscillators = []; let gains = []; let baseVolumes = []; // DO NOT alter these volume controls without checking with the user first. let octaveShift = 0; let masterGain = audioContext.createGain(); masterGain.connect(audioContext.destination); function togglePitch(pitch) { if (activePitch === pitch) { stopSound(); activePitch = null; } else { stopSound(); activePitch = pitch; playTanpura(pitch); } updateButtonStates(); } function toggleOctave(direction) { if ((direction === 'up' && octaveShift === 1) || (direction === 'down' && octaveShift === -1)) { octaveShift = 0; } else { octaveShift = (direction === 'up') ? 1 : -1; } if (activePitch) { stopSound(); playTanpura(activePitch); } updateButtonStates(); } function playTanpura(pitch) { // Ensure AudioContext is resumed (necessary for iOS) if (audioContext.state === 'suspended') { audioContext.resume(); } stopSound(); baseVolumes = []; const masterVol = document.getElementById('masterVolume').value / 100; frequencies[pitch].forEach((freq, i) => { const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); // All strings now use the 'sawtooth' wave type osc.type = 'sawtooth'; osc.frequency.value = freq * Math.pow(2, octaveShift); let baseVol = document.getElementById(`vol${i}`).value / 100; baseVolumes[i] = baseVol; gain.gain.value = baseVol * masterVol; osc.connect(gain).connect(masterGain); osc.start(); oscillators[i] = osc; gains[i] = gain; }); } function stopSound() { oscillators.forEach(osc => osc.stop()); oscillators = []; gains = []; } function updateButtonStates() { document.querySelectorAll(".button-group button").forEach(button => button.classList.remove('active')); if (activePitch) document.getElementById(`btn${activePitch}`).classList.add('active'); if (octaveShift === 1) document.getElementById('octaveUp').classList.add('active'); if (octaveShift === -1) document.getElementById('octaveDown').classList.add('active'); } function updateMasterVolume(value) { const masterVol = value / 100; masterGain.gain.value = masterVol; document.getElementById('masterVolumeValue').textContent = value; gains.forEach((gain, i) => { if (baseVolumes[i] !== undefined) { gain.gain.value = baseVolumes[i] * masterVol; } }); } function updateVolume(index, value) { let baseVol = value / 100; baseVolumes[index] = baseVol; const masterVol = document.getElementById('masterVolume').value / 100; if (gains[index]) gains[index].gain.value = baseVol * masterVol; document.getElementById(`vol${index}Value`).textContent = value; } window.onload = () => { updateMasterVolume(document.getElementById('masterVolume').value); // Add event listeners to resume AudioContext on iOS document.body.addEventListener('touchstart', resumeAudio, false); document.body.addEventListener('click', resumeAudio, false); }; function resumeAudio() { if (audioContext.state === 'suspended') { audioContext.resume(); } }