web-tuner/frontend/dist/app.js

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.';
}
});