Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
427 changes: 322 additions & 105 deletions docs/formats/SmafFileFormat.txt

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions src/mafm/smaf_voice.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,61 @@ void _WM_MAFM_ParseVoiceExclusive(const uint8_t *p, uint32_t n,
if (!p || n < 5) return;
if (p[0] != 0x43) return; /* not a Yamaha maker id */

/* MA-7 form: 43 79 08 7F 21 [bank msb][bank lsb][pc][?][?][form] body
*
* p[10] selects the body layout. Only form 0x00 is decoded here: 3 global
* bytes then TEN bytes per operator, the chip's own layout rather than the
* packed VM35 one the older chips use. Yamaha's middleware converts
* VM35 -> native with a fixed shuffle (M7_EmuSmw7.dll, routine 0x10183980):
*
* native[0..4] = vm35[0..4] native[5] = 0
* native[6] = vm35[6] native[7] = native[8] = 0
* native[9] = vm35[5]
*
* so undoing it recovers a VM35 operator and the existing decoder does the
* rest. Confirmed by round-tripping corpus voices back through that
* routine: every operator byte reproduces exactly. (Bytes 5/7/8 carry
* MA-7-only parameters that VM35 cannot express - zero in voices that were
* authored as VM35, non-zero otherwise - and are ignored either way.)
*
* Forms 0x01 (3 + 7/op, i.e. plain VM35), 0x02, 0x03 and 0x07 also occur
* in the wild; their layouts are not established, so they are declined
* rather than guessed at. See docs/formats/SmafFileFormat.txt. */
if (n >= 14 && p[1] == 0x79 && p[2] == 0x08 && p[3] == 0x7f && p[4] == 0x21 &&
p[10] == 0x00) {
const uint8_t *body = p + 11;
uint32_t bn = n - 11;
uint8_t vm35[3 + 7 * 4];
int alg, ops, i;

if (bn > 0 && p[n - 1] == 0xf7)
bn--; /* drop the terminator if it is included */
if (bn < 3) return;
alg = body[2] & 0x07;
ops = op_count_from_alg(alg);
/* exact fit only: a body that is not 3 + 10*ops is some other form */
if (bn != (uint32_t)(3 + 10 * ops)) return;

out->key.bank_msb = p[5];
out->key.bank_lsb = p[6];
out->key.pc = p[7];
out->key.drum_note = p[8];

vm35[0] = body[0];
vm35[1] = body[1];
vm35[2] = body[2];
for (i = 0; i < ops; i++) {
const uint8_t *o = body + 3 + 10 * i;
uint8_t *v = vm35 + 3 + 7 * i;
v[0] = o[0]; v[1] = o[1]; v[2] = o[2]; v[3] = o[3]; v[4] = o[4];
v[5] = o[9]; /* MULTI / DT */
v[6] = o[6]; /* WS / FB */
}
apply_vm35_body(vm35, (uint32_t)(3 + 7 * ops), ops, &out->patch);
out->valid = 1;
return;
}

/* MA-3 / MA-5 long form: 43 79 06|07 7F 01 [bank...] body */
if (n >= 11 && p[1] == 0x79 && (p[2] == 0x06 || p[2] == 0x07) &&
p[3] == 0x7f && p[4] == 0x01) {
Expand Down
8 changes: 8 additions & 0 deletions src/patches.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ _WM_get_patch_data(struct _mdi *mdi, uint16_t patchid) {

_WM_Lock(&_WM_patch_lock);
search_patch = _find_nearest_patch(patchid);
if (search_patch == NULL && (patchid & 0xff00) != 0) {
/* Nothing at all in the requested bank: fall back to bank 0 rather
* than play silence, as a hardware synth does for an unknown bank.
* SMAF needs this - its scores select Yamaha's own voice banks (0x7c
* and friends), which no GUS/SF2 patch set defines, so without the
* fallback every SMAF file that has no custom FM voices is mute. */
search_patch = _find_nearest_patch(patchid & 0x00ff);
}
_WM_Unlock(&_WM_patch_lock);
return (search_patch);
}
Expand Down
42 changes: 33 additions & 9 deletions src/smaf2mid.c
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,25 @@ static int find_sequence(const uint8_t *in, uint32_t insize,
/* ------------------------------------------------------------------------- */

/* Decode a Mobile Standard (format 0x01/0x02) sequence into MIDI events.
*
* With sequ != 0, decode the MA-7 "SEQU" variant (format 0x03) instead. SEQU
* is the same record stream; only the status byte is packed differently, to
* address 32 channels instead of 16:
*
* bit 7 : channel bank (0 -> channels 0-15, 1 -> channels 16-31)
* bits 6-4 : event type (the low 3 bits of the MIDI status nibble,
* i.e. type n means MIDI status 0x8+n)
* bits 3-0 : channel, low nibble
*
* so 0x1n is a note-with-velocity (MIDI 0x9n), 0x3n a control change (0xBn),
* 0x4n a program change (0xCn), 0x6n a pitch bend (0xEn), and so on. 0xF0 and
* 0xFF keep their literal Mobile meaning. Because ordinary status bytes are
* below 0x80 there is no running status in SEQU: every event carries one.
*
* Returns 0 on success, -1 on allocation failure. */
static int decode_mobile(struct smaf_ctx *ctx, const uint8_t *seq,
uint32_t seqlen, uint32_t ms_dur, uint32_t ms_gate) {
uint32_t seqlen, uint32_t ms_dur, uint32_t ms_gate,
int sequ) {
uint8_t run_vel[MIDI_MAXCHANNELS];
uint8_t run_status = 0;
uint32_t p = 0, cur_ms = 0;
Expand Down Expand Up @@ -347,7 +363,17 @@ static int decode_mobile(struct smaf_ctx *ctx, const uint8_t *seq,
flush_offs(ctx, cur_ms);

s = seq[p];
if (s & 0x80) {
if (sequ) {
p++;
if (s != 0xf0 && s != 0xff) {
/* ponytail: fold the 32 SEQU channels onto MIDI's 16. Every
* MA-7 file seen so far plays only on channels 0-15 and leaves
* 16-31 at their setup defaults, so the fold never collides;
* carrying the upper bank properly would need a second MIDI
* port, which the converter has no way to express. */
s = (uint8_t)(0x80 | (((s >> 4) & 7) << 4) | (s & 0x0f));
}
} else if (s & 0x80) {
p++;
run_status = s;
} else if (run_status) {
Expand Down Expand Up @@ -811,12 +837,9 @@ int _WM_smaf2midi(const uint8_t *in, uint32_t insize,
return -1;
}

/* Supported score-track formats: Mobile Standard (0x01/0x02) and HandyPhone
* Standard (0x00). The MA-7 "SEQU" variant (0x03) is a different,
* compressed encoding that has not been decoded (see
* docs/formats/SmafFileFormat.txt); decline it rather than emit a mangled
* conversion. */
if (fmt != 0x00 && fmt != 0x01 && fmt != 0x02) {
/* Supported score-track formats: Mobile Standard (0x01/0x02), HandyPhone
* Standard (0x00) and MA-7 SEQU (0x03). */
if (fmt > 0x03) {
_WM_GLOBAL_ERROR(WM_ERR_NOT_SMAF, "(unsupported SMAF track format)", 0);
return -1;
}
Expand Down Expand Up @@ -873,7 +896,8 @@ int _WM_smaf2midi(const uint8_t *in, uint32_t insize,
hp_emit(&ctx, &hev);
free(hev.ev);
} else {
if (decode_mobile(&ctx, seq, seqlen, ms_dur, ms_gate) < 0) {
if (decode_mobile(&ctx, seq, seqlen, ms_dur, ms_gate,
fmt == 0x03) < 0) {
_WM_GLOBAL_ERROR(WM_ERR_MEM, NULL, 0);
goto _end;
}
Expand Down
11 changes: 11 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
ADD_EXECUTABLE(test_tokenize test_tokenize.c)
TARGET_LINK_LIBRARIES(test_tokenize libwildmidi-static ${M_LIBRARY})
ADD_TEST(NAME tokenize COMMAND test_tokenize)

ADD_EXECUTABLE(test_smaf_sequ test_smaf_sequ.c)
TARGET_LINK_LIBRARIES(test_smaf_sequ libwildmidi-static ${M_LIBRARY})
ADD_TEST(NAME smaf_sequ COMMAND test_smaf_sequ)

IF (WANT_MAFM)
ADD_EXECUTABLE(test_ma7_voice test_ma7_voice.c)
TARGET_INCLUDE_DIRECTORIES(test_ma7_voice PRIVATE ${CMAKE_SOURCE_DIR}/src)
TARGET_LINK_LIBRARIES(test_ma7_voice libwildmidi-static ${M_LIBRARY})
ADD_TEST(NAME ma7_voice COMMAND test_ma7_voice)
ENDIF (WANT_MAFM)
66 changes: 66 additions & 0 deletions test/test_ma7_voice.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* assert-based smoke test for the MA-7 Mtsu voice-exclusive decode
* (_WM_MAFM_ParseVoiceExclusive, src/mafm/smaf_voice.c).
*
* MA-7 stores its operators in the chip's native 10-byte layout rather than
* the 7-byte packed VM35 one the older chips use; the decoder undoes the
* shuffle Yamaha's middleware applies. See docs/formats/SmafFileFormat.txt.
* The bytes below are the pc=0x0a voice out of AB00221GM7.MMF. */
#include <assert.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>

#include "mafm/smaf_voice.h"

/* the exclusive payload, i.e. what sits between the F0 length and the F7 */
static const uint8_t ma7_voice[] = {
0x43, 0x79, 0x08, 0x7f, 0x21, /* Yamaha, MA-7, voice-set sub-id */
0x7c, 0x02, 0x0a, 0x00, /* bank MSB/LSB, program, drum note */
0x00, /* voice type: 0 = FM */
0x00, /* reserved */
0x00, 0x79, 0x40, /* 3 global bytes; alg = 0x40 & 7 = 0 */
/* operator 0, native layout: bytes 5,7,8 are the reserved zeroes */
0x33, 0x3b, 0x94, 0x70, 0x44, 0x00, 0x41, 0x00, 0x00, 0x90,
/* operator 1 */
0x22, 0x44, 0xdf, 0x02, 0x41, 0x00, 0x00, 0x00, 0x00, 0x10
};

int main(void) {
struct mafm_parsed_voice v;
const struct mafm_op_patch *m, *c;

_WM_MAFM_ParseVoiceExclusive(ma7_voice, (uint32_t)sizeof(ma7_voice), &v);

assert(v.valid);
assert(!v.is_pcm);
assert(v.key.bank_msb == 0x7c);
assert(v.key.bank_lsb == 0x02);
assert(v.key.pc == 0x0a);
assert(v.key.drum_note == 0x00);

m = &v.patch.ops[0]; /* modulator */
c = &v.patch.ops[1]; /* carrier */

/* The fields that live at the SAME offset in both layouts. */
assert(m->ar == 9 && m->sl == 4);
assert(m->rr == 3 && m->dr == 11);
assert(m->tl == 28 && m->ksl == 0);
assert(c->ar == 13 && c->sl == 15);
assert(c->tl == 0);

/* The two that MA-7 moves, and the whole point of this test: MULTI/DT come
* from native byte 9 and WS/FB from native byte 6. Reading them at the
* VM35 offsets instead yields MULTI 0 on both operators - which is what
* every wrong guess at this layout produced, and no real patch has. */
assert(m->multi == 9 && m->dt == 0);
assert(c->multi == 1 && c->dt == 0);
assert(m->wave == 8 && m->fb == 1);
assert(c->wave == 0 && c->fb == 0);

/* A carrier at full level against an attenuated modulator: the sanity
* check that the operator order came out right. */
assert(c->tl < m->tl);

printf("ma7_voice ok\n");
return 0;
}
103 changes: 103 additions & 0 deletions test/test_smaf_sequ.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* assert-based smoke test for the MA-7 "SEQU" (score format_type 0x03) decode
* in smaf2mid.c. SEQU is Mobile Standard with the status byte repacked to
* address 32 channels: bit 7 is the channel bank, bits 6-4 are the event type
* (MIDI status nibble 0x8+n), bits 3-0 are the low channel nibble.
* See docs/formats/SmafFileFormat.txt. */
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

extern int _WM_smaf2midi(const uint8_t *in, uint32_t insize,
uint8_t **out, uint32_t *outsize);

/* The Mtsq event stream under test. Each record is [duration VLQ][event]. */
static const uint8_t seq[] = {
0x00, 0x30, 0x07, 0x64, /* ch0 CC volume 100 -> b0 07 64 */
0x00, 0xb1, 0x07, 0x50, /* ch17 CC volume 80 (bit 7) -> b1 07 50 */
0x00, 0x40, 0x05, /* ch0 program 5 -> c0 05 */
0x00, 0x60, 0x00, 0x50, /* ch0 pitch bend 0,0x50 -> e0 00 50 */
0x00, 0x11, 0x3c, 0x64, 0x0a, /* ch1 note 60 vel 100 gate 10 */
0x14, 0x01, 0x3e, 0x0a, /* ch1 note 62, running velocity 100 */
0x00, 0xff, 0x2f, 0x00 /* end of sequence */
};

/* Build the smallest MMMD container that carries one MA-7 score track. */
static uint8_t *build_mmf(uint32_t *size_out) {
uint32_t mtsq = 8 + (uint32_t)sizeof(seq); /* "Mtsq" + size + body */
uint32_t mtr = 36 + mtsq; /* 4 fixed + 32 chstat + sub */
uint32_t total = 8 + 8 + mtr; /* MMMD hdr + MTR hdr + body */
uint8_t *b = (uint8_t *) calloc(total, 1);
uint32_t p = 0;
assert(b != NULL);

memcpy(b + p, "MMMD", 4); p += 4;
b[p++] = 0; b[p++] = 0;
b[p++] = (uint8_t)((total - 8) >> 8); b[p++] = (uint8_t)(total - 8);

memcpy(b + p, "MTR", 3); p += 3;
b[p++] = 0x07; /* MA-7 score track id byte */
b[p++] = 0; b[p++] = 0;
b[p++] = (uint8_t)(mtr >> 8); b[p++] = (uint8_t)mtr;

b[p++] = 0x03; /* format_type: SEQU */
b[p++] = 0x00; /* sequence_type */
b[p++] = 0x02; /* timebase_dur: 4 ms/tick */
b[p++] = 0x02; /* timebase_gate: 4 ms/tick */
p += 32; /* channel_status, all zero */

memcpy(b + p, "Mtsq", 4); p += 4;
b[p++] = 0; b[p++] = 0;
b[p++] = (uint8_t)(sizeof(seq) >> 8); b[p++] = (uint8_t)sizeof(seq);
memcpy(b + p, seq, sizeof(seq)); p += (uint32_t)sizeof(seq);

assert(p == total);
*size_out = total;
return b;
}

/* Does the emitted MIDI contain this exact byte run anywhere? */
static int contains(const uint8_t *hay, uint32_t n,
const uint8_t *needle, uint32_t m) {
uint32_t i;
if (m > n) return 0;
for (i = 0; i + m <= n; i++)
if (memcmp(hay + i, needle, m) == 0) return 1;
return 0;
}

#define HAS(...) do { \
static const uint8_t want[] = { __VA_ARGS__ }; \
assert(contains(mid, midsize, want, (uint32_t)sizeof(want))); \
} while (0)

int main(void) {
uint8_t *mmf, *mid = NULL;
uint32_t mmfsize = 0, midsize = 0;

mmf = build_mmf(&mmfsize);
assert(_WM_smaf2midi(mmf, mmfsize, &mid, &midsize) == 0);
assert(mid != NULL && midsize > 22);
assert(memcmp(mid, "MThd", 4) == 0);

/* 0x3n -> control change, and 0xb1 proves bit 7 is the channel bank and
* not a MIDI status flag: channel 17 folds onto MIDI channel 1. */
HAS(0xb0, 0x07, 0x64);
HAS(0xb1, 0x07, 0x50);

HAS(0xc0, 0x05); /* 0x4n -> program change */
HAS(0xe0, 0x00, 0x50); /* 0x6n -> pitch bend */

/* 0x1n carries an explicit velocity; 0x0n reuses the channel's running
* velocity, so note 62 must come out at 100 as well. */
HAS(0x91, 0x3c, 0x64);
HAS(0x91, 0x3e, 0x64);

/* Notes carry their own gate time, so each one must get a note-off. */
HAS(0x81, 0x3c, 0x40);
HAS(0x81, 0x3e, 0x40);

free(mid);
free(mmf);
return 0;
}
Loading