# AUTOGENERATED! DO NOT EDIT! File to edit: ../../../nbs/20_guitar_chords.ipynb.

# %% auto 0
__all__ = ['GuitarNote', 'GuitarSoundGenerator']

# %% ../../../nbs/20_guitar_chords.ipynb 2
from enum import Enum
from pathlib import Path
from dataclasses import dataclass
from typing import Literal, TypeAlias

import numpy as np
import soundfile as sf

# %% ../../../nbs/20_guitar_chords.ipynb 5
GuitarNote: TypeAlias = Literal['C', 'D', 'E', 'F', 'G', 'A', 'B']

# %% ../../../nbs/20_guitar_chords.ipynb 6
class GuitarSoundGenerator:
    """Simple guitar sound waveform generator."""

    # Note frequencies in Hz for octave 0
    NOTES = {
        'C': 32.7,
        'D': 36.71,
        'E': 41.2,
        'F': 43.65,
        'G': 49.0,
        'A': 27.5,
        'B': 30.87,
    }

    def __init__(self, sample_rate: int = 44100):
        """
        Initialize the note generator.

        Args:
            sample_rate: Determines how many discrete samples per second are 
                generated for the waveform.
        """
        self.sample_rate = sample_rate

    def get_frequency(self, note: GuitarNote, *, octave: int = 4) -> float:
        """
        Get frequency for a note in a specific octave.
        
        Args:
            note: Note name (e.g., 'A', 'G')
            octave: Determines the pitch register. Each octave increase doubles
                the frequency. For example A0 = 27.5Hz, A1 = 55Hz, A2 = 110Hz, 
                A3 = 220Hz and A4 = 440Hz

        Returns:
            Frequency in Hz
        """
        # A0 is in octave 0, so A4 = A0 * 2^4
        base_freq = self.NOTES[note]
        frequency = base_freq * (2 ** octave)
        return frequency

    def generate_note(
        self, 
        note: GuitarNote, 
        *,
        octave: int = 4, 
        duration: float = 1.0, 
        amplitude: float = 0.5,
    ) -> np.ndarray:
        """
        Generate a waveform for a musical note.
        
        Args:
            note: Note name (e.g., 'A', 'G')
            octave: Octave number
            duration: Duration in seconds
            amplitude: Amplitude (0.0 to 1.0)
            wave_type: Type of wave
        Returns:
            Numpy array containing the waveform
        """
        frequency = self.get_frequency(note, octave=octave)
        
        # numpy array that represent time points in seconds
        tp_in_s = np.linspace(
            0, 
            duration, 
            int(self.sample_rate * duration), 
            False
        )
        
        
        waveform = amplitude * np.sin(2 * np.pi * frequency * tp_in_s)

        # Apply fade in/out to avoid clicks
        fade_samples = int(0.01 * self.sample_rate)  # 10ms fade
        if len(waveform) > 2 * fade_samples:
            waveform[:fade_samples] *= np.linspace(0, 1, fade_samples)
            waveform[-fade_samples:] *= np.linspace(1, 0, fade_samples)
        
        return waveform

    def generate_chord(
        self, 
        notes: list[GuitarNote],
        *,
        octave: int = 4, 
        duration: float = 1.0,
        amplitude: float = 0.3,
    ) -> np.ndarray:
        """
        Generate a chord by combining multiple notes.

        Args:
            notes: List of note names
            octave: Octave number
            duration: Duration in seconds
            amplitude: Amplitude per note

        Returns:
            Numpy array containing the combined waveform
        """
        waveforms = []
        for note in notes:
            waveform = self.generate_note(
                note, 
                octave=octave, 
                duration=duration, 
                amplitude=amplitude
            )
            waveforms.append(waveform)

        # Sum all waveforms
        combined = np.sum(waveforms, axis=0)

        # Normalize to prevent clipping
        max_val = np.max(np.abs(combined))
        if max_val > 1.0:
            combined = combined / max_val

        return combined

    def to_wav(self, waveform: np.ndarray, file: str | Path) -> None:
        """
        Save waveform as WAV file.

        Args:
            waveform: Numpy array containing the waveform
            file_path: Output filename. Can be absolute or relative path.
                Automatically adds .wav extension if not present.
        """
        path = Path(file)

        if path.suffix.lower() != '.wav':
            path = path.with_suffix('.wav')

        path.parent.mkdir(parents=True, exist_ok=True)

        # Ensure waveform is in the correct range and format
        waveform_clipped = np.clip(waveform, -1.0, 1.0)

        # Save as 32-bit float WAV
        sf.write(str(path), waveform_clipped, self.sample_rate, subtype='FLOAT')
