Current release: v0.1 — see CHANGELOG.md.
C++17 shared library that generates MIDI Time Code over ALSA, with a C FFI layer and Python bindings.
- Source / issues: stagesoft/libmtcmaster on GitHub
libmtcmaster is a C++17 shared library developed for the CueMS (Cue Management System)
family. It drives a MIDI Time Code master clock over ALSA, managing a real-time
SCHED_RR-scheduled thread that emits MTC quarter-frame messages at the correct sub-millisecond
interval for frame rates of 24, 25, 29, and 30 fps. The library exposes its full control surface
via a thin C FFI layer (interface.h) that makes it embeddable from any language with a C
foreign-function interface, and ships a ready-to-use Python ctypes wrapper.
It is composed of:
MtcMaster_class— C++17 class that inheritsRtMidiOutand owns the playback thread, timing loop, and time-code arithmetic.interface—extern "C"binding layer that exposes constructor, destructor, and all control functions as opaque-pointer C calls.python/— Pythonctypeswrapper (MtcSender) that loadslibmtcmaster.soat runtime and maps the C FFI to a Pythonic API.test_app/— Standalone interactive terminal application that exercises the C++ API directly and serves as an integration reference.
- Overview
- Architecture
- Core Concepts
- Design Goals
- API Documentation
- Installation
- Usage
- Development
- Contributors
- Release Notes
- Future Developments
- Copyright Notice
- License
libmtcmaster models the MTC playback pipeline as a single-threaded loop running under
real-time Linux scheduling. The diagram below shows the complete data and control path from
caller to ALSA MIDI output:
Caller (C++ / C FFI / Python ctypes)
│
│ play() / pause() / stop() / setTime()
▼
┌──────────────────────────────────────────────────────┐
│ MtcMaster │
│ │
│ setTime() ──► fillMtcTimeVector() ──► sendMtcPosition()
│ (Full Frame SysEx, 10 bytes) │
│ │
│ play() ──► spawn thread [SCHED_RR, priority 99] │
│ │ │
│ ┌──────▼──────────────────────────────┐ │
│ │ threadedMethod() │ │
│ │ sendMtcPosition() [initial seek] │ │
│ │ loop while playing: │ │
│ │ for byteType = 0 .. 7: │ │
│ │ build F1 <nibble> message │ │
│ │ RtMidiOut::sendMessage() │ │
│ │ nanosleep(frameFreq / 4) │ │
│ │ fillMtcTimeVector(mtcTime) │ │
│ └─────────────────────────────────────┘ │
│ │
│ stop() ──► setPlaying(false) + wait on mtx │
└──────────────────────────────────────────────────────┘
│
▼
RtMidiOut::sendMessage()
│
▼
ALSA MIDI Out port ("MTCPort", port 0)
│
▼
MIDI receiver (hardware / software)
Layer responsibilities:
- Caller layer — constructs an
MtcMaster(or uses the C FFI / Python wrapper), drives transport commands, and queries or adjusts the current timecode position. - MtcMaster — owns the playback state machine, timecode arithmetic, the real-time thread,
the mutex that synchronises caller and thread, and the ALSA
RtMidiOutconnection. - threadedMethod — the real-time playback loop. Sends one 10-byte Full Frame SysEx at the
start of each play/seek operation, then cycles through 8 quarter-frame messages per two
video frames, sleeping between each using
nanosleepfor sub-millisecond accuracy. - ALSA MIDI out — the kernel MIDI driver delivers the byte stream to whatever receiver has subscribed to the port (hardware sequencer, virtual MIDI cable, DAW, etc.).
Files: MtcMaster_class.h, MtcMaster_class.cpp
MtcMaster is the sole public C++ class and the heart of the library. It inherits from
RtMidiOut, so every RtMidi output method (openPort, closePort, sendMessage, …) is
available alongside the transport controls defined here. The constructor selects an ALSA MIDI
API, opens port 0 under the name "MTCPort", sets the frame-rate bits, and opens the log
file at /run/log/libmtcmaster.log.
Public members and methods derived strictly from the header and implementation:
| Symbol | Kind | Role |
|---|---|---|
MtcMaster(api, clientName, setUpFR) |
constructor | Opens ALSA port 0, initialises frame-rate bits, opens the log file. |
~MtcMaster() |
destructor | Closes the log file with a timestamped "finished" entry. |
play() |
method | Toggles playback: if stopped, spawns the real-time thread; if playing, sets the playing flag to false and spin-waits for the thread to release mtx. |
pause() |
method | Alias for play() — acts as a toggle, stopping or starting the thread identically. |
stop() |
method | Halts the thread (clears playing, spin-waits on mtx), then resets mtcTime to 0 via setTime(0). |
setTime(nanos) |
method | Seeks to an absolute nanosecond position: pauses if playing, stores the new time, calls fillMtcTimeVector, sends a Full Frame SysEx, then resumes if it was playing. |
getTime() |
method | Returns mtcTime in nanoseconds. |
sendMtcPosition() |
method | Constructs and sends a 10-byte MIDI Full Frame SysEx (F0 7F 7F 01 01 HH MM SS FF F7) encoding the current mtcTimeVector and frame-rate bits. |
fillMtcTimeVector(nanos) |
method | Converts a nanosecond offset into [frames, seconds, minutes, hours] components and stores them in mtcTimeVector. |
mtcTimeVectorString() |
method | Returns the current position as the human-readable string "HHh:MMm:SSs:FFf". |
getApiString() |
method | Returns a human-readable MIDI API name ("Linux ALSA", "MacOSX Core", "Unix Jack", etc.) from RtMidi::getCurrentApi(). |
getPlaying() |
inline | Returns the current value of the static playing flag. |
setPlaying(bool) |
inline | Writes the static playing flag; used internally to signal the thread. |
getFrameRate() |
inline | Returns the current FrameRate enum value. |
setFrameRate(FrameRate) |
inline | Overwrites currentFrameRate; does not automatically update currentFRBits — call before constructing a new session. |
getMtcTime() |
inline | Returns mtcTime directly (duplicate of getTime(); provided for convenience). |
subtractNanos(diff) |
method | Decrements mtcTime by diff (clamps to 0), then calls setTime(mtcTime) — triggers pause/seek/resume. |
addNanos(diff) |
method | Increments mtcTime by diff (wraps to 0 at 24 h = 86 400 000 000 000 ns), then calls setTime(mtcTime). |
instanceCount |
static member | Counts the number of currently live MtcMaster instances; incremented in the constructor, decremented in the destructor. |
mtcTimeVector |
public member | vector<unsigned char> of length 4: [0] = frames, [1] = seconds, [2] = minutes, [3] = hours. Updated by fillMtcTimeVector. |
Private implementation highlights:
threadedMethod()— The real-time playback loop. Acquiresmtx, emits a Full Frame SysEx, then enters the mainwhile (playing)loop. Each iteration sends 8 quarter-frame messages (byte types 0–7), computingnextQuarterToSendas an accumulating timestamp and sleeping viananosleepbetween messages. After the loop the method adjustsmtcTimeby +2 frames to account for partial frame rounding, then releasesmtx.setScheduling(thread, policy, priority)— Callspthread_setschedparamon the thread's native handle to applySCHED_RRat priority 99. Logs and throwsRtMidiError::THREAD_ERRORon failure.logTime()— Writes the current wall-clock timestamp (%F %Tformat) to the log stream.
Files: interface.h, interface.cpp
Thin extern "C" wrapper that erases the C++ type system and exposes MtcMaster through an
opaque void* handle. This layer makes libmtcmaster.so loadable from any language with a C
FFI (Python ctypes, Ruby ffi, Julia ccall, etc.).
| Symbol | Role |
|---|---|
MTCSender_create() |
Heap-allocates a default MtcMaster() and returns it as void*. |
MTCSender_release(void*) |
Deletes the MtcMaster via static_cast<MtcMaster*>. |
MTCSender_openPort(void*, unsigned int, const char*) |
Calls RtMidiOut::openPort(n, name) on the wrapped instance. |
MTCSender_play(void*) |
Delegates to MtcMaster::play(). |
MTCSender_stop(void*) |
Delegates to MtcMaster::stop(). |
MTCSender_pause(void*) |
Delegates to MtcMaster::pause(). |
MTCSender_setTime(void*, uint64_t) |
Delegates to MtcMaster::setTime(nanos). |
Note:
MTCSender_create()internally callsMtcMaster()which already opens port 0 as"MTCPort"in the constructor. CallingMTCSender_openPortimmediately afterwards (as the Python wrapper does) opens an additional port on the sameRtMidiOutobject. Callers relying on the C FFI should be aware that port 0 is opened unconditionally on construction.
Files: python/mtcsender.py, python/mtcsender_test.py
MtcSender is a ctypes-based Python class that loads libmtcmaster.so from a path relative
to the repository root and delegates every call to the C FFI layer.
| Symbol | Role |
|---|---|
MtcSender(fps, port, portname) |
Loads libmtcmaster.so, calls MTCSender_create(), then MTCSender_openPort(port, portname). fps is stored but not yet forwarded to the library (see Known Limitations). |
play() |
Calls MTCSender_play. |
pause() |
Calls MTCSender_pause. |
stop() |
Calls MTCSender_stop. |
settime(seconds) |
Converts seconds → nanoseconds and calls MTCSender_setTime. |
settime_frames(frames) |
Converts frame count → nanoseconds using the stored fps and calls MTCSender_setTime. |
settime_nanos(nanos) |
Calls MTCSender_setTime directly. |
__del__ |
Calls MTCSender_release to free the native object. |
Known limitations:
- The
fpsconstructor argument is stored asself.fpsand used only forsettime_framesconversion. It is not passed toMtcMaster— the C++ library always runs at FR_25 (25 fps) regardless of the value provided in Python. - The library path is resolved as
pathlib.Path.cwd().parent.parent / "libmtcmaster/libmtcmaster.so", which assumes the working directory ispython/at the time of import.
python/mtcsender_test.py is an integration script that exercises play, pause, settime,
settime_frames, settime_nanos, stop, and object destruction in sequence.
Files: test_app/mtcmaster_test_app.cpp, test_app/mtcmaster_test_app.h,
test_app/ScreenMan_class.cpp, test_app/ScreenMan_class.h, test_app/Makefile
A self-contained terminal application that links directly against the uninstalled
MtcMaster_class objects. It uses ANSI escape codes (via ScreenMan) to display the current
MTC position and accepts single-key commands for transport control and seek.
| Symbol | Role |
|---|---|
main() |
Initialises MtcMaster, enters a keyboard-driven event loop, dispatches play, pause, stop, and seek commands. |
ScreenMan |
Minimal terminal helper using ANSI escape codes: clrScr(), mvCursor(row, col), printAt(row, col, str). |
Key bindings exercised by the test app:
| Key | Action |
|---|---|
P / p |
Toggle play / pause |
S / s |
Stop (reset to 00h:00m:00s:00f) |
1–9 |
Add n × 60 s to the current position |
- |
Subtract 60 s from the current position |
| Any other key | Print current MTC position |
Esc |
Exit |
libmtcmaster is single-process and uses one auxiliary thread per playback session:
Main thread (caller)
│
│ play() ──────────────────────────────────────────────────────────┐
│ │
│ threadedMethod [detached]│
│ ┌───────────────────────┐│
│ │ mtx.lock() ││
│ │ sendMtcPosition() ││
│ stop() / play() (pause) ◄─── spin on │ while (playing): ││
│ while(!mtx.try_lock()) mtx ──────►│ 8 × F1 <nibble> ││
│ mtx.unlock() │ nanosleep(q/4) ││
│ │ adjust mtcTime +2f ││
│ │ mtx.unlock() ││
│ └───────────────────────┘│
└───────────────────────────────────────────────────────────────────┘
- The playback thread is spawned with
std::thread(&MtcMaster::threadedMethod, this)and immediately detached after its scheduling policy is set. pthread_setschedparamappliesSCHED_RRat priority 99, the highest real-time round-robin priority available on Linux without a kernel patch.- The
playingflag and themtxmutex are bothstaticclass members, so they are shared across all instances. Only one instance should be active at a time. - The caller signals stop/pause by clearing
playingand then spin-waits viawhile (!mtx.try_lock())until the thread releasesmtxat the end of its loop — this is a deliberate busy-wait to avoid scheduler latency in the synchronisation path. - Log writes (
logFileStream) happen in the main thread (constructor/destructor) and in the real-time thread (timeout warnings) — the log stream is not mutex-protected; writes are coarse-grained and the overlap window is narrow in practice.
| Name | Type | Description |
|---|---|---|
mtcTimeVector |
vector<unsigned char> |
Four-element vector [frames, seconds, minutes, hours] derived from mtcTime. Used to build both quarter-frame nibbles and the Full Frame SysEx. |
mtcTime |
uint64_t |
Master timecode position in nanoseconds. Continuously incremented by frameFreq / 4 in the playback loop. |
currentFrameRate |
FrameRate |
Active frame rate enum; determines the nanosecond-per-frame divisor (1e9 / currentFrameRate). |
currentFRBits |
unsigned char |
SMPTE type bits (0x00/0x20/0x40/0x60) ORed into the hours byte of MTC messages. |
playing |
static bool |
Global playback flag. The thread loops while this is true; setting it false stops the loop. |
mtx |
static std::mutex |
Synchronisation mutex: held by the thread during playback, used by the caller as a completion signal. |
instanceCount |
static unsigned short |
Reference counter for live MtcMaster objects. |
logFileStream |
std::ofstream |
Append-mode log file at /run/log/libmtcmaster.log. |
FrameRate enum:
enum FrameRate { FR_24 = 24, FR_25 = 25, FR_29 = 29, FR_30 = 30 };MidiStatusCodes enum (selected values relevant to MTC):
| Constant | Value | Role |
|---|---|---|
MIDI_STATUS_TIME_CODE |
0xF1 |
First byte of every MTC quarter-frame message. |
MIDI_STATUS_SYSEX |
0xF0 |
Start of Full Frame SysEx. |
MIDI_STATUS_SYSEX_END |
0xF7 |
End of Full Frame SysEx. |
- MIDI Time Code (MTC) — A MIDI-wire protocol that encodes SMPTE timecode as a stream of
two-byte quarter-frame messages. Eight consecutive quarter frames together carry one full
HH:MM:SS:FFposition, so a full update takes two video frames. - Quarter-frame message — A two-byte MIDI message (
0xF1 <data>) where the data byte encodes a 3-bit message type (0–7, selecting which nibble of the timecode to send) and a 4-bit data nibble. Eight of these messages fully encodehours + frame-rate bits,minutes,seconds, andframes. - Full Frame SysEx — A 10-byte Real-Time Universal System Exclusive message
(
F0 7F 7F 01 01 HH MM SS FF F7) that delivers the complete timecode position in a single packet. Sent once at the start of playback and after every seek so receivers can lock immediately without waiting for eight quarter-frame cycles. - Frame rate — MTC supports 24, 25, 29.97 (encoded as 29), and 30 fps. The frame rate
determines the SMPTE type bits and the nanosecond-per-frame divisor used in the timing loop.
In
libmtcmasterthis is set at construction time via theFrameRateenum. nanosTime/mtcTime— All internal time tracking uses nanoseconds as the base unit, converted to[frames, seconds, minutes, hours]only when building MIDI messages.- SCHED_RR scheduling — The playback thread runs under Linux's Round-Robin real-time scheduler at priority 99. This minimises jitter from OS scheduling interference and is essential for maintaining MTC accuracy at the sub-millisecond level required by professional equipment.
- Opaque-pointer FFI — The C binding layer wraps
MtcMaster*asvoid*, allowing any C- compatible FFI runtime to control the library without exposing C++ ABI or vtable details.
- Real-time timing accuracy — The playback thread runs at
SCHED_RRpriority 99 and usesnanosleepfor sub-millisecond sleep precision. The loop logs "TIME OUT!!!" events to the log file whenever it falls behind schedule, making timing regressions observable. - Language-agnostic embedding — The
extern "C"interface layer deliberately erases C++ ABI details, making the shared object loadable from Python, Ruby, Julia, or any other language with a C FFI, without requiring bindings generators or C++ knowledge on the caller side. - Minimal runtime footprint — The library has one external runtime dependency (
librtmidi); all timing is handled with POSIX APIs (nanosleep,pthread_setschedparam). No event loop, no heap allocation in the hot path. - Single-class, single-responsibility design — The entire MTC generation logic is
encapsulated in
MtcMaster. There is no separate scheduler, no plugin system, and no configuration file — construction arguments and method calls are the complete interface. - Transparent seek semantics —
setTime,subtractNanos, andaddNanosall follow the same contract: pause if playing, seek, send a Full Frame SysEx so receivers re-lock immediately, then resume. This prevents partial-frame artefacts on connected devices. - Portable MIDI back-end —
MtcMasteraccepts theRtMidi::Apiconstructor argument, allowing callers to select ALSA, JACK, CoreMIDI, or the dummy back-end at runtime without recompiling. - Observable state —
instanceCount,getPlaying(),getMtcTime(), andmtcTimeVectorString()give callers unambiguous read access to every piece of runtime state without exposing internal storage directly.
Include MtcMaster_class.h and link against libmtcmaster.so.
MtcMaster(
RtMidi::Api api = LINUX_ALSA,
const string& clientName = "MtcMaster",
FrameRate setUpFR = FR_25
);Opens ALSA port 0 as "MTCPort" and opens the log file /run/log/libmtcmaster.log in
append mode. Throws RtMidiError (and calls exit(EXIT_FAILURE)) if no MIDI port is
available, or system_error (and calls exit(EXIT_FAILURE)) if the log directory does not
exist.
Parameters:
| Parameter | Default | Description |
|---|---|---|
api |
LINUX_ALSA |
RtMidi back-end selection. |
clientName |
"MtcMaster" |
ALSA client name visible in aconnect -l. |
setUpFR |
FR_25 |
Initial frame rate (24, 25, 29, or 30 fps). |
~MtcMaster();Writes a timestamped "MTC Master finished" entry and closes the log file. Does not
automatically stop a running playback thread — callers must call stop() before
destroying the object.
void play();Starts playback if stopped (spawns the SCHED_RR thread). Pauses playback if already
running (sets playing = false, spin-waits on mtx until the thread exits the loop).
Acts as a toggle; pause() is an alias.
void pause();Alias for play(). Identical behaviour.
void stop();Halts the playback thread (same mechanism as play() when running) and then seeks to
t = 0 via setTime(0).
void setTime(uint64_t nanos = 0);Seeks to the absolute nanosecond position nanos. If playing, pauses first, seeks, sends
a Full Frame SysEx, then resumes. If not playing, seeks and sends the Full Frame SysEx
without starting the thread.
uint64_t getTime(void);Returns mtcTime (the current playback position in nanoseconds).
uint64_t getMtcTime(void); // inlineEquivalent to getTime().
void subtractNanos(const uint64_t diff);Decrements position by diff nanoseconds (clamped to 0). Calls setTime internally,
so the Full Frame SysEx is sent and playback is resumed if it was running.
void addNanos(const uint64_t diff);Increments position by diff nanoseconds. Wraps to 0 if the result would exceed 24 hours
(86 400 000 000 000 ns). Calls setTime internally.
bool getPlaying(void); // inline
void setPlaying(bool status); // inline — use with care; prefer play()/stop()
FrameRate getFrameRate(void); // inline
void setFrameRate(FrameRate FR); // inline — takes effect on the next play() callstring mtcTimeVectorString(void);Returns the current position as "HHh:MMm:SSs:FFf" (zero-padded to two digits per field),
derived from the current mtcTimeVector.
string getApiString(void);Returns a human-readable name for the active RtMidi::Api ("Linux ALSA", "MacOSX Core",
"Unix Jack", "Windows MM", "RtMidi Dummy", or "Unspecified").
void sendMtcPosition(void);Sends a 10-byte MIDI Full Frame SysEx encoding the current mtcTimeVector and frame-rate
bits. Called automatically by setTime; can also be called directly to re-broadcast the
position without changing it.
void fillMtcTimeVector(uint64_t nanos);Populates mtcTimeVector[0..3] = [frames, seconds, minutes, hours] from nanos.
vector<unsigned char> mtcTimeVector; // [frames, seconds, minutes, hours]
static unsigned short instanceCount; // count of live MtcMaster objectsenum FrameRate { FR_24 = 24, FR_25 = 25, FR_29 = 29, FR_30 = 30 };Declared in interface.h. All functions follow the opaque-pointer void* pattern. Link
against libmtcmaster.so and include interface.h.
void* MTCSender_create(void);
void MTCSender_release(void* mtcsender);
void MTCSender_openPort(void* mtcsender, unsigned int portnumber, const char* portname);
void MTCSender_play(void* mtcsender);
void MTCSender_stop(void* mtcsender);
void MTCSender_pause(void* mtcsender);
void MTCSender_setTime(void* mtcsender, uint64_t nanos);| Function | Description |
|---|---|
MTCSender_create() |
Allocates a new MtcMaster with default arguments (ALSA, FR_25). Returns the opaque handle. |
MTCSender_release(h) |
Frees the MtcMaster object. Must be called exactly once per handle. |
MTCSender_openPort(h, n, name) |
Opens MIDI output port number n under the given name. Port 0 is already opened in the constructor; this call opens an additional or replacement port. |
MTCSender_play(h) |
Delegates to MtcMaster::play(). |
MTCSender_stop(h) |
Delegates to MtcMaster::stop(). |
MTCSender_pause(h) |
Delegates to MtcMaster::pause(). |
MTCSender_setTime(h, nanos) |
Seeks to nanos nanoseconds. |
The Python wrapper lives in python/mtcsender.py. It must be run from a working directory
where ../../libmtcmaster/libmtcmaster.so resolves to the built shared library (i.e., from
inside the python/ subdirectory with the library already built in the repo root).
from mtcsender import MtcSender
sender = MtcSender(fps=25, port=0, portname="SLMTCPort")| Method | Signature | Description |
|---|---|---|
__init__ |
(fps=25, port=0, portname="SLMTCPort") |
Loads libmtcmaster.so, creates the native object, opens the specified port. fps is stored locally for settime_frames; it is not forwarded to the C++ library (library always uses FR_25). |
play |
() |
Calls MTCSender_play. |
pause |
() |
Calls MTCSender_pause. |
stop |
() |
Calls MTCSender_stop. |
settime |
(seconds: float) |
Converts seconds to nanoseconds, calls MTCSender_setTime. |
settime_frames |
(frames: int/float) |
Converts frames → nanoseconds using self.fps, calls MTCSender_setTime. |
settime_nanos |
(nanos: int) |
Calls MTCSender_setTime directly with nanoseconds. |
__del__ |
— | Calls MTCSender_release on the native handle. |
System dependencies (Debian/Ubuntu):
sudo apt-get install -y build-essential librtmidi-devEnsure /run/log/ exists and is writable by the user running the library, as MtcMaster
writes its log there:
sudo mkdir -p /run/log
sudo chmod a+w /run/logBuild and install:
git clone https://github.com/stagesoft/libmtcmaster.git
cd libmtcmaster
make
sudo make install # installs to /usr/local/lib/ by default
sudo ldconfigThe prefix variable can be overridden:
sudo make install prefix=/usrBuild output:
libmtcmaster.so.0.1 # versioned shared object
libmtcmaster.so.0 # soname symlink → libmtcmaster.so.0.1
libmtcmaster.so # unversioned symlink → libmtcmaster.so.0.1
A Debian packaging branch is in development. When available:
git clone --branch debian/bookworm https://github.com/stagesoft/libmtcmaster.git
cd libmtcmaster
dpkg-buildpackage -us -uc
sudo dpkg -i ../libmtcmaster_*.deb#include "MtcMaster_class.h"
int main()
{
// Construct — opens ALSA port 0 as "MTCPort", selects 25 fps
MtcMaster master(RtMidi::Api::LINUX_ALSA, "MyApp", FR_25);
// Start playback from t = 0
master.play();
// Seek forward by 2 minutes (120 seconds = 120 * 1e9 ns)
master.addNanos(static_cast<uint64_t>(120e9));
// Jump to 01h:00m:00s:00f = 3600 * 1e9 ns
master.setTime(static_cast<uint64_t>(3600e9));
// Check position
std::cout << master.mtcTimeVectorString() << std::endl; // "01h:00m:00s:00f"
// Pause
master.play();
// Stop and reset to 0
master.stop();
return 0;
}Compile against the installed library:
g++ -std=c++17 -o my_app my_app.cpp -lmtcmasterOr against the build directory before installation:
g++ -std=c++17 -I /path/to/libmtcmaster -L /path/to/libmtcmaster \
-o my_app my_app.cpp -lmtcmaster -Wl,-rpath,/path/to/libmtcmaster# From inside the python/ directory, with the library already built:
cd python
python3 mtcsender_test.pyOr from your own script (adjusting the library path as needed):
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent / "python"))
from mtcsender import MtcSender
import time
sender = MtcSender(fps=25, port=0, portname="SLMTCPort")
sender.play()
time.sleep(5)
sender.settime(60.0) # seek to 1 minute
time.sleep(2)
sender.settime_frames(750) # seek to frame 750 (30 s at 25 fps)
time.sleep(2)
sender.stop()
del sender # releases the native objectRequirements:
sudo apt-get install -y build-essential librtmidi-dev clang-format clang-tidyBuild (debug, with AddressSanitizer):
The Makefile enables -fsanitize=address in the default LDFLAGS:
makeClean rebuild:
make clean && makeBuild the test application:
cd test_app
make
./mtcmaster_test_appFormat check (all tracked C++ files):
clang-format --dry-run --Werror $(git ls-files '*.cpp' '*.h')Static analysis:
clang-tidy -p . $(git diff --name-only main -- '*.cpp')Compiler flags in use:
-Wall -Werror -Wextra -std=c++17 -O3 -fPIC -g -I /usr/include/rtmidi
-fsanitize=address
Runtime log:
The library writes to /run/log/libmtcmaster.log (append mode). Each session is separated
by a separator line and a timestamped "MTC Master Initialized" / "MTC Master finished" pair.
Timing overruns appear as TIME OUT!!! lines with the current time, the expected next-quarter
timestamp, and the overrun delta in nanoseconds.
Contributions to libmtcmaster follow the full workflow described in
CONTRIBUTORS.md. Key points:
- Non-trivial changes (any logic in
MtcMaster_class.cpp,interface.cpp, orpython/) require a spec document committed on the feature branch before a PR is opened. - TDD is required for Tier 2 changes: write a failing test, confirm it fails, then implement.
- All commits must be signed off with
git commit -s(DCO). - PRs target
main. - Review is by Ion Reguera (@ibiltari) or Adrià Masip (@backenv).
- Every new source file must carry the SPDX header
(
SPDX-FileCopyrightText: <year> Stagelab Coop SCCL/SPDX-License-Identifier: GPL-3.0-or-later).
See CONTRIBUTORS.md for the complete contributing workflow.
See CHANGELOG.md for the full history.
v0.1 — 2026-05-31
Initial public release of libmtcmaster. The library delivers a fully functional MIDI Time
Code master generator over ALSA, with a SCHED_RR real-time playback thread, sub-millisecond
nanosleep-based timing, and support for 24, 25, 29, and 30 fps frame rates. A C extern "C"
FFI layer (interface.h) enables embedding from any C-compatible runtime, and a Python
ctypes wrapper (python/mtcsender.py) provides a Pythonic interface used by the CueMS audio
and media players. The Makefile produces a versioned shared object (libmtcmaster.so.0.1) with
the standard SONAME symlink chain and supports prefix-based installation for Debian packaging.
The items below describe planned infrastructure that is not yet implemented. They are documented here so that each component can be wired in incrementally without architectural surprises.
Currently libmtcmaster ships a manual integration test application (test_app/) and a
Python integration script (python/mtcsender_test.py) but has no automated test suite.
Planned work:
- Add a
tests/directory with a lightweight C++ test harness (e.g. Catch2 or GoogleTest). - Unit-test
fillMtcTimeVectoragainst known nanosecond→HH:MM:SS:FF conversions for all four frame rates. - Unit-test
mtcTimeVectorStringformatting for edge cases (zero, 23:59:59:29, etc.). - Integration-test
sendMtcPositionoutput using theRtMidi::RTMIDI_DUMMYAPI to capture outgoing bytes without a physical MIDI interface. - Wire
ctesttargets into aCMakeLists.txt(or a Makefiletesttarget) so tests are runnable with a single command.
The coverage pipeline will use lcov/gcov and upload to Codecov. The activation step
(visiting https://codecov.io/gh/stagesoft/libmtcmaster and clicking "Activate") is a
one-time manual step that must be performed after the first successful CI run.
Planned: .github/workflows/tests.yml triggered on push to main and on pull requests.
# Sketch — not yet created
name: Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get install -y build-essential librtmidi-dev lcov
- name: Build with coverage
run: |
make CXXFLAGS="--coverage" LDFLAGS="--coverage"
- name: Run tests
run: ctest --test-dir build --output-on-failure
- name: Generate coverage report
run: |
lcov --capture --directory . --output-file coverage.info --ignore-errors inconsistent
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.info
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}The Tests badge and Coverage badge below will become live once this workflow is committed and the repository is activated on Codecov:
[](https://github.com/stagesoft/libmtcmaster/actions/workflows/tests.yml)
[](https://codecov.io/gh/stagesoft/libmtcmaster)
Planned: Doxygen-generated HTML API reference deployed to GitHub Pages via
.github/workflows/docs.yml.
The Doxygen run will parse MtcMaster_class.h and interface.h and produce a browsable
reference at https://stagesoft.github.io/libmtcmaster/. A Doxyfile with
GENERATE_HTML = YES, EXTRACT_ALL = YES, and HAVE_DOT = YES (Graphviz) is the target
configuration. The docs workflow will be kept as a separate concern from tests.yml so
that a documentation-only push does not trigger a full build-and-test cycle.
Once live, the Deploy API documentation badge will be:
[](https://github.com/stagesoft/libmtcmaster/actions/workflows/docs.yml)
Planned: a debian/ packaging branch (debian/bookworm) producing:
libmtcmaster0— the shared library package (libmtcmaster.so.0.1+ SONAME symlinks).libmtcmaster-dev— headers and the unversionedlibmtcmaster.sosymlink for compile-time linking.- A
debian/libmtcmaster.installfile mapping the build outputs to the correctusr/libpaths. - The
Depends:field will listlibrtmidi6(or the current Bookworm version).
Installation via the future Debian package:
sudo dpkg -i libmtcmaster0_0.1_amd64.deb
sudo dpkg -i libmtcmaster-dev_0.1_amd64.debOnce all the above pipelines are live, the full badge row will be:
[](https://www.gnu.org/licenses/gpl-3.0)
[](https://github.com/stagesoft/libmtcmaster/actions/workflows/tests.yml)
[](https://codecov.io/gh/stagesoft/libmtcmaster)
[](https://github.com/stagesoft/libmtcmaster/actions/workflows/docs.yml)libmtcmaster is copyright © 2026 Stagelab Coop SCCL and is free software: you can
redistribute it and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
libmtcmaster is licensed under the GNU General Public License v3.0 or later
(GPL-3.0-or-later).
The full license text is in LICENSE.
SPDX identifier: GPL-3.0-or-later
Runtime dependency licence note: libmtcmaster links against RtMidi, which is
distributed under its own permissive MIT-style licence. RtMidi itself is not redistributed
in this repository — it is a system package (librtmidi-dev). Packagers incorporating
libmtcmaster into a distribution should verify that the RtMidi licence terms are met for
their distribution format.