This commit is contained in:
commit
9d8fb682ae
|
|
@ -0,0 +1,6 @@
|
||||||
|
config2.json
|
||||||
|
output.txt
|
||||||
|
generated_data/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
www/chords.html
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"instrument": "guitar",
|
||||||
|
"tuning": ["E","A","D","G","B","E"],
|
||||||
|
"frets": 4,
|
||||||
|
"max_fingers": 4
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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 = '<link id="theme-stylesheet" rel="stylesheet" href="chords-default.css">'
|
||||||
|
html = html.replace('<head>', f'<head>{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()
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Chord Fingering Matches</title>
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;500;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Matched Chord Positions</h1>
|
||||||
|
<div id="chord-container" data-max-fret="{{ max_fret }}" data-num-strings="{{ num_strings }}">
|
||||||
|
{% for match in chords %}
|
||||||
|
<div class="chord-card">
|
||||||
|
<h2>{{ match.chord }}</h2>
|
||||||
|
<div class="fretboard" data-chord="{{ match.chord }}" data-fingering='{{ match.fingering | tojson }}'></div>
|
||||||
|
{% if match.alternatives %}
|
||||||
|
<div class="alternatives">
|
||||||
|
<h3>Alternatives:</h3>
|
||||||
|
<div class="alternatives-container">
|
||||||
|
{% for alt_fingering in match.alternatives %}
|
||||||
|
<div class="fretboard alternative-fretboard" data-chord="{{ match.chord }} (Alternative)" data-fingering='{{ alt_fingering | tojson }}'></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<!-- JavaScript will load config.json to determine string count and tunings, and render fretboards dynamically -->
|
||||||
|
<script src="chords.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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}")
|
||||||
Binary file not shown.
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 = `
|
||||||
|
<option value="">Sort by Root</option>
|
||||||
|
<option value="asc">Root A-Z</option>
|
||||||
|
<option value="desc">Root Z-A</option>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sortType = document.createElement("select");
|
||||||
|
sortType.innerHTML = `
|
||||||
|
<option value="">Sort by Type</option>
|
||||||
|
<option value="asc">Type A-Z</option>
|
||||||
|
<option value="desc">Type Z-A</option>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const themeSelect = document.createElement("select");
|
||||||
|
themeSelect.innerHTML = `
|
||||||
|
<option value="light">🌞 Light</option>
|
||||||
|
<option value="default">🎸 Default Theme</option>
|
||||||
|
<option value="solarized">🌅 Solarized</option>
|
||||||
|
<option value="tomorrow-amoled">🌌 Tomorrow AMOLED</option>
|
||||||
|
<option value="darcula">🧛 Darcula</option>
|
||||||
|
`;
|
||||||
|
controls.appendChild(themeSelect);
|
||||||
|
|
||||||
|
// Create theme <link> 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();
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Stringed Instrument Tuner</title>
|
||||||
|
<link rel="stylesheet" href="tuner.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Ukulele Tuner</h1>
|
||||||
|
|
||||||
|
<div class="instrument-select">
|
||||||
|
<label for="instrument">Select Instrument:</label>
|
||||||
|
<select id="instrument">
|
||||||
|
<option value="ukulele">Ukulele</option>
|
||||||
|
<option value="guitar">Guitar</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tuning-controls">
|
||||||
|
<div class="a440-control">
|
||||||
|
<label for="a440">A4 Reference (Hz):</label>
|
||||||
|
<input type="number" id="a440" value="440">
|
||||||
|
</div>
|
||||||
|
<div class="transpose-control">
|
||||||
|
<label for="transpose">Transpose (Semitones):</label>
|
||||||
|
<input type="number" id="transpose" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tuning-mode-select">
|
||||||
|
<label for="tuning-mode">Tuning Mode:</label>
|
||||||
|
<select id="tuning-mode">
|
||||||
|
<option value="equal">Equal Temperament</option>
|
||||||
|
<option value="harmonic">Harmonic Tuning</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tuning-select">
|
||||||
|
<label for="tuning">Select Tuning:</label>
|
||||||
|
<select id="tuning">
|
||||||
|
<!-- Ukulele Tunings -->
|
||||||
|
<optgroup label="Ukulele Tunings">
|
||||||
|
<option value="standard">Standard (GCEA)</option>
|
||||||
|
<option value="slack-key">Slack Key (GCEG)</option>
|
||||||
|
<option value="low-g">Low G (GCEA - Low G)</option>
|
||||||
|
<option value="harmonic-minor">Harmonic Minor (G Bb D G)</option>
|
||||||
|
<option value="suspended-fourth">Suspended Fourth (G C F C)</option>
|
||||||
|
<option value="lydian">Lydian (G C E F#)</option>
|
||||||
|
<option value="diminished">Diminished (G B D F)</option>
|
||||||
|
<option value="augmented">Augmented (G C# E G#)</option>
|
||||||
|
<option value="open-fifths">Open Fifths (G D A D)</option>
|
||||||
|
<option value="double-unison">Double Unison (G G C C)</option>
|
||||||
|
<option value="ionian">Ionian (Major) (G C E A)</option>
|
||||||
|
<option value="dorian">Dorian (G Bb D A)</option>
|
||||||
|
<option value="mixo-dorian">Mixo-Dorian (F A# G A)</option>
|
||||||
|
<option value="phrygian">Phrygian (G Ab D A)</option>
|
||||||
|
<option value="mixolydian">Mixolydian (G C D A)</option>
|
||||||
|
<option value="aeolian">Aeolian (Natural Minor) (G Bb D G)</option>
|
||||||
|
<option value="locrian">Locrian (G Ab C G)</option>
|
||||||
|
</optgroup>
|
||||||
|
<!-- Guitar Tunings -->
|
||||||
|
<optgroup label="Guitar Tunings">
|
||||||
|
<option value="standard">Standard (EADGBE)</option>
|
||||||
|
<option value="drop-d">Drop D (DADGBE)</option>
|
||||||
|
<option value="dadgad">DADGAD</option>
|
||||||
|
<option value="open-g">Open G (DGDGBD)</option>
|
||||||
|
<option value="open-d">Open D (DADF#AD)</option>
|
||||||
|
<option value="open-c">Open C (CGCGCE)</option>
|
||||||
|
<option value="half-step-down">Half Step Down (Eb Ab Db Gb Bb Eb)</option>
|
||||||
|
<option value="full-step-down">Full Step Down (D G C F A D)</option>
|
||||||
|
<option value="double-drop-d">Double Drop D (DADGBD)</option>
|
||||||
|
<option value="new-standard">New Standard (CGDAEG)</option>
|
||||||
|
<option value="nashville-high-strung">Nashville High-Strung (EADGBE but high-strung)</option>
|
||||||
|
<option value="orkney">Orkney (CGDGCD)</option>
|
||||||
|
<option value="modal-tuning-1">Modal 1 (CGDGBE)</option>
|
||||||
|
<option value="modal-tuning-2">Modal 2 (EAEAC#E)</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="strings"></div>
|
||||||
|
|
||||||
|
<button id="play-all">Play All Strings</button>
|
||||||
|
|
||||||
|
<div id="output"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="http://unpkg.com/tone"></script>
|
||||||
|
<script src="tuner.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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`;
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue