"""Basic atomic building block for music"""

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

# %% auto 0
__all__ = ['BASE_NOTES', 'CHROMATIC_NOTES', 'INTERVALS', 'NOTE_MAPPING', 'SEMITONES_TO_INTERVAL', 'INTERVAL_TO_SEMITONES',
           'SEMITONES_TO_INTERVAL_FULL', 'INTERVAL_TYPES', 'INV_NOTE_MAPPING', 'Note', 'Interval']

# %% ../nbs/00_note.ipynb 3
import io
import numpy as np
from fastcore.all import *
import scipy.io.wavfile as wav
from IPython.display import Audio

# %% ../nbs/00_note.ipynb 4
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,
    "B#": 0,
    "Dbb": 0,
    "C#": 1,
    "Db": 1,
    "B##": 1,
    "C##": 2,
    "D": 2,
    "Ebb": 2,
    "D#": 3,
    "Eb": 3,
    "Fbb": 3,
    "E": 4,
    "Fb": 4,
    "D##": 4,
    "E#": 5,
    "F": 5,
    "Gbb": 5,
    "F#": 6,
    "Gb": 6,
    "E##": 6,
    "G": 7,
    "F##": 7,
    "Abb": 7,
    "G#": 8,
    "Ab": 8,
    "A": 9,
    "Bbb": 9,
    "G##": 9,
    "A#": 10,
    "Bb": 10,
    "Cbb": 10,
    "B": 11,
    "Cb": 11,
    "A##": 11,
}
SEMITONES_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",
    12: "8", # Octave
    13: "b9",
    14: "9",
    15: "#9",
    16: "10",
    17: "11",
    18: "#11",
    19: "12",
    20: "#12",
    21: "b13",
    22: "13",
    23: "b14",
    24: "14",
}
INTERVAL_TO_SEMITONES = {
    "1": 0, "unison": 0,
    "b2": 1, "♭2": 1,
    "2": 2, "M2": 2,
    "#2": 3, "♯2": 3, "m3": 3,
    "b3": 3, "♭3": 3,
    "3": 4, "M3": 4,
    "#3": 5, "♯3": 5,
    "4": 5, "P4": 5,
    "#4": 6, "♯4": 6, "TT": 6, "b5": 6, "♭5": 6,
    "5": 7, "P5": 7,
    "#5": 8, "♯5": 8,
    "b6": 8, "♭6": 8,
    "6": 9, "M6": 9,
    "#6": 10, "♯6": 10,
    "b7": 10, "♭7": 10,
    "7": 11, "M7": 11,
    "8": 12, "octave": 12,
    "b9": 13, "♭9": 13,
    "9": 14, "M9": 14,
    "#9": 15, "♯9": 15,
    "10": 16, "m10": 16, "b10": 16,
    "11": 17, "P11": 17,
    "#11": 18, "♯11": 18,
    "12": 19,
    "#12": 20, "♯12": 20,
    "b13": 20, "♭13": 20,
    "13": 21, "M13": 21,
    "b14": 23, "♭14": 23,
    "14": 24
}
SEMITONES_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",
    12: "octave",
    13: "minor ninth",
    14: "major ninth",
    15: "minor tenth",
    16: "major tenth",
    17: "perfect eleventh",
    18: "augmented eleventh",
    19: "perfect twelfth",
    20: "augmented twelfth",
    21: "minor thirteenth",
    22: "major thirteenth",
    23: "minor fourteenth",
    24: "double octave",
}
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
}

INV_NOTE_MAPPING = {v: k for k, v in NOTE_MAPPING.items()}

# %% ../nbs/00_note.ipynb 7
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 note in NOTE_MAPPING, f"Note '{note}' is not valid"
            self.note = note

        self.flat = "b" in self.note
        self.sharp = "#" in self.note
        self.accidental = self.flat or self.sharp
        self.natural = not self.flat and not self.sharp
        self.double_flat = "bb" in self.note
        self.double_sharp = "##" in self.note
        self.double_accidental = self.double_flat or self.double_sharp
        self.num_flats = self.note.count("b")
        self.num_sharps = self.note.count("#")


    @classmethod
    def from_int(cls, note_int: int, oct: int = 4):
        assert isinstance(oct, int) and oct > 0, f"Octave must be a positive integer, got oct={oct}."
        assert 0 <= note_int < 12, f"Note must be an integer between 0 and 11, got note_int={note_int}."
        return cls(CHROMATIC_NOTES[note_int % 12], oct=oct)
    
    @classmethod
    def from_midi(cls, midi_note: int):
        assert isinstance(midi_note, int), f"MIDI note must be an integer, got midi_note={midi_note}."
        assert 0 <= midi_note < 128, f"MIDI note must be an integer between 0 and 127, got midi_note={midi_note}."
        return cls.from_int(midi_note % 12, oct=midi_note // 12)
    
    def __index__(self): return self.oct * 12 + int(self) # MIDI Note number
    @property
    def midi(self): return self.__index__()
    def __str__(self): return self.note
    def __int__(self): return NOTE_MAPPING[str(self)]
    def __eq__(self, other): return self.__index__() == other.__index__()
    def __ne__(self, other): return self.__index__() != other.__index__()
    def __lt__(self, other): return self.__index__() < other.__index__()
    def __le__(self, other): return self.__index__() <= other.__index__()
    def __gt__(self, other): return self.__index__() > other.__index__()
    def __ge__(self, other): return self.__index__() >= other.__index__()

    def __repr__(self): return f"{self.note}{self.oct}"

# %% ../nbs/00_note.ipynb 38
@patch
def __add__(self:Note, other): 
    """ Add n semitones. """
    return Note.from_midi(self.midi + other)

# %% ../nbs/00_note.ipynb 45
@patch
def __mod__(self:Note, other):
    """Add n whole notes."""
    return self + other * 2

# %% ../nbs/00_note.ipynb 50
@patch
def m2(self:Note): return self + 1
@patch
def M2(self:Note): return self + 2
@patch
def m3(self:Note): return self + 3
@patch
def M3(self:Note): return self + 4
@patch
def P4(self:Note): return self + 5
@patch
def TT(self:Note): return self + 6
@patch
def P5(self:Note): return self + 7
@patch
def m6(self:Note): return self + 8
@patch
def M6(self:Note): return self + 9
@patch
def m7(self:Note): return self + 10
@patch
def M7(self:Note): return self + 11
@patch
def P8(self:Note): return self + 12
@patch
def m9(self:Note): return self + 13
@patch
def M9(self:Note): return self + 14
@patch
def A9(self: Note): return self + 15
@patch
def P11(self: Note): return self + 17
@patch
def A11(self: Note): return self + 18
@patch
def m13(self: Note): return self + 20
@patch
def M13(self: Note): return self + 21
@patch
def ext(self: Note): return [self.m9(), self.M9(), self.A9(), self.P11(), self.A11(), self.m13(), self.M13()]
@patch
def alt_ext(self: Note): return [self.m9(), self.A9(), self.A11(), self.m13()]

# %% ../nbs/00_note.ipynb 58
@patch
def __sub__(self:Note, other):
    """Subtract n semitones from a note."""
    return Note.from_midi(self.midi - other)

# %% ../nbs/00_note.ipynb 63
@patch
def __floordiv__(self:Note, other):
    """Subtract n whole notes"""
    return self - other * 2

# %% ../nbs/00_note.ipynb 68
@patch
def dm2(self: Note): return self - 1
@patch
def dM2(self: Note): return self - 2
@patch
def dm3(self: Note): return self - 3
@patch
def dM3(self: Note): return self - 4
@patch
def dP4(self: Note): return self - 5
@patch
def dTT(self: Note): return self - 6
@patch
def dP5(self: Note): return self - 7
@patch
def dm6(self: Note): return self - 8
@patch
def dM6(self: Note): return self - 9
@patch
def dm7(self: Note): return self - 10
@patch
def dM7(self: Note): return self - 11
@patch
def dP8(self: Note): return self - 12
@patch
def dm9(self: Note): return self - 13
@patch
def dM9(self: Note): return self - 14
@patch
def dA9(self: Note): return self - 15
@patch
def dP11(self: Note): return self - 17
@patch
def dA11(self: Note): return self - 18
@patch
def dm13(self: Note): return self - 20
@patch
def dM13(self: Note): return self - 21
@patch
def d_ext(self: Note): return [self.dm9(), self.dM9(), self.dA9(), self.dP11(), self.dA11(), self.dm13(), self.dM13()]
@patch
def d_alt_ext(self: Note): return [self.dm9(), self.dA9(), self.dA11(), self.dm13()]

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

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

# %% ../nbs/00_note.ipynb 82
@patch 
def get_audio_array(self:Note, length=1, sr=44100):
    t = np.linspace(0, length, int(sr * length), False)
    def freq(n): return 440 * 2**((12 * (int(n[-1])+1) + NOTE_MAPPING[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_note.ipynb 85
@patch
def play(self:Note, length=1): return Audio(data=self.get_audio_bytes(length))

# %% ../nbs/00_note.ipynb 94
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 SEMITONES_TO_INTERVAL[abs(self.semitones) % 24]
    @property
    def long(self): return SEMITONES_TO_INTERVAL_FULL[abs(self.semitones) % 24]

    def set_semitones(self): return self.note2.midi - self.note1.midi
    
    @classmethod
    def from_semitones(cls, semitones: int, root: Note = Note("C")):
        return cls(root, root+semitones)
    
    def __index__(self):
        mask = 0
        for n in self.notes: mask |= 1 << n.midi
        return mask
    def __int__(self): return self.__index__()
    @property
    def midi(self): return [n.midi for n in self.notes]
    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_note.ipynb 96
@patch
def interval(self:Note, other:Note): return Interval(self, other)

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

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

@patch
def is_consonant(self:Interval): return "Consonant" in self.type()
@patch
def is_dissonant(self:Interval): return "Dissonant" in self.type()
@patch
def is_perfect(self:Interval): return "Perfect" in self.type()
@patch 
def is_contextual(self:Interval): return "Contextual" in self.type()

# %% ../nbs/00_note.ipynb 128
@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)
