Music Synthesis

Symusic provides a built-in synthesizer that can render MIDI files to audio using SoundFont (SF2/SF3) technology. The synthesizer is based on the prestosynth library and offers a simple yet powerful interface for audio rendering.

Basic Synthesis

The simplest way to synthesize a score is to use the Synthesizer class:

from symusic import Score, Synthesizer, BuiltInSF3, dump_wav

# Load a MIDI file
score = Score("path/to/file.mid")

# Create a synthesizer with default settings
synthesizer = Synthesizer(
    sf_path=BuiltInSF3.MuseScoreGeneral().path(download=True),
    sample_rate=44100,
    quality=4 # Default quality setting
)

# Render the score to audio (returns an AudioData object)
audio_data = synthesizer.render(score, stereo=True)

# Save the audio to a WAV file
dump_wav("output.wav", audio_data)

Built-in SoundFonts

Symusic provides several built-in SoundFonts that you can use without having to find and download your own. These are accessed through the BuiltInSF3 class:

from symusic import BuiltInSF3

# Available built-in SoundFonts
sf_path = BuiltInSF3.MuseScoreGeneral().path(download=True)  # MuseScore General (default)
sf_path = BuiltInSF3.FluidR3Mono().path(download=True)       # FluidR3 Mono
sf_path = BuiltInSF3.FluidR3().path(download=True)           # FluidR3 GM

The path() method will return the path to the SoundFont file, downloading it if necessary when the download parameter is set to True.

Synthesizer Parameters

The Synthesizer constructor takes several parameters that control the quality and characteristics of the audio output:

synthesizer = Synthesizer(
    sf_path,            # Path to the SoundFont file (string or Path)
    sample_rate,        # Sample rate in Hz (default: 44100)
    quality             # Synthesis quality (0-6, default: 0 in factory, 4 often good)
)

Working with Audio Data

The render method returns an AudioData object, which contains the synthesized audio data. You can manipulate this data directly or save it to a WAV file:

# Render a score
audio_data = synthesizer.render(score, stereo=True)

# Get audio properties
sample_rate = audio_data.sample_rate
num_channels = audio_data.channels
num_frames = audio_data.frames

# Access the raw audio data as a NumPy array
import numpy as np
np_array = np.array(audio_data)  # Shape: [frames, channels]

# Manipulate the audio (example: amplify by 1.5)
np_array *= 1.5

# Save to a WAV file
dump_wav("output.wav", audio_data)

Time Unit Considerations

The synthesizer works most efficiently with scores in Second time unit. When you pass a score in Tick or Quarter time unit, it will be automatically converted to Second internally:

# Loading a score in any time unit works
score_tick = Score("file.mid", ttype="tick")
score_quarter = Score("file.mid", ttype="quarter")
score_second = Score("file.mid", ttype="second")

# All of these will produce the same audio
audio1 = synthesizer.render(score_tick, stereo=True)
audio2 = synthesizer.render(score_quarter, stereo=True)
audio3 = synthesizer.render(score_second, stereo=True)

Custom Synthesis Workflow

For more control over the synthesis process, you might want to modify the score before rendering:

from symusic import Score, Synthesizer, BuiltInSF3, dump_wav

# Load a score
score = Score("input.mid")

# Modify the score (example: transpose up by 2 semitones)
transposed_score = score.shift_pitch(2)

# Modify tempo (example: increase tempo by 20%)
for tempo in transposed_score.tempos:
    tempo.qpm *= 1.2

# Create a synthesizer
synthesizer = Synthesizer(
    sf_path=BuiltInSF3.MuseScoreGeneral().path(download=True),
    sample_rate=48000,
)

# Render to audio
audio_data = synthesizer.render(transposed_score, stereo=True)

# Save to WAV
dump_wav("output.wav", audio_data)

Handling Multiple Scores

You can reuse the same synthesizer instance to render multiple scores:

from symusic import Score, Synthesizer, BuiltInSF3, dump_wav

# Create a synthesizer once
synthesizer = Synthesizer(
    sf_path=BuiltInSF3.MuseScoreGeneral().path(download=True),
    sample_rate=44100,
)

# Process multiple files
for i in range(1, 4):
    # Load score
    score = Score(f"input{i}.mid")

    # Render to audio
    audio = synthesizer.render(score, stereo=True)

    # Save to WAV
    dump_wav(f"output{i}.wav", audio)

Advanced: Audio Postprocessing

You can perform post-processing on the audio data using NumPy or other audio processing libraries:

import numpy as np
from scipy import signal
from symusic import Score, Synthesizer, BuiltInSF3, dump_wav

# Load and render
score = Score("input.mid")
synthesizer = Synthesizer(
    sf_path=BuiltInSF3.MuseScoreGeneral().path(download=True),
    sample_rate=44100,
)
audio_data = synthesizer.render(score, stereo=True)

# Convert to NumPy array
audio_array = np.array(audio_data)  # Shape: [frames, channels]

# Example: Apply a low-pass filter (cutoff at 4000 Hz)
b, a = signal.butter(4, 4000 / (audio_data.sample_rate / 2), 'low')
filtered_audio = signal.filtfilt(b, a, audio_array, axis=0)

# Example: Normalize the audio
max_val = np.max(np.abs(filtered_audio))
if max_val > 0:
    normalized_audio = filtered_audio / max_val

# Convert back to AudioData (implementation depends on your use case)
# This is a simplified example - you'd need to create a new AudioData object
# with the processed samples

# Save the processed audio (assuming you have a way to convert back to AudioData)
dump_wav("processed_output.wav", audio_data)

Performance Considerations

Synthesis can be CPU-intensive, especially with high-quality settings. Here are some tips for optimizing performance:

  1. Use a lower quality setting for real-time or batch processing
  2. Use a lower sample_rate if high fidelity isn't required
  3. Consider rendering shorter segments of music if working with very long pieces
  4. For batch processing of many files, consider using multiprocessing

Troubleshooting

Common Issues

  1. SoundFont not found: Make sure the SoundFont path is correct and the file exists

    # Check if SoundFont exists before using it
    import os
    sf_path = BuiltInSF3.MuseScoreGeneral().path(download=True)
    assert os.path.exists(sf_path), f"SoundFont not found at {sf_path}"
    
  2. Audio quality issues: Try increasing the quality parameter

    synthesizer = Synthesizer(sf_path, sample_rate=44100, quality=5)  # Higher quality
    
  3. Memory errors: For very long or complex MIDI files, you might run into memory issues

    # Process the score in smaller chunks (example implementation)
    def process_in_chunks(score, chunk_size=60):  # 60 seconds per chunk
        result_chunks = []
        total_duration = score.end()
        for start in range(0, int(total_duration), chunk_size):
            end = min(start + chunk_size, total_duration)
            chunk = score.clip(start, end)
            chunk_audio = synthesizer.render(chunk, stereo=True)
            result_chunks.append(np.array(chunk_audio))
    
        # Combine chunks (simplified example)
        return np.concatenate(result_chunks, axis=0)