sintetizador_fm_para_archivos_midi_en_python

This is an old revision of the document!


poly synth

import mido
import numpy as np
import sounddevice as sd
import time
 
class Note:
    def __init__(self, note_num, velocity, start_time, program_num=None):
        self.note_num = note_num
        self.velocity = velocity
        self.start_time = start_time
        self.end_time = None
        self.samples = None
        self.position = 0
        self.program_num = program_num
 
class FMSynthesizer:
    def __init__(self, sample_rate=44100, block_size=1024, max_polyphony=24):
        self.sample_rate = sample_rate
        self.block_size = block_size
        self.max_polyphony = max_polyphony
        self.active_notes = {}
        self.note_buffer = np.zeros(block_size)
 
    def fm_synth(self, note, velocity, duration):
        carrier_freq = 440 * 2**((note - 69) / 12)
        modulator_freq = carrier_freq * 1/2 + carrier_freq * 2/6 + carrier_freq * 4/10
        modulation_index = 0.5
 
        # Add a tiny bit of padding to avoid edge clicks
        duration += 0.01
 
        t = np.linspace(0, duration, int(self.sample_rate * duration), endpoint=False)
        modulator = np.sin(2 * np.pi * modulator_freq * t) * modulation_index
        carrier = np.sin(2 * np.pi * carrier_freq * t + modulator)
 
        # Simple envelope
        attack_time = 0.05
        release_time = 0.05
        attack_samples = int(self.sample_rate * attack_time)
        release_samples = int(self.sample_rate * release_time)
 
        envelope = np.ones_like(t)
        # Linear attack and release
        if attack_samples > 0:
            envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
        if release_samples > 0:
            envelope[-release_samples:] = np.linspace(1, 0, release_samples)
 
        return carrier * envelope * (velocity / 127)
 
    def note_on(self, note_num, velocity, start_time, program_num):
        if len(self.active_notes) >= self.max_polyphony:
            oldest_time = float('inf')
            oldest_note = None
            for num, note in self.active_notes.items():
                if note.start_time < oldest_time:
                    oldest_time = note.start_time
                    oldest_note = num
            if oldest_note is not None:
                del self.active_notes[oldest_note]
 
        new_note = Note(note_num, velocity, start_time, program_num)
        self.active_notes[note_num] = new_note
 
    def note_off(self, note_num, end_time):
        if note_num in self.active_notes:
            self.active_notes[note_num].end_time = end_time
 
    def get_audio_block(self):
        self.note_buffer.fill(0)
 
        notes_to_process = list(self.active_notes.items())
        finished_notes = []
 
        for note_num, note in notes_to_process:
            if note_num not in self.active_notes:
                continue
 
            if note.samples is None:
                duration = 1
                if note.end_time is not None:
                    duration = max(1, note.end_time - note.start_time)
                try:
                    note.samples = self.fm_synth(note.note_num, note.velocity, duration)
                except Exception as e:
                    print(f"Error generating samples for note {note_num}: {e}")
                    finished_notes.append(note_num)
                    continue
 
            remaining = len(note.samples) - note.position
            if remaining <= 0:
                finished_notes.append(note_num)
                continue
 
            samples_to_add = min(remaining, self.block_size)
            try:
                self.note_buffer[:samples_to_add] += \
                    note.samples[note.position:note.position + samples_to_add]
                note.position += samples_to_add
            except Exception as e:
                print(f"Error mixing samples for note {note_num}: {e}")
                finished_notes.append(note_num)
 
        for note_num in finished_notes:
            self.active_notes.pop(note_num, None)
 
        # Gentle limiting to prevent harsh clipping
        max_val = np.max(np.abs(self.note_buffer))
        if max_val > 1.0:
            self.note_buffer = self.note_buffer / (max_val * 1.2)  # Add a tiny headroom
 
        return self.note_buffer
 
def play_midi_file(midi_path, selected_instruments=None):
    midi = mido.MidiFile(midi_path)
    synth = FMSynthesizer()
    program_changes = {}
 
    def audio_callback(outdata, frames, time, status):
        try:
            block = synth.get_audio_block()
            outdata[:, 0] = block
        except Exception as e:
            print(f"Error in audio callback: {e}")
            outdata.fill(0)
 
    with sd.OutputStream(channels=1, 
                        callback=audio_callback,
                        samplerate=synth.sample_rate,
                        blocksize=synth.block_size):
 
        start_time = time.time()
        current_time = 0
 
        for msg in midi:
            current_time += msg.time
            wait_time = start_time + current_time - time.time()
            if wait_time > 0:
                time.sleep(wait_time)
 
            try:
                if msg.type == 'program_change':
                    program_changes[msg.channel] = msg.program
                    print(f"Program change: Channel {msg.channel} -> Program {msg.program}")
                elif msg.type == 'note_on':
                    if msg.velocity > 0:
                        if selected_instruments is None or program_changes.get(msg.channel) in selected_instruments:
                            synth.note_on(msg.note, msg.velocity, current_time, program_changes.get(msg.channel))
                    else:
                        synth.note_off(msg.note, current_time)
                elif msg.type == 'note_off':
                    synth.note_off(msg.note, current_time)
            except Exception as e:
                print(f"Error processing MIDI message: {e}")
 
        time.sleep(2)
 
if __name__ == "__main__":
    play_midi_file("C:/temp/canyon.mid", selected_instruments=None)
    # Example: Play only piano (Program 1) and electric piano (Program 5), [1, 5]
    #play_midi_file("C:/Users/unapa/Downloads/DXdiag.mid", selected_instruments=[33, 78, 16, 18, 53, 27, 29, 30])

mono synth

import mido  # For MIDI parsing
import numpy as np  # For waveform synthesis
import sounddevice as sd  # For real-time audio playback
 
def read_midi(file_path):
    midi = mido.MidiFile(file_path)
    return midi
 
def fm_synth(note, velocity, duration, sample_rate=44100):
    # Example parameters for FM synthesis
    carrier_freq = 440 * 2**((note - 69) / 12)  # MIDI note to frequency
    modulator_freq = carrier_freq * 2
    modulation_index = 0.33
 
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    modulator = np.sin(2 * np.pi * modulator_freq * t) * modulation_index
    carrier = np.sin(2 * np.pi * carrier_freq * t + modulator)
 
    return carrier * velocity / 127
 
def process_piano_track(midi):
    sample_rate = 44100
    for track in midi.tracks:
        for msg in track:
            if msg.type == 'note_on' and msg.velocity > 0:  # Note on
                duration = msg.time / midi.ticks_per_beat  # Estimate duration from MIDI timing
                note_audio = fm_synth(msg.note, msg.velocity, duration, sample_rate)
 
                # Play the synthesized audio for the note
                sd.play(note_audio, samplerate=sample_rate)
                sd.wait()  # Wait until the audio is finished playing
 
midi = read_midi("C:/temp/canyon.mid")
process_piano_track(midi)
sintetizador_fm_para_archivos_midi_en_python.1736714505.txt.gz · Last modified: 2025/01/12 20:41 by oso