From 9d8fb682aef9801689a9a1470ee01e740ab3fee1 Mon Sep 17 00:00:00 2001 From: Jess Shoes Date: Sun, 30 Mar 2025 08:02:05 -0700 Subject: [PATCH] 1 --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 6 + chords.py | 59 +++++++ config.json | 6 + intervals.py | 75 ++++++++ main.py | 46 +++++ requirements.txt | 2 + template.html | 33 ++++ triad.py | 312 +++++++++++++++++++++++++++++++++ utils.py | 14 ++ www/.DS_Store | Bin 0 -> 6148 bytes www/chords-darcula.css | 113 ++++++++++++ www/chords-default.css | 114 ++++++++++++ www/chords-light.css | 157 +++++++++++++++++ www/chords-solarized.css | 113 ++++++++++++ www/chords-tomorrow-amoled.css | 113 ++++++++++++ www/chords.css | 114 ++++++++++++ www/chords.js | 230 ++++++++++++++++++++++++ www/tuner.css | 86 +++++++++ www/tuner.html | 91 ++++++++++ www/tuner.js | 175 ++++++++++++++++++ 21 files changed, 1859 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 chords.py create mode 100644 config.json create mode 100644 intervals.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 template.html create mode 100644 triad.py create mode 100644 utils.py create mode 100644 www/.DS_Store create mode 100644 www/chords-darcula.css create mode 100644 www/chords-default.css create mode 100644 www/chords-light.css create mode 100644 www/chords-solarized.css create mode 100644 www/chords-tomorrow-amoled.css create mode 100644 www/chords.css create mode 100644 www/chords.js create mode 100644 www/tuner.css create mode 100644 www/tuner.html create mode 100644 www/tuner.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..58fd2413954e992b559e756012db4d488c0abe21 GIT binary patch literal 6148 zcmeHK-AcnS6i&A3GKSC#gbh&QFq7qFriDzjxni?tbRXD`N}*ZM*}iO=IX zNeT{oE#l4@$#;H}=7Z&jF~+^c;#IwcQj=(H8gqlgj-p$QG#=%x}@UQYsEhJqWL(X+Cy# z&sCBIQ8JzDf@m^@l)IZKnW)@Ti)51OTHgdjOSHyLXSwVTPP%e%JY03<^7N<&dVjcD zwZz{3!P({bIetmho1v2f+gf%kmhcXWUCpaMPhyokf~U+bvj~X+Vt^PR1~!`kdoGCf zX462cCkBXt3I=e0kkAlai-keGbwG#LXN)%xQ9#GH1fsO)S}Y7g1caMXKvT-?6N8&_ z@JpNLS}Y8ja>n({FpizMe7tZyJNTteXWTVNJuyHGtTRwIri17I1^i_yANlJgWDx_z zz&~SvH%9)*gGJf1^~dt?td-CnprK%1feHxdYnK2pa3AUDpovS=A', f'{theme_link}') + + os.makedirs("www", exist_ok=True) + with open("www/chords.html", "w") as f: + f.write(html) + print("Rendered HTML report to www/chords.html") + +def main(): + chord.main() + interval.main() + triads.main() + render_chords_html() + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ea7c2a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Jinja2==3.1.6 +MarkupSafe==3.0.2 diff --git a/template.html b/template.html new file mode 100644 index 0000000..197e48d --- /dev/null +++ b/template.html @@ -0,0 +1,33 @@ + + + + + + Chord Fingering Matches + + + + +

Matched Chord Positions

+
+ {% for match in chords %} +
+

{{ match.chord }}

+
+ {% if match.alternatives %} +
+

Alternatives:

+
+ {% for alt_fingering in match.alternatives %} +
+ {% endfor %} +
+
+ {% endif %} +
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/triad.py b/triad.py new file mode 100644 index 0000000..6193ba5 --- /dev/null +++ b/triad.py @@ -0,0 +1,312 @@ +# triads.py +import os +import json +from itertools import product, combinations +from utils import load_config, export_json # Import from utils + +def build_note_map(): + base_notes = ['C', 'C#', 'D', 'D#', 'E', 'F', + 'F#', 'G', 'G#', 'A', 'A#', 'B'] + enharmonic_keys = ['Cb', 'B#', 'Db', 'C##', 'Eb', 'D##', 'Fb', 'E#', 'Gb', 'F##', 'Ab', 'G##', 'Bb', 'A##'] + enharmonic_vals = ['B', 'C', 'C#', 'D', 'D#', 'E', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] + note_map = {} + reverse_note_map = {} + for i, note in enumerate(base_notes): + note_map[note] = i + reverse_note_map[i] = note + for enh, actual in zip(enharmonic_keys, enharmonic_vals): + note_map[enh] = note_map[actual] + return note_map, reverse_note_map + +def load_json(name): + path = os.path.join("generated_data", f"{name}.json") + print(f"Loading JSON from: {path}") + with open(path, "r") as f: + data = json.load(f) + print(f"Loaded data from {name}.json: {data}") + return data + +def count_effective_fingers(fingering, num_strings): + fretted = [(i, int(f)) for i, f in enumerate(fingering) if f not in ("x", "X", "0")] + if not fretted: + return 0 + fingers_used = set() + frets = {} + for idx, fret in fretted: + if fret not in frets: + frets[fret] = [] + frets[fret].append(idx) + + for fret, strings in frets.items(): + if len(strings) >= 2: + start = min(strings) + end = max(strings) + if end - start <= 4: + valid = True + for i in range(start, end + 1): + val = fingering[i] + if val not in ("x", "X"): + try: + if int(val) < fret: + valid = False + break + except ValueError: + valid = False + break + if valid: + fingers_used.add((fret, "barre")) + + for fret, strings in frets.items(): + if fret not in fingers_used: + fingers_used.add(fret) + + return sum(2 if isinstance(f, tuple) and f[1] == "barre" else 1 for f in fingers_used) + +def find_chord_fingerings(config): + chords = load_json("chord_definitions") + results = [] + generated_fingerings = set() + + string_tunings = config.get("tuning", ["G", "C", "E", "A"]) + NUM_STRINGS = len(string_tunings) + MAX_FRET = config.get("frets", 4) + MAX_FINGERS = config.get("max_fingers", 3) + print(f"MAX_FRET: {MAX_FRET}, MAX_FINGERS: {MAX_FINGERS}") + + all_chords = {} + for chord_type, chord_group in chords.items(): # Corrected loop + print(f"Processing chord group: {chord_type}") # Optional debug print + for chord_name_in_group, intervals in chord_group.items(): # Iterate through chords in each group + full_chord_name = f"{chord_name_in_group.capitalize()} {chord_type.capitalize()[:-1]}" # Corrected chord name: "Major Triad" -> "Major" and capitalize names + all_chords[full_chord_name] = { + "intervals": intervals, + "type": chord_type, + "name": chord_name_in_group + } + + print("All chords to search:", all_chords) # Print the FINAL all_chords dictionary + print(f"Using tuning: {config.get('tuning')}") # DEBUG: Print tuning + + note_map, reverse_note_map = build_note_map() + + fret_options_no_x = [str(fret) for fret in range(MAX_FRET + 1)] + ["x"] + + print("Starting chord processing loop...") + for chord_name, chord_data in all_chords.items(): + intervals = chord_data["intervals"] + print(f"\n--- Processing chord: {chord_name}, intervals: {intervals} ---") + interval_set = set(intervals) + print(f" Target interval set (semitones): {interval_set}") # DEBUG + + semitone_intervals_needed = set() # Assuming intervals in chord_definitions are names, convert to semitones + for interval_value in intervals: # Now intervals are already semitones from chord_definitions.json + semitone_intervals_needed.add(interval_value) # Use interval_value directly + + interval_set = semitone_intervals_needed # Now interval_set is in semitones, and correctly populated + + for test_fingering_tuple in product(fret_options_no_x, repeat=NUM_STRINGS): + test_fingering = list(test_fingering_tuple) + + fretted_notes_semitones = [] + for i, fret in enumerate(test_fingering): + if fret not in ("x", "X"): + tuning_note = string_tunings[i].strip() # Changed here + note_semitone = (note_map[tuning_note] + int(fret)) % 12 + fretted_notes_semitones.append(note_semitone) + + def is_valid_mute_config(fingering): + for i, f in enumerate(fingering): + if f in ("x", "X") and i not in (0, len(fingering) - 1): + return False + return True + + if not is_valid_mute_config(test_fingering) or count_effective_fingers(test_fingering, NUM_STRINGS) > MAX_FINGERS: + continue + + unique_fretted_notes = sorted(list(set(fretted_notes_semitones))) # Get unique notes for root check + + if len(unique_fretted_notes) < len(intervals): + continue + + for potential_root_semitone in unique_fretted_notes: # Iterate through unique notes as potential roots + intervals_in_fingering = set() + for note_semitone in fretted_notes_semitones: + interval = (note_semitone - potential_root_semitone) % 12 + intervals_in_fingering.add(interval) + + print(f" Fingering: {test_fingering}, Notes (semitones): {fretted_notes_semitones}, Potential Root: {reverse_note_map.get(potential_root_semitone)}, Intervals in Fingering: {intervals_in_fingering}, Required Intervals: {interval_set}") # ADD THIS + + if intervals_in_fingering == interval_set: # Changed to EXACT MATCH for primary chords + fingering_tuple = tuple(test_fingering) + if fingering_tuple not in generated_fingerings: + root_note_name_for_chord = reverse_note_map.get(potential_root_semitone, str(potential_root_semitone)) # Get root note name for chord name + result_chord_name = f"{root_note_name_for_chord} {chord_name}" # Correctly formatted chord name + result = { + "chord": result_chord_name, # Use the correctly formatted chord name + "fingering": test_fingering, + "intervals": list(interval_set), + "interval_set": interval_set # Added new key for interval set + } + + # if count_effective_fingers(test_fingering) < MAX_FINGERS: + # continue + def detect_barres(fingering): + fretted = [(i, int(f)) for i, f in enumerate(fingering) if f not in ("x", "X", "0")] + if not fretted: + return [] + frets = {} + for idx, fret in fretted: + frets.setdefault(fret, []).append(idx) + barres = [] + for fret, strings in frets.items(): + if len(strings) < 2: + continue + if all( + all(fingering[j] in ("x", "X") or int(fingering[j]) >= fret for j in range(NUM_STRINGS)) + for j in strings + ): + barres.append({"fret": fret, "strings": strings}) + return barres + + result["barres"] = detect_barres(test_fingering) + + results.append(result) + generated_fingerings.add(fingering_tuple) + print(f" Chord FOUND (EXACT MATCH): {result}") # ADD THIS + break # Stop after finding exact match for a root + + + def count_fingers(fingering): + return sum(1 for f in fingering if f not in ("x", "X", "0")) + + def is_same_chord(fingering, chord_name, string_tunings, note_map, intervals): # intervals is interval_set here + print(f" [is_same_chord] Checking fingering: {fingering}, chord_name: {chord_name}, intervals: {intervals}") # DEBUG + fretted = [] + for i, f in enumerate(fingering): + if f not in ("x", "X"): + tuning_note = string_tunings[i].strip() + note = (note_map[tuning_note] + int(f)) % 12 + fretted.append(note) + print(f" [is_same_chord] Fretted notes (semitones): {fretted}") # DEBUG + for root in set(fretted): + interval_set_fingering = set() # rename to avoid confusion + for note in fretted: + interval_set_fingering.add((note - root) % 12) + print(f" [is_same_chord] Potential root: {reverse_note_map.get(root)}, Interval set from fingering: {interval_set_fingering}") # DEBUG + if interval_set_fingering == intervals: # Exact match + print(f" [is_same_chord] EXACT MATCH FOUND for root {reverse_note_map.get(root)}") # DEBUG + return True + print(" [is_same_chord] NO MATCH FOUND") # DEBUG + return False + + def is_open_chord(fingering): + return all(f in ("0", "x", "X") for f in fingering) + + + # Final global filter pass for safety + results = [ + r for r in results + if is_valid_mute_config(r["fingering"]) and count_effective_fingers(r["fingering"], NUM_STRINGS) <= MAX_FINGERS + ] + + # Group results + grouped = {} + for r in results: + chord_key = r["chord"].replace(" (alt)", "") # Group alternatives with primary chords + grouped.setdefault(chord_key, []).append(r["fingering"]) + + final_results = [] + + for chord_name, fingerings in grouped.items(): + checked = set() + primary = None + alternatives = [] + has_fretted_primary = False # Flag to track if a fretted primary has been found + + for fingering in fingerings: + key = tuple(fingering) + if key in checked: + continue + checked.add(key) + + intervals = next((r["intervals"] for r in results if r["fingering"] == list(fingering) and r["chord"] == chord_name), []) + intervals = set(intervals) + is_exact = is_same_chord(fingering, chord_name, string_tunings, note_map, intervals) # Check for exact match + is_open = is_open_chord(fingering) + is_fretted = not is_open # define fretted as not open + + + if is_exact: # Prioritize exact matches + if is_fretted: # If it's a fretted exact match, it's the best primary + if not has_fretted_primary: # if no fretted primary yet, set it. + primary = fingering + has_fretted_primary = True # Mark that we found a fretted primary + elif not has_fretted_primary: # if it's an open exact match and no fretted primary yet, consider it primary for now, but can be replaced + if primary is None: # if no primary yet (and no fretted primary), set open as primary tentatively + primary = fingering + else: # if it's an open chord and we already HAVE a fretted primary, just add as alternative. + alternatives.append(fingering) + + + elif not is_exact: # If not an exact match, consider as alternative if primary is already set (exact match found) or if we have ANY primary set. + if primary is not None: + alternatives.append(fingering) + elif primary is None and not alternatives: # if no primary and no alternatives yet, set as primary if nothing better is found + primary = fingering + else: + alternatives.append(fingering) + + + simpler_versions = [] + if primary is not None and fingering == primary: # Only find simpler versions for the primary fingering + for n in range(1, len(fingering)): + for idxs in combinations(range(len(fingering)), n): + test = fingering[:] + for i in idxs: + test[i] = "x" + if is_valid_mute_config(test) and is_same_chord(test, chord_name, string_tunings, note_map, intervals): # use intervals here + simpler_versions.append(list(test)) # Convert tuple to list + alternatives.extend(simpler_versions) # Add simpler versions to alternatives + + + if primary is None and alternatives: # If no primary after all checks, pick first alternative as primary (shouldn't happen often with exact match priority, but for safety) + primary = alternatives.pop(0) + + if primary is not None: # Ensure we have a primary fingering before adding to final results + final_results.append({ + "chord": chord_name.replace(" Triad", "").replace(" Seventh", "").replace(" Sixth", "").replace(" Ext.", ""), # Clean up chord name + "fingering": primary, + "alternatives": [alt for alt in set(map(tuple, alternatives)) if list(alt) != primary and count_fingers(list(alt)) <= MAX_FINGERS] # Remove duplicates and primary from alternatives and filter by max fingers + }) + + + return final_results + +def generate_chord_positions(): + return find_chord_fingerings(load_config()) + +def convert_to_tab(chord_results): + chord_tabs = [] + for chord in chord_results: + fretted_notes = chord.get('fingering', []) + chord_name = chord.get('chord', 'Unknown') + + tab = list(fretted_notes) + + chord_tabs.append({ + "chord": chord_name, + "tab": tab + }) + +def main(): + config = load_config() + fingerings = find_chord_fingerings(config) + if not fingerings: + print("No fingerings found. Check input data and logic.") + else: + print("Found", len(fingerings), "fingerings.") + export_json(fingerings, "triad_chords") # Use utils.export_json + print(json.dumps(fingerings, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..000d261 --- /dev/null +++ b/utils.py @@ -0,0 +1,14 @@ +# web-tuner/utils.py +import json +import os + +def load_config(path="config.json"): + with open(path) as f: + return json.load(f) + +def export_json(data, name, output_dir="generated_data"): # Added output_dir as parameter with default + os.makedirs(output_dir, exist_ok=True) + path = os.path.join(output_dir, f"{name}.json") + with open(path, "w") as f: + json.dump(data, f, indent=2) + print(f"Exported: {path}") \ No newline at end of file diff --git a/www/.DS_Store b/www/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { + const container = document.getElementById("chord-container"); + + // Create controls + const controls = document.createElement("div"); + controls.style.marginBottom = "1rem"; + + const sortRoot = document.createElement("select"); + sortRoot.innerHTML = ` + + + + `; + + const sortType = document.createElement("select"); + sortType.innerHTML = ` + + + + `; + + const themeSelect = document.createElement("select"); + themeSelect.innerHTML = ` + + + + + + `; + controls.appendChild(themeSelect); + + // Create theme tag if not exists + let themeLink = document.getElementById("theme-stylesheet"); + if (!themeLink) { + themeLink = document.createElement("link"); + themeLink.rel = "stylesheet"; + themeLink.id = "theme-stylesheet"; + document.head.appendChild(themeLink); + } + + themeSelect.addEventListener("change", () => { + const theme = themeSelect.value; + themeLink.href = `chords-${theme}.css`; + }); + + // Load default + themeLink.href = "chords-light.css"; + + controls.appendChild(sortRoot); + controls.appendChild(sortType); + document.body.insertBefore(controls, container); + + function getChordElements() { + return Array.from(container.getElementsByClassName("chord-card")); + } + + function extractChordInfo(el) { + const title = el.querySelector("h2").textContent.trim(); + const [root, ...typeParts] = title.split(" "); + return { + root, + type: typeParts.join(" "), + element: el + }; + } + + function sortAndRender(by = "root", order = "asc") { + const chords = getChordElements().map(extractChordInfo); + chords.sort((a, b) => { + const valA = by === "root" ? a.root : a.type; + const valB = by === "root" ? b.root : b.type; + return order === "asc" + ? valA.localeCompare(valB) + : valB.localeCompare(valA); + }); + + chords.forEach(({ element }) => { + element.style.transition = 'transform 0.2s ease, opacity 0.3s ease'; + element.style.opacity = '0.9'; + element.style.transform = 'scale(1.01)'; + container.appendChild(element); + setTimeout(() => { + element.style.transform = 'scale(1.0)'; + element.style.opacity = '1.0'; + }, 300); + }); + } + + sortRoot.addEventListener("change", () => { + if (sortRoot.value) sortAndRender("root", sortRoot.value); + }); + + sortType.addEventListener("change", () => { + if (sortType.value) sortAndRender("type", sortType.value); + }); + + function renderFretboards() { + const fretboards = document.querySelectorAll(".fretboard"); + fretboards.forEach(fb => { + const fingering = JSON.parse(fb.dataset.fingering); + const maxFret = parseInt(document.getElementById("chord-container").dataset.maxFret, 10); + const numStrings = parseInt(document.getElementById("chord-container").dataset.numStrings, 10); + + const wrapper = document.createElement("div"); + wrapper.className = "fretboard"; + + fb.innerHTML = ""; // Clear existing content + fb.style.display = "inline-block"; + fb.style.marginBottom = "1rem"; + + const fretMatrix = []; + const fretCounts = {}; + fingering.forEach(f => { + if (!isNaN(f)) { + fretCounts[f] = (fretCounts[f] || 0) + 1; + } + }); + + const entries = Object.entries(fretCounts) + .filter(([fret, count]) => count >= 2) + .map(([f, c]) => parseInt(f)); + + let barreFretNum = null; + for (const f of entries.sort((a, b) => a - b)) { + const allBefore = fingering.every(x => x === "x" || isNaN(x) || parseInt(x) >= f); + if (allBefore) { + barreFretNum = f; + break; + } + } + + for (let s = 0; s < numStrings; s++) { + const stringRow = []; + + for (let f = 1; f <= maxFret; f++) { + const fret = document.createElement("div"); + fret.className = "fret"; + fret.dataset.row = s; + fret.dataset.col = f; + + const fretValue = fingering[s]; + const numericFret = parseInt(fretValue, 10); + if (fretValue === "x" && f === 1) { + fret.setAttribute('muted', ''); + fret.textContent = "x"; + } else if (fretValue !== "x" && numericFret === f) { + fret.dataset.dot = "true"; + if (barreFretNum !== null && numericFret === barreFretNum) { + fret.classList.add("barre"); + } + } + + stringRow.push(fret); + } + fretMatrix.push(stringRow); + } + + const barreCols = []; + if (barreFretNum !== null) { + for (let s = 0; s < numStrings; s++) { + if (parseInt(fingering[s]) === barreFretNum) { + barreCols.push(s); + } + } + } + + for (let s = numStrings - 1; s >= 0; s--) { + const stringRow = document.createElement("div"); + stringRow.className = "fret-row"; + for (let f = 1; f <= maxFret; f++) { + const fret = fretMatrix[s][f - 1]; + if (fret) { + stringRow.appendChild(fret); + } + } + wrapper.appendChild(stringRow); + } + + fb.appendChild(wrapper); // Append before computing barre line + + if (barreFretNum !== null && barreCols.length >= 2) { + const start = Math.min(...barreCols); + const end = Math.max(...barreCols); + + const line = document.createElement("div"); + line.className = "barre-line"; + + requestAnimationFrame(() => { + let totalDotCenter = 0; + let dotCount = 0; + + for (let s = start; s <= end; s++) { // Iterate through all barre strings + const dotFret = fretMatrix[s][barreFretNum - 1]; + if (dotFret) { // Ensure fret exists (should always exist in barre scenario) + const rect = dotFret.getBoundingClientRect(); + totalDotCenter += (rect.left + rect.right) / 2; + dotCount++; + } + } + const avgDotCenter = totalDotCenter / dotCount; + const parentRect = wrapper.getBoundingClientRect(); + const dotCenter = avgDotCenter - parentRect.left; + + + const firstDot = fretMatrix[start][barreFretNum - 1]; + const lastDot = fretMatrix[end][barreFretNum - 1]; + const rect1 = firstDot.getBoundingClientRect(); + const rect2 = lastDot.getBoundingClientRect(); + + const top = Math.min(rect1.top, rect2.top) - parentRect.top; + const bottom = Math.max(rect1.bottom, rect2.bottom) - parentRect.top; + const height = bottom - top; + + + line.style.top = `${Math.round(top)}px`; + line.style.height = `${Math.round(height)}px`; + line.style.left = `${Math.round(dotCenter)}px`; + + + // Add finger number (1) + line.textContent = "|"; + + wrapper.appendChild(line); + }); + } + }); + } + + renderFretboards(); +}); \ No newline at end of file diff --git a/www/tuner.css b/www/tuner.css new file mode 100644 index 0000000..e39ce42 --- /dev/null +++ b/www/tuner.css @@ -0,0 +1,86 @@ +body { + font-family: sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: #f4f4f4; +} + +.container { + background-color: #fff; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + text-align: center; +} + +h1 { + margin-bottom: 20px; +} + +.tuning-controls { + display: flex; + gap: 20px; + margin-bottom: 20px; + justify-content: center; +} + +.tuning-controls > div { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.tuning-controls label { + margin-bottom: 5px; +} + +.tuning-controls input, .tuning-controls select { + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.strings { + display: flex; + gap: 10px; + margin-bottom: 20px; + justify-content: center; +} + +.string-button { + padding: 15px 25px; + font-size: 1.2em; + border: none; + border-radius: 5px; + background-color: #4CAF50; /* Green */ + color: white; + cursor: pointer; + transition: background-color 0.3s; +} + +.string-button:hover { + background-color: #45a049; +} + +#play-all { + padding: 12px 20px; + font-size: 1em; + border: none; + border-radius: 5px; + background-color: #008CBA; /* Blue */ + color: white; + cursor: pointer; + transition: background-color 0.3s; +} + +#play-all:hover { + background-color: #0077B3; +} + +#output { + margin-top: 20px; + font-weight: bold; +} \ No newline at end of file diff --git a/www/tuner.html b/www/tuner.html new file mode 100644 index 0000000..0e06912 --- /dev/null +++ b/www/tuner.html @@ -0,0 +1,91 @@ + + + + Stringed Instrument Tuner + + + +
+

Ukulele Tuner

+ +
+ + +
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/www/tuner.js b/www/tuner.js new file mode 100644 index 0000000..4c062b4 --- /dev/null +++ b/www/tuner.js @@ -0,0 +1,175 @@ +document.addEventListener('DOMContentLoaded', () => { + // Initialize Tone.js + Tone.start(); // Ensure audio context starts on user interaction + + const synth = new Tone.Synth().toDestination(); + + const a440Input = document.getElementById('a440'); + const transposeInput = document.getElementById('transpose'); + const instrumentSelect = document.getElementById('instrument'); + const tuningSelect = document.getElementById('tuning'); + const tuningModeSelect = document.getElementById('tuning-mode'); // New tuning mode selector + const stringsDiv = document.getElementById('strings'); + const playAllButton = document.getElementById('play-all'); + const outputDiv = document.getElementById('output'); + + // Expanded tunings for multiple instruments + const instrumentTunings = { + "ukulele": { + "standard": [67 + 12, 60 + 12, 64 + 12, 69 + 12], // G5, C5, E5, A5 + "low-g": [55 + 12, 60 + 12, 64 + 12, 69 + 12], // G4, C5, E5, A5 + "harmonic-minor": [67 + 12, 58 + 12, 62 + 12, 67 + 12], // G5, Bb5, D5, G5 + "suspended-fourth": [67 + 12, 60 + 12, 53 + 12, 60 + 12], // G5, C5, F5, C5 + "lydian": [67 + 12, 60 + 12, 64 + 12, 66 + 12], // G5, C5, E5, F#5 + "diminished": [67 + 12, 59 + 12, 62 + 12, 65 + 12], // G5, B5, D5, F5 + "augmented": [67 + 12, 61 + 12, 64 + 12, 68 + 12], // G5, C#5, E5, G#5 + "open-fifths": [67 + 12, 62 + 12, 69 + 12, 62 + 12], // G5, D5, A5, D5 + "double-unison": [67 + 12, 67 + 12, 60 + 12, 60 + 12], // G5, G5, C5, C5 + "ionian": [67 + 12, 60 + 12, 64 + 12, 69 + 12], // G C E A + "dorian": [67 + 12, 58 + 12, 62 + 12, 69 + 12], // G Bb D A + "mixo-dorian": [65 + 12, 58 + 12, 67 + 12, 69 + 12], // F A# G A + "phrygian": [67 + 12, 56 + 12, 62 + 12, 69 + 12], // G Ab D A + "mixolydian": [67 + 12, 60 + 12, 62 + 12, 69 + 12], // G C D A + "aeolian": [67 + 12, 58 + 12, 62 + 12, 67 + 12], // G Bb D G + "locrian": [67 + 12, 56 + 12, 60 + 12, 67 + 12] // G Ab C G + }, + "guitar": { + "standard": [40, 45, 50, 55, 59, 64], // EADGBE + "drop-d": [38, 45, 50, 55, 59, 64], // DADGBE + "dadgad": [38, 45, 50, 55, 57, 64], // DADGAD + "open-g": [38, 43, 47, 50, 55, 59], // DGDGBD + "open-d": [38, 43, 50, 54, 57, 64], // DADF#AD + "open-c": [36, 40, 43, 48, 52, 57], // CGCGCE + "half-step-down": [39, 43, 48, 52, 55, 60], // Eb Ab Db Gb Bb Eb + "full-step-down": [38, 43, 48, 53, 57, 62], // D G C F A D + "double-drop-d": [38, 43, 48, 50, 55, 59], // DADGBD + "new-standard": [36, 40, 45, 50, 54, 59], // CGDAEG + "nashville-high-strung": [40, 45, 50, 55, 59, 64], // EADGBE but with lighter strings + "orkney": [36, 40, 43, 36, 40, 43], // CGDGCD + "modal-tuning-1": [40, 45, 39, 50, 45, 64], // CGDGBE + "modal-tuning-2": [40, 45, 37, 50, 45, 64] // EAEAC#E + } + }; + let currentTuning = []; + let currentA440 = 440; + let currentTranspose = 0; + let tuningMode = "equal"; // Default tuning mode + + const harmonicFrequencyRatios = { + "C": 1.0, + "Db": 17/16, + "D": 9/8, + "Eb": 19/16, + "E": 5/4, + "F": 21/16, + "Gb": 11/8, + "G": 3/2, + "Ab": 13/8, + "A": 5/3, + "Bb": 7/4, + "B": 15/8, + "C_octave": 2.0 + }; + + function updateInstrument() { + const selectedInstrument = instrumentSelect.value; + tuningSelect.innerHTML = ""; // Clear previous tuning options + Object.keys(instrumentTunings[selectedInstrument]).forEach(tuning => { + let option = document.createElement("option"); + option.value = tuning; + option.textContent = tuning.replace(/-/g, " ").toUpperCase(); + tuningSelect.appendChild(option); + }); + updateTuning(); // Apply default tuning for new instrument + } + + function updateTuning() { + const selectedInstrument = instrumentSelect.value; + const selectedTuning = tuningSelect.value; + currentTuning = instrumentTunings[selectedInstrument][selectedTuning]; + updateStringButtons(); // Update button labels when tuning changes + } + + function updateStringButtons() { + stringsDiv.innerHTML = ""; // Clear existing buttons + currentTuning.forEach((midiNote, index) => { + let button = document.createElement("button"); + button.classList.add("string-button"); + button.textContent = `String ${index + 1}`; + button.addEventListener("click", () => playNote(midiNote)); + stringsDiv.appendChild(button); + }); + } + + function calculateFrequency(midiNote, tuningMode) { + const referenceNote = 60; // Middle C in MIDI + const referenceFreq = currentA440 * Math.pow(2, (referenceNote - 69) / 12); // Middle C in A440 + + if (tuningMode === "harmonic") { + const noteNames = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]; + const octave = Math.floor(midiNote / 12) - 5; + const note = noteNames[midiNote % 12]; + return referenceFreq * harmonicFrequencyRatios[note] * Math.pow(2, octave); + } else { + return 440 * Math.pow(2, (midiNote - 69) / 12); + } + } + + function playNote(midiNote) { + const a440Value = parseFloat(a440Input.value); + const transposeValue = parseInt(transposeInput.value); + + if (!isNaN(a440Value)) { + currentA440 = a440Value; + } + if (!isNaN(transposeValue)) { + currentTranspose = transposeValue; + } + + const adjustedMidiNote = midiNote + currentTranspose; + const frequency = calculateFrequency(adjustedMidiNote, tuningMode); + + // Adjust frequency based on A440 reference (simplified - for precise tuning, more complex calculations needed) + const referenceFrequencyRatio = currentA440 / 440; + const adjustedFrequency = frequency * referenceFrequencyRatio; + + synth.set({ oscillator: { type: 'sine' } }); + synth.triggerAttackRelease(adjustedFrequency, "1.75s"); // Play for 2 seconds duration + + // **DEBUGGING OUTPUTS ADDED HERE** + console.log("MIDI Note (input):", midiNote); + console.log("Adjusted MIDI Note (transpose applied):", adjustedMidiNote); + console.log("Calculated Frequency (before A440 adjust):", frequency); + console.log("Final Frequency (A440 adjusted):", adjustedFrequency); + + outputDiv.textContent = `Playing: ${Tone.Frequency(adjustedFrequency).toNote()} (Freq: ${adjustedFrequency.toFixed(2)} Hz, A4 Ref: ${currentA440} Hz, Transpose: ${currentTranspose} semitones)`; + } + + playAllButton.addEventListener('click', () => { + let delay = 0; + Tone.Transport.stop(); + Tone.Transport.cancel(); // Clear all scheduled events + currentTuning.forEach((midiNote, index) => { + Tone.Transport.scheduleOnce(time => { + playNote(midiNote); + }, `+${delay}`); // Small delay between notes + delay += 0.2; // Increase delay for each note + }); + Tone.Transport.start(); // Start Tone.Transport + }); + + instrumentSelect.addEventListener('change', updateInstrument); + tuningSelect.addEventListener('change', updateTuning); + tuningModeSelect.addEventListener('change', () => { + tuningMode = tuningModeSelect.value; // Update tuning mode based on user selection + }); + updateInstrument(); // Initialize instrument and tuning on page load + + a440Input.addEventListener('change', () => { + outputDiv.textContent = `A4 Reference set to ${a440Input.value} Hz`; + }); + + transposeInput.addEventListener('change', () => { + outputDiv.textContent = `Transpose set to ${transposeInput.value} semitones`; + }); +}); \ No newline at end of file