let _dbgEnabled = false; function dbg(...args) { if (!_dbgEnabled) return; const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '); try { window.go.main.App.JSDebugLog(msg); } catch (_) {} } window.dbg = dbg; document.addEventListener('DOMContentLoaded', () => { const navLinks = document.querySelectorAll('.nav-link'); const views = document.querySelectorAll('.view'); const filterSection = document.querySelector('.filter-section'); const themeSelect = document.getElementById('theme-select'); const themeLink = document.getElementById('theme-stylesheet'); const btnExport = document.getElementById('btn-export'); const btnApply = document.getElementById('btn-apply'); const btnSave = document.getElementById('btn-save'); const btnReset = document.getElementById('btn-reset'); const presetSelect = document.getElementById('preset-select'); const tuningGrid = document.getElementById('tuning-grid'); const fretsInput = document.getElementById('frets-input'); const fingersInput = document.getElementById('fingers-input'); const baselineShiftInput = document.getElementById('baseline-shift'); const rangeDownInput = document.getElementById('range-down'); const rangeUpInput = document.getElementById('range-up'); const loading = document.getElementById('chord-loading'); const allNotes = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; // --- Audio --- const noteToSemitone = {C:0,'C#':1,D:2,'D#':3,E:4,F:5,'F#':6,G:7,'G#':8,A:9,'A#':10,B:11}; const standardMIDI = [40, 45, 50, 55, 59, 64]; function tuningNamesToMIDI(names) { return names.map((n, i) => { const pc = noteToSemitone[n]; if (pc === undefined) return standardMIDI[i] || 40; const std = standardMIDI[i] || 40; for (let m = std - 6; m <= std + 6; m++) { if (((m % 12) + 12) % 12 === pc) return m; } return std; }); } window.currentTuningMIDI = standardMIDI.slice(); let polySynth = null; let audioFilter = null; let audioGain = null; let audioProfile = 15; const volSlider = document.getElementById('audio-vol'); const profileSlider = document.getElementById('audio-profile'); const strumSlider = document.getElementById('audio-strum'); const engineSelect = document.getElementById('audio-engine'); // --- PluckSynth engine (Karplus-Strong) --- let pluckSynths = null; let pluckFilter = null; let pluckGain = null; function buildPluck() { if (!pluckFilter) { pluckFilter = new Tone.Filter({ type: 'lowpass', rolloff: -12 }).toDestination(); pluckGain = new Tone.Gain(volSlider.value / 100).connect(pluckFilter); } if (!pluckSynths) { pluckSynths = []; for (let i = 0; i < 6; i++) { pluckSynths.push(new Tone.PluckSynth().connect(pluckGain)); } } const t = audioProfile / 100; pluckFilter.frequency.value = 1000 + t * 6000; pluckFilter.Q.value = 1.0; pluckSynths.forEach(ps => { ps.set({ attackNoise: 1, dampening: 1000 + t * 6000, resonance: 0.9 + t * 0.09 }); }); } // --- WebAudioFont engine --- let wafPlayer = null; let wafContext = null; let wafGain = null; let wafFilter = null; let guitarPreset = null; function initWAF() { if (wafPlayer) return; wafContext = Tone.context.rawContext; wafGain = wafContext.createGain(); wafGain.gain.value = volSlider.value / 100; wafFilter = wafContext.createBiquadFilter(); wafFilter.type = 'lowpass'; wafFilter.frequency.value = 5000; wafGain.connect(wafFilter); wafFilter.connect(wafContext.destination); wafPlayer = new WebAudioFontPlayer(); guitarPreset = _tone_0250_SoundBlasterOld_sf2; wafPlayer.adjustPreset(wafContext, guitarPreset); } function applyWAFTone() { if (!wafFilter) return; const t = audioProfile / 100; wafFilter.frequency.value = 800 + t * 8000; } // --- Synth engine (existing PolySynth) --- function profilePartials(t) { const p = []; for (let n = 1; n <= 16; n++) { const tri = (n % 2 === 1) ? 1 / (n * n) : 0; const saw = 1 / n; p.push(tri * (1 - t) + saw * t); } return p; } function buildSynth() { if (!audioFilter) { audioFilter = new Tone.Filter({ type: 'lowpass', rolloff: -12 }).toDestination(); audioGain = new Tone.Gain(volSlider.value / 100).connect(audioFilter); } if (!polySynth) { polySynth = new Tone.PolySynth(Tone.Synth, { maxPolyphony: 8 }).connect(audioGain); } polySynth.releaseAll(); const t = audioProfile / 100; audioFilter.frequency.value = 600 + t * 9400; audioFilter.Q.value = 1.5 - t * 1.0; polySynth.set({ oscillator: { type: 'custom', partials: profilePartials(t) }, envelope: { attack: 0.045 - t * 0.04, decay: 0.5 - t * 0.3, sustain: 0.25 + t * 0.25, release: 1.5 - t * 0.7 } }); } // --- Volume slider: update all engines --- volSlider.addEventListener('input', () => { const v = volSlider.value / 100; if (audioGain) audioGain.gain.value = v; if (pluckGain) pluckGain.gain.value = v; if (wafGain) wafGain.gain.value = v; }); // --- Tone slider: update active engine --- profileSlider.addEventListener('input', () => { audioProfile = parseInt(profileSlider.value); const engine = engineSelect.value; if (engine === 'pluck') buildPluck(); else if (engine === 'sample') applyWAFTone(); else buildSynth(); }); // --- playChord: multi-engine with strum --- window.playChord = function(midiNotes) { Tone.start(); const strum = parseInt(strumSlider.value) / 1000; const engine = engineSelect.value; const now = Tone.now(); if (engine === 'pluck') { if (!pluckSynths) buildPluck(); midiNotes.forEach((m, i) => { const freq = 440 * Math.pow(2, (m - 69) / 12); pluckSynths[i % pluckSynths.length].triggerAttack(freq, now + i * strum); }); } else if (engine === 'sample') { initWAF(); midiNotes.forEach((m, i) => { wafPlayer.queueWaveTable(wafContext, wafGain, guitarPreset, wafContext.currentTime + i * strum, m, 2.0); }); } else { if (!polySynth) buildSynth(); polySynth.releaseAll(); midiNotes.forEach((m, i) => { const freq = 440 * Math.pow(2, (m - 69) / 12); polySynth.triggerAttackRelease(freq, '1.5s', now + i * strum); }); } }; const presets = { 'Standard': ['E','A','D','G','B','E'], 'Drop D': ['D','A','D','G','B','E'], 'DADGAD': ['D','A','D','G','A','D'], 'Open G': ['D','G','B','D','G','B'], 'Open D': ['D','A','D','F#','A','D'], 'Open C': ['C','G','C','G','C','E'], 'Half Step Down': ['D#','G#','C#','F#','A#','D#'], 'Full Step Down': ['D','G','C','F','A','D'], 'Custom': null }; const shapesSection = document.getElementById('shapes-section'); let currentConfig = null; let chordsLoaded = false; let shapesInited = false; let lastBaselineShift = 0; // --- Navigation --- window.switchToView = function(target) { navLinks.forEach(l => l.classList.remove('active')); views.forEach(v => v.classList.remove('active')); const navLink = document.querySelector('.nav-link[data-view="' + target + '"]'); if (navLink) navLink.classList.add('active'); document.getElementById('view-' + target).classList.add('active'); filterSection.style.display = target === 'chords' ? '' : 'none'; shapesSection.style.display = (target === 'shapes') ? '' : 'none'; if (target === 'chords' && !chordsLoaded) { loadChords(); } if (target === 'shapes' && !shapesInited) { shapesInited = true; if (window.initShapeExplorer) window.initShapeExplorer(); } }; navLinks.forEach(link => { link.addEventListener('click', () => { window.switchToView(link.dataset.view); }); }); // --- Theme --- themeSelect.addEventListener('change', () => { themeLink.href = 'chords-' + themeSelect.value + '.css'; }); // --- PDF Export --- btnExport.addEventListener('click', () => window.print()); // --- Config Panel --- function populatePresets() { presetSelect.innerHTML = ''; for (const name of Object.keys(presets)) { const opt = document.createElement('option'); opt.value = name; opt.textContent = name; presetSelect.appendChild(opt); } } function buildTuningGrid(tuning) { tuningGrid.innerHTML = ''; tuning.forEach((note, i) => { const sel = document.createElement('select'); allNotes.forEach(n => { const opt = document.createElement('option'); opt.value = n; opt.textContent = n; if (n === note) opt.selected = true; sel.appendChild(opt); }); sel.addEventListener('change', () => { presetSelect.value = 'Custom'; }); tuningGrid.appendChild(sel); }); } function readTuningFromGrid() { return Array.from(tuningGrid.querySelectorAll('select')).map(s => s.value); } function syncConfigPanel(cfg) { fretsInput.value = cfg.frets; fingersInput.value = cfg.max_fingers; lastBaselineShift = cfg.baseline_shift || 0; baselineShiftInput.value = lastBaselineShift; rangeDownInput.value = cfg.range_down || 7; rangeUpInput.value = cfg.range_up || 7; buildTuningGrid(cfg.tuning); let matched = false; for (const [name, notes] of Object.entries(presets)) { if (notes && notes.length === cfg.tuning.length && notes.every((n, i) => n === cfg.tuning[i])) { presetSelect.value = name; matched = true; break; } } if (!matched) presetSelect.value = 'Custom'; } presetSelect.addEventListener('change', () => { const notes = presets[presetSelect.value]; if (notes) { buildTuningGrid(notes); } }); baselineShiftInput.addEventListener('input', () => { const newShift = parseInt(baselineShiftInput.value) || 0; const delta = newShift - lastBaselineShift; if (delta === 0) return; lastBaselineShift = newShift; const selects = tuningGrid.querySelectorAll('select'); selects.forEach(sel => { const pc = allNotes.indexOf(sel.value); if (pc < 0) return; sel.value = allNotes[((pc + delta) % 12 + 12) % 12]; }); presetSelect.value = 'Custom'; }); function readConfigFromPanel() { return { instrument: currentConfig ? currentConfig.instrument : 'guitar', tuning: readTuningFromGrid(), frets: parseInt(fretsInput.value) || 4, max_fingers: parseInt(fingersInput.value) || 4, baseline_shift: parseInt(baselineShiftInput.value) || 0, range_down: parseInt(rangeDownInput.value) || 0, range_up: parseInt(rangeUpInput.value) || 0 }; } btnApply.addEventListener('click', () => { if (!window.go) return; const cfg = readConfigFromPanel(); loading.style.display = ''; loading.textContent = 'Regenerating chords...'; window.go.main.App.UpdateConfig(cfg).then(chords => { currentConfig = cfg; window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning); loading.style.display = 'none'; chordsLoaded = true; if (window.buildChordCards) { window.buildChordCards(chords || [], cfg.frets, cfg.tuning.length); } }).catch(err => { loading.textContent = 'Error: ' + err; }); }); btnSave.addEventListener('click', () => { if (!window.go) return; const cfg = readConfigFromPanel(); window.go.main.App.UpdateConfig(cfg).then(chords => { currentConfig = cfg; window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning); if (chordsLoaded && window.buildChordCards) { window.buildChordCards(chords || [], cfg.frets, cfg.tuning.length); } return window.go.main.App.SaveConfig(); }).then(() => { btnSave.textContent = 'Saved'; setTimeout(() => { btnSave.textContent = 'Save'; }, 1500); }).catch(err => { btnSave.textContent = 'Error'; setTimeout(() => { btnSave.textContent = 'Save'; }, 1500); }); }); btnReset.addEventListener('click', () => { if (!window.go) return; window.go.main.App.ResetConfig().then(cfg => { currentConfig = cfg; window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning); syncConfigPanel(cfg); loadChords(); }); }); window.setTuningFromExplorer = function(noteNames) { const pcs = noteNames.map(n => n.replace(/[-]?\d+$/, '')); buildTuningGrid(pcs); presetSelect.value = 'Custom'; btnApply.click(); }; // --- Load Chords --- function loadChords() { if (!window.go) { loading.textContent = 'Wails runtime not available.'; return; } loading.style.display = ''; loading.textContent = 'Loading chord fingerings...'; window.go.main.App.FindChordFingerings().then(chords => { loading.style.display = 'none'; chordsLoaded = true; if (window.buildChordCards) { window.buildChordCards(chords || [], currentConfig.frets, currentConfig.tuning.length); } }); } // --- Init --- populatePresets(); if (window.go && window.go.main && window.go.main.App) { window.go.main.App.IsDebug().then(d => { if (d) { _dbgEnabled = true; window.DEBUG = true; dbg('=== Frontend debug logging active ==='); } }); window.go.main.App.GetConfig().then(cfg => { currentConfig = cfg; window.currentTuningMIDI = tuningNamesToMIDI(cfg.tuning); syncConfigPanel(cfg); }); } else { loading.textContent = 'Wails runtime not available.'; } });