428 lines
14 KiB
JavaScript
428 lines
14 KiB
JavaScript
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.';
|
|
}
|
|
});
|