MeltySynth

repository·main·Indexed 42 days ago

https://github.com/sinshu/meltysynth

A lightweight SoundFont synthesizer written in C# for MIDI music playback in .NET applications. It generates PCM waveforms compatible with various audio drivers including NAudio, OpenTK, Silk.NET, CSCore, DotFeather, DrippyAL, FNA.NET, MonoGame, and Raylib-cs.

Tokens
12.5K
Snippets
22
Records
23
Agent score
53%

What's inside MeltySynth

  1. Configure MidiFileSequencer playback speed and message filtering

    main

    The MidiFileSequencer allows for real-time adjustments and message interception via the OnSendMessage delegate.

    Change Playback Speed

    Set the Speed property (e.g., 1.5F) to adjust playback tempo. Note that when calculating buffer sizes, you must divide the expected duration by the speed.

    Filter or Modify MIDI Messages

    Use the OnSendMessage delegate to intercept MIDI messages. This allows you to:

    • Mute tracks: Return false (or skip processing) if the channel matches a specific criteria (e.g., channel 9 for percussion).
    • Change instruments: Intercept Program Change commands (0xC0) and modify the data1 parameter to force a different instrument.
    // Example: Muting percussion (channel 9)
    sequencer.OnSendMessage = (synthesizer, channel, command, data1, data2) =>
    {
        if (channel == 9) return;
        synthesizer.ProcessMidiMessage(channel, command, data1, data2);
    };
    
    // Example: Forcing all instruments to electric guitar (30)
    sequencer.OnSendMessage = (synthesizer, channel, command, data1, data2) =>
    {
        if (command == 0xC0) data1 = 30;
        synthesizer.ProcessMidiMessage(channel, command, data1, data2);
    };
  2. Important: MeltySynth Thread Safety Warning

    main

    ⚠️ CAUTION

    MeltySynth does not provide thread safety. If you intend to send MIDI notes (e.g., via NoteOn) in one thread and render the waveform (via Render) in another, you must implement your own synchronization to ensure these methods are not called simultaneously.

  3. Implement a MIDI player with DrippyAL and MeltySynth

    main

    To implement a MIDI player using the DrippyAL library backed by MeltySynth, you need to initialize a MidiPlayer with a SoundFont file, set up an AudioDevice, and pipe the player's audio blocks into an AudioStream.

    Key steps:

    1. Initialize MidiPlayer with the path to a .sf2 SoundFont.
    2. Create an AudioDevice and an AudioStream (specifying sample rate and channels).
    3. Call stream.Play() passing the midiPlayer.FillBlock delegate to start the audio processing loop.
    4. Load a MIDI file using midiPlayer.Play(midiFile, true) to begin playback.

    Note: The true parameter in midiPlayer.Play typically indicates whether to loop or handle specific playback flags depending on the DrippyAL implementation.

    using System;
    using DrippyAL;
    using MeltySynth;
    
    class Program
    {
        static void Main()
        {
            var midiPlayer = new MidiPlayer("TimGM6mb.sf2");
    
            using (var device = new AudioDevice())
            using (var stream = new AudioStream(device, 44100, 2))
            {
                stream.Play(midiPlayer.FillBlock);
    
                // Load the MIDI file.
                var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
                // Play the MIDI file.
                midiPlayer.Play(midiFile, true);
    
                // Wait until any key is pressed.
                Console.ReadKey();
            }
        }
    }
  4. Implement a MIDI player using Sokol_csharp and MeltySynth

    main

    To implement a MIDI player using Sokol_csharp as the audio backend, you must initialize a Synthesizer with a SoundFont, create a MidiFileSequencer linked to that synthesizer, and set up a Sokol audio callback.

    Key steps:

    1. Initialize MeltySynth: Create a Synthesizer instance specifying your .sf2 file and sample rate. Create a MidiFileSequencer passing the synthesizer instance.
    2. Configure Sokol Audio: Define an Audio.Desc object. Set SampleRate to match the synthesizer and provide a StreamCb (stream callback) using an unmanaged function pointer.
    3. Audio Callback: In the StreamCb function (marked with [UnmanagedCallersOnly]), call sequencer.RenderInterleaved to fill the audio buffer. Use a lock (mutex) to ensure thread safety between the audio callback and the main thread when calling playback commands.
    4. Playback Control: Use sequencer.Play(midiFile, loop) to start playback and sequencer.Stop() to halt it.
    using System;
    using System.Runtime.InteropServices;
    using Sokol;
    using MeltySynth;
    
    class Program
    {
        static Synthesizer synthesizer = new Synthesizer("TimGM6mb.sf2", 44100);
        static MidiFileSequencer sequencer = new MidiFileSequencer(synthesizer);
        static object mutex = new object();
    
        unsafe static void Main()
        {
            var desc = new Audio.Desc();
            desc.SampleRate = synthesizer.SampleRate;
            desc.NumChannels = 2;
            desc.StreamCb = (delegate* unmanaged<float*, int, int, void>)&ProcessAudio;
    
            Audio.Setup(in desc);
    
            if (!Audio.Isvalid())
            {
                throw new Exception("Failed to setup audio.");
            }
    
            // Load the MIDI file.
            var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
            // Play the MIDI file.
            Play(midiFile, true);
    
            // Wait until any key is pressed.
            Console.ReadKey();
    
            Audio.Shutdown();
        }
    
        [UnmanagedCallersOnly]
        unsafe static void ProcessAudio(float* buffer, int numFrames, int numChannels)
        {
            lock (mutex)
            {
                sequencer.RenderInterleaved(new Span<float>(buffer, 2 * numFrames));
            }
        }
    
        static void Play(MidiFile midiFile, bool loop)
        {
            lock (mutex)
            {
                sequencer.Play(midiFile, loop);
            }
        }
    
        static void Stop()
        {
            lock (mutex)
            {
                sequencer.Stop();
            }
        }
    }
  5. Use MeltySynth with Silk.NET and OpenAL

    main

    To implement a MIDI player using Silk.NET's OpenAL bindings, you must initialize the OpenAL context and device before creating a MidiPlayer. The MidiPlayer requires an instance of the AL API and a path to a SoundFont file (e.g., .sf2).

    Follow these steps:

    1. Initialize the ALContext and AL APIs using ALContext.GetApi(true) and AL.GetApi(true).
    2. Open a device and create an OpenAL context.
    3. Make the context current using alc.MakeContextCurrent(context).
    4. Instantiate MidiPlayer with the al instance and your SoundFont path.
    5. Load a MidiFile and call player.Play(midiFile, true) to begin playback.
    6. Ensure you properly destroy the context and close the device when finished.
    using System;
    using Silk.NET.OpenAL;
    using MeltySynth;
    
    class Program
    {
        unsafe static void Main()
        {
            using (var alc = ALContext.GetApi(true))
            using (var al = AL.GetApi(true))
            {
                var device = alc.OpenDevice("");
                var context = alc.CreateContext(device, null);
                alc.MakeContextCurrent(context);
                al.GetError();
    
                using (var player = new MidiPlayer(al, "TimGM6mb.sf2"))
                {
                    // Load the MIDI file.
                    var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
                    // Play the MIDI file.
                    player.Play(midiFile, true);
    
                    // Wait until any key is pressed.
                    Console.ReadKey();
                }
    
                alc.DestroyContext(context);
                alc.CloseDevice(device);
            }
        }
    }
  6. Implement a MIDI player using Raylib-cs and MeltySynth

    main

    To build a MIDI player with Raylib-cs, you must integrate MeltySynth's Synthesizer and MidiFileSequencer into the Raylib audio stream update loop.

    1. Initialize Audio: Use Raylib.InitAudioDevice() and Raylib.LoadAudioStream() to create a stream with your desired sample rate and bit depth.
    2. Setup MeltySynth: Create a Synthesizer instance with a SoundFont file and a MidiFileSequencer that uses that synthesizer.
    3. The Audio Loop: Inside your main application loop, check if the audio stream needs more data using Raylib.IsAudioStreamProcessed(stream). If it does, use sequencer.RenderInterleavedInt16(buffer) to fill a buffer with audio data, then pass that buffer to Raylib.UpdateAudioStream().

    Note: The RenderInterleavedInt16 method expects a buffer sized for interleaved channels (e.g., 2 * bufferSize for stereo).

    using System;
    using Raylib_cs;
    using MeltySynth;
    
    class Program
    {
        static int sampleRate = 44100;
        static int bufferSize = 4096;
    
        unsafe static void Main()
        {
            Raylib.InitWindow(800, 450, "MIDI Player");
    
            Raylib.InitAudioDevice();
            Raylib.SetAudioStreamBufferSizeDefault(bufferSize);
    
            var stream = Raylib.LoadAudioStream((uint)sampleRate, 16, 2);
            var buffer = new short[2 * bufferSize];
    
            Raylib.PlayAudioStream(stream);
    
            var synthesizer = new Synthesizer("TimGM6mb.sf2", sampleRate);
            var sequencer = new MidiFileSequencer(synthesizer);
            var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
            sequencer.Play(midiFile, true);
    
            Raylib.SetTargetFPS(60);
    
            while (!Raylib.WindowShouldClose())
            {
                if (Raylib.IsAudioStreamProcessed(stream))
                {
                    sequencer.RenderInterleavedInt16(buffer);
                    fixed (short* p = buffer)
                    {
                        Raylib.UpdateAudioStream(stream, p, bufferSize);
                    }
                }
    
                Raylib.BeginDrawing();
                Raylib.ClearBackground(Color.LIGHTGRAY);
                Raylib.DrawText("MUSIC SHOULD BE PLAYING!", 255, 200, 20, Color.DARKGRAY);
                Raylib.EndDrawing();
            }
    
            Raylib.CloseWindow();
        }
    }
  7. Implement a MIDI player using FNA.NET

    main

    To use MeltySynth within an FNA.NET application, you can integrate the MidiPlayer and MidiFile classes into your Game lifecycle.

    1. Initialize MidiPlayer: In LoadContent, instantiate MidiPlayer by providing the path to a SoundFont file (e.g., .sf2).
    2. Load MidiFile: Instantiate MidiFile with the path to your MIDI file.
    3. Playback Control: In the Update loop, check the midiPlayer.State. If it is SoundState.Stopped, call midiPlayer.Play(midiFile, true) to start playback. The true parameter indicates whether the file should loop.
    4. Cleanup: Ensure you call midiPlayer.Dispose() in UnloadContent to release resources.
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Audio;
    using Microsoft.Xna.Framework.Graphics;
    using MeltySynth;
    
    public class Game1 : Game
    {
        private GraphicsDeviceManager graphics;
    
        private MidiPlayer midiPlayer;
        private MidiFile midiFile;
    
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
        }
    
        protected override void LoadContent()
        {
            // Initialize player with a SoundFont
            midiPlayer = new MidiPlayer("TimGM6mb.sf2");
            // Load the MIDI file
            midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
        }
    
        protected override void UnloadContent()
        {
            // Clean up resources
            midiPlayer.Dispose();
        }
    
        protected override void Update(GameTime gameTime)
        {
            // Play the file if it is stopped (looping enabled)
            if (midiPlayer.State == SoundState.Stopped)
            {
                midiPlayer.Play(midiFile, true);
            }
    
            base.Update(gameTime);
        }
    
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }
    }
  8. Synthesize a MIDI file with MidiFileSequencer

    main

    To play a MIDI file, use MidiFileSequencer. It takes a Synthesizer instance and a MidiFile. You can then call Play to prepare the sequence and Render to generate the waveform based on the file's duration.

    // Create the synthesizer.
    var sampleRate = 44100;
    var synthesizer = new Synthesizer("TimGM6mb.sf2", sampleRate);
    
    // Read the MIDI file.
    var midiFile = new MidiFile("flourish.mid");
    var sequencer = new MidiFileSequencer(synthesizer);
    sequencer.Play(midiFile, false);
    
    // The output buffer.
    var left = new float[(int)(sampleRate * midiFile.Length.TotalSeconds)];
    var right = new float[(int)(sampleRate * midiFile.Length.TotalSeconds)];
    
    // Render the waveform.
    sequencer.Render(left, right);
  9. Implement a MIDI player using SDL2#

    main

    To use MeltySynth as a MIDI player backed by SDL2#, you must initialize the SDL2# audio subsystem and provide MeltySynth's MidiPlayer.ProcessAudio method as the audio callback for the SDL audio device.

    Follow these steps:

    1. Initialize the SDL audio subsystem using SDL.SDL_InitSubSystem(SDL.SDL_INIT_AUDIO).
    2. Configure an SDL.SDL_AudioSpec object, setting the callback property to player.ProcessAudio.
    3. Open an audio device using SDL.SDL_OpenAudioDevice.
    4. Unpause the device with SDL.SDL_PauseAudioDevice(device, 0) to begin playback.
    5. Load a MIDI file using new MidiFile(path) and pass it to player.Play(midiFile, true).
    6. Clean up by closing the audio device and quitting the SDL audio subsystem.
    using System;
    using SDL2;
    using MeltySynth;
    
    class Program
    {
        static void Main()
        {
            // 1. Initialize MeltySynth player with a SoundFont
            var player = new MidiPlayer("TimGM6mb.sf2");
    
            // 2. Initialize SDL2# Audio
            SDL.SDL_InitSubSystem(SDL.SDL_INIT_AUDIO);
    
            // 3. Configure Audio Specifications
            var desired = new SDL.SDL_AudioSpec();
            desired.freq = 44100;
            desired.format = SDL.AUDIO_F32;
            desired.channels = 2;
            desired.samples = 4096;
            desired.callback = player.ProcessAudio; // MeltySynth callback
    
            var obtained = new SDL.SDL_AudioSpec();
    
            // 4. Open Audio Device
            var name = SDL.SDL_GetAudioDeviceName(0, 0);
            var device = SDL.SDL_OpenAudioDevice(name, 0, ref desired, out obtained, (int)SDL.SDL_AUDIO_ALLOW_ANY_CHANGE);
            if (device == 0)
            {
                throw new Exception("Failed to open the audio device.");
            }
    
            // 5. Start Playback
            SDL.SDL_PauseAudioDevice(device, 0);
    
            var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
            player.Play(midiFile, true);
    
            Console.ReadKey();
    
            // 6. Cleanup
            SDL.SDL_CloseAudioDevice(device);
            SDL.SDL_QuitSubSystem(SDL.SDL_INIT_AUDIO);
        }
    }
  10. Implement a MIDI player using Raylib-CsLo and MeltySynth

    main

    To build a MIDI player using Raylib-CsLo for audio output and MeltySynth for synthesis, follow this pattern:

    1. Initialize Raylib Audio: Initialize the window and audio device. Set a default buffer size and load an audio stream with the desired sample rate (e.g., 44100 Hz) and bit depth (16-bit, 2 channels).
    2. Setup MeltySynth: Create a Synthesizer instance by providing a SoundFont file path and the sample rate. Create a MidiFileSequencer using that synthesizer.
    3. Load MIDI: Load a MidiFile from a file path and pass it to sequencer.Play(midiFile, true).
    4. The Audio Loop: In your main application loop, check if the audio stream needs more data using Raylib.IsAudioStreamProcessed(stream). If it does:
      • Use sequencer.RenderInterleavedInt16(buffer) to fill a short array buffer with synthesized audio data.
      • Use Raylib.UpdateAudioStream(stream, buffer.AsSpan(), bufferSize) to push that data to the audio hardware.
    5. Cleanup: Close the Raylib window when the application exits.
    using System;
    using Raylib_CsLo;
    using MeltySynth;
    
    class Program
    {
        static int sampleRate = 44100;
        static int bufferSize = 4096;
    
        static void Main()
        {
            Raylib.InitWindow(800, 450, "MIDI Player");
    
            Raylib.InitAudioDevice();
            Raylib.SetAudioStreamBufferSizeDefault(bufferSize);
    
            var stream = Raylib.LoadAudioStream((uint)sampleRate, 16, 2);
            var buffer = new short[2 * bufferSize];
    
            Raylib.PlayAudioStream(stream);
    
            var synthesizer = new Synthesizer("TimGM6mb.sf2", sampleRate);
            var sequencer = new MidiFileSequencer(synthesizer);
            var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
            sequencer.Play(midiFile, true);
    
            Raylib.SetTargetFPS(60);
    
            while (!Raylib.WindowShouldClose())
            {
                if (Raylib.IsAudioStreamProcessed(stream))
                {
                    sequencer.RenderInterleavedInt16(buffer);
                    Raylib.UpdateAudioStream(stream, buffer.AsSpan(), bufferSize);
                }
    
                Raylib.BeginDrawing();
                Raylib.ClearBackground(Raylib.LIGHTGRAY);
                Raylib.DrawText("MUSIC SHOULD BE PLAYING!", 255, 200, 20, Raylib.DARKGRAY);
                Raylib.EndDrawing();
            }
    
            Raylib.CloseWindow();
        }
    }
  11. Implement a MIDI player using SoundFlow and MeltySynth

    main

    To implement a MIDI player, you can use the SoundFlow library with a MiniAudio backend. The process involves initializing a MiniAudioEngine with a specific sample rate and playback capability, creating a MidiPlayer using a SoundFont file (.sf2), and adding that player as a component to the Mixer.Master. Once configured, you can play a MIDI file using the player.Play() method.

    Key Steps:

    1. Initialize MiniAudioEngine with the desired sample rate (e.g., 44100) and Capability.Playback.
    2. Instantiate MidiPlayer providing the path to your SoundFont and the engine's sample rate.
    3. Register the player with the global mixer via Mixer.Master.AddComponent(player).
    4. Load a MIDI file and call player.Play(midiFile, true) to begin playback.
    using System;
    using SoundFlow.Backends.MiniAudio;
    using SoundFlow.Components;
    using SoundFlow.Enums;
    using MeltySynth;
    
    class Program
    {
        static void Main()
        {
            using (var engine = new MiniAudioEngine(44100, Capability.Playback))
            {
                var player = new MidiPlayer("TimGM6mb.sf2", engine.SampleRate);
                Mixer.Master.AddComponent(player);
    
                // Load the MIDI file.
                var midiFile = new MidiFile(@"C:\Windows\Media\flourish.mid");
    
                // Play the MIDI file.
                player.Play(midiFile, true);
    
                // Wait until any key is pressed.
                Console.ReadKey();
            }
        }
    }