"""Basic building blocks"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['BASE_NOTES', 'CHROMATIC_NOTES', 'INTERVALS', 'NOTE_MAPPING', 'INTERVAL_NAMES', 'INTERVAL_HALF_STEPS',
           'STEPS_TO_INTERVAL', 'STEPS_TO_INTERVAL_FULL', 'INTERVAL_TYPES', 'SCALES', 'd', 'INV_SCALES_BY_INTERVAL',
           'INV_SCALES_BY_NAME', 'Note', 'Interval', 'Chord', 'PolyChord', 'Scale']

# %% ../nbs/00_core.ipynb 3
import io
import numpy as np
import pandas as pd
from fastcore.all import *
import scipy.io.wavfile as wav
from IPython.display import Audio
from itertools import combinations
from collections import defaultdict
from mingus.core import chords as mingus_chords, notes as mingus_notes

# %% ../nbs/00_core.ipynb 6
BASE_NOTES = ["C", "D", "E", "F", "G", "A", "B"]
CHROMATIC_NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
INTERVALS = ["1", "b2", "2", "b3", "3", "4", "#4", "5", "b6", "6", "b7", "7"]
NOTE_MAPPING = {
    "C": 0,
    "C#": 1,
    "Db": 1,
    "D": 2,
    "D#": 3,
    "Eb": 3,
    "E": 4,
    "Fb": 4,
    "E#": 5,
    "F": 5,
    "F#": 6,
    "Gb": 6,
    "G": 7,
    "G#": 8,
    "Ab": 8,
    "A": 9,
    "A#": 10,
    "Bb": 10,
    "B": 11,
    "Cb": 11,
    "B#": 0
}
INTERVAL_NAMES = {
    "1": "unison",
    "b2": "minor second",
    "2": "major second",
    "#2": "augmented second",
    "b3": "minor third",
    "3": "major third",
    "b4": "minor fourth",
    "4": "perfect fourth",
    "#4": "augmented fourth",
    "b5": "diminished fifth",
    "5": "perfect fifth",
    "#5": "augmented fifth",
    "b6": "minor sixth",
    "6": "major sixth",
    "#6": "augmented sixth",
    "b7": "minor seventh",
    "7": "major seventh",
    "8": "octave",
    "b9": "minor ninth",
    "9": "major ninth",
    "#9": "augmented ninth",
    "b10": "minor tenth",
    "10": "major tenth",
    "b11": "minor eleventh",
    "11": "major eleventh",
    "#11": "augmented eleventh",
    "b12": "minor twelfth",
    "12": "major twelfth",
    "b13": "minor thirteenth",
    "13": "major thirteenth",
    "#13": "augmented thirteenth"
}
INTERVAL_HALF_STEPS = {
    "1": 0,
    "#1": 1,
    "b2": 1,
    "2": 2,
    "#2": 3,
    "b3": 3,
    "3": 4,
    "b4": 4,
    "4": 5,
    "#4": 6,
    "b5": 6,
    "5": 7,
    "#5": 8,
    "b6": 8,
    "6": 9,
    "#6": 10,
    "b7": 10,
    "7": 11,
    "8": 12,
    "b9": 13,
    "9": 14,
    "#9": 15,
    "10": 16,
    "11": 17,
    "#11": 18,
    "12": 19,
    "b13": 20,
    "13": 21,
    "#13": 22,
    "14": 23,
    "15": 24
}
STEPS_TO_INTERVAL = {
    0: "1",
    1: "b2",
    2: "2",
    3: "b3",
    4: "3",
    5: "4",
    6: "b5",
    7: "5",
    8: "b6",
    9: "6",
    10: "b7",
    11: "7",
}

STEPS_TO_INTERVAL_FULL = {
    0: "unison",
    1: "minor second",
    2: "major second",
    3: "minor third",
    4: "major third",
    5: "perfect fourth",
    6: "tritone",
    7: "perfect fifth",
    8: "minor sixth",
    9: "major sixth",
    10: "minor seventh",
    11: "major seventh",
}
INTERVAL_TYPES = {
    0: "Perfect Consonant", # Unison
    1: "Sharp Dissonant", # Minor Second
    2: "Mild Dissonant", # Major Second
    3: "Soft Consonant", # Minor Third
    4: "Soft Consonant", # Major Third
    5: "Contextual", # Perfect Fourth
    6: "Neutral", # Tritone/Augmented Fourth/Diminished Fifth
    7: "Perfect Consonant", # Perfect Fifth
    8: "Soft Consonant", # Minor Sixth
    9: "Soft Consonant", # Major Sixth
    10: "Mild Dissonant", # Minor Seventh
    11: "Sharp Dissonant", # Major Seventh
}

SCALES = {# Major modes
          "ionian": ["1", "2", "3", "4", "5", "6", "7"],
          "major": ["1", "2", "3", "4", "5", "6", "7"],
          "minor": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "natural minor": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "dorian": ["1", "2", "b3", "4", "5", "6", "b7"],
          "phrygian": ["1", "b2", "b3", "4", "5", "b6", "b7"],
          "lydian": ["1", "2", "3", "#4", "5", "6", "7"],
          "mixolydian": ["1", "2", "3", "4", "5", "6", "b7"],
          "aeolian": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "locrian": ["1", "b2", "b3", "4", "b5", "b6", "b7"],
          # Melodic Minor modes
          "melodic minor": ["1", "2", "b3", "4", "5", "6", "7"],
          "dorian b2": ["1", "b2", "b3", "4", "5", "6", "b7"],
          "lydian augmented": ["1", "2", "3", "#4", "#5", "6", "7"],
          "lydian b7": ["1", "2", "3", "#4", "5", "6", "b7"],
          "lydian dominant": ["1", "2", "3", "#4", "5", "6", "b7"],
          "acoustic": ["1", "2", "3", "#4", "5", "6", "b7"],
          "mixolydian #11": ["1", "2", "3", "#4", "5", "6", "b7"],
          "locrian n2": ["1", "2", "b3", "4", "b5", "b6", "7"],
          "altered": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "altered dominant": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "diminished whole-tone": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "dominant whole-tone": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "aeolian dominant": ["1", "2", "3", "4", "5", "b6", "b7"],
          # Harmonic minor modes
          "harmonic minor": ["1", "2", "b3", "4", "5", "b6", "7"],
          "locrian 6": ["1", "b2", "b3", "4", "b5", "6", "b7"],
          "ionian augmented": ["1", "2", "3", "4", "#5", "6", "7"],
          "dorian #4": ["1", "2", "b3", "#4", "5", "6", "b7"],
          "phrygian major": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "phrygian dominant": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish phrygian": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish major": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "lydian #2": ["1", "#2", "3", "#4", "5", "6", "7"],
          "lydian #9": ["1", "#2", "3", "#4", "5", "6", "7"],
          "altered dominant bb7": ["1", "b2", "b3", "b4", "b5", "b6", "6"],
          "altered 13": ["1", "b2", "b3", "b4", "b5", "b6", "6"],
          # Harmonic major modes
          "harmonic major": ["1", "2", "3", "4", "5", "b6", "7"],
          "ionian b6": ["1", "2", "3", "4", "5", "b6", "7"],
          "dorian b5": ["1", "2", "b3", "4", "b5", "6", "b7"],
          "phrygian b4": ["1", "b2", "b3", "b4", "5", "b6", "b7"],
          "lydian b3": ["1", "2", "b3", "#4", "5", "6", "7"],
          "melodic minor #4": ["1", "2", "b3", "#4", "5", "6", "7"],
          "mixolydian b2": ["1", "b2", "3", "4", "5", "6", "b7"],
          # Lydian augmented already defined in melodic minor modes
          "locrian bb7": ["1", "b2", "b3", "4", "b5", "b6", "6"],
          # Double harmonic major modes
          "double harmonic major": ["1", "b2", "3", "4", "5", "b6", "7"],
          "double harmonic": ["1", "b2", "3", "4", "5", "b6", "7"],
          "lydian #2#6": ["1", "#2", "3", "#4", "5", "#6", "7"],
          "ultraphrygian": ["1", "b2", "b3", "b4", "5", "b6", "6"],
          "hungarian minor": ["1", "2", "b3", "#4", "5", "b6", "7"],
          "oriental": ["1", "b2", "3", "4", "b5", "6", "b7"], # Mixolydian b2 with lowered 5
          "ionian aug#2": ["1", "#2", "3", "4", "#5", "6", "7"],
          "locrian bb3bb7": ["1", "b2", "2", "4", "b5", "b6", "6"],
          # Bebop scales
          "bebop dominant": ["1", "2", "3", "4", "5", "6", "#6", "7"],
          "bebop major": ["1", "2", "3", "4", "5", "b6", "6", "7"],
          "bebop blues": ["1", "2", "b3", "3", "4", "5", "6", "b7"],
          "bebop melodic minor": ["1", "2", "b3", "4", "5", "b6", "6", "7"],
          # Pentatonic scales
          "pentatonic major": ["1", "2", "3", "5", "6"],
          "major pentatonic": ["1", "2", "3", "5", "6"],
          "pentatonic minor": ["1", "b3", "4", "5", "b7"],
          "minor pentatonic": ["1", "b3", "4", "5", "b7"],
          "pentatonic blues": ["1", "b3", "4", "b5", "5", "b7"],
          "blues pentatonic": ["1", "b3", "4", "b5", "5", "b7"],
          "pentatonic neutral": ["1", "2", "4", "5", "b7"],
          "neutral pentatonic": ["1", "2", "4", "5", "b7"],
          "pentatonic rock": ["1", "b3", "4", "#5", "b7"],
          "rock pentatonic": ["1", "b3", "4", "#5", "b7"],
          "jue": ["1", "b3", "4", "#5", "b7"], # Same intervals as rock pentatonic
          "pentatonic scottish": ["1", "2", "4", "5", "6"],
          "scottish pentatonic": ["1", "2", "4", "5", "6"],
          # Blues scales
          "blues major": ["1", "2", "b3", "3", "5", "6"],
          "blues minor": ["1", "b3", "4", "b5", "5", "b7"],
          "blues": ["1", "b3", "4", "b5", "5", "b7"], # Shorthand for Blues Minor
          "blues diminished": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"],
          "aux diminished blues": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"], 
          "half-whole": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"],
          "balinese": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"], 
          # Gypsy scales
          "gypsy major": ["1", "b2", "3", "4", "5", "b6", "7"],
          "gypsy minor": ["1", "2", "b3", "b5", "5", "b6", "7"],
          "spanish gypsy": ["1", "b2", "3", "4", "5", "b6", "b7"], # Same as Phrygian dominant
          "hungarian gypsy": ["1", "2", "b3", "#4", "5", "b6", "b7"], # Hungarian minor with lowered 7
          # Diminished scales
          "diminished": ["1", "2", "b3", "4", "b5", "b6", "6", "7"],
          "tonic diminished": ["1", "2", "b3", "4", "b5", "b6", "6", "7"],
          "whole-half": ["1", "2", "b3", "4", "#4", "#5", "6", "7"],
          "dominant diminished": ["1", "b2", "b3", "3", "#4", "5", "6", "b7"],
          "whole": ["1", "2", "3", "#4", "#5", "#6"],
          "whole-tone": ["1", "2", "3", "#4", "#5", "#6"],
          "aux augmented": ["1", "2", "3", "#4", "#5", "#6"],
          # Maqam
          "bayati shuri": ["1", "2", "b3", "4", "b5", "6", "b7"],
          "hijaz": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "hijaz kar": ["1", "b2", "3", "4", "5", "6", "7"],
          "huzam": ["1", "b2", "b3", "3", "5", "b6", "b7"],
          "nikriz": ["1", "2", "3", "#4", "5", "6", "7"],
          "tunisian": ["1", "2", "3", "#4", "5", "6", "7"],
          "saba": ["1", "2", "b3", "3", "5", "b6", "b7"],
          "sabah": ["1", "2", "b3", "3", "5", "b6", "b7"],
          "suznak": ["1", "2", "3", "4", "5", "#5", "7"],
          "neveseri": ["1", "b2", "b3", "3", "5", "b6", "b7", "7"],
          # Greek
          "pireaus": ["1", "b2", "3", "#4", "5", "#5", "7"],
          "tsinganikos": ["1", "b2", "3", "4", "#4", "6", "#6"],
          # Hindustani
          "marwa": ["1", "b2", "3", "#4", "5", "6", "7"],
          "poorvi": ["1", "b2", "3", "#4", "5", "6", "7"],
          "segah": ["1", "#2", "3", "4", "5", "#5", "7"],
          "todi": ["1", "b2", "b3", "#4", "5", "b6", "7"],
          # Carnatic
          "charukeshi": ["1", "2", "3", "4", "5", "b6", "b7"],
          "dharmaavati": ["1", "2", "b3", "b5", "5", "6", "7"],
          "lataangi": ["1", "2", "3", "#4", "5", "6", "7"],
          "vachaspati": ["1", "2", "3", "#4", "5", "6", "#6"],
          "natakpriya": ["1", "b2", "b3", "4", "5", "6", "b7"],
          "rampriya": ["1", "b2", "3", "#4", "5", "6", "#6"],
          "suryakant": ["1", "b2", "3", "4", "5", "6", "7"],
          # Japanese
          "joshi akikaze": ["1", "2", "b3", "5", "6"],
          "akikaze joshi": ["1", "2", "b3", "5", "6"],
          "joshi hira": ["1", "2", "b3", "5", "b6"],
          "hira joshi": ["1", "2", "b3", "5", "b6"],
          "joshi iwato": ["1", "b2", "4", "b5", "b7"],
          "iwato joshi": ["1", "b2", "4", "b5", "b7"],
          "joshi kokin": ["1", "2", "4", "5", "b6"],
          "kokin joshi": ["1", "2", "4", "5", "b6"],
          "joshi kumoi": ["1", "b2", "4", "5", "b6"],
          "kumoi joshi": ["1", "b2", "4", "5", "b6"],
          "joshi okinawa": ["1", "3", "4", "5", "7"],
          "okinawa joshi": ["1", "3", "4", "5", "7"],
          "sen in": ["1", "b2", "4", "5", "b7"],
          "in sen": ["1", "b2", "4", "5", "b7"],
          # Misc.
          "chromatic": ["1", "b2", "2", "b3", "3", "4", "#4", "5", "#5", "6", "b7", "7"],
          "augmented": ["1", "#2", "3", "5", "#5", "7"],
          "enigmatic ascending": ["1", "b2", "3", "#4", "#5", "#6", "7"],
          "enigmatic descending": ["1", "b2", "3", "4", "b6", "b7", "7"],
          "hungarian major": ["1", "b3", "3", "b5", "5", "6", "b7"],
          "neapolitan major": ["1", "b2", "b3", "4", "5", "6", "7"],
          "neapolitan minor": ["1", "b2", "b3", "4", "5", "b6", "7"],
          "prometheus": ["1", "2", "3", "b5", "6", "b7"],
          "mystic": ["1", "2", "3", "b5", "6", "b7"],
          "prometheus neapolitan": ["1", "b2", "3", "b5", "6", "b7"], # Prometheus with lowered 2
          "spanish 8 tone": ["1", "b2", "b3", "3", "4", "b5", "b6", "b7"],
          }

# Invert scales so alternative names can be identified by intervals or name
d = defaultdict(list)
[d[tuple(v)].append(k) for k,v in SCALES.items()]
INV_SCALES_BY_INTERVAL = dict(d)
INV_SCALES_BY_NAME = {k: d[tuple(v)] for k,v in SCALES.items()}

# %% ../nbs/00_core.ipynb 10
class Note(BasicRepr):
    def __init__(self, note: str, oct: int = 4):
        assert isinstance(oct, int) and oct > 0, f"Octave must be a positive integer, got oct={oct}."
        store_attr()
        # Transform note to uppercase
        if isinstance(note, str):
            note = note[0].upper() + note[1:]
            assert mingus_notes.is_valid_note(note), f"Note '{note}' is not valid"
            self.note = self.postprocess_note(mingus_notes.remove_redundant_accidentals(note))

    @staticmethod
    def postprocess_note(note: str):
        """ Get rid of unnecessary accidentals."""
        if note == "B#": note = "C"
        elif note == "E#": note = "F"
        elif note == "Cb": note = "B"
        elif note == "Fb": note = "E"
        elif note.endswith("##"):
            note = BASE_NOTES[BASE_NOTES.index(note[0])+1]
        elif note.endswith("bb"):
            note = BASE_NOTES[BASE_NOTES.index(note[0])-1]
        return str(note)
    
    def __str__(self): return self.note
    def __int__(self): return NOTE_MAPPING[str(self)]
    def rel(self): return self.oct * 12 + int(self)
    def __eq__(self, other): return self.rel() == other.rel()
    def __ne__(self, other): return self.rel() != other.rel()
    def __lt__(self, other): return self.rel() < other.rel()
    def __le__(self, other): return self.rel() <= other.rel()
    def __gt__(self, other): return self.rel() > other.rel()
    def __ge__(self, other): return self.rel() >= other.rel()

# %% ../nbs/00_core.ipynb 20
@patch
def __add__(self:Note, other):
    """Add n semitones to a note."""
    octave_change = (other + int(self)) // 12
    return Note(CHROMATIC_NOTES[(int(self) + other) % 12], oct=self.oct + octave_change)

@patch
def __mod__(self:Note, other):
    """Add n whole notes."""
    return self + other * 2

# %% ../nbs/00_core.ipynb 30
@patch
def __sub__(self:Note, other):
    """Subtract n semitones from a note."""
    octave_change = (other + int(self)) // 12
    return Note(CHROMATIC_NOTES[(int(self) - other) % 12], oct=self.oct - octave_change)

@patch
def __floordiv__(self:Note, other):
    """Subtract n whole notes"""
    return self - other * 2

# %% ../nbs/00_core.ipynb 39
class Interval:
    def __init__(self, note1: Note, note2: Note):
        store_attr()
        self.notes = [note1, note2]
        self.semitones = self.set_semitones()
    
    @property
    def short(self): return STEPS_TO_INTERVAL[abs(self.semitones)]
    @property
    def long(self): return STEPS_TO_INTERVAL_FULL[abs(self.semitones)]

    def set_semitones(self):
        # TODO Add upper extensions to semitone calculation and interval names (2+ octaves)
        return (12 + int(self.note2) - int(self.note1) + (self.note2.oct - self.note1.oct) * 12) % 12

    def __repr__(self): return f"{str(self.long)} ({str(self.short)})"
    def __eq__(self, other): return self.semitones == other.semitones
    def __ne__(self, other): return not self.semitones == other.semitones
    def __lt__(self, other): return self.semitones < other.semitones
    def __le__(self, other): return self.semitones <= other.semitones
    def __gt__(self, other): return self.semitones > other.semitones
    def __ge__(self, other): return self.semitones >= other.semitones
    def __abs__(self): 
        c = Interval(self.note1, self.note2)
        c.semitones = abs(c.semitones)
        return c

# %% ../nbs/00_core.ipynb 41
@patch
def interval(self:Note, other:Note): return Interval(self, other)

@patch
def __and__(self:Note, other:Note): return self.interval(other)

# %% ../nbs/00_core.ipynb 59
@patch
def type(self:Interval): return INTERVAL_TYPES[abs(self.semitones) % 12]

# %% ../nbs/00_core.ipynb 69
@patch
def __add__(self:Interval, other):
    return Interval(self.note1, self.note2+other)

@patch 
def __sub__(self:Interval, other):
    return Interval(self.note1, self.note2-other)

# %% ../nbs/00_core.ipynb 77
@patch
def minor(self:Note): return self - 3

# %% ../nbs/00_core.ipynb 80
@patch
def major(self:Note): return self + 3

# %% ../nbs/00_core.ipynb 84
@patch 
def get_audio_array(self:Note, length=1, sr=44100):
    a = {'C':0,'C#':1,'Db':1,'D':2,'D#':3,'Eb':3,'E':4,'F':5,
         'F#':6,'Gb':6,'G':7,'G#':8,'Ab':8,'A':9,'A#':10,'Bb':10,'B':11}
    t = np.linspace(0, length, int(sr * length), False)
    def freq(n): return 440 * 2**((12 * (int(n[-1])+1) + a[n[:-1]] - 69)/12)
    wave = np.sin(2 * np.pi * freq(f"{self.note}{self.oct}") * t)
    wave = (wave / np.max(np.abs(wave)) * 32767).astype(np.int16)
    return wave

@patch
def get_audio_bytes(self:Note, length=1, sr=44100):
    buf = io.BytesIO(); wav.write(buf, sr, self.get_audio_array(length, sr))
    return buf.getvalue()

# %% ../nbs/00_core.ipynb 87
@patch
def play(self:Note, length=1): return Audio(data=self.get_audio_bytes(length))

# %% ../nbs/00_core.ipynb 96
class Chord(BasicRepr):
    def __init__(self, notes: List[Note]):
        self.notes = [Note(n) if isinstance(n, str) else n for n in notes]
        self.s_notes = [str(n) for n in self.notes]
        self.short_s_notes = [f"{n.note}{n.oct}" for n in self.notes]
        self.first = self.notes[0]
        self.s_first = str(self.first)

    def names(self) -> list[str]: return mingus_chords.determine(self.s_notes)
    def name(self): return self.names()[0] if self.names() else "No chord found."

    @classmethod
    def from_short(cls, c: str): return cls(mingus_chords.from_shorthand(c)) 

    def __repr__(self): return f"Chord: '{self.name()}'. Notes: {self.short_s_notes}"
    def __add__(self, other): return Chord([n + other for n in self.notes])
    def __sub__(self, other): return Chord([n - other for n in self.notes])
    def __mod__(self, other): return Chord([n % other for n in self.notes])
    def __floordiv__(self, other): return Chord([n // other for n in self.notes])
    def __iter__(self) -> list[str]: return iter(self.notes)
    
    def _compare_notes(self, other, op): return all(op(n1, n2) for n1, n2 in zip(self.notes, other.notes))
    def __eq__(self, other): return self.first == other.first and self._compare_notes(other, lambda x, y: x == y)
    def __ne__(self, other): return not self == other
    def __lt__(self, other): return self.first < other.first or (self.first == other.first and self._compare_notes(other, lambda x, y: x < y))
    def __le__(self, other): return self.first < other.first or (self.first == other.first and self._compare_notes(other, lambda x, y: x <= y))
    def __gt__(self, other): return self.first > other.first or (self.first == other.first and self._compare_notes(other, lambda x, y: x > y))
    def __ge__(self, other): return self.first > other.first or (self.first == other.first and self._compare_notes(other, lambda x, y: x >= y))


class PolyChord(Chord):
    def __init__(self, chords: list[Chord]):
        self.chords = chords
        super().__init__([note for chord in chords for note in chord.notes])
    def __repr__(self): return f"PolyChord: '{'|'.join([c.name() for c in self.chords])}'. Notes: {self.short_s_notes}"

# %% ../nbs/00_core.ipynb 111
@patch
def __mul__(self:Note, other: Note):
    return Chord([self, other])

@patch
def __mul__(self:Chord, other):
    if isinstance(other, Note):
        return Chord(self.notes + [other])
    elif isinstance(other, Chord):
        return PolyChord([self, other])
    else:
        raise ValueError("Chord objects can only be multiplied with Note or other Chord objects")

# %% ../nbs/00_core.ipynb 116
@patch
def invert(self:Chord, n: int = 1):
    assert n > 0 and n < len(self.s_notes), f"Invalid inversion '{n}' for chord with '{len(self.s_notes)}' notes."
    return Chord(self.notes[n:] + [Note(str(note), oct=note.oct + 1) for note in self.notes[:n]])

@patch
def invert(self:PolyChord, n: int = 1):
    return PolyChord([c.invert(n) for c in self.chords])

# %% ../nbs/00_core.ipynb 120
@patch
def intervals(self:Chord):
    return [Interval(n1, n2) for n1, n2 in zip(self.notes, self.notes[1:])]

# %% ../nbs/00_core.ipynb 125
@patch
def get_audio_array(self:Chord, length=1):
    return np.sum([n.get_audio_array(length) for n in self.notes], axis=0)

@patch
def play(self:Chord, length=1): 
    return Audio(self.get_audio_array(length), rate=44100)

# %% ../nbs/00_core.ipynb 134
class Scale:
    def __init__(self, name: str):
        self.name = name.lower()
        self.intervals = SCALES.get(self.name, "scale not found.")

    @classmethod
    def available_scales(cls): return list(SCALES.keys())
    
    @classmethod
    def from_intervals(cls, name: str, intervals: list[str]):
        """Create a custom scale from a list of intervals."""
        for i in intervals:
            assert i in list(INTERVAL_NAMES), f"Interval '{i}' not valid. Available intervals: '{list(INTERVAL_NAMES.keys())}'"
        custom_scale = cls(name)
        custom_scale.intervals = intervals
        return custom_scale
    
    @property
    def rel_semitones(self):
        return [INTERVAL_HALF_STEPS[interval] for interval in self.intervals]
    
    @property
    def abs_semitones(self):
        rel = self.rel_semitones
        abs = []
        for i, r in enumerate(rel[1:]):
            abs.append(r - rel[i])
        # Last remaining interval
        abs.append(12-sum(abs))
        return abs
    
    @property
    def interval_names(self):
        return [INTERVAL_NAMES[i] for i in self.intervals[1:]]
    
    def __repr__(self): return f"Scale: {self.name.title()}. Intervals: {self.intervals}"
    def __eq__(self, other): return self.intervals == other.intervals
    def __ne__(self, other): return not self == other
    def __iter__(self) -> list[str]: return iter(self.intervals)

# %% ../nbs/00_core.ipynb 149
@patch
def get_notes(self:Scale, root, oct=4):
    """Get the notes of a scale from a root note."""
    root = Note(root, oct=oct) if isinstance(root, str) else root
    return [root + int(INTERVAL_HALF_STEPS[i]) for i in self.intervals]

# %% ../nbs/00_core.ipynb 154
@patch
def get_diatonic_chords(self:Scale, root, min_notes=3):
    assert min_notes > 1, "min_notes must be greater than 1."
    notes = self.get_notes(root)
    return [Chord(combo) for n in range(min_notes, len(notes)+1) for combo in combinations(notes, n)]

# %% ../nbs/00_core.ipynb 158
@patch
def get_interval_names(self:Scale, short=False):
    return self.intervals if short else self.interval_names

# %% ../nbs/00_core.ipynb 163
@patch
def get_scale_names(self:Scale):
    return INV_SCALES_BY_INTERVAL.get(tuple(self.intervals), [])

# %% ../nbs/00_core.ipynb 168
@patch
def get_audio_array(self:Scale, root, oct=4, length=0.3):
    notes = self.get_notes(root, oct=oct)
    octave = Note(root, oct=oct+1).get_audio_array(length=length)
    return np.concatenate([n.get_audio_array(length) for n in notes] + [octave])

@patch
def play(self:Scale, root, oct=4, length=0.3): 
    return Audio(self.get_audio_array(root, oct=oct, length=length), rate=44100)

# %% ../nbs/00_core.ipynb 175
@patch
def get_triads(self:Scale, root):
    """Get all triads in scale starting from root note."""
    notes = self.get_notes(root)
    return [Chord([notes[i], 
                  Note(str(notes[(i+2)%7]), oct=notes[i].oct + (i+2)//7),
                  Note(str(notes[(i+4)%7]), oct=notes[i].oct + (i+4)//7)]) 
            for i in range(len(notes))]

# %% ../nbs/00_core.ipynb 177
@patch
def play_triads(self:Scale, root):
    """Play all triads in scale starting from root note."""
    return Audio(np.concatenate([c.get_audio_array() for c in self.get_triads(root)]), rate=44100)

# %% ../nbs/00_core.ipynb 182
@patch
def get_sevenths(self:Scale, root):
    """Get all seventh chords in scale starting from root note."""
    notes = self.get_notes(root)
    return [Chord([notes[i], 
                  Note(str(notes[(i+2)%7]), oct=notes[i].oct + (i+2)//7),
                  Note(str(notes[(i+4)%7]), oct=notes[i].oct + (i+4)//7),
                  Note(str(notes[(i+6)%7]), oct=notes[i].oct + (i+6)//7)]) 
            for i in range(len(notes))]

# %% ../nbs/00_core.ipynb 185
@patch
def play_sevenths(self:Scale, root):
    """Play all seventh chords in scale starting from root note."""
    return Audio(np.concatenate([c.get_audio_array() for c in self.get_sevenths(root)]), rate=44100)

# %% ../nbs/00_core.ipynb 189
@patch
def to_frame(self:Scale, root=None):
    d = {
        "Intervals": self.intervals,
        "Relative Semitones": self.rel_semitones,
        "Absolute Semitones": self.abs_semitones,
    }
    if root:
        d.update({
            "Notes": self.get_notes(root),
            "Triads": [t.name() for t in self.get_triads(root)],
            "Seventh Chords": [s.name() for s in self.get_sevenths(root)],
        })
    return pd.DataFrame(d)
