// 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 `