Reeds (Timbre)
Male
Female
Bass
Coupler
Tanpura Drone (Drone)
Reverb (Space)
Tabla Beats (Rhythm)
None
Teentaal
Dadra
Keherwa
Rupak
Practice Melodies (Melody)
Select a melody to see sargam notations.
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const NOTES = [ { n: 'C', s: 'Sa', f: 261.63 }, { n: 'C#', s: 're', f: 277.18 }, { n: 'D', s: 'Re', f: 293.66 }, { n: 'D#', s: 'ga', f: 311.13 }, { n: 'E', s: 'Ga', f: 329.63 }, { n: 'F', s: 'Ma', f: 349.23 }, { n: 'F#', s: 'ma', f: 369.99 }, { n: 'G', s: 'Pa', f: 392.00 }, { n: 'G#', s: 'dha', f: 415.30 }, { n: 'A', s: 'Dha', f: 440.00 }, { n: 'A#', s: 'ni', f: 466.16 }, { n: 'B', s: 'Ni', f: 493.88 }, { n: 'C2', s: 'Sa.', f: 523.25 } ];
const MELODIES = [ { name: "Alankar 1", notation: "Sa Re Ga Ma Pa Dha Ni Sa. | Sa. Ni Dha Pa Ma Ga Re Sa", notes: [0,2,4,5,7,9,11,12,12,11,9,7,5,4,2,0] }, { name: "Alankar 2", notation: "SaRe GaMa PaDha NiSa. | Sa.Ni DhaPa MaGa ReSa", notes: [0,2,2,4,4,5,5,7,7,9,9,11,11,12] }, { name: "Om Jai Jagdish", notation: "Sa Sa Sa Re Ga Ma Pa | Pa Ma Ga Re Sa Re Ga", notes: [0,0,0,2,4,5,7,7,5,4,2,0,2,4] }, { name: "Jana Gana Mana", notation: "Sa Re Ga Ga Ga Ga Ga | Ga Ga Ga Ga Re Ga Ma", notes: [0,2,4,4,4,4,4,4,4,4,4,2,4,5] } ];
const BEAT_PATTERNS = { teentaal:[1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], dadra:[1,0,0,0,0,0], keherwa:[1,0,0,0,1,0,0,0], rupak:[0,0,0,1,0,1,0] };
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const mainGain = ctx.createGain(); mainGain.connect(ctx.destination); mainGain.gain.setValueAtTime(0.3, ctx.currentTime);
const reverbGain = ctx.createGain(); reverbGain.connect(ctx.destination);
const activeOscs = {}; const reeds = { male: true, female: true, bass: true, coupler: false };
function createHarmoniumVoice(freq) {
const group = ctx.createGain(); group.connect(mainGain);
const voices = [];
const addOsc = (f, vol) => {
const osc = ctx.createOscillator(); const g = ctx.createGain(); const filter = ctx.createBiquadFilter();
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(f, ctx.currentTime);
filter.type = 'lowpass'; filter.frequency.setValueAtTime(2000, ctx.currentTime);
g.gain.setValueAtTime(0, ctx.currentTime); g.gain.linearRampToValueAtTime(vol, ctx.currentTime+0.1);
osc.connect(filter); filter.connect(g); g.connect(group); osc.start();
voices.push({ osc, g, filter });
};
if (reeds.bass) addOsc(freq/2, 0.2); if (reeds.male) addOsc(freq, 0.4); if (reeds.female) addOsc(freq*2, 0.2); if (reeds.coupler) addOsc(freq*4, 0.1);
return { group, voices };
}
function playNote(idx) {
if (activeOscs[idx]) return;
const freq = NOTES[(idx+24)%12].f * Math.pow(2, Math.floor((idx+24)/12)-2);
activeOscs[idx] = createHarmoniumVoice(freq);
const el = document.querySelector(`[data-idx="${idx}"]`); if (el) el.classList.add('active');
}
function stopNote(idx) {
if (!activeOscs[idx]) return;
const vset = activeOscs[idx].voices;
const releaseTime = 0.6; // Longer natural release
vset.forEach(v => {
v.g.gain.cancelScheduledValues(ctx.currentTime);
v.g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + releaseTime);
v.filter.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + releaseTime);
setTimeout(() => v.osc.stop(), releaseTime * 1000 + 100);
});
delete activeOscs[idx];
const el = document.querySelector(`[data-idx="${idx}"]`); if (el) el.classList.remove('active');
}
let beatInterval = null;
function startBeats(tala) {
clearInterval(beatInterval); if (tala === 'none') { document.getElementById('visualizer').innerHTML = ''; return; }
const p = BEAT_PATTERNS[tala]; const v = document.getElementById('visualizer'); v.innerHTML = '';
p.forEach((_,i)=>{ const d=document.createElement('div'); d.className='beat-dot'+(i===0?' sam':''); v.appendChild(d); });
let cur = 0;
beatInterval = setInterval(()=>{
if (p[cur]) { const o=ctx.createOscillator(); const g=ctx.createGain(); g.gain.setValueAtTime(cur===0?0.6:0.3, ctx.currentTime); g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime+0.4); o.frequency.setValueAtTime(cur===0?60:150, ctx.currentTime); o.connect(g); g.connect(ctx.destination); o.start(); o.stop(ctx.currentTime+0.5); }
document.querySelectorAll('.beat-dot').forEach((d,i)=>d.classList.toggle('active', i===cur));
cur = (cur+1)%p.length;
}, 500);
}
function init() {
const container = document.getElementById('harmonium-keys');
const keyMap = { 'z': -12, 's': -11, 'x': -10, 'd': -9, 'c': -8, 'v': -7, 'g': -6, 'b': -5, 'h': -4, 'n': -3, 'j': -2, 'm': -1, 'a': 0, 'w': 1, 's': 2, 'e': 3, 'd': 4, 'f': 5, 't': 6, 'g': 7, 'y': 8, 'h': 9, 'u': 10, 'j': 11, 'k': 12 };
const revMap = Object.entries(keyMap).reduce((acc, [k, v]) => (acc[v] = k, acc), {});
for (let i=-12; i<=12; i++) {
const n = NOTES[(i+24)%12]; const k = document.createElement('div');
const shortcut = revMap[i] ? revMap[i].toUpperCase() : '';
k.className = `key ${n.n.includes('#') ? 'black' : ''}`; k.dataset.idx = i;
k.innerHTML = `<span class="note-name">${n.s}</span><span class="key-shortcut">${shortcut}</span>`;
k.onmousedown = () => playNote(i); k.onmouseup = () => stopNote(i); k.onmouseleave = () => stopNote(i);
container.appendChild(k);
}
MELODIES.forEach((m, idx) => {
const c = document.createElement('div'); c.className = 'chip'; c.innerText = m.name;
c.onclick = () => { document.querySelectorAll('.chip').forEach(x=>x.classList.remove('active')); c.classList.add('active'); document.getElementById('notation').innerText = m.notation; document.getElementById('play-demo').onclick = async () => { for (let n of m.notes) { playNote(n); await new Promise(r=>setTimeout(r,400)); stopNote(n); } } };
document.getElementById('melody-chips').appendChild(c);
});
document.querySelectorAll('.switch').forEach(s => {
s.onclick = () => {
if (s.dataset.reed) { reeds[s.dataset.reed] = !reeds[s.dataset.reed]; s.classList.toggle('active'); }
if (s.dataset.beat) { document.querySelectorAll('[data-beat]').forEach(x=>x.classList.remove('active')); s.classList.add('active'); startBeats(s.dataset.beat); }
}
});
window.addEventListener('keydown', e => { if (e.repeat) return; const idx = keyMap[e.key.toLowerCase()]; if (idx !== undefined) playNote(idx); if (e.key === ' ') document.getElementById('bellows').style.transform = 'scaleY(1.5)'; });
window.addEventListener('keyup', e => { const idx = keyMap[e.key.toLowerCase()]; if (idx !== undefined) stopNote(idx); if (e.key === ' ') document.getElementById('bellows').style.transform = 'scaleY(1)'; });
}
init();