// Initialize audio context lazily to avoid autoplay restrictions let audioCtx; const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const SVARA_NAMES = { full: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], short: ["S", "R", "G", "M", "P", "D", "N"], malayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"] }; const semitoneMap = { C: 0, "C#": 1, D: 2, "D#": 3, E: 4, F: 5, "F#": 6, G: 7, "G#": 8, A: 9, "A#": 10, B: 11, Ab: 8, Bb: 10 }; const ragams = { Bhair: { intervals: [0, 2, 3, 6, 7, 9, 11], namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], namesShort: ["S", "R", "G", "M", "P", "D", "N"], namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], variantNumbers: ["", "2", "2", "1", "", "2", "2"] }, Kalyani: { intervals: [0, 2, 4, 6, 7, 9, 11], namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], namesShort: ["S", "R", "G", "M", "P", "D", "N"], namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], variantNumbers: ["", "2", "3", "2", "", "2", "3"] }, Mayamalavagowla: { intervals: [0, 1, 4, 5, 7, 8, 11], namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], namesShort: ["S", "R", "G", "M", "P", "D", "N"], namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], variantNumbers: ["", "1", "3", "1", "", "1", "3"] }, Mohanam: { intervals: [0, 2, 4, 7, 9], namesFull: ["Sa", "Ri", "Ga", "Pa", "Dha"], namesShort: ["S", "R", "G", "P", "D"], namesMalayalam: ["സ", "രി", "ഗ", "പ", "ധ"], variantNumbers: ["", "2", "3", "", "2"] }, Shankarabharanam: { intervals: [0, 2, 4, 5, 7, 9, 11], namesFull: ["Sa", "Ri", "Ga", "Ma", "Pa", "Dha", "Ni"], namesShort: ["S", "R", "G", "M", "P", "D", "N"], namesMalayalam: ["സ", "രി", "ഗ", "മ", "പ", "ധ", "നി"], variantNumbers: ["", "2", "3", "1", "", "2", "3"] } }; const ROOT_OCTAVE = 4; const SOUND_OCTAVE_OFFSET = -12; const svaraNamesSelect = document.getElementById("svaraNames"); const svaraStyleSelect = document.getElementById("svaraStyle"); const startingNoteSelect = document.getElementById("startingNote"); const ragamSelect = document.getElementById("ragamSelect"); const pianoElement = document.getElementById("piano"); function normalizeSemitone(value) { return ((value % 12) + 12) % 12; } function absFrequency(absSemitone) { const a4Abs = 4 * 12 + semitoneMap.A; // fixed A4 reference for real pitch const soundAbsSemitone = absSemitone + SOUND_OCTAVE_OFFSET; return 440 * Math.pow(2, (soundAbsSemitone - a4Abs) / 12); } function noteNameToAbs(noteName) { return ROOT_OCTAVE * 12 + semitoneMap[noteName]; } function getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { const diff = normalizeSemitone(absSemitone - startAbs); const intervalIndex = ragam.intervals.indexOf(diff); if (intervalIndex === -1) { return ""; } const baseLabel = svaraNames[intervalIndex]; if (svaraStyle === "variant" && ragam.variantNumbers) { const variant = ragam.variantNumbers[intervalIndex]; if (variant) { return `${baseLabel}${variant}`; } } return baseLabel; } function buildKeyHtml(absSemitone, startAbs, svaraNames, ragam, svaraStyle) { const noteName = NOTE_NAMES[normalizeSemitone(absSemitone)]; const isSharp = noteName.includes("#"); const label = getSwaraLabel(absSemitone, startAbs, svaraNames, ragam, svaraStyle); const classes = ["key", isSharp ? "black" : "white"]; if (absSemitone < startAbs) { classes.push("lower"); } else if (absSemitone > startAbs + 11) { classes.push("upper"); } if (normalizeSemitone(absSemitone - startAbs) === 0) { classes.push("sa-root"); } if (label) { classes.push("has-label"); } const content = label ? `${label}` : ""; return `
${content}
`; } function renderKeyboard() { const startNote = startingNoteSelect.value; const svaraNames = SVARA_NAMES[svaraNamesSelect.value] || SVARA_NAMES.full; const svaraStyle = svaraStyleSelect.value; const ragam = ragams[ragamSelect.value]; const startAbs = noteNameToAbs(startNote); const lowerPaAbs = startAbs - 5; const upperPaAbs = startAbs + 19; let html = ""; for (let abs = lowerPaAbs; abs <= upperPaAbs; abs += 1) { html += buildKeyHtml(abs, startAbs, svaraNames, ragam, svaraStyle); } pianoElement.innerHTML = html; attachKeyListeners(); } function attachKeyListeners() { const keys = pianoElement.querySelectorAll(".key"); keys.forEach((key) => { key.addEventListener("click", () => { playNote(parseInt(key.dataset.abs, 10)); }); }); } function ensureAudioContext() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } return audioCtx; } async function playNote(absSemitone) { const ctx = ensureAudioContext(); if (ctx.state === "suspended") { await ctx.resume(); } const freq = absFrequency(absSemitone); const oscillator = ctx.createOscillator(); const gainNode = ctx.createGain(); oscillator.type = "sine"; oscillator.frequency.setValueAtTime(freq, ctx.currentTime); oscillator.connect(gainNode); gainNode.connect(ctx.destination); gainNode.gain.setValueAtTime(0.2, ctx.currentTime); oscillator.start(); oscillator.stop(ctx.currentTime + 0.5); } renderKeyboard(); svaraNamesSelect.addEventListener("change", renderKeyboard); svaraStyleSelect.addEventListener("change", renderKeyboard); startingNoteSelect.addEventListener("change", renderKeyboard); ragamSelect.addEventListener("change", renderKeyboard);