renderer.py

4.1 kB · python · 103 lines

1import numpy as np2import os3import wave4from typing import Callable, List, Tuple5from .config import Config6from .enums import WaveType78# RENDERER910class Renderer:1112    def __init__(self, config: Config, wave_type: WaveType = WaveType.SINE,13                 num_harmonics: int = 1, harmonic_wave: WaveType = WaveType.TRIANGLE):14        self.config = config15        self.wave_type = wave_type16        self.num_harmonics = num_harmonics17        self.harmonic_wave = harmonic_wave1819    # RENDER2021    def render(self, track: List[Tuple[List[int], float]],22               voice_fn: Callable = None) -> np.ndarray:23        amplitude = np.iinfo(np.int16).max24        audio_segments = []25        for chord, duration in track:26            frame_duration_samples = int(duration * self.config.sample_rate)27            frame_audio = np.zeros(frame_duration_samples, dtype=np.float32)28            for midi_note in chord:29                if midi_note is not None:30                    if voice_fn is not None:31                        wt, nh, hw = voice_fn()32                    else:33                        wt = self.wave_type34                        nh = self.num_harmonics35                        hw = self.harmonic_wave36                    note_freq = _midi_to_freq(midi_note)37                    note_wave = _generate_wave(38                        config=self.config,39                        wave_type=wt,40                        frequency=note_freq,41                        duration=duration,42                        num_harmonics=nh,43                        harmonic_wave=hw,44                    )45                    frame_audio += note_wave46            frame_audio = _fade(frame_audio, self.config.fade_duration, self.config.sample_rate)47            audio_segments.append(frame_audio)48        audio_data = np.concatenate(audio_segments)49        peak_amplitude = np.max(np.abs(audio_data))50        if peak_amplitude > 0:51            audio_data /= peak_amplitude52        return (audio_data * amplitude).astype(np.int16)5354    # SAVE5556    def save(self, filepath: str, audio: np.ndarray):57        os.makedirs(os.path.dirname(filepath), exist_ok=True)58        with wave.open(filepath, "w") as wf:59            wf.setnchannels(1)60            wf.setsampwidth(2)61            wf.setframerate(self.config.sample_rate)62            wf.writeframes(audio.tobytes())6364# MIDI6566def _midi_to_freq(midi_note: int) -> float:67    return 440 * 2**((midi_note - 69) / 12)6869# WAVES7071def _wave(t_array: np.ndarray, freq: float, wave_type: WaveType) -> np.ndarray:72    t = t_array * freq73    wave_factory = {74        WaveType.SINE: np.sin(2. * np.pi * t),75        WaveType.TRIANGLE: 2 * np.abs(2 * (t - np.floor(t + 0.5))) - 1,76        WaveType.SQUARE: np.sign(np.sin(2. * np.pi * t)),77        WaveType.SAWTOOTH: 2 * (t - np.floor(0.5 + t)),78    }79    return wave_factory[wave_type]8081def _generate_wave(config: Config, wave_type: WaveType, frequency: float, duration: float, num_harmonics: int = None, harmonic_wave: WaveType = None) -> np.ndarray:82    time_array = np.linspace(0.0, duration, int(config.sample_rate * duration), endpoint=False)83    output_wave = np.zeros_like(time_array)84    harmonic_factory = {85        WaveType.SQUARE: {i: 1.0 / i for i in range(1, num_harmonics + 1, 2)},86        WaveType.TRIANGLE: {i: 1.0 / (i * i) for i in range(1, num_harmonics + 1, 2)},87        WaveType.SAWTOOTH: {i: 1.0 / i for i in range(1, num_harmonics + 1)},88    }89    for harmonic, amplitude_factor in harmonic_factory[harmonic_wave].items():90        output_wave += amplitude_factor * _wave(time_array, frequency * harmonic, wave_type)91    if np.max(np.abs(output_wave)) > 0:92        output_wave /= np.max(np.abs(output_wave))93    return output_wave9495def _fade(audio_segment: np.ndarray, fade_duration: float, sample_rate: int) -> np.ndarray:96    fade_length = int(sample_rate * fade_duration)97    actual_fade_length = min(fade_length, len(audio_segment) // 2)98    if actual_fade_length > 0:99        fade_in = np.linspace(0.0, 1.0, actual_fade_length)100        fade_out = np.linspace(1.0, 0.0, actual_fade_length)101        audio_segment[:actual_fade_length] *= fade_in102        audio_segment[-actual_fade_length:] *= fade_out103    return audio_segment