import wave
import numpy as np
import math

# Nombre d’octets par échantillon
samp_width = 2

# Valeur maximale d’un échantillon
max_val = 2 ** (8 * samp_width - 1) - 1

# Fréquence d’échantillonnage (Hertz)
samp_rate = 44100


def silence(dur):
    """
    Génère un signal silencieux.

    Paramètres:
        dur (float): Durée en secondes

    Retourne:
        (ndarray): Signal généré
    """
    return np.zeros((int(samp_rate * dur),))


def sine(dur, freq, value=1):
    """
    Génère un signal sinusoïdal.

    Paramètres:
        dur (float): Durée en secondes
        freq (float): Fréquence de la sinusoïde en hertz
        value (float): Amplitude du signal (valeur relative entre 0 et 1)

    Retourne:
        (ndarray): Signal généré
    """
    x = np.arange(int(samp_rate * dur))
    return value * max_val * np.sin(2 * np.pi * freq * x / samp_rate)


def envelope(attack, decay, release, signal):
    """
    Applique une enveloppe à une note.

    Paramètres:
        attack (float): Temps d’attaque de la note (en secondes)
        decay (float): Temps de chute de la note vers la phase
            de maintien (en secondes)
        release (float): Temps de relâche à la fin de la note (en secondes)
        signal (ndarray): Signal original

    Retourne:
        (ndarray): Signal généré
    """
    total = len(signal)
    attack = int(attack * samp_rate)
    decay = int(decay * samp_rate)
    release = int(release * samp_rate)

    if attack + decay + release > total:
        raise ValueError('Note trop courte pour l’application de l’enveloppe')

    sustain = total - attack - decay - release

    return signal * np.concatenate((
        np.linspace(start=0, stop=1, num=attack, endpoint=False),
        np.linspace(start=1, stop=2/3, num=decay, endpoint=False),
        np.linspace(start=2/3, stop=2/3, num=sustain, endpoint=False),
        np.linspace(start=2/3, stop=0, num=release, endpoint=True),
    ))


def add_signal(dest, start, source):
    """
    Ajoute un signal source dans un autre signal.

    Paramètres:
        dest (ndarray): Signal dans lequel le signal source sera ajouté
        start (float): Temps en secondes à partir duquel le signal est ajouté
        source (ndarray): Signal source

    Retourne: None
    """
    dest[int(samp_rate * start):int(samp_rate * start) + len(source)] += source


def chord(instr, dur, freqs, value=1):
    """
    Construit un accord de notes.

    Paramètres:
        instr (function): Instrument à utiliser pour générer les notes
        dur (float): Durée de chaque note (en secondes)
        freqs (list): Fréquence de chaque note
        value (float): Amplitude totale partagée par les notes

    Retourne:
        (ndarray): Signal généré
    """
    signal = np.ndarray(0)

    for freq in freqs:
        new_signal = instr(dur, freq, value / len(freqs))

        if len(new_signal) > len(signal):
            signal.resize(len(new_signal))

        signal += new_signal

    return signal

notes = {
    'si#': 0, 'do': 0,
    'reb': 1, 'do#': 1,
    're': 2,
    'mib': 3, 're#': 3,
    'fab': 4, 'mi': 4,
    'mi#': 5, 'fa': 5,
    'solb': 6, 'fa#': 6,
    'sol': 7,
    'lab': 8, 'sol#': 8,
    'la': 9,
    'sib': 10, 'la#': 10,
    'dob': 11, 'si': 11,
}

rev_notes = dict(zip(*reversed(list(zip(*notes.items())))))

def note_freq(note, octave):
    """
    Calcule la fréquence correspondant à une note.

    Paramètres:
        note (str): Nom de la note
        octave (int): Numéro de l’octave

    Retourne:
        (float): Fréquence correspondante en hertz
    """
    return (440
        * (2 ** (octave - 3))
        * math.pow(2, (notes[note] - 9) / 12))


def freq_note(freq):
    """
    Retrouve la note correspondant au mieux à une fréquence.

    Paramètres:
        note
    """
    log = math.log2(freq / note_freq('do', 3))
    octave = math.floor(log) + 3
    note = round(12 * log - 12 * math.floor(log))

    if note == 12:
        octave += 1
        note = 0

    return (rev_notes[note], octave)


def note_freqs(notes):
    """Calcule la fréquence correspondant à un ensemble de notes."""
    return list(map(lambda info: note_freq(*info), notes))


def save_signal(out_name, signal):
    """
    Écrit un signal dans un fichier au format WAV.

    Paramètres:
        out_name (str): Chemin vers le fichier
        signal (ndarray): Signal à enregistrer
    """
    with wave.open(out_name, 'w') as file:
        file.setnchannels(1)
        file.setsampwidth(samp_width)
        file.setframerate(samp_rate)
        file.writeframesraw(signal.astype('<h').tostring())


def load_signal(in_name):
    """
    Charge un signal depuis un fichier au format WAV.

    Paramètres:
        in_name (str): Chemin vers le fichier

    Retourne:
        (ndarray): Signal décodé
    """
    with wave.open(in_name, 'r') as file:
        assert file.getnchannels() == 1
        assert file.getsampwidth() == samp_width
        assert file.getframerate() == samp_rate
        size = file.getnframes()
        return np.ndarray((size,), '<h', file.readframes(size))
