diff --git a/app.go b/app.go index feb38f7..34109af 100644 --- a/app.go +++ b/app.go @@ -7,9 +7,11 @@ import ( ) type App struct { - ctx context.Context - config Config - configPath string + ctx context.Context + config Config + configPath string + scoreSets ScoreSetsData + scoreSetsPath string } func NewApp() *App { @@ -19,35 +21,62 @@ func NewApp() *App { func (a *App) startup(ctx context.Context) { a.ctx = ctx a.loadConfig() + a.loadScoreSets() +} + +func dataDir() string { + home, _ := os.UserHomeDir() + dir := filepath.Join(home, "web-tuner") + os.MkdirAll(dir, 0755) + return dir } func (a *App) loadConfig() { - exe, _ := os.Executable() - dir := filepath.Dir(exe) - - candidates := []string{ - filepath.Join(dir, "config.json"), - "config.json", - } - - for _, path := range candidates { - cfg, err := LoadConfig(path) - if err == nil { - a.config = cfg - a.configPath = path - return + a.configPath = filepath.Join(dataDir(), "config.json") + cfg, err := LoadConfig(a.configPath) + if err == nil { + if cfg.RangeDown == 0 && cfg.RangeUp == 0 { + cfg.RangeDown = 7 + cfg.RangeUp = 7 } + a.config = cfg + return } - - a.configPath = "config.json" a.config = Config{ Instrument: "guitar", Tuning: []string{"E", "A", "D", "G", "B", "E"}, Frets: 4, MaxFingers: 4, + RangeDown: 7, + RangeUp: 7, } } +func (a *App) loadScoreSets() { + a.scoreSetsPath = filepath.Join(dataDir(), "scoresets.json") + data, err := LoadScoreSets(a.scoreSetsPath) + if err == nil { + a.scoreSets = data + return + } + a.scoreSets = DefaultScoreSetsData() +} + +func (a *App) GetScoreSets() ScoreSetsData { + return a.scoreSets +} + +func (a *App) SaveScoreSets(data ScoreSetsData) error { + if len(data.Sets) == 0 { + data = DefaultScoreSetsData() + } + if data.Selected < 0 || data.Selected >= len(data.Sets) { + data.Selected = 0 + } + a.scoreSets = data + return SaveScoreSets(a.scoreSetsPath, data) +} + func (a *App) GetConfig() Config { return a.config } @@ -96,3 +125,11 @@ func (a *App) GetDefaultShapes() []ShapeDefinition { func (a *App) FindShapeTunings(query ShapeQuery, companions []ShapeDefinition) ([]TuningCandidate, error) { return findTuningsForShape(query, companions) } + +func (a *App) IdentifyShape(frets []int) string { + return identifyShape(frets, a.config) +} + +func (a *App) FindDensityTunings(query ShapeQuery) ([]DensityCandidate, error) { + return findDensityTunings(query, a.config.Frets, a.config.MaxFingers) +} diff --git a/config.go b/config.go index 8e47034..5fe7459 100644 --- a/config.go +++ b/config.go @@ -7,10 +7,13 @@ import ( ) type Config struct { - Instrument string `json:"instrument"` - Tuning []string `json:"tuning"` - Frets int `json:"frets"` - MaxFingers int `json:"max_fingers"` + Instrument string `json:"instrument"` + Tuning []string `json:"tuning"` + Frets int `json:"frets"` + MaxFingers int `json:"max_fingers"` + BaselineShift int `json:"baseline_shift"` + RangeDown int `json:"range_down"` + RangeUp int `json:"range_up"` } func LoadConfig(path string) (Config, error) { diff --git a/debug.go b/debug.go index 2b85983..f45e023 100644 --- a/debug.go +++ b/debug.go @@ -2,9 +2,45 @@ package main -import "log" +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +var debugLogger *log.Logger func init() { - log.SetFlags(log.Ltime | log.Lshortfile) - log.Println("[debug] debug build active") + home, _ := os.UserHomeDir() + logDir := filepath.Join(home, "web-tuner") + os.MkdirAll(logDir, 0755) + logPath := filepath.Join(logDir, "debug.log") + + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + debugLogger = log.New(os.Stderr, "[DEBUG] ", log.Ltime|log.Lshortfile) + debugLogger.Printf("Could not open %s: %v — logging to stderr", logPath, err) + return + } + + debugLogger = log.New(f, "[DEBUG] ", log.Ltime|log.Lshortfile) + debugLogger.Printf("=== web-tuner debug session started %s ===", time.Now().Format(time.RFC3339)) + debugLogger.Printf("Log file: %s", logPath) + fmt.Fprintf(os.Stderr, "[web-tuner] Debug logging to %s\n", logPath) +} + +func debugLog(format string, args ...any) { + if debugLogger != nil { + debugLogger.Printf(format, args...) + } +} + +func (a *App) JSDebugLog(msg string) { + debugLog("[JS] %s", msg) +} + +func (a *App) IsDebug() bool { + return true } diff --git a/debug.sh b/debug.sh index 4e260e7..2786993 100755 --- a/debug.sh +++ b/debug.sh @@ -1,10 +1,25 @@ #!/bin/bash -set -euo pipefail +set -e +pkill -f "web-tuner" 2>/dev/null || true +sleep 0.5 export CC=/usr/bin/clang export CXX=/usr/bin/clang++ export SDKROOT=$(xcrun --show-sdk-path) +export CGO_ENABLED=1 export PATH="$HOME/go/bin:$PATH" -echo "Starting dev build with debug tag..." -wails dev -tags debug -loglevel debug +WAILS=$(command -v wails || echo "$HOME/go/bin/wails") + +echo "Building web-tuner (DEBUG)..." +"$WAILS" build -skipbindings -tags debug + +echo "" +echo "Debug build complete. Logs will be written to ~/web-tuner/debug.log" +echo "Launching..." +open build/bin/web-tuner.app + +sleep 1 +echo "" +echo "=== Tailing ~/web-tuner/debug.log (Ctrl-C to stop) ===" +tail -f ~/web-tuner/debug.log 2>/dev/null || echo "Waiting for log file..." diff --git a/density.go b/density.go new file mode 100644 index 0000000..8b5216f --- /dev/null +++ b/density.go @@ -0,0 +1,249 @@ +package main + +import ( + "fmt" + "sort" + "sync" +) + +type DensityChord struct { + Chord string `json:"chord"` + Root string `json:"root"` + Quality string `json:"quality"` + Frets []int `json:"frets"` + Fingers int `json:"fingers"` + Notes []string `json:"notes"` +} + +type DensityCandidate struct { + Tuning []string `json:"tuning"` + TuningMIDI []int `json:"tuning_midi"` + Root string `json:"root"` + Chord string `json:"chord"` + Chords []DensityChord `json:"chords"` + ValidChords int `json:"valid_chords"` + MajMinCount int `json:"maj_min_count"` + HighCompat bool `json:"high_compat"` + AvgFingers float64 `json:"avg_fingers"` +} + +func findDensityTunings(query ShapeQuery, maxFret, maxFingers int) ([]DensityCandidate, error) { + tunings, err := findCandidateTunings(query) + if err != nil { + return nil, err + } + + nStrings := len(query.Shape.Frets) + results := make([]DensityCandidate, len(tunings)) + + var wg sync.WaitGroup + for idx, ct := range tunings { + wg.Add(1) + go func(i int, t candidateTuning) { + defer wg.Done() + + chords := scoreTuningDensity(t.tuningMIDI, maxFret, maxFingers) + + tuningNames := make([]string, nStrings) + for s := 0; s < nStrings; s++ { + tuningNames[s] = midiToNoteName(t.tuningMIDI[s]) + } + + majMin := 0 + totalFingers := 0 + for _, c := range chords { + if c.Quality == "major" || c.Quality == "minor" { + majMin++ + } + totalFingers += c.Fingers + } + + avg := 0.0 + if len(chords) > 0 { + avg = float64(totalFingers) / float64(len(chords)) + } + + results[i] = DensityCandidate{ + Tuning: tuningNames, + TuningMIDI: t.tuningMIDI, + Root: SemitoneToNote[t.root], + Chord: SemitoneToNote[t.root] + " " + query.TargetQuality, + Chords: chords, + ValidChords: len(chords), + MajMinCount: majMin, + HighCompat: majMin > 5, + AvgFingers: avg, + } + }(idx, ct) + } + wg.Wait() + + sort.Slice(results, func(i, j int) bool { + if results[i].ValidChords != results[j].ValidChords { + return results[i].ValidChords > results[j].ValidChords + } + return results[i].AvgFingers < results[j].AvgFingers + }) + + return results, nil +} + +func scoreTuningDensity(tuningMIDI []int, maxFret, maxFingers int) []DensityChord { + nStrings := len(tuningMIDI) + defs := GetChordDefinitions() + + type chordDef struct { + quality string + intervals []int + } + var allDefs []chordDef + for _, cat := range defs { + for name, ivs := range cat { + allDefs = append(allDefs, chordDef{name, ivs}) + } + } + + type chordKey struct { + root int + quality string + } + best := make(map[chordKey]DensityChord) + + optCount := maxFret + 2 + totalCombinations := 1 + for i := 0; i < nStrings; i++ { + totalCombinations *= optCount + } + + fingering := make([]string, nStrings) + for combo := 0; combo < totalCombinations; combo++ { + tmp := combo + for s := nStrings - 1; s >= 0; s-- { + val := tmp % optCount + tmp /= optCount + if val == maxFret+1 { + fingering[s] = "x" + } else { + fingering[s] = fmt.Sprintf("%d", val) + } + } + + if !isValidMuteConfig(fingering) { + continue + } + fingers := countEffectiveFingers(fingering, nStrings) + if fingers > maxFingers { + continue + } + + var soundedPCs []int + frets := make([]int, nStrings) + notes := make([]string, nStrings) + for s := 0; s < nStrings; s++ { + if fingering[s] == "x" { + frets[s] = -1 + notes[s] = "x" + continue + } + fn := atoi(fingering[s]) + frets[s] = fn + midi := tuningMIDI[s] + fn + pc := midi % 12 + soundedPCs = append(soundedPCs, pc) + notes[s] = midiToNoteName(midi) + } + + if len(soundedPCs) < 2 { + continue + } + + uniquePCs := uniqueInts(soundedPCs) + + // try each unique PC as root, match against chord dictionary + type candidate struct { + root int + quality string + size int + bassIdx int + } + var candidates []candidate + for ri, root := range uniquePCs { + ivSet := make(map[int]bool) + for _, pc := range soundedPCs { + ivSet[(pc-root+12)%12] = true + } + for _, cd := range allDefs { + if len(ivSet) != len(cd.intervals) { + continue + } + ok := true + for _, iv := range cd.intervals { + if !ivSet[iv] { + ok = false + break + } + } + if ok { + candidates = append(candidates, candidate{root, cd.quality, len(cd.intervals), ri}) + } + } + } + + if len(candidates) == 0 { + continue + } + + // pick simplest: fewest intervals, bass preference + pick := candidates[0] + for _, c := range candidates[1:] { + if c.size < pick.size || (c.size == pick.size && c.bassIdx < pick.bassIdx) { + pick = c + } + } + + ck := chordKey{pick.root, pick.quality} + existing, exists := best[ck] + if !exists || fingers < existing.Fingers { + fretsCopy := make([]int, nStrings) + copy(fretsCopy, frets) + notesCopy := make([]string, nStrings) + copy(notesCopy, notes) + + best[ck] = DensityChord{ + Chord: SemitoneToNote[pick.root] + " " + pick.quality, + Root: SemitoneToNote[pick.root], + Quality: pick.quality, + Frets: fretsCopy, + Fingers: fingers, + Notes: notesCopy, + } + } + } + + result := make([]DensityChord, 0, len(best)) + for _, dc := range best { + result = append(result, dc) + } + + sort.Slice(result, func(i, j int) bool { + ri := qualityRank(result[i].Quality) + rj := qualityRank(result[j].Quality) + if ri != rj { + return ri < rj + } + return result[i].Fingers < result[j].Fingers + }) + + return result +} + +func qualityRank(q string) int { + switch q { + case "major": + return 0 + case "minor": + return 1 + default: + return 2 + } +} diff --git a/fingerings.go b/fingerings.go index a0818fb..e02ab79 100644 --- a/fingerings.go +++ b/fingerings.go @@ -249,7 +249,7 @@ func findChordFingerings(cfg Config) []ChordResult { if seen[k] { continue } - if countFingers(alt) > maxFingers { + if countEffectiveFingers(alt, len(alt)) > maxFingers { continue } seen[k] = true @@ -279,81 +279,35 @@ func isValidMuteConfig(fingering []string) bool { } func countEffectiveFingers(fingering []string, numStrings int) int { - type fretInfo struct { - fret int - strings []int - } - - frets := make(map[int][]int) + fretStrings := make(map[int][]int) for i, f := range fingering { if f == "x" || f == "0" { continue } fv := atoi(f) - frets[fv] = append(frets[fv], i) - } - - if len(frets) == 0 { - return 0 - } - - type fingerKey struct { - fret int - barre bool - } - used := make(map[fingerKey]bool) - - for fret, strings := range frets { - if len(strings) >= 2 { - start := strings[0] - end := strings[len(strings)-1] - for _, s := range strings { - if s < start { - start = s - } - if s > end { - end = s - } - } - if end-start <= 4 { - valid := true - for i := start; i <= end; i++ { - if fingering[i] == "x" { - continue - } - fv := atoi(fingering[i]) - if fv < fret { - valid = false - break - } - } - if valid { - used[fingerKey{fret, true}] = true - } - } - } - } - - for fret := range frets { - found := false - for k := range used { - if k.fret == fret { - found = true - break - } - } - if !found { - used[fingerKey{fret, false}] = true - } + fretStrings[fv] = append(fretStrings[fv], i) } count := 0 - for k := range used { - if k.barre { - count += 2 - } else { + for fret, strings := range fretStrings { + if len(strings) == 1 { count++ + continue } + groups := 1 + for i := 1; i < len(strings); i++ { + consecutive := true + for s := strings[i-1] + 1; s < strings[i]; s++ { + if fingering[s] == "x" || atoi(fingering[s]) < fret { + consecutive = false + break + } + } + if !consecutive { + groups++ + } + } + count += groups } return count } @@ -439,7 +393,7 @@ func generateMutedVariations(primary []string, tuning []string, intervals map[in if !isValidMuteConfig(test) { return } - if countFingers(test) > maxFingers { + if countEffectiveFingers(test, len(test)) > maxFingers { return } if isSameChord(test, tuning, intervals) { @@ -486,16 +440,6 @@ func isOpenChord(fingering []string) bool { return true } -func countFingers(fingering []string) int { - count := 0 - for _, f := range fingering { - if f != "x" && f != "0" { - count++ - } - } - return count -} - func fingeringKey(f []string) string { return strings.Join(f, ",") } diff --git a/frontend/dist/app.js b/frontend/dist/app.js index 55fcee9..f27ba15 100644 --- a/frontend/dist/app.js +++ b/frontend/dist/app.js @@ -1,3 +1,13 @@ +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'); @@ -12,6 +22,9 @@ document.addEventListener('DOMContentLoaded', () => { 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']; @@ -35,21 +48,149 @@ document.addEventListener('DOMContentLoaded', () => { window.currentTuningMIDI = standardMIDI.slice(); let polySynth = null; - window.playChord = function(midiNotes) { - Tone.start(); - if (!polySynth) { - polySynth = new Tone.PolySynth(Tone.Synth, {maxPolyphony: 8}).toDestination(); - polySynth.set({ - oscillator: {type: 'triangle'}, - envelope: {attack: 0.01, decay: 0.3, sustain: 0.4, release: 1.0} + 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 now = Tone.now(); - midiNotes.forEach((m, i) => { - const freq = 440 * Math.pow(2, (m - 69) / 12); - polySynth.triggerAttackRelease(freq, '1.5s', now + i * 0.03); + 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 = { @@ -69,25 +210,30 @@ document.addEventListener('DOMContentLoaded', () => { 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', () => { - const target = link.dataset.view; - navLinks.forEach(l => l.classList.remove('active')); - views.forEach(v => v.classList.remove('active')); - link.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(); - } + window.switchToView(link.dataset.view); }); }); @@ -135,6 +281,10 @@ document.addEventListener('DOMContentLoaded', () => { 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; @@ -156,14 +306,35 @@ document.addEventListener('DOMContentLoaded', () => { } }); - btnApply.addEventListener('click', () => { - if (!window.go) return; - const cfg = { + 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 + 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...'; @@ -182,7 +353,15 @@ document.addEventListener('DOMContentLoaded', () => { btnSave.addEventListener('click', () => { if (!window.go) return; - window.go.main.App.SaveConfig().then(() => { + 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 => { @@ -201,6 +380,13 @@ document.addEventListener('DOMContentLoaded', () => { }); }); + 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) { @@ -223,6 +409,13 @@ document.addEventListener('DOMContentLoaded', () => { 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); diff --git a/frontend/dist/chords-default.css b/frontend/dist/chords-default.css index a876b7b..3fec743 100644 --- a/frontend/dist/chords-default.css +++ b/frontend/dist/chords-default.css @@ -20,8 +20,8 @@ .fret { border: .5px solid var(--border, #444746); - border-left: 2px solid transparent; - border-right: 2px; + border-left: 2px; + border-right: 2px solid transparent; background: var(--bg-overlay, #282a2c); color: transparent; } diff --git a/frontend/dist/chords-light.css b/frontend/dist/chords-light.css index 1caf030..dea4ef5 100644 --- a/frontend/dist/chords-light.css +++ b/frontend/dist/chords-light.css @@ -24,8 +24,8 @@ .fret { border: .5px solid #0002; - border-left: 2px solid #fff; - border-right: 2px solid #0001; + border-left: 2px solid #0001; + border-right: 2px solid #fff; background: #fff; color: #fff; } diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 27e48c4..379d42a 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -14,6 +14,19 @@