Create ai-gen.py

This commit is contained in:
Trance-0
2025-12-12 16:51:08 -06:00
parent c6bcfc9b01
commit 9f21b2061b

396
ai-gen.py Normal file
View File

@@ -0,0 +1,396 @@
# ============================================
# Simple chord + melody analysis helper
# ============================================
from typing import List, Dict, Any
# --------------------------------------------
# Basic pitch-class helpers
# --------------------------------------------
NOTE_TO_PC = {
"C": 0, "B#": 0,
"C#": 1, "Db": 1,
"D": 2,
"D#": 3, "Eb": 3,
"E": 4, "Fb": 4,
"F": 5, "E#": 5,
"F#": 6, "Gb": 6,
"G": 7,
"G#": 8, "Ab": 8,
"A": 9,
"A#": 10, "Bb": 10,
"B": 11, "Cb": 11,
}
PC_TO_SHARP_NAME = {
0: "C", 1: "C#", 2: "D", 3: "Eb",
4: "E", 5: "F", 6: "F#", 7: "G",
8: "Ab", 9: "A", 10: "Bb", 11: "B",
}
def normalize_note(name: str) -> str:
"""Normalize note name (upper-case, strip spaces)."""
return name.strip().replace('', 'b').replace('', '#').upper()
def note_to_pc(name: str) -> int:
name = normalize_note(name)
if name not in NOTE_TO_PC:
raise ValueError(f"Unknown note name: {name}")
return NOTE_TO_PC[name]
# --------------------------------------------
# Scales and modes
# --------------------------------------------
# Major scale intervals from tonic: 17
MAJOR_INTERVALS = [0, 2, 4, 5, 7, 9, 11]
MODE_NAMES = {
1: "Ionian (major)",
2: "Dorian",
3: "Phrygian",
4: "Lydian",
5: "Mixolydian",
6: "Aeolian (natural minor)",
7: "Locrian",
}
def build_major_scale_pcs(tonic: str) -> List[int]:
t_pc = note_to_pc(tonic)
return [(t_pc + i) % 12 for i in MAJOR_INTERVALS]
def rotate_intervals(mode_degree: int) -> List[int]:
"""Return intervals (011) for a mode, relative to its tonic,
built by rotating the major scale intervals."""
# Rotate major intervals so that mode_degree becomes 1
shifted = MAJOR_INTERVALS[mode_degree - 1:] + MAJOR_INTERVALS[:mode_degree - 1]
tonic_offset = shifted[0]
return [ (i - tonic_offset) % 12 for i in shifted ]
# --------------------------------------------
# Chord parsing
# --------------------------------------------
def parse_chord_symbol(symbol: str) -> Dict[str, Any]:
"""
Very simple parser.
Supports roots like 'G', 'Gb', 'F#' and qualities like:
- maj7, M7
- m, m7
- 7, 9, 13, b9, #9, b13, #11, etc (dominant)
"""
s = symbol.strip()
s = s.replace('', 'b').replace('', '#')
# root: first letter + optional #/b
if len(s) >= 2 and s[1] in ['#', 'b']:
root = s[:2]
rest = s[2:]
else:
root = s[0]
rest = s[1:]
root = normalize_note(root)
quality = "maj" # default
dominant = False
minor = False
maj7 = False
extensions = []
r_lower = rest.lower()
if "maj" in r_lower or "ma7" in r_lower or "maj7" in r_lower or "" in r_lower:
quality = "maj"
maj7 = True
elif "m" in r_lower and "maj" not in r_lower: # crude: 'm' means minor
quality = "min"
minor = True
elif "7" in r_lower or "9" in r_lower or "13" in r_lower:
quality = "dom"
dominant = True
# Dominant chord if has 7/9/13 but not maj or minor
# Collect alterations (b9, #9, b13, #11, etc)
for alt in ["b9", "#9", "b13", "#11"]:
if alt in r_lower:
extensions.append(alt)
return {
"symbol": symbol,
"root": root,
"quality": quality, # 'maj', 'min', 'dom'
"dominant": dominant,
"minor": minor,
"maj7": maj7,
"extensions": extensions,
}
# --------------------------------------------
# Roman numeral + mode in global key
# --------------------------------------------
def get_degree_in_key(root: str, global_key: str) -> int:
"""Return scale degree (17) of root in global major key, or 0 if not diatonic."""
pcs = build_major_scale_pcs(global_key)
root_pc = note_to_pc(root)
if root_pc in pcs:
return pcs.index(root_pc) + 1
return 0
def roman_for_degree(degree: int, chord_quality: str) -> str:
romans = {1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI", 7: "VII"}
if degree == 0:
return "N/A"
base = romans[degree]
if chord_quality == "maj":
return base
elif chord_quality == "min":
return base.lower()
elif chord_quality == "dom":
return base # dominant often uppercase
else:
return base
def mode_for_degree(degree: int) -> str:
return MODE_NAMES.get(degree, "Non-diatonic")
def detect_tritone_sub(chord_info: Dict[str, Any], global_key: str) -> str:
"""Very conservative: bII7 in major key = tritone sub of V."""
if not chord_info["dominant"]:
return ""
tonic_pc = note_to_pc(global_key)
b2_pc = (tonic_pc + 1) % 12
root_pc = note_to_pc(chord_info["root"])
if root_pc == b2_pc:
return "Tritone substitution of V7"
return ""
def detect_modal_borrowing(degree: int, chord_info: Dict[str, Any]) -> str:
"""Very simple: minor iv in major key."""
if degree == 4 and chord_info["minor"]:
return "Borrowed from parallel minor (iv)"
return ""
# --------------------------------------------
# Map melody notes to chord-scale degrees
# --------------------------------------------
DEGREE_LABELS = {
0: "1",
1: "b2",
2: "2",
3: "b3",
4: "3",
5: "4",
6: "b5",
7: "5",
8: "#5",
9: "6",
10: "b7",
11: "7",
}
def build_mode_scale(root: str, degree_in_key: int) -> List[int]:
"""
Build a 7-note mode scale for the chord, using the church mode
associated with its scale degree in the global key.
"""
root_pc = note_to_pc(root)
# mode built from root with appropriate interval pattern
intervals = rotate_intervals(degree_in_key if degree_in_key != 0 else 1)
return [(root_pc + i) % 12 for i in intervals]
def pitch_to_chord_degree(pc: int, chord_root_pc: int) -> str:
"""Return scale-degree label (1, b2, 2, b3, 3, ..., 7) relative to chord root."""
semitone = (pc - chord_root_pc) % 12
return DEGREE_LABELS.get(semitone, f"?({semitone})")
# --------------------------------------------
# Melody function classification
# --------------------------------------------
def analyze_melody_functions(
melody_pcs: List[int],
chord_scale_pcs: List[int],
chord_root_pc: int,
) -> Dict[int, List[str]]:
"""
VERY simple heuristic classification of notes in a single-chord context.
Returns dict: index -> [labels].
Labels: 'guide_tone', 'arpeggio', 'scale_line',
'upper_neighbor', 'lower_neighbor',
'passing', 'chromatic_passing'
"""
n = len(melody_pcs)
labels: Dict[int, List[str]] = {i: [] for i in range(n)}
# Identify chord tones (1,3,5,7) in this mode (use major-ish mapping)
chord_tone_semitones = [0, 4, 7, 11] # 1,3,5,7 above root in semitones
chord_tones = [ (chord_root_pc + x) % 12 for x in chord_tone_semitones ]
guide_tones = [ (chord_root_pc + 4) % 12, (chord_root_pc + 11) % 12 ] # 3 & 7
for i, pc in enumerate(melody_pcs):
if pc in guide_tones:
labels[i].append("guide_tone")
elif pc in chord_tones:
labels[i].append("chord_tone")
# Scale lines: three consecutive stepwise notes
for i in range(1, n - 1):
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
d1 = (b - a) % 12
d2 = (c - b) % 12
if d1 in (1, 2, 10, 11) and d2 == d1: # crude ascending/descending step
labels[i - 1].append("scale_line")
labels[i].append("scale_line")
labels[i + 1].append("scale_line")
# Neighbor tones (upper / lower)
for i in range(1, n - 1):
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
if a == c and a in chord_tones:
step = (b - a) % 12
if step in (1, 2): # up
labels[i].append("upper_neighbor")
elif step in (10, 11): # down
labels[i].append("lower_neighbor")
# Passing and chromatic passing
for i in range(1, n - 1):
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
# require chord tones on ends, non-chord in middle
if a in chord_tones and c in chord_tones and b not in chord_tones:
up = (b - a) % 12
down = (c - b) % 12
if up in (1, 2, 10, 11) and down in (1, 2, 10, 11):
# if b in chord scale -> passing; else chromatic passing
if b in chord_scale_pcs:
labels[i].append("passing_tone")
else:
labels[i].append("chromatic_passing_tone")
return labels
# --------------------------------------------
# Main entry function
# --------------------------------------------
def analyze_chord_and_melody(
chord_symbol: str,
global_key: str,
melody_notes: List[str],
) -> Dict[str, Any]:
"""
Main function user will call.
Returns a dict with:
- 'chord_info'
- 'degree_in_key'
- 'roman'
- 'mode'
- 'tritone_sub', 'modal_borrowing'
- 'note_degrees' (list of degree labels per melody note)
- 'functions' (index -> [labels])
"""
chord_info = parse_chord_symbol(chord_symbol)
degree = get_degree_in_key(chord_info["root"], global_key)
roman = roman_for_degree(degree, chord_info["quality"])
mode_name = mode_for_degree(degree)
tri_sub = detect_tritone_sub(chord_info, global_key)
modal_borrow = detect_modal_borrowing(degree, chord_info)
# Build chord-scale as mode from chord root
if degree == 0:
# non-diatonic: just use major scale from root as crude fallback
chord_scale_pcs = build_major_scale_pcs(chord_info["root"])
else:
chord_scale_pcs = build_mode_scale(chord_info["root"], degree)
chord_root_pc = note_to_pc(chord_info["root"])
melody_pcs = [note_to_pc(n) for n in melody_notes]
note_degrees = [
pitch_to_chord_degree(pc, chord_root_pc) for pc in melody_pcs
]
functions = analyze_melody_functions(melody_pcs, chord_scale_pcs, chord_root_pc)
return {
"chord_info": chord_info,
"degree_in_key": degree,
"roman": roman,
"mode": mode_name,
"tritone_sub": tri_sub,
"modal_borrowing": modal_borrow,
"note_degrees": note_degrees,
"functions": functions,
}
# --------------------------------------------
# Example usage
# --------------------------------------------
if __name__ == "__main__":
GLOBAL_KEY = "G" # preconfigured global major key
print("Interactive chord/melody analyzer")
print("Global key is set to:", GLOBAL_KEY)
print("Type lines like: Eb7: D, E, G, B")
print('Type "quit" to exit.\n')
while True:
line = input("Enter [chord]: key1, key2, ... > ").strip()
# quit condition
if line.lower() == "quit":
print("Goodbye.")
break
# basic validation
if ":" not in line:
print("Error: input must be in the form [chord]: key1, key2, ...")
continue
chord_part, melody_part = line.split(":", 1)
chord = chord_part.strip()
melody_raw = melody_part.strip()
if not chord or not melody_raw:
print("Error: missing chord or melody notes. Try again.")
continue
# parse melody notes, allow extra spaces
melody_tokens = [tok.strip() for tok in melody_raw.split(",")]
melody = [tok for tok in melody_tokens if tok]
if not melody:
print("Error: no valid note names found after ':'. Try again.")
continue
try:
result = analyze_chord_and_melody(chord, GLOBAL_KEY, melody)
except ValueError as e:
# catches unknown note names or similar parsing issues
print(f"Error while analyzing: {e}")
print("Please check your chord and note names and try again.")
continue
print("\n====================================")
print("Chord:", chord)
print("In key of", GLOBAL_KEY)
print("Degree in key:", result["degree_in_key"])
print("Roman numeral:", result["roman"])
print("Mode:", result["mode"])
if result["tritone_sub"]:
print("Note:", result["tritone_sub"])
if result["modal_borrowing"]:
print("Note:", result["modal_borrowing"])
print("\nMelody note degrees (per chord):")
for note, deg in zip(melody, result["note_degrees"]):
print(f" {note:3} -> {deg}")
print("\nFunctions per note index:")
for i, labels in result["functions"].items():
if labels:
print(f" index {i}, note {melody[i]}: {', '.join(labels)}")
print("====================================\n")