Create ai-gen.py
This commit is contained in:
396
ai-gen.py
Normal file
396
ai-gen.py
Normal 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: 1–7
|
||||||
|
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 (0–11) 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 (1–7) 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")
|
||||||
Reference in New Issue
Block a user