From a8ae3d0e8024655d3cfa5e9220ee0c588deafde9 Mon Sep 17 00:00:00 2001 From: Alexander Rossmanith Date: Sun, 26 Jul 2026 15:47:29 +0530 Subject: [PATCH] Upload files to "/" --- index.html | 47 +++++++++++++ script.js | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 150 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 styles.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..19b2966 --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Carnatic Piano + + + +

Carnatic Piano

+ +
+ + + + + + + + +
+ +
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..b223d52 --- /dev/null +++ b/script.js @@ -0,0 +1,193 @@ +// 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); + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..89ad44a --- /dev/null +++ b/styles.css @@ -0,0 +1,150 @@ +body { + font-family: sans-serif; + text-align: center; + background: #2a2a2a; + padding: 20px; +} + +h1 { + color: #fff; +} + +.controls { + margin-bottom: 30px; +} + +.controls label { + color: #fff; + margin-right: 10px; + font-size: 16px; +} + +.controls select { + padding: 8px 12px; + font-size: 14px; + border-radius: 5px; + border: none; + background: #fff; + color: #333; + cursor: pointer; +} + +.piano { + display: flex; + justify-content: center; + align-items: flex-end; + margin-top: 50px; + background: #111; + padding: 20px; + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); +} + +.key { + display: flex; + align-items: flex-end; + justify-content: center; + font-size: 14px; + cursor: pointer; + border: 1px solid #222; + border-radius: 5px; + user-select: none; + transition: all 0.05s; + position: relative; + min-height: 1em; +} + +/* Sharp keys may show Carnatic swara labels if they correspond to the chosen scale */ +.key.sharp.sa-root { + color: #fff; + font-size: 14px; +} + +.key.has-label .key-label { + position: relative; + display: inline-flex; +} + +.key.has-label .key-label sub { + font-size: 0.65em; + display: inline-block; + vertical-align: text-bottom; + line-height: 1; + position: relative; + top: 0.18em; +} + +.key.lower.has-label .key-label::after, +.key.upper.has-label .key-label::after { + position: absolute; + content: '•'; + font-size: 12px; + color: currentColor; + pointer-events: none; + left: 50%; + transform: translateX(-50%); +} + +.key.black.has-label .key-label::after { + color: #777; +} + +.key.lower.has-label .key-label::after { + top: 100%; + margin-top: -2px; +} + +.key.upper.has-label .key-label::after { + bottom: 100%; + margin-bottom: -5px; +} + +/* White keys */ +.key.white { + width: 60px; + height: 250px; + background: linear-gradient(135deg, #fff 0%, #f5f5f5 100%); + color: #333; + margin: 0 -1px; + padding-bottom: 15px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); +} + +.key.white:active { + background: linear-gradient(135deg, #e0e0e0 0%, #d5d5d5 100%); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), inset 0 1px 3px rgba(0, 0, 0, 0.2); + transform: translateY(2px); +} + +/* Black keys */ +.key.black { + width: 40px; + height: 160px; + background: linear-gradient(135deg, #222 0%, #111 100%); + color: #fff; + position: relative; + z-index: 10; + margin: 0 -20px; + padding-top: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); + align-self: flex-start; +} + +.key.black:active { + background: linear-gradient(135deg, #111 0%, #000 100%); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), inset 0 1px 2px rgba(255, 255, 255, 0.05); + transform: translateY(2px); +} + +.key.sa-root.white { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 -1px 3px rgba(0, 0, 0, 0.1); +} + +.key.sa-root.black { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8), inset 0 1px 2px rgba(255, 255, 255, 0.1); +} + +.key.sa-root:not(.sharp) { + color: #d84315; + font-weight: 700; +}