Scheduling a Midi event after a certain amount of ms

I have a Midi-OX log file in text format like this:

00004AD6 1 2 90 1D 7F 1 F 1 Note On
00004BB3 1 2 80 1D 40 1 F 1 Note Off
00004CA2 1 2 90 1D 7F 1 F 1 Note On
00004D2D 1 2 80 1D 40 1 F 1 Note Off

The first number (8 hex digits) is the time in ms when the midi message should be launched.
Is there a way to achieve this using an SDL library function or do I have to set up my own unix time event function?

SDL provides SDL_Delay (same in SDL2). But based on your other posts, I think your question is really about RtMidi, since you’re using that for MIDI I/O. SDL does not provide any meaningful MIDI support. I haven’t used RtMidi (for now I use PortMidi instead), but it almost certainly provides some infrastructure for message timing.

There’s also the SDL_Timer api SDL_AddTimer
Never mind, the timer api would be OK for a single track, or even a dozen or more tracks, but I suspect that eventually it fails to scale well. (So I would not recommend it for songs with many overlapping instruments).

I’m seeing SDL2_Mixer has Midi support when run along with Fluidsynth and/or Timidity.
Midi isn’t quite my thing, so I’m a bit out of my depth.
You likely need to set a timidity config file, and then load sound fonts:
https://wiki.libsdl.org/SDL2_mixer/Mix_SetTimidityCfg
https://wiki.libsdl.org/SDL2_mixer/Mix_SetSoundFonts

I don’t know if you need to build SDL_Mixer from source after installing FluidSynth/Timidity to ensure that the libraries are built with the proper setup. I’m trying to find a good tutorial, but not hitting gold at the moment.

Midi support seems to have been skipped in SDL3_Mixer and is now a separately maintained library, here’s the quote:

Native MIDI has been removed. We have ripped this piece out of SDL2_mixer and packaged it as a separate library that can be used alongside SDL3_mixer, or without SDL_mixer at all: GitHub - libsdl-org/SDL_native_midi: Cross-platform code to play MIDI files through system facilities.

SDL_native_midi does not have timidity config and sound fonts

As far as I can tell, just init the lib, load the music, and play. Here’s the test code from that github link showing a program that opens and plays a list of midi files:

#include <SDL3_native_midi/SDL_native_midi.h>

int main(int argc, char **argv)
{
    int i;

    if (argc == 1) {
        SDL_Log("USAGE: %s [file1.mid] [file2.mid] ...", argv[0]);
        return 1;
    }

    if (!NativeMidi_Init()) {
        SDL_Log("NativeMidi_Init failed: %s", SDL_GetError());
        return 1;
    }

    for (i = 1; i < argc; i++) {
        const char *path = argv[i];
        NativeMidi_Song *song;

        SDL_Log("Loading song '%s' ...", path);
        song = NativeMidi_LoadSong(path);
        if (!song) {
            SDL_Log("Failed to load '%s': %s", path, SDL_GetError());
            continue;
        }

        SDL_Log("Starting song '%s' ...", path);
        NativeMidi_Start(song, 0);

        while (NativeMidi_Active()) {
            SDL_Delay(300);
        }

        SDL_Log("Song '%s' done", path);
        NativeMidi_DestroySong(song);
    }

    SDL_Log("Quitting...");
    NativeMidi_Quit();

    return 0;
}

Hi,

thanks for your comments.Your example is fine but it doesn’t cover my demands. It plays a Midi file which is a different content and format as my cited Midi-Ox file. I‘m presently at the Unix usleep(us) function. This allows me to wait a number of microseconds between writing the midi messages, while they are read from the input file.

1 Like

I’m afraid there is widespread misunderstanding about the difference between a MIDI file and sending (or receiving) MIDI serial messages in real time. Many people have only encountered MIDI in the former context, even though MIDI messages came first historically.

Hi, I have an idea that might solve your problem. I wrote some code that could serve as an example for you. To send the message via USB, the program creates a stream that uses SDL functions to open a connection with the USB driver. Then the transfer class starts a thread that will send each instruction in succession, respecting the set deadlines.

Here is the code:
OutputStream.hpp

#pragma once

#include <cstdint>
#include <vector>

class OutputStream {
public:
    virtual ~OutputStream() = default;

    virtual bool open() = 0;
    virtual void close() = 0;

    // Envoi d’un buffer brut
    virtual bool send(const uint8_t* data, size_t size) = 0;

    bool send(const std::vector<uint8_t>& data) {
        return send(data.data(), data.size());
    }
};

OutputStreamUSB.hpp

#pragma once

#include "OutputStream.hpp"
#include <SDL3/SDL_hidapi.h>

class OutputStreamUSB : public OutputStream {
private:
    SDL_hid_device* device = nullptr;
    unsigned short vendor_id;
    unsigned short product_id;

public:
    OutputStreamUSB(unsigned short vid, unsigned short pid)
        : vendor_id(vid), product_id(pid) {}

    bool open() override {
        if (SDL_hid_init() != 0)
            return false;

        device = SDL_hid_open(vendor_id, product_id, nullptr);
        return device != nullptr;
    }

    void close() override {
        if (device) {
            SDL_hid_close(device);
            device = nullptr;
        }
        SDL_hid_exit();
    }

    bool send(const uint8_t* data, size_t size) override {
        if (!device) return false;

        int result = SDL_hid_write(device, data, size);
        return result >= 0;
    }
};

TransfertFile.hpp

#pragma once

#include <SDL3/SDL.h>
#include <format>
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <thread>
#include <atomic>

// --- Classe de base TransfertFile ---
class TransfertFile {
protected:
    std::thread worker;
    std::atomic<bool> running{false};
    std::vector<MidiCommand> commands;

    void postEvent(Uint32 type, int current, int total) {
        SDL_Event event;
        SDL_zero(event);
        event.type = type;
        event.user.code = current; // Utilisation de code pour l'index
        event.user.data1 = (void*)(uintptr_t)total; // Utilisation de data1 pour le total
        SDL_PushEvent(&event);
    }

public:
    virtual ~TransfertFile() {
        stop();
    }

    virtual bool loadFile(const std::string& path) = 0;

    void start() {
        if (running) return;
        running = true;
        worker = std::thread(&TransfertFile::run, this);
    }

    void stop() {
        running = false;
        if (worker.joinable()) worker.join();
    }

    virtual void run() = 0;
};

TransfertMidiOx.hpp

#pragma once

#include "TransfertFile.hpp"
#include "OutputStream.hpp"

// --- Définition des événements personnalisés ---
Uint32 FILE_TRANSFERT_IN_PROGRESS = 0;
Uint32 FILE_TRANSFERT_FINISH = 0;
Uint32 FILE_TRANSFERT_FAILED = 0;

struct MidiCommand {
    uint32_t timestamp; // en ms
    std::string data;   // Le reste de la ligne
};

// --- Classe dérivée TransfertMidiOx ---
class TransfertMidiOx : public TransfertFile {
private:
    OutputStream& stream;

public:
    TransfertMidiOx(OutputStream &s) : stream(s) {}

    bool loadFile(const std::string& path) override {
        std::ifstream file(path);
        if (!file.is_open()) return false;

        std::string line;
        while (std::getline(file, line)) {
            if (line.empty()) continue;
            
            std::stringstream ss(line);
            std::string hexTime;
            ss >> hexTime; // Lit les 8 premiers caractĂšres hex

            try {
                uint32_t ts = std::stoul(hexTime, nullptr, 16);
                std::string rest;
                std::getline(ss, rest);
                commands.push_back({ts, rest});
            } catch (...) { continue; }
        }
        return !commands.empty();
    }

    void run() override {
        if (commands.empty()) return;

        uint64_t startTime = SDL_GetTicks();
        int total = static_cast<int>(commands.size());

        for (int i = 0; i < total; ++i) {
            if (!running) break;

            uint64_t now = SDL_GetTicks() - startTime;
            if (now < commands[i].timestamp) {
                SDL_Delay(commands[i].timestamp - (uint32_t)now);
            }

            std::vector<uint8_t> packet = parseMidiLine(commands[i]);

            if (!stream.send(packet)) {
                postEvent(FILE_TRANSFERT_FAILED, i, total);
                stream.close();
                return;
            }

            postEvent(FILE_TRANSFERT_IN_PROGRESS, i + 1, total);
        }

        if (running) postEvent(FILE_TRANSFERT_FINISH, total, total);
        running = false;
    }

    std::vector<uint8_t> parseMidiLine(const MidiCommand& cmd) {
        std::stringstream ss(cmd.data);
        std::string byteStr;
        std::vector<uint8_t> result;

        for (int i = 0; i < 3 && ss >> byteStr; ++i) {
            try {
                result.push_back((uint8_t)std::stoul(byteStr, nullptr, 16));
            } catch (...) {
                break;
            }
        }

        return result;
    }
};

Main.cpp

#include <SDL3/SDL.h>
#include <format>
#include <iostream>
#include <string>
#include <map>
#include <memory>
#include "OutputStreamUSB.hpp"
#include "TransfertMidiOx.hpp"

constexpr int screenWidth{1024};
constexpr int screenHeight{768};
const char* screenTitle = "Midi-Ox transmitter";

SDL_Window* window{nullptr};
SDL_Renderer* renderer{nullptr};
std::unique_ptr<TransfertMidiOx> transmitter;
std::string midiFile = "";
int USBVendorID = 0;
int USBProductID = 0;
int progress = 0;
int failed_message = 0;

void listUSBDevices() {
    if (SDL_hid_init() != 0) {
        std::cerr << "HID init failed\n";
        return;
    }

    SDL_hid_device_info* devs = SDL_hid_enumerate(0x0, 0x0);
    SDL_hid_device_info* cur = devs;

    while (cur)
    {
        std::cout << "Device found:\n";
        std::cout << " VID: 0x" << std::hex << cur->vendor_id << "\n";
        std::cout << " PID: 0x" << std::hex << cur->product_id << "\n";

        if (cur->manufacturer_string)
            std::wcout << L" Manufacturer: " << cur->manufacturer_string << L"\n";

        if (cur->product_string)
            std::wcout << L" Product: " << cur->product_string << L"\n";

        std::cout << "-----------------------\n";

        cur = cur->next;
    }

    SDL_hid_free_enumeration(devs);
    SDL_hid_exit();
}

bool init() {
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) return false;

    // Enregistrement des événements
    FILE_TRANSFERT_IN_PROGRESS = SDL_RegisterEvents(3);
    FILE_TRANSFERT_FINISH = FILE_TRANSFERT_IN_PROGRESS + 1;
    FILE_TRANSFERT_FAILED = FILE_TRANSFERT_IN_PROGRESS + 2;

    if (!SDL_CreateWindowAndRenderer(screenTitle, screenWidth, screenHeight, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
        return false;
    }

    SDL_SetRenderVSync(renderer, 1);
    return true;
}

bool start() {
    auto usb = std::make_unique<OutputStreamUSB>(USBVendorID, USBProductID);
    transmitter = std::make_unique<TransfertMidiOx>(*usb);
    
    if (transmitter->loadFile(midiFile)) {
        transmitter->start();
        return true;
    }
    return false;
}

void loop() {
    bool quit{false};
    SDL_Event event;

    while (!quit) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT) {
                quit = true;
            } 
            else if (event.type == FILE_TRANSFERT_IN_PROGRESS) {
                int current = event.user.code;
                int total = (int)(uintptr_t)event.user.data1;
                progress = (current * 100) / total;
            } 
            else if (event.type == FILE_TRANSFERT_FINISH) {
                progress = 100;
            } 
            else if (event.type == FILE_TRANSFERT_FAILED) {
                failed_message = event.user.code;
            }
        }

        // --- Rendu ---
        SDL_SetRenderDrawColor(renderer, 20, 20, 25, 255);
        SDL_RenderClear(renderer);

        if (failed_message != 0) {
            SDL_SetRenderDrawColor(renderer, 255, 50, 50, 255);
            SDL_RenderDebugTextFormat(renderer, 10, 80, "Transmission failed at msg %d", failed_message);
        } else {
            if (progress < 100) {
                SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
                SDL_RenderDebugTextFormat(renderer, 10, 80, "Transmission: %d%%", progress);
            } else {
                SDL_SetRenderDrawColor(renderer, 50, 255, 50, 255);
                SDL_RenderDebugText(renderer, 10, 80, "Transmission finished!");
            }
        }

        SDL_RenderPresent(renderer);
    }
}

void shutdown() {
    if (transmitter) transmitter->stop();
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}

int main(int argc, char* argv[]) {
    if (argc <= 1) {
        std::cerr << "Usage:\n"
                << "  ./midi-ox-transmitter --file <filename> --vid <hexadecimal integer> --pid <hexadecimal integer>\n"
                << "  ./midi-ox-transmitter --listUSB\n";
        return 1;
    }

    for (int i = 1; i < argc; ++i) {
        if (SDL_strcmp(argv[i], "--file") == 0) {
            if (i + 1 >= argc) {
                std::cerr << "Error: --file requires a filename\n";
                return 1;
            }

            midiFile = argv[++i];
        } else if (SDL_strcmp(argv[i], "--vid") == 0) {
            if (i + 1 >= argc) {
                std::cerr << "Error: --vid requires a hexadecimal integer\n";
                return 1;
            }
            size_t idx = 0;
            USBVendorID = std::stoul(argv[++i], &idx, 16);
            return 0;
        } else if (SDL_strcmp(argv[i], "--pid") == 0) {
            if (i + 1 >= argc) {
                std::cerr << "Error: --pid requires a hexadecimal integer\n";
                return 1;
            }
            size_t idx = 0;
            USBProductID = std::stoul(argv[++i], &idx, 16);
            return 0;
        } else if (SDL_strcmp(argv[i], "--listUSB") == 0) {
            listUSBDevices();
            return 0;
        } else {
            std::cerr << "Unknown argument: " << argv[i] << "\n";
            return 1;
        }
    }

    if (midiFile.empty()) {
        std::cerr << "No MIDI file specified.\n";
        return 1;
    }

    if (init()) {
        start();
        loop();
    }
    shutdown();
    return 0;
}

Use the command “./midi-ox-transmitter --list” to scan for connected devices.
Use the command “./midi-ox-transmitter --file my_instructions.txt --vid 0x1234 --pid 0x4568” to send your file.

I hope this solves your problem.

1 Like

Interesting, your approach. Thanks. Actually, I solved the problem already by embedding a little C program using SDL2 API calls. Maybe will post it later. Gotta fight some sytax error I got in the Midi-Ox-Parser.