2564 lines
72 KiB
C++
2564 lines
72 KiB
C++
// =============================================================================
|
|
//
|
|
// The Fmidi library - a free software toolkit for MIDI file processing
|
|
// Single-file implementation, based on software revision: c513b4f
|
|
//
|
|
// =============================================================================
|
|
// Copyright Jean Pierre Cimalando 2018-2020.
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE.md or copy at
|
|
// http://www.boost.org/LICENSE_1_0.txt)
|
|
// =============================================================================
|
|
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <vector>
|
|
|
|
struct fmidi_raw_track {
|
|
std::unique_ptr<uint8_t[]> data;
|
|
uint32_t length;
|
|
};
|
|
|
|
struct fmidi_smf {
|
|
fmidi_smf_info_t info;
|
|
std::unique_ptr<fmidi_raw_track[]> track;
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
uintptr_t fmidi_event_pad(uintptr_t size);
|
|
fmidi_event_t *fmidi_event_alloc(std::vector<uint8_t> &buf, uint32_t datalen);
|
|
unsigned fmidi_message_sizeof(uint8_t id);
|
|
|
|
//------------------------------------------------------------------------------
|
|
inline uintptr_t fmidi_event_pad(uintptr_t size)
|
|
{
|
|
uintptr_t nb = size % alignof(fmidi_event_t);
|
|
return nb ? (size + alignof(fmidi_event_t) - nb) : size;
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
|
|
#if !defined(FMIDI_DISABLE_DESCRIBE_API)
|
|
//------------------------------------------------------------------------------
|
|
struct printfmt_quoted {
|
|
printfmt_quoted(const char *text, size_t length)
|
|
: text(text), length(length) {}
|
|
const char *text = nullptr;
|
|
size_t length = 0;
|
|
};
|
|
std::ostream &operator<<(std::ostream &out, const printfmt_quoted &q);
|
|
|
|
//------------------------------------------------------------------------------
|
|
struct printfmt_bytes {
|
|
printfmt_bytes(const uint8_t *data, size_t size)
|
|
: data(data), size(size) {}
|
|
const uint8_t *data = nullptr;
|
|
size_t size = 0;
|
|
};
|
|
std::ostream &operator<<(std::ostream &out, const printfmt_bytes &b);
|
|
#endif // !defined(FMIDI_DISABLE_DESCRIBE_API)
|
|
|
|
//------------------------------------------------------------------------------
|
|
extern thread_local fmidi_error_info_t fmidi_last_error;
|
|
|
|
#if defined(FMIDI_DEBUG)
|
|
# define RET_FAIL(x, e) do { \
|
|
fmidi_error_info_t &fmidi__err = fmidi_last_error; \
|
|
fmidi__err.file = __FILE__; fmidi__err.line = __LINE__; \
|
|
fmidi__err.code = (e); return (x); } while (0)
|
|
#else
|
|
# define RET_FAIL(x, e) \
|
|
do { fmidi_last_error.code = (e); return (x); } while (0)
|
|
#endif
|
|
|
|
//------------------------------------------------------------------------------
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <type_traits>
|
|
#include <stdio.h>
|
|
#include <assert.h>
|
|
|
|
class Writer {
|
|
public:
|
|
virtual ~Writer() {}
|
|
virtual void put(uint8_t byte) = 0;
|
|
virtual void write(const void *data, size_t size) = 0;
|
|
virtual void rwrite(const void *data, size_t size) = 0;
|
|
virtual void writeLE(const void *data, size_t size) = 0;
|
|
virtual void writeBE(const void *data, size_t size) = 0;
|
|
virtual off_t tell() const = 0;
|
|
virtual bool seek(off_t offset, int whence) = 0;
|
|
};
|
|
|
|
template <class T> class WriterT : public Writer {
|
|
public:
|
|
virtual ~WriterT() {}
|
|
void rwrite(const void *data, size_t size) override;
|
|
void writeLE(const void *data, size_t size) override;
|
|
void writeBE(const void *data, size_t size) override;
|
|
};
|
|
|
|
class Memory_Writer : public WriterT<Memory_Writer> {
|
|
public:
|
|
explicit Memory_Writer(std::vector<uint8_t> &mem)
|
|
: mem(mem), index(mem.size()) {}
|
|
void put(uint8_t byte) override;
|
|
void write(const void *data, size_t size) override;
|
|
off_t tell() const override;
|
|
bool seek(off_t offset, int whence) override;
|
|
private:
|
|
std::vector<uint8_t> &mem;
|
|
size_t index = 0;
|
|
};
|
|
|
|
class Stream_Writer : public WriterT<Stream_Writer> {
|
|
public:
|
|
explicit Stream_Writer(FILE *stream)
|
|
: stream(stream) {}
|
|
void put(uint8_t byte) override;
|
|
void write(const void *data, size_t size) override;
|
|
off_t tell() const override;
|
|
bool seek(off_t offset, int whence) override;
|
|
private:
|
|
FILE *stream = nullptr;
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
union Endian_check {
|
|
uint32_t value;
|
|
uint8_t head_byte;
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
template <class T>
|
|
void WriterT<T>::rwrite(const void *data, size_t size)
|
|
{
|
|
const uint8_t *bytes = (const uint8_t *)data;
|
|
for (size_t i = size; i-- > 0;)
|
|
static_cast<T *>(this)->put(bytes[i]);
|
|
}
|
|
|
|
template <class T>
|
|
void WriterT<T>::writeLE(const void *data, size_t size)
|
|
{
|
|
switch(Endian_check{0x11223344}.head_byte) {
|
|
case 0x11: static_cast<T *>(this)->rwrite(data, size); break;
|
|
case 0x44: static_cast<T *>(this)->write(data, size); break;
|
|
default: assert(false);
|
|
}
|
|
}
|
|
|
|
template <class T>
|
|
void WriterT<T>::writeBE(const void *data, size_t size)
|
|
{
|
|
switch(Endian_check{0x11223344}.head_byte) {
|
|
case 0x11: static_cast<T *>(this)->write(data, size); break;
|
|
case 0x44: static_cast<T *>(this)->rwrite(data, size); break;
|
|
default: assert(false);
|
|
}
|
|
}
|
|
|
|
inline off_t Memory_Writer::tell() const
|
|
{
|
|
return index;
|
|
}
|
|
|
|
inline void Stream_Writer::put(uint8_t byte)
|
|
{
|
|
fputc(byte, stream);
|
|
}
|
|
|
|
inline void Stream_Writer::write(const void *data, size_t size)
|
|
{
|
|
fwrite(data, size, 1, stream);
|
|
}
|
|
|
|
inline bool Stream_Writer::seek(off_t offset, int whence)
|
|
{
|
|
return fseek(stream, offset, whence) == 0;
|
|
}
|
|
|
|
inline off_t Stream_Writer::tell() const
|
|
{
|
|
return ftell(stream);
|
|
}
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <tuple>
|
|
|
|
enum memstream_status { // make it match fmidi status codes
|
|
ms_ok,
|
|
ms_err_format,
|
|
ms_err_eof,
|
|
};
|
|
|
|
class memstream {
|
|
public:
|
|
memstream(const uint8_t *data, size_t length);
|
|
|
|
size_t endpos() const;
|
|
size_t getpos() const;
|
|
memstream_status setpos(size_t off);
|
|
|
|
memstream_status skip(size_t count);
|
|
memstream_status skipbyte(unsigned byte);
|
|
|
|
const uint8_t *peek(size_t length);
|
|
const uint8_t *read(size_t length);
|
|
|
|
memstream_status peekbyte(unsigned *retp);
|
|
memstream_status readbyte(unsigned *retp);
|
|
|
|
memstream_status readintLE(uint32_t *retp, unsigned length);
|
|
memstream_status readintBE(uint32_t *retp, unsigned length);
|
|
memstream_status readvlq(uint32_t *retp);
|
|
memstream_status peekvlq(uint32_t *retp);
|
|
|
|
private:
|
|
const uint8_t *base_ = nullptr;
|
|
size_t length_ = 0;
|
|
size_t offset_ = 0;
|
|
typedef std::tuple<memstream_status, uint32_t, unsigned> vlq_result;
|
|
vlq_result doreadvlq();
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
inline memstream::memstream(const uint8_t *data, size_t length)
|
|
: base_(data), length_(length) {
|
|
}
|
|
|
|
inline size_t memstream::endpos() const {
|
|
return length_;
|
|
}
|
|
|
|
inline size_t memstream::getpos() const {
|
|
return offset_;
|
|
}
|
|
#if !defined(FMIDI_DISABLE_DESCRIBE_API)
|
|
#include <fmt/format.h>
|
|
#include <fmt/ostream.h>
|
|
#endif
|
|
#include <string>
|
|
|
|
double fmidi_smpte_time(const fmidi_smpte *smpte)
|
|
{
|
|
const uint8_t *d = smpte->code;
|
|
static const double spftable[4] = { 1.0/24, 1.0/25, 1001.0/30000, 1.0/30 };
|
|
uint8_t hh = d[0];
|
|
double spf = spftable[(hh >> 5) & 0b11];
|
|
hh &= 0b11111;
|
|
uint8_t mm = d[1], ss = d[2], fr = d[3], ff = d[4];
|
|
return (fr + 0.01 * ff) * spf + ss + mm * 60 + hh * 3600;
|
|
}
|
|
|
|
double fmidi_delta_time(double delta, uint16_t unit, uint32_t tempo)
|
|
{
|
|
if (unit & (1 << 15)) {
|
|
unsigned tpf = unit & 0xff; // delta units per frame
|
|
unsigned fps = -(int8_t)(unit >> 8); // frames per second
|
|
return delta / (tpf * fps);
|
|
}
|
|
else {
|
|
unsigned dpqn = unit; // delta units per 1/4 note
|
|
double tpqn = 1e-6 * tempo; // 1/4 note duration
|
|
return delta * tpqn / dpqn;
|
|
}
|
|
}
|
|
|
|
double fmidi_time_delta(double time, uint16_t unit, uint32_t tempo)
|
|
{
|
|
if (unit & (1 << 15)) {
|
|
unsigned tpf = unit & 0xff; // delta units per frame
|
|
unsigned fps = -(int8_t)(unit >> 8); // frames per second
|
|
return time * (tpf * fps);
|
|
}
|
|
else {
|
|
unsigned dpqn = unit; // delta units per 1/4 note
|
|
double tpqn = 1e-6 * tempo; // 1/4 note duration
|
|
return time * dpqn / tpqn;
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
fmidi_event_t *fmidi_event_alloc(std::vector<uint8_t> &buf, uint32_t datalen)
|
|
{
|
|
size_t pos = buf.size();
|
|
size_t evsize = fmidi_event_sizeof(datalen);
|
|
size_t padsize = fmidi_event_pad(evsize);
|
|
buf.resize(buf.size() + padsize);
|
|
fmidi_event_t *event = (fmidi_event_t *)&buf[pos];
|
|
return event;
|
|
}
|
|
|
|
unsigned fmidi_message_sizeof(uint8_t id)
|
|
{
|
|
if ((id >> 7) == 0) {
|
|
return 0;
|
|
}
|
|
else if ((id >> 4) != 0b1111) {
|
|
static const uint8_t sizetable[8] = {
|
|
3, 3, 3, 3, 2, 2, 3 };
|
|
return sizetable[(id >> 4) & 0b111];
|
|
}
|
|
else {
|
|
static const uint8_t sizetable[16] = {
|
|
0, 2, 3, 2, 1, 1, 1, 0,
|
|
1, 1, 1, 1, 1, 1, 1, 1 };
|
|
return sizetable[id & 0b1111];
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
class fmidi_category_t : public std::error_category {
|
|
public:
|
|
const char *name() const noexcept override
|
|
{ return "fmidi"; }
|
|
std::string message(int condition) const override
|
|
{ return fmidi_strerror((fmidi_status_t)condition); }
|
|
};
|
|
|
|
static fmidi_category_t the_category;
|
|
|
|
const std::error_category &fmidi_category() {
|
|
return the_category;
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
#if !defined(FMIDI_DISABLE_DESCRIBE_API)
|
|
template <class OutputStreamRef>
|
|
static bool fmidi_repr_meta(OutputStreamRef out, const uint8_t *data, uint32_t len)
|
|
{
|
|
if (len <= 0)
|
|
return false;
|
|
|
|
unsigned tag = *data++;
|
|
--len;
|
|
|
|
printfmt_quoted qtext{(const char *)data, len};
|
|
|
|
switch (tag) {
|
|
default:
|
|
fmt::print(out, "(meta/unknown :tag #x{:02x})", tag);
|
|
return true;
|
|
case 0x00: { // sequence number
|
|
if (len < 2) return false;
|
|
unsigned number = (data[0] << 8) | data[1];
|
|
fmt::print(out, "(meta/seq-number {})", number);
|
|
return true;
|
|
}
|
|
case 0x01:
|
|
fmt::print(out, "(meta/text {})", qtext);
|
|
return true;
|
|
case 0x02:
|
|
fmt::print(out, "(meta/copyright {})", qtext);
|
|
return true;
|
|
case 0x03:
|
|
fmt::print(out, "(meta/track {})", qtext);
|
|
return true;
|
|
case 0x04:
|
|
fmt::print(out, "(meta/instrument {})", qtext);
|
|
return true;
|
|
case 0x05:
|
|
fmt::print(out, "(meta/lyric {})", qtext);
|
|
return true;
|
|
case 0x06:
|
|
fmt::print(out, "(meta/marker {})", qtext);
|
|
return true;
|
|
case 0x07:
|
|
fmt::print(out, "(meta/cue-point {})", qtext);
|
|
return true;
|
|
case 0x09:
|
|
fmt::print(out, "(meta/device-name {})", qtext);
|
|
return true;
|
|
case 0x20:
|
|
if (len < 1) return false;
|
|
fmt::print(out, "(meta/channel-prefix {})", data[0]);
|
|
return true;
|
|
case 0x21:
|
|
if (len < 1) return false;
|
|
fmt::print(out, "(meta/port {})", data[0]);
|
|
return true;
|
|
case 0x2f:
|
|
case 0x3f:
|
|
fmt::print(out, "(meta/end)");
|
|
return true;
|
|
case 0x51: {
|
|
if (len < 3) return false;
|
|
unsigned t = (data[0] << 16) | (data[1] << 8) | data[2];
|
|
fmt::print(out, "(meta/tempo {} #|{} bpm|#)", t, 60. / (t * 1e-6));
|
|
return true;
|
|
}
|
|
case 0x54: {
|
|
if (len < 5) return false;
|
|
static const char *fpstable[] = {"24", "25", "30000/1001", "30"};
|
|
uint8_t hh = data[0];
|
|
const char *fps = fpstable[(hh >> 5) & 0b11];
|
|
fmt::print(
|
|
out, "(meta/offset {:02d} {:02d} {:02d} {:02d} {:02d}/100 :frames/second {})",
|
|
hh & 0b11111, data[1], data[2], data[3], data[4], fps);
|
|
return true;
|
|
}
|
|
case 0x58:
|
|
if (len < 4) return false;
|
|
fmt::print(out, "(meta/time-sig {} {} {} {})",
|
|
data[0], data[1], data[2], data[3]);
|
|
return true;
|
|
case 0x59: {
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(meta/key-sig {} :{})",
|
|
(int8_t)data[0], data[1] ? "minor" : "major");
|
|
return true;
|
|
}
|
|
case 0x7f:
|
|
fmt::print(out, "(meta/sequencer-specific {})", printfmt_bytes{data, len});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template <class OutputStreamRef>
|
|
static bool fmidi_repr_midi(OutputStreamRef out, const uint8_t *data, uint32_t len)
|
|
{
|
|
if (len <= 0)
|
|
return false;
|
|
|
|
unsigned status = *data++;
|
|
--len;
|
|
|
|
auto b7 = [data](unsigned i)
|
|
{ return data[i] & 0x7f; };
|
|
auto b14 = [data](unsigned i)
|
|
{ return (data[i] & 0x7f) | (data[i + 1] & 0x7f) << 7; };
|
|
|
|
if (status >> 4 == 0xf) {
|
|
unsigned op = status & 0xf;
|
|
|
|
switch (op) {
|
|
case 0b0000:
|
|
fmt::print(out, "(sysex #xf0 {})", printfmt_bytes{data, len});
|
|
return true;
|
|
case 0b0001: {
|
|
if (len < 1) return false;
|
|
unsigned tc = b7(0);
|
|
fmt::print(out, "(time-code {} {})", tc >> 4, tc & 0b1111);
|
|
return true;
|
|
}
|
|
case 0b0010:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(song-position {})", b14(0));
|
|
return true;
|
|
case 0b0011:
|
|
if (len < 1) return {};
|
|
fmt::print(out, "(song-select {})", b7(0));
|
|
return true;
|
|
case 0b0110:
|
|
fmt::print(out, "(tune-request)");
|
|
return true;
|
|
case 0b1000:
|
|
fmt::print(out, "(timing-clock)");
|
|
return true;
|
|
case 0b1010:
|
|
fmt::print(out, "(start)");
|
|
return true;
|
|
case 0b1011:
|
|
fmt::print(out, "(continue)");
|
|
return true;
|
|
case 0b1100:
|
|
fmt::print(out, "(stop)");
|
|
return true;
|
|
case 0b1110:
|
|
fmt::print(out, "(active-sensing)");
|
|
return true;
|
|
case 0b1111:
|
|
fmt::print(out, "(reset)");
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
unsigned op = status >> 4;
|
|
unsigned ch = status & 0xf;
|
|
|
|
switch (op) {
|
|
case 0b1000:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(note-off {} :velocity {} :channel {})", b7(0), b7(1), ch);
|
|
return true;
|
|
case 0b1001:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(note-on {} :velocity {} :channel {})", b7(0), b7(1), ch);
|
|
return true;
|
|
case 0b1010:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(poly-aftertouch {} :pressure {} :channel {})", b7(0), b7(1), ch);
|
|
return true;
|
|
case 0b1011:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(control #x{:02x} {} :channel {})", b7(0), b7(1), ch);
|
|
return true;
|
|
case 0b1100:
|
|
if (len < 1) return false;
|
|
fmt::print(out, "(program {} :channel {})", b7(0), ch);
|
|
return true;
|
|
case 0b1101:
|
|
if (len < 1) return false;
|
|
fmt::print(out, "(aftertouch :pressure {} :channel {})", b7(0), ch);
|
|
return true;
|
|
case 0b1110:
|
|
if (len < 2) return false;
|
|
fmt::print(out, "(pitch-bend {} :channel {})", b14(0), ch);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool fmidi_identify_sysex(const uint8_t *msg, size_t len, std::string &text)
|
|
{
|
|
if (len < 4 || msg[0] != 0xf0 || msg[len - 1] != 0xf7)
|
|
return false;
|
|
|
|
unsigned manufacturer = msg[1];
|
|
unsigned deviceid = msg[2];
|
|
|
|
switch (manufacturer) {
|
|
case 0x7e: // universal non-realtime
|
|
if (len >= 6) {
|
|
switch ((msg[3] << 8) | msg[4]) {
|
|
case 0x0901: text = "GM system on"; return true;
|
|
case 0x0902: text = "GM system off"; return true;
|
|
}
|
|
}
|
|
break;
|
|
case 0x7f: // universal realtime
|
|
if (len >= 6) {
|
|
switch ((msg[3] << 8) | msg[4]) {
|
|
case 0x0401: text = "GM master volume"; return true;
|
|
case 0x0402: text = "GM master balance"; return true;
|
|
}
|
|
}
|
|
break;
|
|
case 0x41: // Roland
|
|
if (len >= 9) {
|
|
unsigned model = msg[3];
|
|
unsigned mode = msg[4];
|
|
unsigned address = (msg[5] << 16) | (msg[6] << 8) | msg[7];
|
|
if (mode == 0x12) { // send
|
|
switch ((model << 24) | address) {
|
|
case (0x42u << 24) | 0x00007fu: text = "GS system mode set"; return true;
|
|
case (0x42u << 24) | 0x40007fu: text = "GS mode set"; return true;
|
|
default: text = fmt::format("GS parameter #x{:06x}", address); return true;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 0x43: // Yamaha
|
|
if (len >= 5) {
|
|
unsigned model = msg[3];
|
|
switch((model << 8) | (deviceid & 0xf0))
|
|
{
|
|
case (0x4c << 8) | 0x10: // XG
|
|
if (len >= 8) {
|
|
unsigned address = (msg[4] << 16) | (msg[5] << 8) | msg[6];
|
|
switch (address) {
|
|
case 0x00007e: text = "XG system on"; return true;
|
|
default: text = fmt::format("XG parameter #x{:06x}", address); return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template <class OutputStreamRef>
|
|
static void fmidi_repr_smf(OutputStreamRef out, const fmidi_smf_t &smf)
|
|
{
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(&smf);
|
|
fmt::print(out, "(midi-file");
|
|
fmt::print(out, "\n :format {}", info->format);
|
|
|
|
unsigned unit = info->delta_unit;
|
|
if (unit & (1 << 15))
|
|
fmt::print(out, "\n :delta-unit (smpte-based :units/frame {} :frames/second {})",
|
|
unit & 0xff, -(int8_t)(unit >> 8));
|
|
else
|
|
fmt::print(out, "\n :delta-unit (tempo-based :units/beat {})", unit);
|
|
|
|
fmt::print(out, "\n :tracks"
|
|
"\n (", unit);
|
|
|
|
struct RPN_Info {
|
|
unsigned lsb = 127, msb = 127;
|
|
bool nrpn = false;
|
|
};
|
|
RPN_Info channel_rpn[16];
|
|
|
|
std::string strbuf;
|
|
strbuf.reserve(256);
|
|
|
|
for (unsigned i = 0, n = info->track_count; i < n; ++i) {
|
|
fmidi_track_iter_t it;
|
|
fmidi_smf_track_begin(&it, i);
|
|
if (i > 0)
|
|
fmt::print(out, "\n ");
|
|
fmt::print(out, "(;;--- track {} ---;;", i);
|
|
while (const fmidi_event_t *evt = fmidi_smf_track_next(&smf, &it)) {
|
|
RPN_Info *rpn = nullptr;
|
|
|
|
const uint8_t *data = evt->data;
|
|
uint32_t datalen = evt->datalen;
|
|
|
|
if (evt->type == fmidi_event_message) {
|
|
unsigned status = data[0];
|
|
unsigned channel = status & 0x0f;
|
|
// controllers
|
|
if (datalen == 3 && (status & 0xf0) == 0xb0) {
|
|
unsigned ctl = data[1] & 0x7f;
|
|
switch (ctl) {
|
|
case 0x62: case 0x64: // (N)RPN LSB
|
|
rpn = &channel_rpn[channel];
|
|
rpn->lsb = data[2] & 0x7f, rpn->nrpn = ctl == 0x62;
|
|
break;
|
|
case 0x63: case 0x65: // (N)RPN MSB
|
|
rpn = &channel_rpn[channel];
|
|
rpn->msb = data[2] & 0x7f, rpn->nrpn = ctl == 0x63;
|
|
break;
|
|
case 0x06: case 0x26: // Data Entry MSB, LSB
|
|
rpn = &channel_rpn[channel];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt::print(out, "\n (:delta {:<5} {}", evt->delta, *evt);
|
|
if (rpn)
|
|
fmt::print(out, " #|{}RPN #x{:02x} #x{:02x}|#",
|
|
rpn->nrpn ? "N" : "", rpn->msb, rpn->lsb);
|
|
else if (fmidi_identify_sysex(data, datalen, strbuf))
|
|
fmt::print(out, " #|{}|#", strbuf);
|
|
fmt::print(out, ")");
|
|
}
|
|
fmt::print(out, ")");
|
|
}
|
|
fmt::print(out, "))\n");
|
|
}
|
|
|
|
std::ostream &operator<<(std::ostream &out, const fmidi_smf_t &smf)
|
|
{
|
|
fmidi_repr_smf<std::ostream &>(out, smf);
|
|
return out;
|
|
}
|
|
|
|
void fmidi_smf_describe(const fmidi_smf_t *smf, FILE *stream)
|
|
{
|
|
fmidi_repr_smf<FILE *>(stream, *smf);
|
|
}
|
|
|
|
template <class OutputStreamRef>
|
|
static void fmidi_repr_event(OutputStreamRef out, const fmidi_event_t &evt)
|
|
{
|
|
const uint8_t *data = evt.data;
|
|
uint32_t len = evt.datalen;
|
|
|
|
switch (evt.type) {
|
|
case fmidi_event_meta: {
|
|
if (!fmidi_repr_meta<OutputStreamRef>(out, data, len))
|
|
fmt::print(out, "(meta/unknown)");
|
|
break;
|
|
}
|
|
case fmidi_event_message: {
|
|
if (!fmidi_repr_midi<OutputStreamRef>(out, data, len))
|
|
fmt::print(out, "(unknown)");
|
|
break;
|
|
}
|
|
case fmidi_event_escape: {
|
|
fmt::print(out, "(raw {})", printfmt_bytes{data, len});
|
|
break;
|
|
}
|
|
case fmidi_event_xmi_timbre: {
|
|
fmt::print(out, "(xmi/timbre :patch {} :bank {})", evt.data[0], evt.data[1]);
|
|
break;
|
|
}
|
|
case fmidi_event_xmi_branch_point: {
|
|
fmt::print(out, "(xmi/branch-point {})", evt.data[0]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::ostream &operator<<(std::ostream &out, const fmidi_event_t &evt)
|
|
{
|
|
fmidi_repr_event<std::ostream &>(out, evt);
|
|
return out;
|
|
}
|
|
|
|
void fmidi_event_describe(const fmidi_event_t *evt, FILE *stream)
|
|
{
|
|
fmidi_repr_event<FILE *>(stream, *evt);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
std::ostream &operator<<(std::ostream &out, const printfmt_quoted &q)
|
|
{
|
|
const char *text = q.text;
|
|
size_t length = q.length;
|
|
out.put('"');
|
|
for (size_t i = 0; i < length; ++i) {
|
|
char c = text[i];
|
|
if (c == '\\' || c == '"') out.put('\\');
|
|
out.put(c);
|
|
}
|
|
return out.put('"');
|
|
}
|
|
|
|
std::ostream &operator<<(std::ostream &out, const printfmt_bytes &b)
|
|
{
|
|
const uint8_t *data = b.data;
|
|
for (size_t i = 0, n = b.size; i < n; ++i) {
|
|
if (i > 0) out.put(' ');
|
|
fmt::print(out, "#x{:02x}", data[i]);
|
|
}
|
|
return out;
|
|
}
|
|
#endif // !defined(FMIDI_DISABLE_DESCRIBE_API)
|
|
|
|
|
|
thread_local fmidi_error_info_t fmidi_last_error;
|
|
|
|
fmidi_status_t fmidi_errno()
|
|
{
|
|
return fmidi_last_error.code;
|
|
}
|
|
|
|
const fmidi_error_info_t *fmidi_errinfo()
|
|
{
|
|
return &fmidi_last_error;
|
|
}
|
|
|
|
const char *fmidi_strerror(fmidi_status_t status)
|
|
{
|
|
switch (status) {
|
|
case fmidi_ok: return "success";
|
|
case fmidi_err_format: return "invalid format";
|
|
case fmidi_err_eof: return "premature end of file";
|
|
case fmidi_err_input: return "input error";
|
|
case fmidi_err_largefile: return "file too large";
|
|
case fmidi_err_output: return "output error";
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
void Memory_Writer::put(uint8_t byte)
|
|
{
|
|
size_t size = mem.size();
|
|
size_t index = this->index;
|
|
|
|
if (index < size)
|
|
mem[index] = byte;
|
|
else
|
|
{
|
|
assert(index == size);
|
|
mem.push_back(byte);
|
|
}
|
|
|
|
this->index = index + 1;
|
|
}
|
|
|
|
void Memory_Writer::write(const void *data, size_t size)
|
|
{
|
|
size_t memsize = mem.size();
|
|
size_t index = this->index;
|
|
|
|
const uint8_t *bytes = (const uint8_t *)data;
|
|
size_t ncopy = std::min(size, memsize - index);
|
|
std::copy(bytes, bytes + ncopy, &mem[index]);
|
|
|
|
mem.insert(mem.end(), bytes + ncopy, bytes + size);
|
|
this->index = index + size;
|
|
}
|
|
|
|
bool Memory_Writer::seek(off_t offset, int whence)
|
|
{
|
|
std::make_unsigned<off_t>::type uoffset(offset);
|
|
size_t size = mem.size();
|
|
size_t index = this->index;
|
|
|
|
switch (whence) {
|
|
case SEEK_SET:
|
|
if (uoffset > size)
|
|
return false;
|
|
this->index = uoffset;
|
|
break;
|
|
case SEEK_CUR:
|
|
if (offset >= 0) {
|
|
if (size - index < uoffset)
|
|
return false;
|
|
this->index = index + uoffset;
|
|
}
|
|
else {
|
|
if (index < uoffset)
|
|
return false;
|
|
this->index = index - uoffset;
|
|
}
|
|
break;
|
|
case SEEK_END:
|
|
if (uoffset > size)
|
|
return false;
|
|
this->index = size - uoffset;
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <assert.h>
|
|
|
|
struct fmidi_player_context {
|
|
fmidi_player_t *plr;
|
|
fmidi_seq_u seq;
|
|
double timepos;
|
|
double speed;
|
|
bool have_event;
|
|
fmidi_seq_event_t sqevt;
|
|
void (*cbfn)(const fmidi_event_t *, void *);
|
|
void *cbdata;
|
|
void (*finifn)(void *);
|
|
void *finidata;
|
|
};
|
|
|
|
struct fmidi_player {
|
|
bool running;
|
|
fmidi_player_context ctx;
|
|
};
|
|
|
|
fmidi_player_t *fmidi_player_new(fmidi_smf_t *smf)
|
|
{
|
|
fmidi_player_u plr(new fmidi_player_t);
|
|
plr->running = false;
|
|
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
ctx.plr = plr.get();
|
|
ctx.seq.reset(fmidi_seq_new(smf));
|
|
ctx.timepos = 0;
|
|
ctx.speed = 1;
|
|
ctx.have_event = false;
|
|
ctx.cbfn = nullptr;
|
|
ctx.cbdata = nullptr;
|
|
ctx.finifn = nullptr;
|
|
ctx.finidata = nullptr;
|
|
|
|
return plr.release();
|
|
}
|
|
|
|
void fmidi_player_tick(fmidi_player_t *plr, double delta)
|
|
{
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
fmidi_seq_t &seq = *ctx.seq;
|
|
void (*cbfn)(const fmidi_event_t *, void *) = ctx.cbfn;
|
|
void *cbdata = ctx.cbdata;
|
|
|
|
double timepos = ctx.timepos;
|
|
bool have_event = ctx.have_event;
|
|
fmidi_seq_event_t &sqevt = ctx.sqevt;
|
|
|
|
timepos += ctx.speed * delta;
|
|
|
|
bool more = have_event || fmidi_seq_next_event(&seq, &sqevt);
|
|
if (more) {
|
|
have_event = true;
|
|
while (more && timepos > sqevt.time) {
|
|
const fmidi_event_t &event = *sqevt.event;
|
|
if (cbfn)
|
|
cbfn(&event, cbdata);
|
|
have_event = more = fmidi_seq_next_event(&seq, &sqevt);
|
|
}
|
|
}
|
|
|
|
ctx.have_event = have_event;
|
|
ctx.timepos = timepos;
|
|
|
|
if (!more) {
|
|
plr->running = false;
|
|
if (ctx.finifn)
|
|
ctx.finifn(ctx.finidata);
|
|
}
|
|
}
|
|
|
|
void fmidi_player_free(fmidi_player_t *plr)
|
|
{
|
|
delete plr;
|
|
}
|
|
|
|
void fmidi_player_start(fmidi_player_t *plr)
|
|
{
|
|
plr->running = true;
|
|
}
|
|
|
|
void fmidi_player_stop(fmidi_player_t *plr)
|
|
{
|
|
plr->running = false;
|
|
}
|
|
|
|
void fmidi_player_rewind(fmidi_player_t *plr)
|
|
{
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
fmidi_seq_rewind(ctx.seq.get());
|
|
ctx.timepos = 0;
|
|
ctx.have_event = false;
|
|
}
|
|
|
|
bool fmidi_player_running(const fmidi_player_t *plr)
|
|
{
|
|
return plr->running;
|
|
}
|
|
|
|
double fmidi_player_current_time(const fmidi_player_t *plr)
|
|
{
|
|
return plr->ctx.timepos;
|
|
}
|
|
|
|
void fmidi_player_goto_time(fmidi_player_t *plr, double time)
|
|
{
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
fmidi_seq_t &seq = *ctx.seq;
|
|
|
|
uint8_t programs[16];
|
|
uint8_t controls[16 * 128];
|
|
std::fill_n(programs, 16, 0);
|
|
std::fill_n(controls, 16 * 128, 255);
|
|
|
|
fmidi_player_rewind(plr);
|
|
|
|
for (fmidi_seq_event_t sqevt;
|
|
fmidi_seq_peek_event(&seq, &sqevt) && sqevt.time < time;) {
|
|
const fmidi_event_t &evt = *sqevt.event;
|
|
if (evt.type == fmidi_event_message) {
|
|
uint8_t status = evt.data[0];
|
|
if (status >> 4 == 0b1100 && evt.datalen == 2) { // program change
|
|
uint8_t channel = status & 0xf;
|
|
programs[channel] = evt.data[1] & 127;
|
|
}
|
|
else if (status >> 4 == 0b1011 && evt.datalen == 3) { // control change
|
|
uint8_t channel = status & 0xf;
|
|
uint8_t id = evt.data[1] & 127;
|
|
controls[channel * 128 + id] = evt.data[2] & 127;
|
|
}
|
|
}
|
|
fmidi_seq_next_event(&seq, nullptr);
|
|
}
|
|
|
|
ctx.timepos = time;
|
|
|
|
if (ctx.cbfn) {
|
|
uint8_t evtbuf[fmidi_event_sizeof(3)];
|
|
fmidi_event_t *evt = (fmidi_event_t *)evtbuf;
|
|
evt->type = fmidi_event_message;
|
|
evt->delta = 0;
|
|
|
|
for (unsigned c = 0; c < 16; ++c) {
|
|
// all sound off
|
|
evt->datalen = 3;
|
|
evt->data[0] = (0b1011 << 4) | c;
|
|
evt->data[1] = 120;
|
|
evt->data[2] = 0;
|
|
ctx.cbfn(evt, ctx.cbdata);
|
|
// reset all controllers
|
|
evt->datalen = 3;
|
|
evt->data[0] = (0b1011 << 4) | c;
|
|
evt->data[1] = 121;
|
|
evt->data[2] = 0;
|
|
ctx.cbfn(evt, ctx.cbdata);
|
|
// program change
|
|
evt->datalen = 2;
|
|
evt->data[0] = (0b1100 << 4) | c;
|
|
evt->data[1] = programs[c];
|
|
ctx.cbfn(evt, ctx.cbdata);
|
|
// control change
|
|
for (unsigned id = 0; id < 128; ++id) {
|
|
uint8_t val = controls[c * 128 + id];
|
|
if (val < 128) {
|
|
evt->datalen = 3;
|
|
evt->data[0] = (0b1011 << 4) | c;
|
|
evt->data[1] = id;
|
|
evt->data[2] = val;
|
|
ctx.cbfn(evt, ctx.cbdata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double fmidi_player_current_speed(const fmidi_player_t *plr)
|
|
{
|
|
return plr->ctx.speed;
|
|
}
|
|
|
|
void fmidi_player_set_speed(fmidi_player_t *plr, double speed)
|
|
{
|
|
plr->ctx.speed = speed;
|
|
}
|
|
|
|
void fmidi_player_event_callback(
|
|
fmidi_player_t *plr, void (*cbfn)(const fmidi_event_t *, void *), void *cbdata)
|
|
{
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
ctx.cbfn = cbfn;
|
|
ctx.cbdata = cbdata;
|
|
}
|
|
|
|
void fmidi_player_finish_callback(
|
|
fmidi_player_t *plr, void (*cbfn)(void *), void *cbdata)
|
|
{
|
|
fmidi_player_context &ctx = plr->ctx;
|
|
ctx.finifn = cbfn;
|
|
ctx.finidata = cbdata;
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <memory>
|
|
#include <string.h>
|
|
|
|
struct fmidi_seq_timing {
|
|
fmidi_smpte startoffset;
|
|
uint32_t tempo;
|
|
};
|
|
|
|
struct fmidi_seq_pending_event {
|
|
const fmidi_event_t *event;
|
|
double delta;
|
|
};
|
|
|
|
struct fmidi_seq_track_info {
|
|
double timepos;
|
|
fmidi_track_iter_t iter;
|
|
fmidi_seq_pending_event next;
|
|
std::shared_ptr<fmidi_seq_timing> timing;
|
|
};
|
|
|
|
struct fmidi_seq {
|
|
const fmidi_smf_t *smf;
|
|
std::unique_ptr<fmidi_seq_track_info[]> track;
|
|
};
|
|
|
|
static double fmidi_convert_delta(
|
|
const fmidi_seq_t *seq, uint16_t trkno, double delta)
|
|
{
|
|
uint16_t unit = fmidi_smf_get_info(seq->smf)->delta_unit;
|
|
uint32_t tempo = seq->track[trkno].timing->tempo;
|
|
return fmidi_delta_time(delta, unit, tempo);
|
|
}
|
|
|
|
fmidi_seq_t *fmidi_seq_new(const fmidi_smf_t *smf)
|
|
{
|
|
std::unique_ptr<fmidi_seq_t> seq(new fmidi_seq_t);
|
|
seq->smf = smf;
|
|
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(smf);
|
|
uint16_t format = info->format;
|
|
uint16_t ntracks = info->track_count;
|
|
|
|
seq->track.reset(new fmidi_seq_track_info[ntracks]);
|
|
|
|
for (unsigned i = 0; i < ntracks; ++i) {
|
|
fmidi_seq_track_info &track = seq->track[i];
|
|
|
|
std::shared_ptr<fmidi_seq_timing> timing;
|
|
if (format == 2 || i == 0)
|
|
timing.reset(new fmidi_seq_timing);
|
|
else
|
|
timing = seq->track[0].timing;
|
|
track.timing = timing;
|
|
}
|
|
|
|
fmidi_seq_rewind(seq.get());
|
|
return seq.release();
|
|
}
|
|
|
|
void fmidi_seq_free(fmidi_seq_t *seq)
|
|
{
|
|
delete seq;
|
|
}
|
|
|
|
void fmidi_seq_rewind(fmidi_seq_t *seq)
|
|
{
|
|
const fmidi_smf_t *smf = seq->smf;
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(smf);
|
|
uint16_t ntracks = info->track_count;
|
|
bool independent_multi_track =
|
|
ntracks > 1 && seq->track[0].timing != seq->track[1].timing;
|
|
|
|
for (unsigned i = 0; i < ntracks; ++i) {
|
|
fmidi_seq_track_info &track = seq->track[i];
|
|
std::shared_ptr<fmidi_seq_timing> timing = track.timing;
|
|
fmidi_smpte &startoffset = timing->startoffset;
|
|
fmidi_smf_track_begin(&track.iter, i);
|
|
track.next.event = nullptr;
|
|
memset(startoffset.code, 0, 5);
|
|
timing->tempo = 500000;
|
|
track.timepos = fmidi_smpte_time(&startoffset);
|
|
}
|
|
|
|
for (unsigned i = 0; i < ntracks; ++i) {
|
|
fmidi_seq_track_info &track = seq->track[i];
|
|
std::shared_ptr<fmidi_seq_timing> timing = track.timing;
|
|
fmidi_smpte &startoffset = timing->startoffset;
|
|
|
|
const fmidi_event_t *evt;
|
|
fmidi_track_iter_t it;
|
|
fmidi_smf_track_begin(&it, i);
|
|
while ((evt = fmidi_smf_track_next(smf, &it)) &&
|
|
evt->delta == 0 && evt->type == fmidi_event_meta) {
|
|
uint8_t id = evt->data[0];
|
|
if (id == 0x54 && evt->datalen == 6) { // SMPTE offset
|
|
// disregard SMPTE offset for format 1 MIDI and similar
|
|
if (independent_multi_track)
|
|
memcpy(startoffset.code, &evt->data[1], 5);
|
|
}
|
|
if (id == 0x51 && evt->datalen == 4) { // set tempo
|
|
const uint8_t *d24 = &evt->data[1];
|
|
timing->tempo = (d24[0] << 16) | (d24[1] << 8) | d24[2];
|
|
}
|
|
}
|
|
track.timepos = fmidi_smpte_time(&startoffset);
|
|
}
|
|
}
|
|
|
|
static fmidi_seq_pending_event *fmidi_seq_track_current_event(
|
|
fmidi_seq_t *seq, uint16_t trkno)
|
|
{
|
|
const fmidi_smf_t *smf = seq->smf;
|
|
fmidi_seq_track_info &track = seq->track[trkno];
|
|
|
|
fmidi_seq_pending_event *pending;
|
|
if (track.next.event)
|
|
return &track.next;
|
|
|
|
const fmidi_event_t *evt = fmidi_smf_track_next(smf, &track.iter);
|
|
if (!evt)
|
|
return nullptr;
|
|
|
|
if (evt->type == fmidi_event_meta) {
|
|
uint8_t tag = evt->data[0];
|
|
if (tag == 0x2f || tag == 0x3f) // end of track
|
|
return nullptr; // stop now even if the final event has delta
|
|
}
|
|
|
|
pending = &track.next;
|
|
pending->event = evt;
|
|
pending->delta = evt->delta;
|
|
return pending;
|
|
}
|
|
|
|
static int fmidi_seq_next_track(fmidi_seq_t *seq)
|
|
{
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(seq->smf);
|
|
unsigned ntracks = info->track_count;
|
|
|
|
unsigned trkno = 0;
|
|
fmidi_seq_pending_event *pevt;
|
|
|
|
pevt = fmidi_seq_track_current_event(seq, 0);
|
|
while (!pevt && ++trkno < ntracks)
|
|
pevt = fmidi_seq_track_current_event(seq, trkno);
|
|
|
|
if (!pevt)
|
|
return -1;
|
|
|
|
double nearest = fmidi_convert_delta(seq, trkno, pevt->delta) +
|
|
seq->track[trkno].timepos;
|
|
for (unsigned i = trkno + 1; i < ntracks; ++i) {
|
|
if ((pevt = fmidi_seq_track_current_event(seq, i))) {
|
|
double time = fmidi_convert_delta(seq, i, pevt->delta) +
|
|
seq->track[i].timepos;
|
|
if (time < nearest) {
|
|
trkno = i;
|
|
nearest = time;
|
|
}
|
|
}
|
|
}
|
|
|
|
return trkno;
|
|
}
|
|
|
|
bool fmidi_seq_peek_event(fmidi_seq_t *seq, fmidi_seq_event_t *sqevt)
|
|
{
|
|
unsigned trkno = fmidi_seq_next_track(seq);
|
|
if ((int)trkno == -1)
|
|
return false;
|
|
|
|
fmidi_seq_track_info &nexttrk = seq->track[trkno];
|
|
|
|
const fmidi_seq_pending_event *pevt =
|
|
fmidi_seq_track_current_event(seq, trkno);
|
|
if (!pevt)
|
|
return false;
|
|
|
|
if (sqevt) {
|
|
sqevt->time = fmidi_convert_delta(seq, trkno, pevt->delta) + nexttrk.timepos;
|
|
sqevt->track = trkno;
|
|
sqevt->event = pevt->event;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void fmidi_seq_track_advance_by(
|
|
fmidi_seq_t *seq, unsigned trkno, double time)
|
|
{
|
|
const fmidi_smf_t *smf = seq->smf;
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(smf);
|
|
uint16_t unit = info->delta_unit;
|
|
fmidi_seq_track_info &trk = seq->track[trkno];
|
|
fmidi_seq_timing &tim = *trk.timing;
|
|
|
|
fmidi_seq_pending_event *evt = fmidi_seq_track_current_event(seq, trkno);
|
|
if (evt)
|
|
evt->delta -= fmidi_time_delta(time, unit, tim.tempo);
|
|
trk.timepos += time;
|
|
}
|
|
|
|
bool fmidi_seq_next_event(fmidi_seq_t *seq, fmidi_seq_event_t *sqevt)
|
|
{
|
|
fmidi_seq_event_t pltmp;
|
|
sqevt = sqevt ? sqevt : &pltmp;
|
|
|
|
if (!fmidi_seq_peek_event(seq, sqevt))
|
|
return false;
|
|
|
|
double time = sqevt->time;
|
|
unsigned trkno = sqevt->track;
|
|
const fmidi_event_t *evt = sqevt->event;
|
|
fmidi_seq_track_info &trk = seq->track[trkno];
|
|
|
|
const fmidi_smf_t *smf = seq->smf;
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(smf);
|
|
unsigned ntracks = info->track_count;
|
|
double elapsed = time - trk.timepos;
|
|
|
|
for (unsigned i = 0; i < ntracks; ++i)
|
|
if (i != trkno)
|
|
fmidi_seq_track_advance_by(seq, i, elapsed);
|
|
|
|
if (evt->type == fmidi_event_meta) {
|
|
if (evt->data[0] == 0x51 && evt->datalen == 4) { // set tempo
|
|
const uint8_t *d24 = &evt->data[1];
|
|
trk.timing->tempo = (d24[0] << 16) | (d24[1] << 8) | d24[2];
|
|
}
|
|
}
|
|
|
|
trk.timepos = time;
|
|
trk.next.event = nullptr;
|
|
return true;
|
|
}
|
|
|
|
|
|
#include <memory>
|
|
#include <stdio.h>
|
|
|
|
////////////////////////
|
|
// FILE PATH ENCODING //
|
|
////////////////////////
|
|
|
|
FILE *fmidi_fopen(const char *path, const char *mode);
|
|
|
|
///////////////
|
|
// FILE RAII //
|
|
///////////////
|
|
|
|
struct FILE_deleter;
|
|
typedef std::unique_ptr<FILE, FILE_deleter> unique_FILE;
|
|
|
|
struct FILE_deleter {
|
|
void operator()(FILE *stream) const
|
|
{ fclose(stream); }
|
|
};
|
|
#if defined(_WIN32)
|
|
# include <windows.h>
|
|
# include <memory>
|
|
# include <errno.h>
|
|
#endif
|
|
|
|
FILE *fmidi_fopen(const char *path, const char *mode)
|
|
{
|
|
#if !defined(_WIN32)
|
|
return fopen(path, mode);
|
|
#else
|
|
auto toWideString = [](const char *utf8) -> wchar_t * {
|
|
unsigned wsize = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0);
|
|
if (wsize == 0)
|
|
return nullptr;
|
|
wchar_t *wide = new wchar_t[wsize];
|
|
wsize = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, wsize);
|
|
if (wsize == 0) {
|
|
delete[] wide;
|
|
return nullptr;
|
|
}
|
|
return wide;
|
|
};
|
|
std::unique_ptr<wchar_t[]> wpath(toWideString(path));
|
|
if (!wpath) {
|
|
errno = EINVAL;
|
|
return nullptr;
|
|
}
|
|
std::unique_ptr<wchar_t[]> wmode(toWideString(mode));
|
|
if (!wmode) {
|
|
errno = EINVAL;
|
|
return nullptr;
|
|
}
|
|
return _wfopen(wpath.get(), wmode.get());
|
|
#endif
|
|
}
|
|
|
|
|
|
memstream_status memstream::setpos(size_t off)
|
|
{
|
|
if (off > length_)
|
|
return ms_err_eof;
|
|
offset_ = off;
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::skip(size_t count)
|
|
{
|
|
if (length_ - offset_ < count)
|
|
return ms_err_eof;
|
|
offset_ += count;
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::skipbyte(unsigned byte)
|
|
{
|
|
unsigned otherbyte;
|
|
memstream_status status = peekbyte(&otherbyte);
|
|
if (status)
|
|
return status;
|
|
if (byte != otherbyte)
|
|
return ms_err_format;
|
|
++offset_;
|
|
return ms_ok;
|
|
}
|
|
|
|
const uint8_t *memstream::peek(size_t length)
|
|
{
|
|
if (length > length_ - offset_)
|
|
return nullptr;
|
|
return base_ + offset_;
|
|
}
|
|
|
|
const uint8_t *memstream::read(size_t length)
|
|
{
|
|
const uint8_t *ptr = peek(length);
|
|
if (ptr)
|
|
offset_ += length;
|
|
return ptr;
|
|
}
|
|
|
|
memstream_status memstream::peekbyte(unsigned *retp)
|
|
{
|
|
if (length_ <= offset_)
|
|
return ms_err_eof;
|
|
if (retp)
|
|
*retp = base_[offset_];
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::readbyte(unsigned *retp)
|
|
{
|
|
memstream_status ret = peekbyte(retp);
|
|
if (ret)
|
|
return ret;
|
|
++offset_;
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::readintLE(uint32_t *retp, unsigned length)
|
|
{
|
|
const uint8_t *ptr = read(length);
|
|
if (!ptr)
|
|
return ms_err_eof;
|
|
uint32_t ret = 0;
|
|
for (unsigned i = length; i-- > 0;)
|
|
ret = (ret << 8) | ptr[i];
|
|
if (retp)
|
|
*retp = ret;
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::readintBE(uint32_t *retp, unsigned length)
|
|
{
|
|
const uint8_t *ptr = read(length);
|
|
if (!ptr)
|
|
return ms_err_eof;
|
|
uint32_t ret = 0;
|
|
for (unsigned i = 0; i < length; ++i)
|
|
ret = (ret << 8) | ptr[i];
|
|
if (retp)
|
|
*retp = ret;
|
|
return ms_ok;
|
|
}
|
|
|
|
memstream_status memstream::readvlq(uint32_t *retp)
|
|
{
|
|
memstream_status ret;
|
|
uint32_t value;
|
|
unsigned length;
|
|
std::tie(ret, value, length) = doreadvlq();
|
|
offset_ += length;
|
|
if (retp)
|
|
*retp = value;
|
|
return ret;
|
|
}
|
|
|
|
memstream_status memstream::peekvlq(uint32_t *retp)
|
|
{
|
|
memstream_status ret;
|
|
uint32_t value;
|
|
unsigned length;
|
|
std::tie(ret, value, length) = doreadvlq();
|
|
if (retp)
|
|
*retp = value;
|
|
return ret;
|
|
}
|
|
|
|
memstream::vlq_result memstream::doreadvlq()
|
|
{
|
|
uint32_t ret = 0;
|
|
unsigned length;
|
|
bool cont = true;
|
|
for (length = 0; cont && length < 4; ++length) {
|
|
if (offset_ + length >= length_)
|
|
return vlq_result{ms_err_eof, 0, 0};
|
|
uint8_t byte = base_[offset_ + length];
|
|
ret = (ret << 7) | (byte & ((1u << 7) - 1));
|
|
cont = byte & (1u << 7);
|
|
}
|
|
if (cont)
|
|
return vlq_result{ms_err_format, 0, 0};
|
|
return vlq_result{ms_ok, ret, length};
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <algorithm>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#if defined(_WIN32)
|
|
# define fileno _fileno
|
|
#endif
|
|
|
|
#define FOURCC(x) \
|
|
(((uint8_t)(x)[0] << 24) | \
|
|
((uint8_t)(x)[1] << 16) | \
|
|
((uint8_t)(x)[2] << 8) | \
|
|
((uint8_t)(x)[3]))
|
|
|
|
struct fmidi_xmi_timb {
|
|
uint32_t patch;
|
|
uint32_t bank;
|
|
};
|
|
|
|
struct fmidi_xmi_rbrn {
|
|
uint32_t id;
|
|
uint32_t dest;
|
|
};
|
|
|
|
struct fmidi_xmi_note {
|
|
uint32_t delta;
|
|
uint8_t channel;
|
|
uint8_t note;
|
|
uint8_t velo;
|
|
};
|
|
|
|
static bool operator<(const fmidi_xmi_note &a, const fmidi_xmi_note &b)
|
|
{
|
|
return a.delta < b.delta;
|
|
}
|
|
|
|
static void fmidi_xmi_emit_noteoffs(
|
|
uint32_t *pdelta, std::vector<fmidi_xmi_note> ¬eoffs,
|
|
std::vector<uint8_t> &evbuf)
|
|
{
|
|
uint32_t delta = *pdelta;
|
|
|
|
std::sort(noteoffs.begin(), noteoffs.end());
|
|
|
|
size_t i = 0;
|
|
size_t n = noteoffs.size();
|
|
|
|
for (; i < n; ++i) {
|
|
fmidi_xmi_note xn = noteoffs[i];
|
|
if (delta < xn.delta)
|
|
break;
|
|
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, 3);
|
|
event->type = fmidi_event_message;
|
|
event->delta = xn.delta;
|
|
event->datalen = 3;
|
|
|
|
uint8_t *data = event->data;
|
|
data[0] = 0x80 | xn.channel;
|
|
data[1] = xn.note;
|
|
data[2] = xn.velo;
|
|
|
|
delta -= xn.delta;
|
|
for (size_t k = i + 1; k < n; ++k)
|
|
noteoffs[k].delta -= xn.delta;
|
|
}
|
|
|
|
size_t j = 0;
|
|
for (; i < n; ++i) {
|
|
fmidi_xmi_note xn = noteoffs[i];
|
|
noteoffs[j++] = xn;
|
|
}
|
|
noteoffs.resize(j);
|
|
|
|
*pdelta = delta;
|
|
}
|
|
|
|
static bool fmidi_xmi_read_events(
|
|
memstream &mb, fmidi_raw_track &track,
|
|
const fmidi_xmi_timb *timb, uint32_t timb_count,
|
|
const fmidi_xmi_rbrn *rbrn, uint32_t rbrn_count)
|
|
{
|
|
memstream_status ms;
|
|
std::vector<uint8_t> evbuf;
|
|
evbuf.reserve(8192);
|
|
|
|
std::vector<fmidi_xmi_note> noteoffs;
|
|
noteoffs.reserve(128);
|
|
|
|
for (uint32_t i = 0; i < timb_count; ++i) {
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, 2);
|
|
event->type = fmidi_event_xmi_timbre;
|
|
event->delta = 0;
|
|
event->datalen = 2;
|
|
uint8_t * data = event->data;
|
|
data[0] = timb[i].patch;
|
|
data[1] = timb[i].bank;
|
|
}
|
|
|
|
bool eot = false;
|
|
while (!eot) {
|
|
uint32_t delta = 0;
|
|
unsigned status = 0;
|
|
|
|
size_t branch = ~(size_t)0;
|
|
for (uint32_t i = 0; i < rbrn_count && branch == ~(size_t)0; ++i) {
|
|
if (rbrn[i].dest == mb.getpos())
|
|
branch = i;
|
|
}
|
|
|
|
while (!(status & 128)) {
|
|
if ((ms = mb.readbyte(&status)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
delta += (status & 128) ? 0 : status;
|
|
}
|
|
|
|
if (branch != ~(size_t)0) {
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, 1);
|
|
event->type = fmidi_event_xmi_branch_point;
|
|
event->delta = delta;
|
|
event->datalen = 1;
|
|
event->data[0] = rbrn[branch].id;
|
|
delta = 0;
|
|
}
|
|
|
|
fmidi_xmi_emit_noteoffs(&delta, noteoffs, evbuf);
|
|
|
|
if (status == 0xff) {
|
|
unsigned type;
|
|
uint32_t length;
|
|
if ((ms = mb.readbyte(&type)) ||
|
|
(ms = mb.readvlq(&length)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
const uint8_t *data = mb.read(length);
|
|
if (!data)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
|
|
eot = type == 0x2F;
|
|
|
|
if (eot) {
|
|
// emit later
|
|
}
|
|
else if (type == 0x51) {
|
|
// don't emit tempo change
|
|
}
|
|
else {
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, length + 1);
|
|
event->type = fmidi_event_meta;
|
|
event->delta = delta;
|
|
event->datalen = length + 1;
|
|
event->data[0] = type;
|
|
memcpy(event->data + 1, data, length);
|
|
}
|
|
}
|
|
else if (status == 0xf0) {
|
|
uint32_t length;
|
|
if ((ms = mb.readvlq(&length)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
const uint8_t *data = mb.read(length);
|
|
if (!data)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, length + 1);
|
|
event->type = fmidi_event_message;
|
|
event->delta = delta;
|
|
event->datalen = length + 1;
|
|
event->data[0] = 0xf0;
|
|
memcpy(event->data + 1, data, length);
|
|
}
|
|
else if (status == 0xf7) {
|
|
RET_FAIL(false, fmidi_err_format);
|
|
}
|
|
else if ((status & 0xf0) == 0x90) {
|
|
mb.setpos(mb.getpos() - 1);
|
|
const uint8_t *data = mb.read(3);
|
|
if (!data)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
|
|
uint32_t interval;
|
|
if ((ms = mb.readvlq(&interval)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, 3);
|
|
event->type = fmidi_event_message;
|
|
event->delta = delta;
|
|
event->datalen = 3;
|
|
memcpy(event->data, data, 3);
|
|
|
|
fmidi_xmi_note noteoff;
|
|
noteoff.delta = interval;
|
|
noteoff.channel = data[0] & 15;
|
|
noteoff.note = data[1];
|
|
noteoff.velo = data[2];
|
|
noteoffs.push_back(noteoff);
|
|
}
|
|
else {
|
|
unsigned length = fmidi_message_sizeof(status);
|
|
mb.setpos(mb.getpos() - 1);
|
|
const uint8_t *data = mb.read(length);
|
|
if (!data)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, length);
|
|
event->type = fmidi_event_message;
|
|
event->delta = delta;
|
|
event->datalen = length;
|
|
memcpy(event->data, data, length);
|
|
}
|
|
}
|
|
|
|
{
|
|
uint32_t delta = UINT32_MAX;
|
|
fmidi_xmi_emit_noteoffs(&delta, noteoffs, evbuf);
|
|
}
|
|
|
|
{
|
|
fmidi_event_t *event = fmidi_event_alloc(evbuf, 1);
|
|
event->type = fmidi_event_meta;
|
|
event->delta = 0;
|
|
event->datalen = 1;
|
|
event->data[0] = 0x2F;
|
|
}
|
|
|
|
uint32_t evdatalen = track.length = evbuf.size();
|
|
uint8_t *evdata = new uint8_t[evdatalen];
|
|
track.data.reset(evdata);
|
|
memcpy(evdata, evbuf.data(), evdatalen);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool fmidi_xmi_read_track(memstream &mb, fmidi_raw_track &track)
|
|
{
|
|
memstream_status ms;
|
|
|
|
const uint8_t *fourcc;
|
|
if (!(fourcc = mb.read(4)))
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
if (memcmp(fourcc, "FORM", 4))
|
|
RET_FAIL(false, fmidi_err_format);
|
|
|
|
uint32_t formsize;
|
|
if ((ms = mb.readintBE(&formsize, 4)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
const uint8_t *formdata = mb.read(formsize);
|
|
if (!formdata)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
memstream mbform(formdata, formsize);
|
|
|
|
if (!(fourcc = mbform.read(4)))
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
if (memcmp(fourcc, "XMID", 4))
|
|
RET_FAIL(false, fmidi_err_format);
|
|
|
|
std::unique_ptr<fmidi_xmi_timb[]> timb;
|
|
uint32_t timb_count = 0;
|
|
std::unique_ptr<fmidi_xmi_rbrn[]> rbrn;
|
|
uint32_t rbrn_count = 0;
|
|
|
|
while (mbform.getpos() < mbform.endpos()) {
|
|
if (!(fourcc = mbform.read(4)))
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
|
|
uint32_t chunksize;
|
|
if ((ms = mbform.readintBE(&chunksize, 4)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
const uint8_t *chunkdata = mbform.read(chunksize);
|
|
if (!chunkdata)
|
|
RET_FAIL(false, fmidi_err_eof);
|
|
memstream mbchunk(chunkdata, chunksize);
|
|
|
|
switch (FOURCC(fourcc)) {
|
|
case FOURCC("TIMB"): {
|
|
if ((ms = mbchunk.readintLE(&timb_count, 2)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
timb.reset(new fmidi_xmi_timb[timb_count]);
|
|
for (uint32_t i = 0; i < timb_count; ++i) {
|
|
if ((ms = mbchunk.readintLE(&timb[i].patch, 1)) ||
|
|
(ms = mbchunk.readintLE(&timb[i].bank, 1)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FOURCC("RBRN"): {
|
|
if ((ms = mbchunk.readintLE(&rbrn_count, 2)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
rbrn.reset(new fmidi_xmi_rbrn[rbrn_count]);
|
|
for (uint32_t i = 0; i < rbrn_count; ++i) {
|
|
if ((ms = mbchunk.readintLE(&rbrn[i].id, 2)) ||
|
|
(ms = mbchunk.readintLE(&rbrn[i].dest, 4)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
if (rbrn[i].id >= 128)
|
|
RET_FAIL(false, fmidi_err_format);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FOURCC("EVNT"):
|
|
if (!fmidi_xmi_read_events(
|
|
mbchunk, track,
|
|
timb.get(), timb_count, rbrn.get(), rbrn_count))
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
|
|
if (mb.getpos() & 1) {
|
|
if ((ms = mb.skip(1)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32_t fmidi_xmi_update_unit(fmidi_smf_t *smf)
|
|
{
|
|
uint32_t res = 1;
|
|
const fmidi_event_t *evt;
|
|
fmidi_track_iter_t it;
|
|
fmidi_smf_track_begin(&it, 0);
|
|
bool found = false;
|
|
while (!found && (evt = fmidi_smf_track_next(smf, &it))) {
|
|
if (evt->type == fmidi_event_meta) {
|
|
uint8_t id = evt->data[0];
|
|
if (id == 0x51 && evt->datalen == 4) { // set tempo
|
|
const uint8_t *d24 = &evt->data[1];
|
|
uint32_t tempo = (d24[0] << 16) | (d24[1] << 8) | d24[2];
|
|
res = 3;
|
|
smf->info.delta_unit = tempo * res * 120 / 1000000;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_xmi_mem_read(const uint8_t *data, size_t length)
|
|
{
|
|
const uint8_t header[] = {
|
|
'F', 'O', 'R', 'M', 0, 0, 0, 14,
|
|
'X', 'D', 'I', 'R', 'I', 'N', 'F', 'O', 0, 0, 0, 2
|
|
};
|
|
|
|
const uint8_t *start = std::search(
|
|
data, data + length, header, header + sizeof(header));
|
|
if (start == data + length)
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
length = length - (start - data);
|
|
data = start;
|
|
|
|
// ensure padding to even size (The Lost Vikings)
|
|
std::unique_ptr<uint8_t[]> padded;
|
|
if (length & 1) {
|
|
padded.reset(new uint8_t[length + 1]);
|
|
memcpy(padded.get(), data, length);
|
|
padded[length] = 0;
|
|
data = padded.get();
|
|
length = length + 1;
|
|
}
|
|
|
|
memstream mb(data + sizeof(header), length - sizeof(header));
|
|
memstream_status ms;
|
|
|
|
uint32_t ntracks;
|
|
if ((ms = mb.readintLE(&ntracks, 2)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (ntracks < 1)
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
const uint8_t *fourcc;
|
|
if (!(fourcc = mb.read(4)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
if (memcmp(fourcc, "CAT ", 4))
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
uint32_t catsize;
|
|
if ((ms = mb.readintBE(&catsize, 4)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (mb.endpos() - mb.getpos() < catsize)
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
|
|
if (!(fourcc = mb.read(4)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
if (memcmp(fourcc, "XMID", 4))
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
fmidi_smf_u smf(new fmidi_smf);
|
|
smf->info.format = (ntracks > 1) ? 2 : 0;
|
|
smf->info.track_count = ntracks;
|
|
smf->info.delta_unit = 60;
|
|
smf->track.reset(new fmidi_raw_track[ntracks]);
|
|
|
|
for (uint32_t i = 0; i < ntracks; ++i) {
|
|
if (!fmidi_xmi_read_track(mb, smf->track[i]))
|
|
return nullptr;
|
|
if (mb.getpos() & 1) {
|
|
if ((ms = mb.skip(1)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
}
|
|
}
|
|
|
|
uint32_t res = fmidi_xmi_update_unit(smf.get());
|
|
if (res == 0)
|
|
return nullptr;
|
|
|
|
for (uint32_t i = 0; i < ntracks; ++i) {
|
|
fmidi_track_iter_t it;
|
|
fmidi_smf_track_begin(&it, i);
|
|
fmidi_event_t *event;
|
|
while ((event = const_cast<fmidi_event_t *>(
|
|
fmidi_smf_track_next(smf.get(), &it)))) {
|
|
event->delta *= res;
|
|
}
|
|
}
|
|
|
|
return smf.release();
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_xmi_file_read(const char *filename)
|
|
{
|
|
unique_FILE fh(fmidi_fopen(filename, "rb"));
|
|
if (!fh)
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
fmidi_smf_t *smf = fmidi_xmi_stream_read(fh.get());
|
|
return smf;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_xmi_stream_read(FILE *stream)
|
|
{
|
|
struct stat st;
|
|
size_t length;
|
|
|
|
rewind(stream);
|
|
|
|
if (fstat(fileno(stream), &st) != 0)
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
length = st.st_size;
|
|
if (length > fmidi_file_size_limit)
|
|
RET_FAIL(nullptr, fmidi_err_largefile);
|
|
|
|
bool pad = length & 1;
|
|
std::unique_ptr<uint8_t[]> buf(new uint8_t[length + pad]);
|
|
if (!fread(buf.get(), length, 1, stream))
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
if (pad)
|
|
buf[length] = 0;
|
|
|
|
fmidi_smf_t *smf = fmidi_xmi_mem_read(buf.get(), length + pad);
|
|
return smf;
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <vector>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#if defined(_WIN32)
|
|
# define fileno _fileno
|
|
#endif
|
|
|
|
const fmidi_smf_info_t *fmidi_smf_get_info(const fmidi_smf_t *smf)
|
|
{
|
|
return &smf->info;
|
|
}
|
|
|
|
double fmidi_smf_compute_duration(const fmidi_smf_t *smf)
|
|
{
|
|
double duration = 0;
|
|
fmidi_seq_u seq(fmidi_seq_new(smf));
|
|
fmidi_seq_event_t sqevt;
|
|
while (fmidi_seq_next_event(seq.get(), &sqevt))
|
|
duration = sqevt.time;
|
|
return duration;
|
|
}
|
|
|
|
static fmidi_event_t *fmidi_read_meta_event(
|
|
memstream &mb, std::vector<uint8_t> &evbuf, uint32_t delta)
|
|
{
|
|
memstream_status ms;
|
|
unsigned id;
|
|
if ((ms = mb.readbyte(&id)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
|
|
uint32_t datalen;
|
|
const uint8_t *data;
|
|
if (id == 0x2f || id == 0x3f) { // end of track
|
|
if (mb.skipbyte(0)) {
|
|
// omitted final null byte in some broken files
|
|
}
|
|
else {
|
|
// repeated end of track events
|
|
for (bool again = true; again;) {
|
|
size_t offset = mb.getpos();
|
|
again = !mb.readvlq(nullptr) && !mb.skipbyte(0xff) &&
|
|
(!mb.skipbyte(0x2f) || !mb.skipbyte(0x3f));
|
|
if (!again)
|
|
mb.setpos(offset);
|
|
else
|
|
again = !mb.skipbyte(0);
|
|
}
|
|
}
|
|
datalen = 0;
|
|
data = nullptr;
|
|
}
|
|
else {
|
|
if ((ms = mb.readvlq(&datalen)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (!(data = mb.read(datalen)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
}
|
|
fmidi_event_t *evt = fmidi_event_alloc(evbuf, datalen + 1);
|
|
evt->type = fmidi_event_meta;
|
|
evt->delta = delta;
|
|
evt->datalen = datalen + 1;
|
|
evt->data[0] = id;
|
|
memcpy(&evt->data[1], data, datalen);
|
|
return evt;
|
|
}
|
|
|
|
static fmidi_event_t *fmidi_read_escape_event(
|
|
memstream &mb, std::vector<uint8_t> &evbuf, uint32_t delta)
|
|
{
|
|
memstream_status ms;
|
|
uint32_t datalen;
|
|
const uint8_t *data;
|
|
if ((ms = mb.readvlq(&datalen)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (!(data = mb.read(datalen)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
|
|
fmidi_event_t *evt = fmidi_event_alloc(evbuf, datalen);
|
|
evt->type = fmidi_event_escape;
|
|
evt->delta = delta;
|
|
evt->datalen = datalen;
|
|
memcpy(&evt->data[0], data, datalen);
|
|
return evt;
|
|
}
|
|
|
|
static fmidi_event_t *fmidi_read_sysex_event(
|
|
memstream &mb, std::vector<uint8_t> &evbuf, uint32_t delta)
|
|
{
|
|
memstream_status ms;
|
|
fmidi_event_t *evt;
|
|
|
|
std::vector<uint8_t> syxbuf;
|
|
syxbuf.reserve(256);
|
|
syxbuf.push_back(0xf0);
|
|
|
|
uint32_t partlen;
|
|
const uint8_t *part;
|
|
if ((ms = mb.readvlq(&partlen)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (!(part = mb.read(partlen)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
|
|
bool term = false;
|
|
const uint8_t *endp;
|
|
|
|
// handle files having multiple concatenated sysex events in one
|
|
while ((endp = (const uint8_t *)memchr(part, 0xf7, partlen))) {
|
|
syxbuf.insert(syxbuf.end(), part, endp + 1);
|
|
|
|
evt = fmidi_event_alloc(evbuf, syxbuf.size());
|
|
evt->type = fmidi_event_message;
|
|
evt->delta = delta;
|
|
evt->datalen = syxbuf.size();
|
|
memcpy(&evt->data[0], &syxbuf[0], syxbuf.size());
|
|
|
|
uint32_t reallen = endp + 1 - part;
|
|
partlen -= reallen;
|
|
part += reallen;
|
|
|
|
if (partlen == 0)
|
|
return evt;
|
|
|
|
if (part[0] != 0xf0) {
|
|
#if 1
|
|
// trailing garbage, ignore
|
|
#else
|
|
// sierra: incorrect length covering part of the next event. repair
|
|
mb.setpos(mb.getpos() - partlen);
|
|
#endif
|
|
return evt;
|
|
}
|
|
++part;
|
|
--partlen;
|
|
|
|
syxbuf.clear();
|
|
syxbuf.push_back(0xf0);
|
|
}
|
|
|
|
// handle the rest in multiple parts (Casio MIDI)
|
|
while (!term) {
|
|
term = endp;
|
|
if (term && endp + 1 != part + partlen) {
|
|
// ensure no excess bytes
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
}
|
|
syxbuf.insert(syxbuf.end(), part, part + partlen);
|
|
|
|
if (!term) {
|
|
size_t offset = mb.getpos();
|
|
bool havecont = false;
|
|
|
|
uint32_t contdelta;
|
|
unsigned id;
|
|
if (!mb.readvlq(&contdelta) && !mb.readbyte(&id)) {
|
|
// raw sequence incoming? use it as next sysex part
|
|
havecont = id == 0xf7;
|
|
}
|
|
if (havecont) {
|
|
if ((ms = mb.readvlq(&partlen)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if (!(part = mb.read(partlen)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
endp = (const uint8_t *)memchr(part, 0xf7, partlen);
|
|
}
|
|
else {
|
|
// no next part? assume unfinished message and repair
|
|
mb.setpos(offset);
|
|
syxbuf.push_back(0xf7);
|
|
term = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
evt = fmidi_event_alloc(evbuf, syxbuf.size());
|
|
evt->type = fmidi_event_message;
|
|
evt->delta = delta;
|
|
evt->datalen = syxbuf.size();
|
|
memcpy(&evt->data[0], &syxbuf[0], syxbuf.size());
|
|
return evt;
|
|
}
|
|
|
|
static fmidi_event_t *fmidi_read_message_event(
|
|
memstream &mb, std::vector<uint8_t> &evbuf, unsigned id, uint32_t delta)
|
|
{
|
|
uint32_t datalen = fmidi_message_sizeof(id);
|
|
const uint8_t *data;
|
|
if (datalen <= 0)
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
if (!(data = mb.read(datalen - 1)))
|
|
RET_FAIL(nullptr, fmidi_err_eof);
|
|
|
|
fmidi_event_t *evt = fmidi_event_alloc(evbuf, datalen);
|
|
evt->type = fmidi_event_message;
|
|
evt->delta = delta;
|
|
evt->datalen = datalen;
|
|
evt->data[0] = id;
|
|
memcpy(&evt->data[1], data, datalen - 1);
|
|
return evt;
|
|
}
|
|
|
|
static fmidi_event_t *fmidi_read_event(
|
|
memstream &mb, std::vector<uint8_t> &evbuf, uint8_t *runstatus)
|
|
{
|
|
memstream_status ms;
|
|
uint32_t delta;
|
|
unsigned id;
|
|
if ((ms = mb.readvlq(&delta)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
if ((ms = mb.readbyte(&id)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
|
|
fmidi_event_t *evt;
|
|
if (id == 0xff) {
|
|
evt = fmidi_read_meta_event(mb, evbuf, delta);
|
|
}
|
|
else if (id == 0xf7) {
|
|
evt = fmidi_read_escape_event(mb, evbuf, delta);
|
|
}
|
|
else if (id == 0xf0) {
|
|
evt = fmidi_read_sysex_event(mb, evbuf, delta);
|
|
}
|
|
else {
|
|
if (id & 128) {
|
|
*runstatus = id;
|
|
}
|
|
else {
|
|
id = *runstatus;
|
|
mb.setpos(mb.getpos() - 1);
|
|
}
|
|
evt = fmidi_read_message_event(mb, evbuf, id, delta);
|
|
}
|
|
|
|
return evt;
|
|
}
|
|
|
|
void fmidi_smf_track_begin(fmidi_track_iter_t *it, uint16_t track)
|
|
{
|
|
it->track = track;
|
|
it->index = 0;
|
|
}
|
|
|
|
const fmidi_event_t *fmidi_smf_track_next(
|
|
const fmidi_smf_t *smf, fmidi_track_iter_t *it)
|
|
{
|
|
if (it->track >= smf->info.track_count)
|
|
return nullptr;
|
|
|
|
const fmidi_raw_track &trk = smf->track[it->track];
|
|
const uint8_t *trkdata = trk.data.get();
|
|
|
|
const fmidi_event_t *evt = (const fmidi_event_t *)&trkdata[it->index];
|
|
if ((const uint8_t *)evt == trkdata + trk.length)
|
|
return nullptr;
|
|
|
|
it->index += fmidi_event_pad(fmidi_event_sizeof(evt->datalen));
|
|
return evt;
|
|
}
|
|
|
|
static bool fmidi_smf_read_contents(fmidi_smf_t *smf, memstream &mb)
|
|
{
|
|
uint16_t ntracks = smf->info.track_count;
|
|
smf->track.reset(new fmidi_raw_track[ntracks]);
|
|
|
|
std::vector<uint8_t> evbuf;
|
|
evbuf.reserve(8192);
|
|
|
|
uint8_t runstatus = 0; // status runs from track to track
|
|
|
|
for (unsigned itrack = 0; itrack < ntracks; ++itrack) {
|
|
fmidi_raw_track &trk = smf->track[itrack];
|
|
size_t trkoffset = mb.getpos();
|
|
|
|
memstream_status ms;
|
|
const uint8_t *trackmagic;
|
|
uint32_t tracklen;
|
|
|
|
if (!(trackmagic = mb.read(4))) {
|
|
// file has less tracks than promised, repair
|
|
smf->info.track_count = ntracks = itrack;
|
|
break;
|
|
}
|
|
|
|
if (memcmp(trackmagic, "MTrk", 4)) {
|
|
if (mb.getpos() == mb.endpos()) {
|
|
// some kind of final junk header, ignore
|
|
smf->info.track_count = ntracks = itrack;
|
|
break;
|
|
}
|
|
RET_FAIL(false, fmidi_err_format);
|
|
}
|
|
if ((ms = mb.readintBE(&tracklen, 4)))
|
|
RET_FAIL(false, (fmidi_status)ms);
|
|
|
|
// check track length, broken in many files. disregard if invalid
|
|
bool tracklengood = !mb.skip(tracklen) &&
|
|
(mb.getpos() == mb.endpos() ||
|
|
((trackmagic = mb.peek(4)) && !memcmp(trackmagic, "MTrk", 4)));
|
|
mb.setpos(trkoffset + 8);
|
|
|
|
fmidi_event_t *evt;
|
|
size_t evoffset = mb.getpos();
|
|
bool endoftrack = false;
|
|
evbuf.clear();
|
|
while (!endoftrack && (evt = fmidi_read_event(mb, evbuf, &runstatus))) {
|
|
// some files use 3F instead or 2F for end of track
|
|
endoftrack = evt->type == fmidi_event_meta &&
|
|
(evt->data[0] == 0x2f || evt->data[0] == 0x3f);
|
|
// fmt::print(stderr, "T{} @{:#x} {}\n", itrack, evoffset, *evt);
|
|
evoffset = mb.getpos();
|
|
if (tracklengood && evoffset > trkoffset + 8 + tracklen)
|
|
// next track overlap
|
|
RET_FAIL(false, fmidi_err_format);
|
|
}
|
|
|
|
if (!endoftrack) {
|
|
switch (fmidi_last_error.code) {
|
|
case fmidi_err_eof:
|
|
// truncated track? stop reading
|
|
smf->info.track_count = ntracks = itrack + 1;
|
|
break;
|
|
case fmidi_err_format:
|
|
// event with absurdly high delta time? ignore the rest of
|
|
// the track and if possible proceed to the next
|
|
mb.setpos(evoffset);
|
|
if (mb.peekvlq(nullptr) == ms_err_format) {
|
|
if (!tracklengood)
|
|
smf->info.track_count = ntracks = itrack + 1;
|
|
break;
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (endoftrack) {
|
|
// permit meta events coming after end of track
|
|
const uint8_t *head;
|
|
while ((head = mb.peek(2)) && head[0] == 0x00 && head[1] == 0xff) {
|
|
if (!(evt = fmidi_read_event(mb, evbuf, &runstatus))) {
|
|
if (fmidi_last_error.code == fmidi_err_eof)
|
|
smf->info.track_count = ntracks = itrack + 1;
|
|
else
|
|
return false;
|
|
}
|
|
else if (tracklengood && mb.getpos() > trkoffset + 8 + tracklen)
|
|
// next track overlap
|
|
RET_FAIL(false, fmidi_err_format);
|
|
}
|
|
}
|
|
|
|
uint32_t evdatalen = trk.length = evbuf.size();
|
|
uint8_t *evdata = new uint8_t[evdatalen];
|
|
trk.data.reset(evdata);
|
|
memcpy(evdata, evbuf.data(), evdatalen);
|
|
|
|
if (tracklengood)
|
|
mb.setpos(trkoffset + 8 + tracklen);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_smf_mem_read(const uint8_t *data, size_t length)
|
|
{
|
|
memstream mb(data, length);
|
|
memstream_status ms;
|
|
const uint8_t *filemagic;
|
|
uint32_t headerlen;
|
|
uint32_t format;
|
|
uint32_t ntracks;
|
|
uint32_t deltaunit;
|
|
|
|
while ((filemagic = mb.peek(4)) && memcmp(filemagic, "MThd", 4))
|
|
mb.skip(1);
|
|
mb.skip(4);
|
|
|
|
if (!filemagic)
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
if ((ms = mb.readintBE(&headerlen, 4)) ||
|
|
(ms = mb.readintBE(&format, 2)) ||
|
|
(ms = mb.readintBE(&ntracks, 2)) ||
|
|
(ms = mb.readintBE(&deltaunit, 2)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
|
|
if (ntracks < 1 || headerlen < 6)
|
|
RET_FAIL(nullptr, fmidi_err_format);
|
|
|
|
if ((ms = mb.skip(headerlen - 6)))
|
|
RET_FAIL(nullptr, (fmidi_status)ms);
|
|
|
|
std::unique_ptr<fmidi_smf_t> smf(new fmidi_smf_t);
|
|
smf->info.format = format;
|
|
smf->info.track_count = ntracks;
|
|
smf->info.delta_unit = deltaunit;
|
|
|
|
if (!fmidi_smf_read_contents(smf.get(), mb))
|
|
return nullptr;
|
|
|
|
return smf.release();
|
|
}
|
|
|
|
void fmidi_smf_free(fmidi_smf_t *smf)
|
|
{
|
|
delete smf;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_smf_file_read(const char *filename)
|
|
{
|
|
unique_FILE fh(fmidi_fopen(filename, "rb"));
|
|
if (!fh)
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
fmidi_smf_t *smf = fmidi_smf_stream_read(fh.get());
|
|
return smf;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_smf_stream_read(FILE *stream)
|
|
{
|
|
struct stat st;
|
|
size_t length;
|
|
|
|
rewind(stream);
|
|
|
|
if (fstat(fileno(stream), &st) != 0)
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
length = st.st_size;
|
|
if (length > fmidi_file_size_limit)
|
|
RET_FAIL(nullptr, fmidi_err_largefile);
|
|
|
|
std::unique_ptr<uint8_t[]> buf(new uint8_t[length]);
|
|
if (!fread(buf.get(), length, 1, stream))
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
fmidi_smf_t *smf = fmidi_smf_mem_read(buf.get(), length);
|
|
return smf;
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <string.h>
|
|
|
|
fmidi_fileformat_t fmidi_mem_identify(const uint8_t *data, size_t length)
|
|
{
|
|
const uint8_t smf_magic[4] = {'M', 'T', 'h', 'd'};
|
|
if (length >= 4 && memcmp(data, smf_magic, 4) == 0)
|
|
return fmidi_fileformat_smf;
|
|
|
|
const uint8_t rmi_magic1[4] = {'R', 'I', 'F', 'F'};
|
|
const uint8_t rmi_magic2[8] = {'R', 'M', 'I', 'D', 'd', 'a', 't', 'a'};
|
|
if (length >= 16 && memcmp(data, rmi_magic1, 4) == 0 && memcmp(data + 8, rmi_magic2, 8) == 0)
|
|
return fmidi_fileformat_smf;
|
|
|
|
const uint8_t xmi_magic[20] = {
|
|
'F', 'O', 'R', 'M', 0, 0, 0, 14,
|
|
'X', 'D', 'I', 'R', 'I', 'N', 'F', 'O', 0, 0, 0, 2
|
|
};
|
|
if (length >= 20 && memcmp(data, xmi_magic, 20) == 0)
|
|
return fmidi_fileformat_xmi;
|
|
|
|
RET_FAIL((fmidi_fileformat_t)-1, fmidi_err_format);
|
|
}
|
|
|
|
fmidi_fileformat_t fmidi_stream_identify(FILE *stream)
|
|
{
|
|
rewind(stream);
|
|
|
|
uint8_t magic[32];
|
|
size_t size = fread(magic, 1, sizeof(magic), stream);
|
|
if (ferror(stream))
|
|
RET_FAIL((fmidi_fileformat_t)-1, fmidi_err_input);
|
|
|
|
return fmidi_mem_identify(magic, size);
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_auto_mem_read(const uint8_t *data, size_t length)
|
|
{
|
|
switch (fmidi_mem_identify(data, length)) {
|
|
case fmidi_fileformat_smf:
|
|
return fmidi_smf_mem_read(data, length);
|
|
case fmidi_fileformat_xmi:
|
|
return fmidi_xmi_mem_read(data, length);
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_auto_file_read(const char *filename)
|
|
{
|
|
unique_FILE fh(fmidi_fopen(filename, "rb"));
|
|
if (!fh)
|
|
RET_FAIL(nullptr, fmidi_err_input);
|
|
|
|
fmidi_smf_t *smf = fmidi_auto_stream_read(fh.get());
|
|
return smf;
|
|
}
|
|
|
|
fmidi_smf_t *fmidi_auto_stream_read(FILE *stream)
|
|
{
|
|
switch (fmidi_stream_identify(stream)) {
|
|
case fmidi_fileformat_smf:
|
|
return fmidi_smf_stream_read(stream);
|
|
case fmidi_fileformat_xmi:
|
|
return fmidi_xmi_stream_read(stream);
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
#include "fmidi/fmidi.h"
|
|
#include <cstring>
|
|
#include <cassert>
|
|
|
|
static void write_vlq(uint32_t value, Writer &writer)
|
|
{
|
|
unsigned shift = 28;
|
|
unsigned mask = (1u << 7) - 1;
|
|
while (shift > 0 && ((value >> shift) & mask) == 0)
|
|
shift -= 7;
|
|
while (shift > 0) {
|
|
writer.put(((value >> shift) & mask) | (1u << 7));
|
|
shift -= 7;
|
|
}
|
|
writer.put(value & mask);
|
|
}
|
|
|
|
static bool fmidi_smf_write(const fmidi_smf_t *smf, Writer &writer)
|
|
{
|
|
writer.write("MThd", 4);
|
|
|
|
const uint32_t header_size = 6;
|
|
writer.writeBE(&header_size, 4);
|
|
|
|
const fmidi_smf_info_t *info = fmidi_smf_get_info(smf);
|
|
const uint16_t track_count = info->track_count;
|
|
writer.writeBE(&info->format, 2);
|
|
writer.writeBE(&track_count, 2);
|
|
writer.writeBE(&info->delta_unit, 2);
|
|
|
|
for (unsigned i = 0; i < track_count; ++i) {
|
|
writer.write("MTrk", 4);
|
|
|
|
off_t off_track_length = writer.tell();
|
|
|
|
uint32_t track_length = 0;
|
|
writer.writeBE(&track_length, 4);
|
|
|
|
int running_status = -1;
|
|
|
|
fmidi_track_iter_t iter;
|
|
fmidi_smf_track_begin(&iter, i);
|
|
|
|
const fmidi_event_t *event;
|
|
while ((event = fmidi_smf_track_next(smf, &iter))) {
|
|
switch (event->type) {
|
|
case fmidi_event_meta:
|
|
write_vlq(event->delta, writer);
|
|
writer.put(0xff);
|
|
writer.put(event->data[0]);
|
|
write_vlq(event->datalen - 1, writer);
|
|
writer.write(event->data + 1, event->datalen - 1);
|
|
running_status = -1;
|
|
break;
|
|
case fmidi_event_message:
|
|
{
|
|
write_vlq(event->delta, writer);
|
|
uint8_t status = event->data[0];
|
|
if (status == 0xf0) {
|
|
writer.put(0xf0);
|
|
write_vlq(event->datalen - 1, writer);
|
|
writer.write(event->data + 1, event->datalen - 1);
|
|
running_status = -1;
|
|
}
|
|
else if ((int)status == running_status)
|
|
writer.write(event->data + 1, event->datalen - 1);
|
|
else {
|
|
writer.write(event->data, event->datalen);
|
|
running_status = status;
|
|
}
|
|
break;
|
|
}
|
|
case fmidi_event_escape:
|
|
write_vlq(event->delta, writer);
|
|
writer.put(0xf7);
|
|
write_vlq(event->datalen, writer);
|
|
writer.write(event->data, event->datalen);
|
|
running_status = -1;
|
|
break;
|
|
case fmidi_event_xmi_timbre:
|
|
case fmidi_event_xmi_branch_point:
|
|
break;
|
|
}
|
|
}
|
|
|
|
off_t off_track_end = writer.tell();
|
|
|
|
track_length =
|
|
std::make_unsigned<off_t>::type(off_track_end) -
|
|
std::make_unsigned<off_t>::type(off_track_length) - 4;
|
|
|
|
writer.seek(off_track_length, SEEK_SET);
|
|
writer.writeBE(&track_length, 4);
|
|
writer.seek(off_track_end, SEEK_SET);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool fmidi_smf_mem_write(const fmidi_smf_t *smf, uint8_t **data, size_t *length)
|
|
{
|
|
std::vector<uint8_t> mem;
|
|
mem.reserve(8192);
|
|
|
|
Memory_Writer writer(mem);
|
|
if (!fmidi_smf_write(smf, writer))
|
|
return false;
|
|
|
|
assert(data);
|
|
assert(length);
|
|
|
|
if (!(*data = (uint8_t *)malloc(mem.size())))
|
|
throw std::bad_alloc();
|
|
|
|
memcpy(*data, mem.data(), mem.size());
|
|
*length = mem.size();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool fmidi_smf_file_write(const fmidi_smf_t *smf, const char *filename)
|
|
{
|
|
unique_FILE fh(fmidi_fopen(filename, "wb"));
|
|
if (!fh)
|
|
RET_FAIL(false, fmidi_err_output);
|
|
|
|
return fmidi_smf_stream_write(smf, fh.get());
|
|
}
|
|
|
|
bool fmidi_smf_stream_write(const fmidi_smf_t *smf, FILE *stream)
|
|
{
|
|
Stream_Writer writer(stream);
|
|
if (!fmidi_smf_write(smf, writer))
|
|
return false;
|
|
|
|
if (fflush(stream) != 0)
|
|
RET_FAIL(false, fmidi_err_output);
|
|
|
|
return true;
|
|
}
|