commit 9d8fb682aef9801689a9a1470ee01e740ab3fee1 Author: Jess Shoes Date: Sun Mar 30 08:02:05 2025 -0700 1 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..58fd241 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72b6d5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config2.json +output.txt +generated_data/ +venv/ +__pycache__/ +www/chords.html diff --git a/chords.py b/chords.py new file mode 100644 index 0000000..d8f14be --- /dev/null +++ b/chords.py @@ -0,0 +1,59 @@ +import json +import os + +def load_config(path="config.json"): + """Load configuration from a JSON file.""" + with open(path, "r") as f: + return json.load(f) + +OUTPUT_DIR = "generated_data" +os.makedirs(OUTPUT_DIR, exist_ok=True) + +def export_json(data, name): + """Helper function to export a dictionary to a JSON file.""" + 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}") + +def generate_chord_definitions(config=None): + """Generate chord definitions for triads, 7ths, 6ths, and extended chords.""" + chords = { + "triads": { + "major": [0, 4, 7], + "minor": [0, 3, 7], + "diminished": [0, 3, 6] + }, + "sevenths": { + "maj7": [0, 4, 7, 11], + "min7": [0, 3, 7, 10], + "dom7": [0, 4, 7, 10], + "m7b5": [0, 3, 6, 10] + }, + "sixths": { + "major6": [0, 4, 7, 9], + "minor6": [0, 3, 7, 9], + "dim": [0, 3, 6, 9], + "6_9": [0, 2, 4, 7, 9] + }, + "ext": { + "maj9": [0, 2, 4, 7, 11], + "sus2": [0, 2, 7], + "sus4": [0, 5, 7], + "majmin7": [0, 4, 7, 10], + "augmented": [0, 4, 8], + "dim7": [0, 3, 6, 9], + "#11": [0, 4, 6, 11], + "5maj9": [0, 2, 7], + "5maj7_9": [0, 2, 7, 11] + } + } + export_json(chords, "chord_definitions") + return chords + +def main(): + config = load_config() + generate_chord_definitions(config) + +if __name__ == "__main__": + main() diff --git a/config.json b/config.json new file mode 100644 index 0000000..007e81f --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "instrument": "guitar", + "tuning": ["E","A","D","G","B","E"], + "frets": 4, + "max_fingers": 4 +} diff --git a/intervals.py b/intervals.py new file mode 100644 index 0000000..0a959fe --- /dev/null +++ b/intervals.py @@ -0,0 +1,75 @@ +import os +import json +from itertools import combinations, product +from triad import build_note_map + +NOTE_INDEX, _ = build_note_map() + +def load_config(path="config.json"): + print(f"Loading config from: {path}") # DEBUG + with open(path, "r") as f: + config = json.load(f) + print(f"Loaded config: {config}") # DEBUG + return config + +def interval_name(semitones): + interval_map = { + 0: "P1", 1: "m2", 2: "M2", 3: "m3", 4: "M3", 5: "P4", + 6: "TT", 7: "P5", 8: "m6", 9: "M6", 10: "m7", 11: "M7" + } + return interval_map.get(semitones % 12, f"+{semitones}") + +def export_json(data, name): + output_dir = "generated_data" + 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}") + +def generate_interval_pairs(config): + tuning = config["tuning"] + max_frets = config.get("frets") + num_strings = len(tuning) + + interval_data = [] + pair_count_before_filter = 0 + + print(f"Number of strings (num_strings): {num_strings}") # DEBUG: Print num_strings value + + for size in range(2, num_strings + 1): # Support chord shapes of size 2 to full string count + for string_group in combinations(range(num_strings), size): + print(f"String group: {string_group}") # DEBUG + for fret_group in product(range(6), repeat=size): + print(f" Fret group: {fret_group}") # DEBUG + pair_count_before_filter += 1 + + pairwise_intervals = [] + for i in range(size): + for j in range(i + 1, size): + s_i, s_j = string_group[i], string_group[j] + f_i, f_j = fret_group[i], fret_group[j] + semitones = (NOTE_INDEX[tuning[s_j]] + f_j - NOTE_INDEX[tuning[s_i]] - f_i) % 12 + pairwise_intervals.append({ + "strings": [s_i, s_j], + "name": interval_name(semitones), + "semitones": semitones + }) + + interval_data.append({ + "string_group": list(string_group), + "fret_positions": list(fret_group), + "intervals": pairwise_intervals + }) + + export_json(interval_data, "interval_triads") + print(f"Generated {len(interval_data)} interval triads with max_frets={max_frets} and {num_strings} strings.") # Changed print message + print(f"Total pairs generated before shape filter: {pair_count_before_filter}") # Renamed print message + return interval_data + +def main(): + config = load_config() + generate_interval_pairs(config) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..216391a --- /dev/null +++ b/main.py @@ -0,0 +1,46 @@ +import chords as chord +import intervals as interval +import triad as triads +from utils import load_config +import json +from jinja2 import Template +import os + +def render_chords_html(): + config = load_config() + max_fret = config.get("frets", 4) + num_strings = len(config.get("tuning", ["G", "C", "E", "A"])) + + with open("generated_data/triad_chords.json") as f: + matched_chords = json.load(f) + + filtered_chords = [] + for match in matched_chords: + if "fingering" in match: + match["fret_positions"] = match["fingering"] + filtered_chords.append(match) + else: + print(f"Warning: Skipped match without fret_positions: {match}") + + with open("template.html") as f: + template = Template(f.read()) + + html = template.render(chords=filtered_chords, max_fret=max_fret, num_strings=num_strings, config=config) + + # Inject theme stylesheet link + theme_link = '' + html = html.replace('', 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 0000000..5008ddf Binary files /dev/null and b/www/.DS_Store differ diff --git a/www/chords-darcula.css b/www/chords-darcula.css new file mode 100644 index 0000000..2c3c8f3 --- /dev/null +++ b/www/chords-darcula.css @@ -0,0 +1,113 @@ +body { + font-family: "Work Sans", sans-serif; + background-color: #383838; /* Darcula background */ + color: #a9b7c6; /* Darcula text color */ + padding: 1rem; + line-height: 1.5; +} + +#chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.chord-card { + border: 1px solid #555; /* Darcula card border */ + border-radius: 8px; + padding: 1rem; + background-color: #2b2b2b; /* Darker card background */ + width: max-content; +} + +.chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #cdd3de; /* Darcula heading color */ +} + +.fretboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-left: .5rem; + border: .5px; + background-color: #515151; /* Darcula fretboard background */ +} + +.fretboard .fret { + background-color: #333!important; /* Darcula fret color */ +} + +.fret-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + gap: 2px; +} + +.fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #666; /* Darcula fret border */ + border-left: 2px solid #555; + border-right: 2px solid #555; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #454545; /* Darcula fret */ + position: relative; + color: transparent; +} + +.fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #f2777a; /* Darcula red accent */ + border: 1px solid #d95a5a; + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(242, 119, 122, 0.7); + border-color: #d95a5a; +} + +.fret:empty { + background-color: #454545; /* Darcula empty fret */ +} + +.alternatives { + margin-top: 1rem; +} + +.alternatives h3 { + color: #a9b7c6; /* Darcula alternative heading */ + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.alternative-fretboard { + margin-inline: 1.8rem; +} + +.alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; +} \ No newline at end of file diff --git a/www/chords-default.css b/www/chords-default.css new file mode 100644 index 0000000..39ee2f4 --- /dev/null +++ b/www/chords-default.css @@ -0,0 +1,114 @@ +body { + font-family: "Work Sans", sans-serif; + background-color: #121212; + color: #eec4e7; + padding: 1rem; + line-height: 1.5; + } + + #chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + } + + .chord-card { + border: 1px solid rgb(151, 16, 137); + border-radius: 8px; + padding: 1rem; + background-color: #1a1a1a; + width: max-content; + } + + .chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #ffd9f0; + } + + .fretboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-left: .5rem; + border: .5px; + background-color: #56208bbe; + } + + .fretboard .fret { + background-color: #130404!important; + } + + .fret-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + gap: 2px; + } + + .fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #48b9debd; + border-left: 2px solid #55555500; + border-right: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #1e1e1e; + + position: relative; + color: transparent; + } + + .fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #ff6b6b; + border: 1px solid white; + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color:rgba(255, 0, 0, 0.346); + border-color:rgb(255, 6, 230) + } + + .fret:empty { + background-color: #1e1e1e; + } + + .alternatives { + margin-top: 1rem; + } + + .alternatives h3 { + color: #ffffff; + font-size: 1rem; + margin-bottom: 0.5rem; + } + + .alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; + } + + .alternative-fretboard { + margin-inline: 1.8rem; + } + + .alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; + } \ No newline at end of file diff --git a/www/chords-light.css b/www/chords-light.css new file mode 100644 index 0000000..f9bad9e --- /dev/null +++ b/www/chords-light.css @@ -0,0 +1,157 @@ +.body { + font-family: "Work Sans", sans-serif; + background-color: #f9f9f9; /* Light background */ + color: #333; /* Dark text */ + padding: 1rem; + line-height: 1.5; +} + +#chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.chord-card { + border: 1px solid #ccc; /* Light border */ + border-radius: 8px; + padding: 1rem; + background-color: #fff; /* White card background */ + width: max-content; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Subtle shadow for depth */ +} + +.chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #222; /* Darker heading text */ +} + +.fretboard { + display: flex; + flex-direction: column; + align-items: flex-start; /* Align items to the start to prevent extra width */ + gap: 0px; + margin-left: .5rem; + border: .1px solid #000000; /* Light fretboard border */ + background-color: #c53737d1; /* Light fretboard background */ + position: relative; + width: fit-content; /* Fit content to prevent extra width */ +} + +.fretboard .fret { + background-color: #ffffff!important; /* Light fret background */ +} + +.fret-row { + display: flex; + flex-direction: row; + justify-content: flex-start; /* Align frets to the start */ + align-items: stretch; + gap: 0px; +} + +.fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #000000; + border-left: 2px solid #ffffff; + border-right: 2px solid #000000; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #fff; + color: #fff; /* Set to same as background for invisibility */ + position: relative; /* Needed for dot positioning */ +} + +.fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #1dc9fe; + border: 1px solid #0a02a4; + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.fret:empty { + background-color: #fff; /* White empty fret */ +} + +.alternatives { + margin-top: 1rem; +} + +.alternatives h3 { + color: #555; /* Slightly darker alternative heading */ + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.alternative-fretboard { + margin-inline: 1.8rem; +} + +.alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; +} + +.fret.barre { + /* remove background and border color overrides */ +} + +.fret.barre::before { + content: none; /* remove overlay effect */ +} + +.fret.barre::after { + background-color: transparent; + border: 1px solid #360148; + z-index: 1; +} + +.barre-line { + position: absolute; + background-color: #000; + border-radius: 5rem; + pointer-events: none; + z-index: 2; + width: 1rem; + left: 0; /* Barre line starts from the left edge of the column */ + /* right: 0; Remove right: 0 to allow left positioning to control */ + margin: 0; /* Remove auto margins */ + display: flex; + justify-content: center; + align-items: center; + font-size: 0.7rem; + color: white; + font-family: sans-serif; + font-weight: bold; + transform: translateX(calc(50% - 1.1rem)); /* Nudge barre line to center, adjust as needed */ +} + +.fret[muted] { + color: #000; + font-weight: bold; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/www/chords-solarized.css b/www/chords-solarized.css new file mode 100644 index 0000000..7d96ce8 --- /dev/null +++ b/www/chords-solarized.css @@ -0,0 +1,113 @@ +body { + font-family: "Work Sans", sans-serif; + background-color: #002b36; /* Solarized Dark base03 */ + color: #839496; /* Solarized Dark base0 */ + padding: 1rem; + line-height: 1.5; + } + + #chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + } + + .chord-card { + border: 1px solid #073642; /* Solarized Dark base02 */ + border-radius: 8px; + padding: 1rem; + background-color: #002b36; /* Solarized Dark base03 - same as body for a subtle card */ + width: max-content; + } + + .chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #b58900; /* Solarized Dark yellow */ + } + + .fretboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-left: .5rem; + border: .5px; + background-color: #073642; /* Solarized Dark base02 */ + } + + .fretboard .fret { + background-color: #002b36!important; /* Solarized Dark base03 - same as body */ + } + + .fret-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + gap: 2px; + } + + .fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #586e75; /* Solarized Dark base01 */ + border-left: 2px solid #073642; /* Solarized Dark base02 */ + border-right: 2px solid #073642; /* Solarized Dark base02 */ + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #073642; /* Solarized Dark base02 */ + position: relative; + color: transparent; + } + + .fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #dc322f; /* Solarized Dark red */ + border: 1px solid #cb4b16; /* Solarized Dark orange border for red dot */ + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(220, 50, 47, 0.7); + border-color: #cb4b16; + } + + .fret:empty { + background-color: #073642; /* Solarized Dark base02 */ + } + + .alternatives { + margin-top: 1rem; + } + + .alternatives h3 { + color: #839496; /* Solarized Dark base0 */ + font-size: 1rem; + margin-bottom: 0.5rem; + } + + .alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; + } + + .alternative-fretboard { + margin-inline: 1.8rem; + } + + .alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; + } \ No newline at end of file diff --git a/www/chords-tomorrow-amoled.css b/www/chords-tomorrow-amoled.css new file mode 100644 index 0000000..b652d8c --- /dev/null +++ b/www/chords-tomorrow-amoled.css @@ -0,0 +1,113 @@ +body { + font-family: "Work Sans", sans-serif; + background-color: #000000; /* Tomorrow AMOLED background - pure black */ + color: #ffffff; /* White text for contrast */ + padding: 1rem; + line-height: 1.5; +} + +#chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.chord-card { + border: 1px solid #2a2a29; /* Very dark border, almost invisible */ + border-radius: 8px; + padding: 1rem; + background-color: #111111; /* Very dark card background */ + width: max-content; +} + +.chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #ffffff; /* White heading */ +} + +.fretboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-left: .5rem; + border: .5px; + background-color: #222222; /* Dark fretboard background */ +} + +.fretboard .fret { + background-color: #111111!important; /* Very dark fret */ +} + +.fret-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + gap: 2px; +} + +.fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #333333; /* Dark fret border */ + border-left: 2px solid #222222; + border-right: 2px solid #222222; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #2a2a2a; /* Darker fret */ + position: relative; + color: transparent; +} + +.fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #ffcd56; /* Tomorrow Yellow - bright accent */ + border: 1px solid #f7bb33; + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(255, 205, 86, 0.7); + border-color: #f7bb33; +} + +.fret:empty { + background-color: #2a2a2a; /* Darker empty fret */ +} + +.alternatives { + margin-top: 1rem; +} + +.alternatives h3 { + color: #ffffff; /* White alternative heading */ + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.alternative-fretboard { + margin-inline: 1.8rem; +} + +.alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; +} \ No newline at end of file diff --git a/www/chords.css b/www/chords.css new file mode 100644 index 0000000..9552709 --- /dev/null +++ b/www/chords.css @@ -0,0 +1,114 @@ +body { + font-family: "Work Sans", sans-serif; + background-color: #121212; + color: #eec4e7; + padding: 1rem; + line-height: 1.5; +} + +#chord-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.chord-card { + border: 1px solid rgb(151, 16, 137); + border-radius: 8px; + padding: 1rem; + background-color: #1a1a1a; + width: max-content; +} + +.chord-card h2 { + margin-top: 0; + font-size: 1.2rem; + color: #ffd9f0; +} + +.fretboard { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-left: .5rem; + border: .5px; + background-color: #56208bbe; +} + +.fretboard .fret { + background-color: #130404!important; +} + +.fret-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + gap: 2px; +} + +.fret { + width: 2.1rem; + height: 1.6rem; + box-sizing: border-box; + border: .5px solid #48b9debd; + border-left: 2px solid #55555500; + border-right: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + text-align: center; + line-height: 1.6rem; + font-weight: bold; + background-color: #1e1e1e; + + position: relative; + color: transparent; +} + +.fret[data-dot]::after { + content: ''; + width: 0.55rem; + height: 0.55rem; + background-color: #ff6b6b; + border: 1px solid white; + border-radius: 50%; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color:rgba(255, 0, 0, 0.346); + border-color:rgb(255, 6, 230) +} + +.fret:empty { + background-color: #1e1e1e; +} + +.alternatives { + margin-top: 1rem; +} + +.alternatives h3 { + color: #ffffff; + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.alternatives-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.alternative-fretboard { + margin-inline: 1.8rem; +} + +.alternative-fretboard .fret { + width: 1.8rem; + height: 1.3rem; +} \ No newline at end of file diff --git a/www/chords.js b/www/chords.js new file mode 100644 index 0000000..89dfc87 --- /dev/null +++ b/www/chords.js @@ -0,0 +1,230 @@ +document.addEventListener("DOMContentLoaded", () => { + 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