Files
Dalton MessmerandGustavo Chicato 70aeceeff9 MidiExport: Support multi-track export, banks/programs, and more (#8048)
- Map each instrument track to a different incremental MIDI channel (except for channel 10 which is reserved for drums)
- Support Sf2 Player and Gig Player patches
- Major refactoring
- Allow instruments to provide their MIDI bank/program
- Add std::filesystem::path/string conversion utilities + unit tests
- Remove Qt usage from MidiExport
- Support bank select events in MidiExport and MidiImport
- Rework endian_handling.h, adding polyfill for std::byteswap
- Remove extraneous patch automation tracks
- Fix initial playback after MIDI import

---------

Co-authored-by: Gustavo Chicato <gustavo.chicato@usp.br>
2026-09-15 19:17:45 -04:00

248 lines
6.9 KiB
C++

/*
* MidiFile.h - support for exporting MIDI files
*
* Copyright (c) 2009 Mark Conway Wirt <emergentmusics) at (gmail . com>
* Copyright (c) 2015 Mohamed Abdel Maksoud <mohamed at amaksoud.com>
* Copyright (c) 2020 EmoonX
* Copyright (c) 2026 Dalton Messmer <messmer.dalton/at/gmail.com>
*
* This file was originally based on the Python module MidiFile.py from MIDIUtil
* by Mark Conway Wirt, which was later rewritten in C++ by Mohamed Abdel Maksoud.
*
* --------------------------------------------------------------------------
* MIDUTIL, Copyright (c) 2009, Mark Conway Wirt
* <emergentmusics) at (gmail . com>
*
* This software is distributed under an Open Source license, the
* details of which follow.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* --------------------------------------------------------------------------
*
*/
#ifndef LMMS_MIDI_FILE_H
#define LMMS_MIDI_FILE_H
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <span>
#include <string>
#include <vector>
#include "LmmsTypes.h"
#include "MidiPatch.h"
namespace lmms
{
//! MIDI file class for exporting purposes
class MidiFile
{
public:
//! Default number of ticks per single beat
static constexpr tick_t TicksPerBeat = 128;
//! Maximum size of buffers used for each section
static constexpr std::size_t BufferSize = 50 * 1024;
private:
//! Base class for sections of MIDI file
class Section
{
public:
//! Constant-capacity buffer vector to be written to stream
std::vector<std::uint8_t> m_buffer;
protected:
//! Reserve constant space for @ref BufferSize capacity vector
Section();
//! @brief Write bytes to buffer
//! @param bytes List of bytes to be written
void writeBytes(std::span<const std::uint8_t> bytes);
//! @brief Write bytes to buffer
//! @param bytes List of bytes to be written
//! TODO C++26: Remove once std::span supports construction from std::initializer_list
void writeBytes(std::initializer_list<std::uint8_t> bytes);
//! @brief Write a MIDI-compatible variable length stream
//! @param val A four-byte value
//!
//! The MIDI format is a little strange, and makes use of so-called
//! variable length quantities. These quantities are a stream of bytes.
//! If the most significant bit is 1, then more bytes follow. If it is
//! zero, then the byte in question is the last in the stream.
void writeVarLength(std::uint32_t val);
//! Buffer gets four 8-bit values from left to right
void writeBigEndian4(std::uint32_t val);
//! Buffer gets two 8-bit values from left to right
void writeBigEndian2(std::uint16_t val);
//! Write section info to buffer
virtual void writeToBuffer() {}
};
public:
//! Represents MIDI header info
class Header : public Section
{
public:
//! Constructor
explicit Header(std::size_t numTracks, tick_t ticksPerBeat = TicksPerBeat);
//! Write header info to buffer
void writeToBuffer() override;
private:
//! Number of tracks in MIDI file
const std::size_t m_numTracks;
//! How many ticks each beat has
const tick_t m_ticksPerBeat;
};
private:
//! Represents a track event. See @ref Event::Type for more info
struct Event
{
//! Possible event types, ordered from most important to least important
enum class Type
{
NoteOn, NoteOff, Tempo, BankSelectMSB, BankSelectLSB, ProgramChange, TrackName
};
using enum Type;
Type type = Type::NoteOn;
//! Time count when event happens
tick_t time = 0;
//! Channel number where event is
std::uint8_t channel = 0;
//! Union for saving space
union
{
//! Note properties
struct
{
//! Note pitch
std::uint8_t pitch;
//! Note volume
std::uint8_t volume;
}
note;
//! Tempo of event (in BPM)
std::uint32_t tempo = 0;
//! Bank select and program change
MidiPatch patch;
};
//! Name of track where event is
// (too much trouble putting it inside union...)
std::string trackName;
//! @brief Comparison operator
//! Events are sorted by their time (or, in case of tie, type priority)
bool operator<(const Event& b) const noexcept
{
if (time < b.time) { return true; }
return (time == b.time && type > b.type);
}
};
public:
//! Represents a MIDI track
class Track : public Section
{
public:
void setChannel(std::uint8_t channel) { m_channel = channel; }
//! @brief Add both NOTE_ON and NOTE_OFF effects
//! @param pitch Note pitch
//! @param volume Note volume
//! @param realTime Time of note start
//! @param duration How long the note lasts
void addNote(std::uint8_t pitch, std::uint8_t volume,
double realTime, double duration);
//! Add a tempo mark
void addTempo(std::uint32_t tempo, tick_t time);
//! Add bank select and program change events
void addPatch(MidiPatch patch, tick_t time);
//! Add a track name event
void addName(std::string name, tick_t time);
//! Write MIDI track to buffer
void writeToBuffer();
private:
//! Append a single event to vector
void addEvent(Event&& event, tick_t time);
//! Write the meta data and note data to buffer
void writeMIDIToBuffer();
//! Write the sorted events in @ref m_events to buffer
void writeEventsToBuffer();
//! Write a single event to buffer
void writeSingleEventToBuffer(Event& event);
//! Variable-length vector of events
std::vector<Event> m_events;
//! Track channel number
std::uint8_t m_channel = 0;
};
//! The sole file header
Header m_header;
//! List of tracks
std::vector<Track> m_tracks;
//! @brief Open data stream for writing to file and create list of tracks
//! @param file The file to be opened
//! @param numTracks Number of instrument (pattern and non-pattern) tracks
MidiFile(const std::filesystem::path& file, std::size_t numTracks);
//! Write all data (both header and tracks) to stream
void writeAllToStream();
private:
//! Write-only data stream
std::ofstream m_stream;
};
} // namespace lmms
#endif // LMMS_MIDI_FILE_H