'+p.key+'</div>' + '<div class=\"muted\" style=\"font-size:12px\">House: '+p.house+'</div></div>' + '<div style=\"text-align:right\"><div class=\"muted\" style=\"font-size:12px\">Note</div>' + '<div>'+NOTE_NAMES[ pcMap[p.house-1] ]+'</div></div>'; list.appendChild(row); } } function defaultDate(){ const now = new Date(); now.setMinutes(now.getMinutes() - now.getTimezoneOffset()); if(dtInput) dtInput.value = now.toISOString().slice(0,16); } /* ------------------------ Chart Drawing ------------------------ */ function drawChart(){ try{ if(chartStyleSel && chartStyleSel.value==='patrika') drawPatrikaChart(); else drawModernChart(); }catch(err){ reportError(err); } } function drawModernChart(){ const W=600, H=600; vedicSVG.setAttribute('viewBox','0 0 '+W+' '+H); vedicSVG.innerHTML=''; const g = (tag, attrs)=>{ const e=document.createElementNS('http://www.w3.org/2000/svg', tag); for(const k in attrs) e.setAttribute(k, attrs[k]); vedicSVG.appendChild(e); return e }; g('rect',{x:30,y:30,width:540,height:540,fill:'#fff',stroke:'#000','stroke-width':2}); const mid=300; const x1=30, x2=570, y1=30, y2=570; const lines=[ [mid,y1, x2,mid], [x2,mid, mid,y2], [mid,y2, x1,mid], [x1,mid, mid,y1] ]; const inner=[ [mid,y1+135, x2-135,mid], [x2-135,mid, mid,y2-135], [mid,y2-135, x1+135,mid], [x1+135,mid, mid,y1+135] ]; [].concat(lines, inner).forEach(L=> g('line',{x1:L[0],y1:L[1],x2:L[2],y2:L[3],stroke:'#000','stroke-width':1.5})); const centers = [ [300,120],[435,165],[510,300],[435,435],[300,480],[165,435],[90,300],[165,165],[300,205],[385,300],[300,395],[215,300] ]; annotateHouses(centers, g); } function drawPatrikaChart(){ const W=600, H=600; vedicSVG.setAttribute('viewBox','0 0 '+W+' '+H); vedicSVG.innerHTML=''; const g = (tag, attrs)=>{ const e=document.createElementNS('http://www.w3.org/2000/svg', tag); for(const k in attrs) e.setAttribute(k, attrs[k]); vedicSVG.appendChild(e); return e }; g('rect',{x:20,y:20,width:560,height:560,fill:'#fff',stroke:'#000','stroke-width':2}); const mid=300; const top=20,bottom=580,left=20,right=580; const d1 = 'M '+mid+' '+top+' L '+right+' '+mid+' L '+mid+' '+bottom+' L '+left+' '+mid+' Z'; g('path',{d:d1, fill:'none', stroke:'#000','stroke-width':1.8}); const tris = [ 'M '+left+' '+top+' L '+mid+' '+top+' L '+left+' '+mid+' Z', 'M '+right+' '+top+' L '+right+' '+mid+' L '+mid+' '+top+' Z', 'M '+right+' '+bottom+' L '+mid+' '+bottom+' L '+right+' '+mid+' Z', 'M '+left+' '+bottom+' L '+left+' '+mid+' L '+mid+' '+bottom+' Z' ]; tris.forEach(d=> g('path',{d, fill:'none', stroke:'#000','stroke-width':1.2})); const k=110; const curves=[ 'M '+mid+' '+top+' C '+(mid+k)+' '+(top+k/2)+', '+(right- k/2)+' '+(mid-k)+', '+right+' '+mid, 'M '+right+' '+mid+' C '+(right-k/2)+' '+(mid+k)+', '+(mid+k)+' '+(bottom-k/2)+', '+mid+' '+bottom, 'M '+mid+' '+bottom+' C '+(mid-k)+' '+(bottom-k/2)+', '+(left+k/2)+' '+(mid+k)+', '+left+' '+mid, 'M '+left+' '+mid+' C '+(left+k/2)+' '+(mid-k)+', '+(mid-k)+' '+(top+k/2)+', '+mid+' '+top ]; curves.forEach(d=> g('path',{d, fill:'none', stroke:'#000','stroke-width':1.2})); const cs=34; g('rect',{x:300-cs/2,y:300-cs/2,width:cs,height:cs,fill:'#fff',stroke:'#000','stroke-width':1}); const centers = [ [mid,110],[440,150],[520,300],[440,450],[300,510],[160,450],[80,300],[160,150],[300,205],[385,300],[300,395],[215,300] ]; annotateHouses(centers, g); } function annotateHouses(centers, g){ const tonicIdx = NOTE_NAMES.indexOf(tonicSel.value); const pcMap = houseToPitchClasses(tonicIdx); const byHouse = new Map(); for(const p of planetState){ if(!byHouse.has(p.house)) byHouse.set(p.house, []); byHouse.get(p.house).push(p); } centers.forEach((c,idx)=>{ const house = idx+1; const note = NOTE_NAMES[ pcMap[idx] ]; const x=c[0], y=c[1]; g('text',{x:x, y:y-22, fill:'#000', 'text-anchor':'middle', 'font-size':'11'}).textContent = 'House '+house; g('text',{x:x, y:y-2, fill:'#000', 'text-anchor':'middle', 'font-size':'14', 'font-weight':'700'}).textContent = note; const ps = byHouse.get(house) || []; ps.forEach((p,i)=>{ g('text',{x:x, y:y+16 + i*14, fill:'#000', 'text-anchor':'middle', 'font-size':'12', 'font-weight':'700'}).textContent = p.abbr; }); }); } /* ------------------------ Compute & Autoload ------------------------ */ function computeChart(){ try{ const dateISO = (dtInput && dtInput.value) ? dtInput.value : new Date().toISOString().slice(0,16); let lat = parseFloat(latInput && latInput.value); let lon = parseFloat(lonInput && lonInput.value); if(Number.isNaN(lat)) lat = -8.5069; if(Number.isNaN(lon)) lon = 115.2625; const placements = seededPlacement(dateISO, lat, lon); updatePlanetHouses(placements); if(statusEl) statusEl.textContent = ''; }catch(err){ reportError(err); } } function autoload(){ defaultDate(); if(navigator.geolocation){ navigator.geolocation.getCurrentPosition((pos)=>{ latInput.value = pos.coords.latitude.toFixed(4); lonInput.value = pos.coords.longitude.toFixed(4); computeChart(); }, ()=>{ computeChart(); }, { enableHighAccuracy:true, timeout:5000, maximumAge:60000 }); } else { computeChart(); } } function safeBoot(){ try{ autoload(); renderPlanetList(); drawChart(); } catch(err){ reportError(err); } } if (document.readyState !== 'loading') { safeBoot(); } else { document.addEventListener('DOMContentLoaded', safeBoot); } chartStyleSel && chartStyleSel.addEventListener('change', drawChart); calcBtn.addEventListener('click', computeChart); tonicSel.addEventListener('change', ()=>{ renderPlanetList(); drawChart(); }); tempo.addEventListener('input', ()=> tempoVal.textContent = tempo.value ); attack.addEventListener('input', ()=> { const v = attack.value; const el = document.getElementById('attackVal'); if(el) el.textContent = v; }); release.addEventListener('input', ()=> { const v = release.value; const el = document.getElementById('releaseVal'); if(el) el.textContent = v; }); melVol.addEventListener('input', ()=> { const el = document.getElementById('melVolVal'); if(el) el.textContent = (+melVol.value).toFixed(2); }); cutoff.addEventListener('input', ()=>{ if(cutoffVal) cutoffVal.textContent = cutoff.value; if(melFilter) melFilter.frequency.value = parseFloat(cutoff.value); }); res.addEventListener('input', ()=>{ if(resVal) resVal.textContent = (+res.value).toFixed(2); if(melFilter) melFilter.Q.value = parseFloat(res.value); }); noiseMix.addEventListener('input', ()=>{ if(noiseMixVal) noiseMixVal.textContent = (+noiseMix.value).toFixed(2); }); /* ------------------------ Audio Engine ------------------------ */ let audioCtx = null, scheduler=null, isPlaying=false, currentStep=0; let masterGain, dryGain, wetGain, convolver; let melodyGain, subGain, percGain, percWetSend; let melFilter = null; let noiseBufWhite=null, noiseBufPink=null; let melodySampleBuffer = null; function startAudio(){ if(!audioCtx){ audioCtx = new (window.AudioContext || window.webkitAudioContext)(); masterGain = audioCtx.createGain(); masterGain.gain.value = 1.0; masterGain.connect(audioCtx.destination); dryGain = audioCtx.createGain(); dryGain.gain.value = 1.0; dryGain.connect(masterGain); wetGain = audioCtx.createGain(); wetGain.gain.value = 0.25; wetGain.connect(masterGain); convolver = audioCtx.createConvolver(); convolver.connect(wetGain); buildReverb(); melodyGain = audioCtx.createGain(); melodyGain.gain.value = parseFloat(melVol.value); melodyGain.connect(dryGain); subGain = audioCtx.createGain(); subGain.gain.value = parseFloat(subVol.value); subGain.connect(dryGain); percGain = audioCtx.createGain(); percGain.gain.value = parseFloat(percVol.value); percGain.connect(dryGain); percWetSend = audioCtx.createGain(); percWetSend.gain.value = parseFloat(percRevMix.value); percWetSend.connect(convolver); melFilter = audioCtx.createBiquadFilter(); melFilter.type='lowpass'; melFilter.frequency.value = parseFloat(cutoff.value); melFilter.Q.value = parseFloat(res.value); buildNoiseBuffers(); } else { melodyGain.gain.value = parseFloat(melVol.value); subGain.gain.value = parseFloat(subVol.value); percGain.gain.value = parseFloat(percVol.value); if(melFilter){ melFilter.frequency.value = parseFloat(cutoff.value); melFilter.Q.value = parseFloat(res.value); } } } function buildReverb(){ if(!audioCtx) return; const duration = 2.5, rate = audioCtx.sampleRate, length = Math.floor(rate * duration); const impulse = audioCtx.createBuffer(2, length, rate); for(let c=0;c<2;c++){ const ch = impulse.getChannelData(c); for(let i=0;i<length;i++){ ch[i] = (Math.random()*2-1) * Math.pow(1 - i/length, 2.5); } } convolver.buffer = impulse; } function buildNoiseBuffers(){ const rate = audioCtx.sampleRate, len = rate * 2; const wb = audioCtx.createBuffer(1, len, rate); const wd = wb.getChannelData(0); for(let i=0;i<len;i++) wd[i] = Math.random()*2-1; noiseBufWhite = wb; const pb = audioCtx.createBuffer(1, len, rate); const pd = pb.getChannelData(0); let b0=0,b1=0,b2=0,b3=0,b4=0,b5=0,b6=0; for(let i=0;i<len;i++){ let white=Math.random()*2-1; b0=0.99886*b0 + white*0.0555179; b1=0.99332*b1 + white*0.0750759; b2=0.96900*b2 + white*0.1538520; b3=0.86650*b3 + white*0.3104856; b4=0.55000*b4 + white*0.5329522; b5=-0.7616*b5 - white*0.0168980; pd[i]=b0+b1+b2+b3+b4+b5+b6+white*0.5362; pd[i]*=0.11; b6=white*0.115926; } noiseBufPink = pb; } function applyEnv(env, when, peak, atk, rel){ env.gain.setValueAtTime(0, when); env.gain.linearRampToValueAtTime(peak, when + Math.max(0.001, atk)); env.gain.exponentialRampToValueAtTime(0.0001, when + Math.max(0.05, atk + rel)); } function playTone(freq, when, gain=0.18){ const atk = (parseInt(attack.value,10)||0)/1000; const rel = (parseInt(release.value,10)||280)/1000; if(useSample && useSample.checked && melodySampleBuffer){ playSampleTone(freq, when, gain, atk, rel); return; } const osc = audioCtx.createOscillator(); osc.type = waveSel.value; osc.frequency.value = freq; const nmix = parseFloat(noiseMix.value)||0; let noiseSrc = null; let noiseGain = null; const merge = audioCtx.createGain(); if(nmix > 0){ noiseSrc = audioCtx.createBufferSource(); noiseSrc.buffer = noiseBufWhite; noiseSrc.loop = true; noiseGain = audioCtx.createGain(); noiseGain.gain.value = nmix * gain * 0.6; noiseSrc.connect(noiseGain); noiseGain.connect(merge); } osc.connect(merge); const env = audioCtx.createGain(); merge.connect(melFilter); melFilter.connect(env); env.connect(melodyGain); melFilter.connect(convolver); applyEnv(env, when, gain, atk, rel); const stopTime = when + Math.max(0.05, atk + rel) + 0.05; osc.start(when); osc.stop(stopTime); if(noiseSrc){ noiseSrc.start(when); noiseSrc.stop(stopTime); } } function playSampleTone(freq, when, gain=0.18, atk=0.01, rel=0.3){ const src = audioCtx.createBufferSource(); src.buffer = melodySampleBuffer; src.loop = false; const note = sampleRootNote.value; const oct = parseInt(sampleRootOct.value,10)||4; const rootPc = NOTE_NAMES.indexOf(note); const baseMidi = noteToMidi(rootPc, oct); const baseFreq = midiToFreq(baseMidi); src.playbackRate.value = Math.max(0.01, freq / baseFreq); const pre = audioCtx.createGain(); pre.gain.value = (sampleVol? parseFloat(sampleVol.value):1) * gain / 0.18; const env = audioCtx.createGain(); src.connect(pre); pre.connect(melFilter); melFilter.connect(env); env.connect(melodyGain); melFilter.connect(convolver); applyEnv(env, when, 1.0, atk, rel); const stopTime = when + Math.max(0.05, atk + rel) + 0.1; src.start(when); src.stop(stopTime); } function playSub(freq, when){ const rel = Math.max(0.08, (parseInt(subRel.value,10)||1200)/1000); const atk = 0.006; const dur = atk + rel; const osc = audioCtx.createOscillator(); osc.type='sine'; osc.frequency.value=freq; const env = audioCtx.createGain(); applyEnv(env, when, 0.28, atk, rel); osc.connect(env); env.connect(subGain); osc.start(when); osc.stop(when+dur+0.05); } function playPerc(when){ const rel = Math.max(0.04, (parseInt(percRel.value,10)||220)/1000); const atk = 0.003; const dur = atk + rel; const buf = (percColor.value==='pink') ? noiseBufPink : noiseBufWhite; const src = audioCtx.createBufferSource(); src.buffer = buf; src.loop = false; const lpf = audioCtx.createBiquadFilter(); lpf.type='lowpass'; lpf.frequency.setValueAtTime(parseFloat(percCut.value)||1800, when); const env = audioCtx.createGain(); applyEnv(env, when, 0.6, atk, rel); src.connect(lpf); lpf.connect(env); env.connect(percGain); lpf.connect(percWetSend); src.start(when); src.stop(when + dur + 0.05); } function schedulePerc(when, stepDur, stepIndex, gated){ if(!gated) return; const prob = parseFloat(percProb.value)||0; const offset = (parseInt(percOffset.value,10)||0)/1000; const hits = []; switch(percRateMus.value){ case '1/1': if(stepIndex % 4 === 0) hits.push(0); break; case '1/2': if(stepIndex % 2 === 0) hits.push(0); break; case '1/4': hits.push(0); break; case '1/8': hits.push(0, 0.5); break; case '1/8T': hits.push(0, 1/3, 2/3); break; case '1/16': hits.push(0, 0.25, 0.5, 0.75); break; } for(const f of hits){ if(Math.random() < prob) playPerc(when + f*stepDur + offset); } } function tick(){ if(!isPlaying) return; startAudio(); melodyGain.gain.value = parseFloat(melVol.value); subGain.gain.value = parseFloat(subVol.value); percGain.gain.value = parseFloat(percVol.value); if(melFilter){ melFilter.frequency.value = parseFloat(cutoff.value); melFilter.Q.value = parseFloat(res.value); } const bpm = parseInt(tempo.value,10); const stepDur = 60/Math.max(30, Math.min(240, bpm)); const when = audioCtx.currentTime + 0.02; const tonicIdx = NOTE_NAMES.indexOf(tonicSel.value); const mode = modeSel.value; const activeHouses = [...new Set(planetState.map(p=>p.house))]; if(activeHouses.length===0){ scheduler = setTimeout(tick, stepDur*1000); return; } const house = activeHouses[currentStep % activeHouses.length]; const midi = houseToMidi(house, tonicIdx, mode); const freq = midiToFreq(midi); const planetsHere = planetState.filter(p=>p.house===house); let extra = null; for(const p of planetsHere){ const h=planetHarmony(p); if(h){ extra = (extra===null)?h:extra; } } playTone(freq, when, 0.18); if(extra!==null) playTone(midiToFreq(midi+extra), when, 0.14); if(subOn.checked){ const rate = parseInt(subRate.value,10)||4; const prob = parseFloat(subProb.value)||0; if(currentStep % rate === 0 && Math.random() < prob){ let subMidi; if(subRootOnly.checked){ const rootMidi = houseToMidi(1, tonicIdx, mode); subMidi = rootMidi - 12*parseInt(subOct.value,10); } else { subMidi = midi - 12*parseInt(subOct.value,10); } playSub(midiToFreq(subMidi), when); } } if(percOn.checked){ schedulePerc(when, stepDur, currentStep, planetsHere.length>0); } currentStep = (currentStep + 1) % parseInt(stepsSel.value,10); scheduler = setTimeout(tick, stepDur*1000); } if(sampleFile) sampleFile.addEventListener('change', async (e)=>{ if(!e.target.files || e.target.files.length===0) return; try{ startAudio(); const file = e.target.files[0]; const arr = await file.arrayBuffer(); audioCtx.decodeAudioData(arr, (buf)=>{ melodySampleBuffer = buf; }, (err)=>{ reportError(err); }); }catch(err){ reportError(err); } }); startBtn.addEventListener('click', async ()=>{ startAudio(); try{ await audioCtx.resume(); }catch(e){} }); playBtn.addEventListener('click', async ()=>{ if(!audioCtx) startAudio(); try{ await audioCtx.resume(); }catch(e){} if(isPlaying) return; isPlaying=true; currentStep=0; if(!planetState.some(p=>p.house)) computeChart(); tick(); }); stopBtn.addEventListener('click', ()=>{ isPlaying=false; if(scheduler) clearTimeout(scheduler); }); </script> </body> </html>">