906 lines
31 KiB
C
906 lines
31 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// modToNtp.c - a Protracker module (.MOD) as a NinjaTrackerPlus song (.NTP,
|
|
// version 2), the form the IIgs port's replayer plays (tools/joeymod's
|
|
// .ntp output).
|
|
//
|
|
// modToNtp IN.MOD OUT.NTP
|
|
//
|
|
// Written from NinjaTrackerPlus' own documentation of the file
|
|
// (file_format.txt) and its replayer's source; how a module's parts become
|
|
// that file's was worked out by converting test modules with Ninjaforce's
|
|
// converter and reading the results, so this makes the same files. The
|
|
// rules:
|
|
//
|
|
// Samples become instruments, empty ones (no length) left out and the
|
|
// rest numbered in order. Their bytes are made the DOC's: unsigned (each
|
|
// gains 128), and 0, which stops a DOC oscillator, made 1. An instrument
|
|
// whose length is a power of two from 256 on is ideal (its table is
|
|
// exactly it); any other is followed by 8 zero bytes that stop the
|
|
// oscillator, and its table is the smallest power of two from 256 that
|
|
// holds it. A sample with no loop (a repeat length of 2 bytes or less) is
|
|
// one instrument. A looped one (the loop cut back to the sample's end):
|
|
// - starting at byte 0 or 2 is one instrument of its loop alone, made to
|
|
// play long enough: a loop under 256 bytes is repeated, to 512 when its
|
|
// length is a power of two (ideal), else as many times as fit in 504;
|
|
// - starting later is a header (the sample up to the loop's quietest
|
|
// byte, the first nearest the DOC's center) and a loop instrument (the
|
|
// loop from that byte round to it, repeated as above), played by two
|
|
// oscillators in turn.
|
|
// An instrument past 32760 bytes, one played with a sample offset (effect
|
|
// 9), or one that DOC RAM cannot hold is streamed from memory through a
|
|
// 512-byte buffer for each track that plays it: the sample whole (8
|
|
// stopper bytes after it), or a looped one up to its loop, the loop laid
|
|
// down as many times as it takes to fill a buffer, and the loop's first
|
|
// 256 bytes again (ideal when that comes to a power of two). When the
|
|
// instruments do not fit, the longest sample not streamed yet is
|
|
// streamed, and the whole placement is tried again.
|
|
//
|
|
// DOC RAM is 256 pages. The instruments in order, then a buffer for each
|
|
// track that plays a streamed sample (sample by sample, the tracks playing
|
|
// it in the order they first play anything), are sorted most pages first,
|
|
// step for step as PHP 8 sorts them: Ninjaforce's converter is PHP, and it
|
|
// compares a buffer and an instrument of equal pages so that each goes
|
|
// after the other, which leaves their order to the sort's workings. Each
|
|
// is placed at the lowest address its table size divides where its pages
|
|
// are free. Page 0 is the replayer's interrupt timer's, which must never
|
|
// read a 0: it takes only an instrument filling it.
|
|
//
|
|
// A track plays through two oscillators when it plays any instrument that
|
|
// needs two (a streamed one, a header and loop, or a loop that is not
|
|
// ideal), else one; track by track, it gets the lowest free oscillator, or
|
|
// the lowest free even and odd pair. Tracks sound on the right, left,
|
|
// left, right in fours. Notes become NTP's note numbers (1 is C-0, period
|
|
// 1712; 72 is B-5) by period, each note taking the periods from its own
|
|
// down to half way to the next; effects pass unchanged.
|
|
//
|
|
// A module whose sample's loop starts at or past its end is converted with
|
|
// that sample unlooped (Ninjaforce's converter stops on it); M!K!, CD81,
|
|
// OKTA, and OCTA modules are read as the 4- and 8-channel modules they
|
|
// are.
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "toolBuf.h"
|
|
#include "toolFile.h"
|
|
|
|
#define TOOL_NAME "modToNtp"
|
|
#define USAGE_EXIT 2
|
|
|
|
// The module.
|
|
#define MOD_NAME_BYTES 20u
|
|
#define MOD_SAMPLES 31u
|
|
#define MOD_SAMPLE_AT 20u
|
|
#define MOD_SAMPLE_BYTES 30u
|
|
#define MOD_SAMPLE_NAME 22u
|
|
#define MOD_LENGTH_AT 22u
|
|
#define MOD_FINE_AT 24u
|
|
#define MOD_VOLUME_AT 25u
|
|
#define MOD_REP_AT 26u
|
|
#define MOD_REPLEN_AT 28u
|
|
#define MOD_ORDER_COUNT_AT 950u
|
|
#define MOD_ORDER_AT 952u
|
|
#define MOD_ORDER_MAX 128u
|
|
#define MOD_MAGIC_AT 1080u
|
|
#define MOD_MAGIC_BYTES 4u
|
|
#define MOD_HEADER_BYTES 1084u
|
|
#define MOD_ROWS 64u
|
|
#define MOD_CELL_BYTES 4u
|
|
#define MOD_TRACKS_MAX 8u
|
|
#define FINE_MASK 0x0Fu
|
|
#define SAMPLE_HIGH_MASK 0xF0u
|
|
#define PERIOD_HIGH_MASK 0x0Fu
|
|
#define EFFECT_MASK 0x0Fu
|
|
#define EFFECT_OFFSET 0x09u
|
|
#define NIBBLE_BITS 4u
|
|
#define BYTE_BITS 8u
|
|
// A loop of 2 bytes or less is none; one starting before byte 4 has no
|
|
// header.
|
|
#define LOOP_MIN 2u
|
|
#define HEADER_START_MIN 4u
|
|
|
|
// The DOC.
|
|
#define DOC_BYTES 65536u
|
|
#define PAGE_BYTES 256u
|
|
#define DOC_PAGES (DOC_BYTES / PAGE_BYTES)
|
|
#define DOC_CENTER 128u
|
|
#define DOC_STOP 0u
|
|
#define DOC_STOP_FIX 1u
|
|
#define STOPPER_BYTES 8u
|
|
#define TABLE_MIN 256u
|
|
#define WAVE_SIZE_SCALE 9u
|
|
#define STREAM_MAX 32760u
|
|
#define STREAM_BUFFER 512u
|
|
#define STREAM_LOOP_COPY 256u
|
|
// A loop under LOOP_REPEAT_UNDER bytes is repeated: to LOOP_IDEAL when it
|
|
// is a power of two, else as many times as fit in LOOP_FILL.
|
|
#define LOOP_REPEAT_UNDER 256u
|
|
#define LOOP_IDEAL 512u
|
|
#define LOOP_FILL (LOOP_IDEAL - STOPPER_BYTES)
|
|
|
|
// NTP.
|
|
#define NTP_MAGIC "nfc!"
|
|
#define NTP_MAGIC_BYTES 4u
|
|
#define NTP_VERSION 2u
|
|
#define TYPE_STREAMED 0x08u
|
|
#define TYPE_IDEAL 0x04u
|
|
#define TYPE_PLAIN 0x00u
|
|
#define TYPE_LOOPED 0x01u
|
|
#define TYPE_HEADER 0x02u
|
|
#define TYPE_LOOP 0x03u
|
|
#define OSC_TWO 0x80u
|
|
#define CHANNEL_LEFT 0u
|
|
#define CHANNEL_RIGHT 1u
|
|
#define NOTES 72u
|
|
#define OSCS_MAX (MOD_TRACKS_MAX * 2u)
|
|
|
|
// Placement: the claims on DOC RAM (at most two instruments a sample and a
|
|
// buffer a track), sorted as PHP sorts: by insertion up to SORT_SMALL of
|
|
// them (under SORT_LINEAR, the first SORT_NETWORK by a fixed network;
|
|
// else the first SORT_LINEAR a place at a time, the rest two).
|
|
#define ITEMS_MAX (MOD_SAMPLES * 2u + MOD_TRACKS_MAX)
|
|
#define SORT_SMALL 16u
|
|
#define SORT_LINEAR 6u
|
|
#define SORT_NETWORK 3u
|
|
|
|
// A module's sample: what the module says of it, its bytes (those past the
|
|
// file's end read as 0), and how it converts.
|
|
typedef struct SampleT {
|
|
char name[MOD_SAMPLE_NAME + 1u];
|
|
uint32_t length;
|
|
uint32_t rep;
|
|
uint32_t repLen;
|
|
uint8_t fine;
|
|
uint8_t volume;
|
|
const uint8_t *data;
|
|
uint32_t have;
|
|
bool looped;
|
|
bool streamed;
|
|
uint32_t first;
|
|
} SampleT;
|
|
|
|
// An NTP instrument: its type, its DOC bytes, its table, and where it was
|
|
// placed.
|
|
typedef struct InstT {
|
|
const SampleT *sample;
|
|
uint8_t type;
|
|
ToolBufT bytes;
|
|
uint32_t table;
|
|
uint32_t address;
|
|
} InstT;
|
|
|
|
// A claim on DOC RAM: an instrument's bytes or a track's stream buffer,
|
|
// its table size, and where its address goes.
|
|
typedef struct ClaimT {
|
|
uint32_t size;
|
|
uint32_t table;
|
|
uint32_t *where;
|
|
bool buffer;
|
|
} ClaimT;
|
|
|
|
typedef struct ModT {
|
|
const uint8_t *file;
|
|
size_t len;
|
|
char name[MOD_NAME_BYTES + 1u];
|
|
uint32_t tracks;
|
|
uint32_t patterns;
|
|
uint32_t orders;
|
|
const uint8_t *orderTable;
|
|
const uint8_t *cells;
|
|
SampleT samples[MOD_SAMPLES];
|
|
InstT insts[MOD_SAMPLES * 2u];
|
|
uint32_t instCount;
|
|
bool twoOsc[MOD_TRACKS_MAX];
|
|
uint32_t plays[MOD_TRACKS_MAX];
|
|
uint32_t trackOrder[MOD_TRACKS_MAX];
|
|
uint32_t tracksSeen;
|
|
uint32_t buffer[MOD_TRACKS_MAX];
|
|
} ModT;
|
|
|
|
static void addLoop(ToolBufT *out, const SampleT *s, uint32_t start, uint32_t loopLen, uint32_t rotate);
|
|
static bool claimAfter(const ClaimT *c, const uint32_t *o, uint32_t a, uint32_t b);
|
|
static uint8_t docByte(const SampleT *s, uint32_t at);
|
|
static void finish(InstT *inst, uint8_t type);
|
|
static void instAdd(ModT *m, const SampleT *s, uint8_t type);
|
|
static void instruments(ModT *m);
|
|
static bool isIdeal(size_t len);
|
|
static bool isPowerOfTwo(uint32_t v);
|
|
static uint32_t loopRepeats(uint32_t loopLen, bool streamed);
|
|
static void modRead(ModT *m, const char *path);
|
|
static bool needsTwo(const ModT *m, const SampleT *s);
|
|
static uint32_t noteOf(uint32_t period);
|
|
static void ntpWrite(const ModT *m, const char *path);
|
|
static void orderSwap(uint32_t *o, uint32_t a, uint32_t b);
|
|
static uint32_t pagesOf(uint32_t bytes);
|
|
static uint32_t partition(const ClaimT *c, uint32_t *o, uint32_t n);
|
|
static bool place(ModT *m);
|
|
static void sinkLast(const ClaimT *c, uint32_t *o, uint32_t last);
|
|
static void sort3(const ClaimT *c, uint32_t *o, uint32_t x, uint32_t y, uint32_t z);
|
|
static void sortClaims(const ClaimT *c, uint32_t *o, uint32_t n);
|
|
static void sortInsert(const ClaimT *c, uint32_t *o, uint32_t n);
|
|
static uint32_t splitAt(const SampleT *s, uint32_t start, uint32_t loopLen);
|
|
static void tracksScan(ModT *m);
|
|
|
|
// The least period of each note but the last (note n takes the periods from
|
|
// its own down to half way to note n + 1's).
|
|
static const uint16_t kNoteFloor[NOTES - 1u] = {
|
|
1664, 1570, 1482, 1398, 1318, 1244, 1174, 1108, 1046, 988, 933, 881,
|
|
832, 785, 741, 699, 659, 622, 587, 554, 523, 494, 467, 441,
|
|
416, 393, 371, 350, 330, 311, 294, 277, 262, 247, 233, 220,
|
|
208, 196, 185, 175, 165, 156, 147, 139, 131, 124, 117, 110,
|
|
104, 98, 93, 88, 83, 78, 73, 69, 65, 62, 58, 55,
|
|
52, 49, 46, 44, 41, 39, 36, 34, 32, 31, 29
|
|
};
|
|
|
|
|
|
// A loop's bytes: loopLen of them from start, beginning rotate bytes in
|
|
// and running round, as many times over as loopRepeats says.
|
|
static void addLoop(ToolBufT *out, const SampleT *s, uint32_t start, uint32_t loopLen, uint32_t rotate) {
|
|
uint32_t times;
|
|
uint32_t t;
|
|
uint32_t i;
|
|
|
|
times = loopRepeats(loopLen, s->streamed);
|
|
for (t = 0; t < times; t++) {
|
|
for (i = 0; i < loopLen; i++) {
|
|
toolBufByte(out, docByte(s, start + (rotate + i) % loopLen));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Whether the claim at order position a goes after the one at b: fewer
|
|
// pages, else later in the list; but a buffer and an instrument of equal
|
|
// pages each go after the other (Ninjaforce's converter compares them so),
|
|
// which leaves their order to how sortClaims runs.
|
|
static bool claimAfter(const ClaimT *c, const uint32_t *o, uint32_t a, uint32_t b) {
|
|
uint32_t pagesA;
|
|
uint32_t pagesB;
|
|
|
|
pagesA = pagesOf(c[o[a]].size);
|
|
pagesB = pagesOf(c[o[b]].size);
|
|
if (pagesA != pagesB) {
|
|
return pagesA < pagesB;
|
|
}
|
|
return c[o[a]].buffer != c[o[b]].buffer || o[a] > o[b];
|
|
}
|
|
|
|
|
|
// A sample byte as the DOC's: unsigned, and never the 0 that stops it.
|
|
static uint8_t docByte(const SampleT *s, uint32_t at) {
|
|
uint8_t v;
|
|
|
|
v = (uint8_t)((at < s->have ? s->data[at] : 0u) ^ DOC_CENTER);
|
|
return v == DOC_STOP ? DOC_STOP_FIX : v;
|
|
}
|
|
|
|
|
|
// An instrument's type, and its stoppers and table: ideal when its length
|
|
// is a power of two from TABLE_MIN on, else the stoppers after it.
|
|
static void finish(InstT *inst, uint8_t type) {
|
|
uint32_t table;
|
|
|
|
if (isIdeal(inst->bytes.len)) {
|
|
type = (uint8_t)(type | TYPE_IDEAL);
|
|
} else {
|
|
toolBufZeros(&inst->bytes, STOPPER_BYTES);
|
|
}
|
|
inst->type = type;
|
|
for (table = TABLE_MIN; table < inst->bytes.len; table *= 2u) {
|
|
}
|
|
inst->table = table;
|
|
}
|
|
|
|
|
|
static void instAdd(ModT *m, const SampleT *s, uint8_t type) {
|
|
InstT *inst;
|
|
|
|
inst = &m->insts[m->instCount++];
|
|
memset(inst, 0, sizeof(*inst));
|
|
inst->sample = s;
|
|
inst->type = type;
|
|
}
|
|
|
|
|
|
// The instruments the samples make, as things stand (which are streamed).
|
|
static void instruments(ModT *m) {
|
|
SampleT *s;
|
|
InstT *inst;
|
|
uint32_t i;
|
|
uint32_t k;
|
|
uint32_t loopLen;
|
|
uint32_t split;
|
|
|
|
for (i = 0; i < m->instCount; i++) {
|
|
toolBufFree(&m->insts[i].bytes);
|
|
}
|
|
m->instCount = 0u;
|
|
for (i = 0; i < MOD_SAMPLES; i++) {
|
|
s = &m->samples[i];
|
|
if (s->length == 0u) {
|
|
continue;
|
|
}
|
|
s->first = m->instCount + 1u;
|
|
loopLen = s->looped ? (s->rep + s->repLen > s->length ? s->length - s->rep : s->repLen) : 0u;
|
|
instAdd(m, s, TYPE_PLAIN);
|
|
inst = &m->insts[m->instCount - 1u];
|
|
if (s->streamed) {
|
|
if (s->looped) {
|
|
for (k = 0; k < s->rep; k++) {
|
|
toolBufByte(&inst->bytes, docByte(s, k));
|
|
}
|
|
addLoop(&inst->bytes, s, s->rep, loopLen, 0u);
|
|
// The loop's start again, for the buffer's wrap.
|
|
for (k = 0; k < STREAM_LOOP_COPY; k++) {
|
|
toolBufByte(&inst->bytes, inst->bytes.data[s->rep + k]);
|
|
}
|
|
inst->type = (uint8_t)(TYPE_STREAMED | TYPE_LOOPED | (isIdeal(inst->bytes.len) ? TYPE_IDEAL : 0u));
|
|
} else {
|
|
for (k = 0; k < s->length; k++) {
|
|
toolBufByte(&inst->bytes, docByte(s, k));
|
|
}
|
|
toolBufZeros(&inst->bytes, STOPPER_BYTES);
|
|
inst->type = TYPE_STREAMED;
|
|
}
|
|
} else if (!s->looped) {
|
|
for (k = 0; k < s->length; k++) {
|
|
toolBufByte(&inst->bytes, docByte(s, k));
|
|
}
|
|
finish(inst, TYPE_PLAIN);
|
|
} else if (s->rep < HEADER_START_MIN) {
|
|
addLoop(&inst->bytes, s, s->rep, loopLen, 0u);
|
|
finish(inst, TYPE_LOOPED);
|
|
} else {
|
|
split = splitAt(s, s->rep, loopLen);
|
|
for (k = 0; k < split; k++) {
|
|
toolBufByte(&inst->bytes, docByte(s, k));
|
|
}
|
|
finish(inst, TYPE_HEADER);
|
|
instAdd(m, s, TYPE_LOOP);
|
|
inst = &m->insts[m->instCount - 1u];
|
|
addLoop(&inst->bytes, s, s->rep, loopLen, split - s->rep);
|
|
finish(inst, TYPE_LOOP);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// A length a table is exactly: a power of two from TABLE_MIN on.
|
|
static bool isIdeal(size_t len) {
|
|
return len >= TABLE_MIN && isPowerOfTwo((uint32_t)len);
|
|
}
|
|
|
|
|
|
static bool isPowerOfTwo(uint32_t v) {
|
|
return v != 0u && (v & (v - 1u)) == 0u;
|
|
}
|
|
|
|
|
|
// How many times a loop is laid down: a streamed one until it fills a
|
|
// stream buffer, any other short one enough to play long enough (to
|
|
// LOOP_IDEAL when that is a whole number of it).
|
|
static uint32_t loopRepeats(uint32_t loopLen, bool streamed) {
|
|
if (streamed) {
|
|
return (STREAM_BUFFER + loopLen - 1u) / loopLen;
|
|
}
|
|
if (loopLen >= LOOP_REPEAT_UNDER) {
|
|
return 1u;
|
|
}
|
|
return isPowerOfTwo(loopLen) ? LOOP_IDEAL / loopLen : LOOP_FILL / loopLen;
|
|
}
|
|
|
|
|
|
// The module at path; a fatal error when it is not one.
|
|
static void modRead(ModT *m, const char *path) {
|
|
uint8_t *file;
|
|
const uint8_t *magic;
|
|
const uint8_t *h;
|
|
const uint8_t *cell;
|
|
size_t at;
|
|
uint32_t i;
|
|
uint32_t smp;
|
|
SampleT *s;
|
|
|
|
memset(m, 0, sizeof(*m));
|
|
if (!toolReadFile(path, &file, &m->len)) {
|
|
toolFail("cannot read %s", path);
|
|
}
|
|
m->file = file;
|
|
if (m->len < MOD_HEADER_BYTES) {
|
|
toolFail("%s is too short for a module", path);
|
|
}
|
|
magic = file + MOD_MAGIC_AT;
|
|
if (memcmp(magic, "M.K.", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "M!K!", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "FLT4", MOD_MAGIC_BYTES) == 0) {
|
|
m->tracks = 4u;
|
|
} else if (memcmp(magic, "6CHN", MOD_MAGIC_BYTES) == 0) {
|
|
m->tracks = 6u;
|
|
} else if (memcmp(magic, "8CHN", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "FLT8", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "CD81", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "OKTA", MOD_MAGIC_BYTES) == 0 || memcmp(magic, "OCTA", MOD_MAGIC_BYTES) == 0) {
|
|
m->tracks = 8u;
|
|
} else {
|
|
toolFail("%s does not look like a module (no signature at %u)", path, MOD_MAGIC_AT);
|
|
}
|
|
memcpy(m->name, file, MOD_NAME_BYTES);
|
|
m->orders = file[MOD_ORDER_COUNT_AT] < MOD_ORDER_MAX ? file[MOD_ORDER_COUNT_AT] : MOD_ORDER_MAX;
|
|
m->orderTable = file + MOD_ORDER_AT;
|
|
for (i = 0; i < MOD_ORDER_MAX; i++) {
|
|
if (m->orderTable[i] + 1u > m->patterns) {
|
|
m->patterns = m->orderTable[i] + 1u;
|
|
}
|
|
}
|
|
if (m->orders == 0u) {
|
|
toolFail("%s has an empty order list", path);
|
|
}
|
|
at = MOD_HEADER_BYTES + (size_t)m->patterns * MOD_ROWS * m->tracks * MOD_CELL_BYTES;
|
|
m->cells = file + MOD_HEADER_BYTES;
|
|
if (at > m->len) {
|
|
toolFail("%s ends inside its patterns", path);
|
|
}
|
|
for (i = 0; i < MOD_SAMPLES; i++) {
|
|
s = &m->samples[i];
|
|
h = file + MOD_SAMPLE_AT + i * MOD_SAMPLE_BYTES;
|
|
memcpy(s->name, h, MOD_SAMPLE_NAME);
|
|
s->length = (((uint32_t)h[MOD_LENGTH_AT] << BYTE_BITS) | h[MOD_LENGTH_AT + 1u]) * 2u;
|
|
s->fine = h[MOD_FINE_AT] & FINE_MASK;
|
|
s->volume = h[MOD_VOLUME_AT];
|
|
s->rep = (((uint32_t)h[MOD_REP_AT] << BYTE_BITS) | h[MOD_REP_AT + 1u]) * 2u;
|
|
s->repLen = (((uint32_t)h[MOD_REPLEN_AT] << BYTE_BITS) | h[MOD_REPLEN_AT + 1u]) * 2u;
|
|
s->data = file + (at < m->len ? at : m->len);
|
|
s->have = at < m->len ? (uint32_t)(m->len - at < s->length ? m->len - at : s->length) : 0u;
|
|
s->looped = s->repLen > LOOP_MIN;
|
|
if (s->looped && s->rep >= s->length && s->length > 0u) {
|
|
fprintf(stderr, "%s: sample %u's loop starts at or past its end; it plays unlooped\n", TOOL_NAME, i + 1u);
|
|
s->looped = false;
|
|
}
|
|
s->streamed = s->length > STREAM_MAX;
|
|
at += s->length;
|
|
}
|
|
// A sample played with a sample offset (effect 9, a nonzero offset)
|
|
// streams: the replayer offsets only streamed samples.
|
|
for (cell = m->cells; cell < m->cells + (size_t)m->patterns * MOD_ROWS * m->tracks * MOD_CELL_BYTES; cell += MOD_CELL_BYTES) {
|
|
smp = (cell[0] & SAMPLE_HIGH_MASK) | (uint32_t)(cell[2] >> NIBBLE_BITS);
|
|
if (smp != 0u && (cell[2] & EFFECT_MASK) == EFFECT_OFFSET && cell[3] != 0u) {
|
|
m->samples[smp - 1u].streamed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The sample needs two oscillators: streamed, a header and a loop, or a
|
|
// loop that is not ideal.
|
|
static bool needsTwo(const ModT *m, const SampleT *s) {
|
|
const InstT *inst;
|
|
|
|
if (s->streamed) {
|
|
return true;
|
|
}
|
|
inst = &m->insts[s->first - 1u];
|
|
return (inst->type & TYPE_LOOP) == TYPE_HEADER || (inst->type & (TYPE_IDEAL | TYPE_LOOP)) == TYPE_LOOPED;
|
|
}
|
|
|
|
|
|
// A period as NTP's note number; 0 for none.
|
|
static uint32_t noteOf(uint32_t period) {
|
|
uint32_t n;
|
|
|
|
if (period == 0u) {
|
|
return 0u;
|
|
}
|
|
for (n = 0; n < NOTES - 1u && period < kNoteFloor[n]; n++) {
|
|
}
|
|
return n + 1u;
|
|
}
|
|
|
|
|
|
// The NTP file.
|
|
static void ntpWrite(const ModT *m, const char *path) {
|
|
const uint8_t *cell;
|
|
const InstT *inst;
|
|
ToolBufT out;
|
|
uint32_t i;
|
|
uint32_t p;
|
|
uint32_t r;
|
|
uint32_t t;
|
|
uint32_t osc;
|
|
uint32_t step;
|
|
uint32_t smp;
|
|
uint32_t code;
|
|
uint32_t period;
|
|
bool oscUsed[OSCS_MAX];
|
|
|
|
memset(&out, 0, sizeof(out));
|
|
toolBufAdd(&out, NTP_MAGIC, NTP_MAGIC_BYTES);
|
|
toolBufByte(&out, NTP_VERSION);
|
|
toolBufByte(&out, m->tracks);
|
|
toolBufByte(&out, m->instCount);
|
|
toolBufByte(&out, m->patterns);
|
|
toolBufByte(&out, m->orders);
|
|
toolBufByte(&out, (uint32_t)strlen(m->name));
|
|
toolBufAdd(&out, m->name, strlen(m->name));
|
|
memset(oscUsed, 0, sizeof(oscUsed));
|
|
for (t = 0; t < m->tracks; t++) {
|
|
toolBufByte(&out, (t % 4u == 0u || t % 4u == 3u) ? CHANNEL_RIGHT : CHANNEL_LEFT);
|
|
toolBufByte(&out, m->buffer[t] / PAGE_BYTES);
|
|
// The lowest free oscillator, or the lowest free DOC pair (even
|
|
// then odd).
|
|
step = m->twoOsc[t] ? 2u : 1u;
|
|
for (osc = 0; oscUsed[osc] || oscUsed[osc + step - 1u]; osc += step) {
|
|
}
|
|
oscUsed[osc] = true;
|
|
oscUsed[osc + step - 1u] = true;
|
|
toolBufByte(&out, (m->twoOsc[t] ? OSC_TWO : 0u) | osc);
|
|
}
|
|
for (i = 0; i < m->instCount; i++) {
|
|
inst = &m->insts[i];
|
|
toolBufByte(&out, inst->type);
|
|
toolBufLe16(&out, (uint32_t)inst->bytes.len);
|
|
toolBufByte(&out, (uint32_t)(inst->bytes.len >> 16));
|
|
toolBufLe16(&out, inst->sample->rep);
|
|
toolBufByte(&out, inst->sample->rep >> 16);
|
|
toolBufLe16(&out, inst->sample->repLen);
|
|
toolBufByte(&out, inst->sample->repLen >> 16);
|
|
toolBufByte(&out, inst->sample->volume);
|
|
toolBufByte(&out, inst->sample->fine);
|
|
if ((inst->type & TYPE_STREAMED) != 0u) {
|
|
toolBufByte(&out, 0u);
|
|
toolBufByte(&out, 0u);
|
|
} else {
|
|
for (code = 0; (TABLE_MIN << code) < inst->table; code++) {
|
|
}
|
|
toolBufByte(&out, inst->address / PAGE_BYTES);
|
|
toolBufByte(&out, code * WAVE_SIZE_SCALE);
|
|
}
|
|
toolBufByte(&out, (uint32_t)strlen(inst->sample->name));
|
|
toolBufAdd(&out, inst->sample->name, strlen(inst->sample->name));
|
|
}
|
|
toolBufAdd(&out, m->orderTable, m->orders);
|
|
for (p = 0; p < m->patterns; p++) {
|
|
for (r = 0; r < MOD_ROWS; r++) {
|
|
for (t = 0; t < m->tracks; t++) {
|
|
cell = m->cells + (((size_t)p * MOD_ROWS + r) * m->tracks + t) * MOD_CELL_BYTES;
|
|
smp = (cell[0] & SAMPLE_HIGH_MASK) | (uint32_t)(cell[2] >> NIBBLE_BITS);
|
|
period = ((uint32_t)(cell[0] & PERIOD_HIGH_MASK) << BYTE_BITS) | cell[1];
|
|
toolBufByte(&out, noteOf(period));
|
|
toolBufByte(&out, smp == 0u ? 0u : m->samples[smp - 1u].first);
|
|
toolBufByte(&out, cell[2] & EFFECT_MASK);
|
|
toolBufByte(&out, cell[3]);
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < m->instCount; i++) {
|
|
toolBufAdd(&out, m->insts[i].bytes.data, m->insts[i].bytes.len);
|
|
}
|
|
if (!toolWriteFile(path, out.data, out.len)) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
toolBufFree(&out);
|
|
}
|
|
|
|
|
|
static void orderSwap(uint32_t *o, uint32_t a, uint32_t b) {
|
|
uint32_t held;
|
|
|
|
held = o[a];
|
|
o[a] = o[b];
|
|
o[b] = held;
|
|
}
|
|
|
|
|
|
// The DOC pages bytes take.
|
|
static uint32_t pagesOf(uint32_t bytes) {
|
|
return (bytes + PAGE_BYTES - 1u) / PAGE_BYTES;
|
|
}
|
|
|
|
|
|
// One quicksort pass of sortClaims: the claims from position 2 on split
|
|
// about the pivot at 1; returns the position past those it goes after.
|
|
static uint32_t partition(const ClaimT *c, uint32_t *o, uint32_t n) {
|
|
uint32_t i;
|
|
uint32_t j;
|
|
|
|
i = 2u;
|
|
j = n - 1u;
|
|
for (;;) {
|
|
while (claimAfter(c, o, 1u, i)) {
|
|
i++;
|
|
if (i == j) {
|
|
return i;
|
|
}
|
|
}
|
|
j--;
|
|
if (j == i) {
|
|
return i;
|
|
}
|
|
while (claimAfter(c, o, j, 1u)) {
|
|
j--;
|
|
if (j == i) {
|
|
return i;
|
|
}
|
|
}
|
|
orderSwap(o, i, j);
|
|
i++;
|
|
if (i == j) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The instruments and then the tracks' stream buffers, as one list, sorted
|
|
// by sortClaims and each placed at the lowest address its table size
|
|
// divides where its pages are free. Page 0 is the interrupt timer's when
|
|
// no instrument fills a page. False when something does not fit.
|
|
static bool place(ModT *m) {
|
|
ClaimT claims[ITEMS_MAX];
|
|
uint32_t order[ITEMS_MAX];
|
|
bool used[DOC_PAGES];
|
|
bool given[MOD_TRACKS_MAX];
|
|
ClaimT *claim;
|
|
uint32_t count;
|
|
uint32_t i;
|
|
uint32_t j;
|
|
uint32_t t;
|
|
uint32_t pages;
|
|
uint32_t addr;
|
|
uint32_t p;
|
|
bool timer;
|
|
bool fits;
|
|
|
|
count = 0u;
|
|
timer = true;
|
|
for (i = 0; i < m->instCount; i++) {
|
|
if ((m->insts[i].type & TYPE_STREAMED) == 0u) {
|
|
claims[count++] = (ClaimT){ (uint32_t)m->insts[i].bytes.len, m->insts[i].table, &m->insts[i].address, false };
|
|
timer = timer && m->insts[i].bytes.len < PAGE_BYTES;
|
|
}
|
|
}
|
|
// A buffer for each track that plays a streamed sample: sample by
|
|
// sample, the tracks playing it in the order they first play anything.
|
|
memset(m->buffer, 0, sizeof(m->buffer));
|
|
memset(given, 0, sizeof(given));
|
|
for (i = 0; i < MOD_SAMPLES; i++) {
|
|
if (!m->samples[i].streamed) {
|
|
continue;
|
|
}
|
|
for (j = 0; j < m->tracksSeen; j++) {
|
|
t = m->trackOrder[j];
|
|
if ((m->plays[t] >> i & 1u) != 0u && !given[t]) {
|
|
given[t] = true;
|
|
claims[count++] = (ClaimT){ STREAM_BUFFER, STREAM_BUFFER, &m->buffer[t], true };
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < count; i++) {
|
|
order[i] = i;
|
|
}
|
|
sortClaims(claims, order, count);
|
|
memset(used, 0, sizeof(used));
|
|
used[0] = timer;
|
|
for (i = 0; i < count; i++) {
|
|
claim = &claims[order[i]];
|
|
pages = pagesOf(claim->size);
|
|
fits = false;
|
|
for (addr = 0; addr + claim->table <= DOC_BYTES && !fits; addr += claim->table) {
|
|
fits = true;
|
|
for (p = addr / PAGE_BYTES; p < addr / PAGE_BYTES + pages; p++) {
|
|
fits = fits && !used[p];
|
|
}
|
|
if (fits) {
|
|
*claim->where = addr;
|
|
for (p = addr / PAGE_BYTES; p < addr / PAGE_BYTES + pages; p++) {
|
|
used[p] = true;
|
|
}
|
|
}
|
|
}
|
|
if (!fits) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// The claim at position last moved down, a place at a time, past those it
|
|
// goes before.
|
|
static void sinkLast(const ClaimT *c, uint32_t *o, uint32_t last) {
|
|
uint32_t k;
|
|
|
|
for (k = last; k > 0u && claimAfter(c, o, k - 1u, k); k--) {
|
|
orderSwap(o, k - 1u, k);
|
|
}
|
|
}
|
|
|
|
|
|
// The claims at positions x, y, and z put in order (PHP's way, which
|
|
// claimAfter's equal buffers and instruments make matter).
|
|
static void sort3(const ClaimT *c, uint32_t *o, uint32_t x, uint32_t y, uint32_t z) {
|
|
if (!claimAfter(c, o, x, y)) {
|
|
if (claimAfter(c, o, y, z)) {
|
|
orderSwap(o, y, z);
|
|
if (claimAfter(c, o, x, y)) {
|
|
orderSwap(o, x, y);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (!claimAfter(c, o, z, y)) {
|
|
orderSwap(o, x, z);
|
|
return;
|
|
}
|
|
orderSwap(o, x, y);
|
|
if (claimAfter(c, o, y, z)) {
|
|
orderSwap(o, y, z);
|
|
}
|
|
}
|
|
|
|
|
|
// The claims in placing order, sorted step for step as PHP 8 sorts
|
|
// (zend_sort, after libc++'s std::sort), which is what Ninjaforce's
|
|
// converter's placements come from: over SORT_SMALL claims, the middle one
|
|
// sorted with the first and last, moved to position 1 as the pivot, the
|
|
// rest split about it, and the smaller part sorted first; SORT_SMALL or
|
|
// fewer by sortInsert. (PHP takes its pivot from five claims past 1023,
|
|
// many more than a module makes.)
|
|
static void sortClaims(const ClaimT *c, uint32_t *o, uint32_t n) {
|
|
uint32_t i;
|
|
|
|
while (n > SORT_SMALL) {
|
|
sort3(c, o, 0u, n / 2u, n - 1u);
|
|
orderSwap(o, 1u, n / 2u);
|
|
i = partition(c, o, n);
|
|
orderSwap(o, 1u, i - 1u);
|
|
if (i - 1u < n - i) {
|
|
sortClaims(c, o, i - 1u);
|
|
o += i;
|
|
n -= i;
|
|
} else {
|
|
sortClaims(c, o + i, n - i);
|
|
n = i - 1u;
|
|
}
|
|
}
|
|
sortInsert(c, o, n);
|
|
}
|
|
|
|
|
|
// PHP's insertion sort: under SORT_LINEAR claims, the first three by
|
|
// sort3 and the rest moved down a place at a time; more, the first
|
|
// SORT_LINEAR moved down a place at a time and the rest found a place for
|
|
// two at a time.
|
|
static void sortInsert(const ClaimT *c, uint32_t *o, uint32_t n) {
|
|
uint32_t i;
|
|
uint32_t j;
|
|
uint32_t k;
|
|
uint32_t held;
|
|
|
|
i = 1u;
|
|
if (n < SORT_LINEAR && n >= SORT_NETWORK) {
|
|
sort3(c, o, 0u, 1u, 2u);
|
|
i = SORT_NETWORK;
|
|
}
|
|
for (; i < n && i < SORT_LINEAR; i++) {
|
|
sinkLast(c, o, i);
|
|
}
|
|
for (; i < n; i++) {
|
|
if (!claimAfter(c, o, i - 1u, i)) {
|
|
continue;
|
|
}
|
|
j = i - 1u;
|
|
for (;;) {
|
|
j -= 2u;
|
|
if (!claimAfter(c, o, j, i)) {
|
|
j += claimAfter(c, o, j + 1u, i) ? 1u : 2u;
|
|
break;
|
|
}
|
|
if (j == 0u) {
|
|
break;
|
|
}
|
|
if (j == 1u) {
|
|
j = claimAfter(c, o, i, 0u) ? 1u : 0u;
|
|
break;
|
|
}
|
|
}
|
|
held = o[i];
|
|
for (k = i; k > j; k--) {
|
|
o[k] = o[k - 1u];
|
|
}
|
|
o[j] = held;
|
|
}
|
|
}
|
|
|
|
|
|
// The first byte of the loop (loopLen bytes from start) nearest the DOC's
|
|
// center: where a header ends and its loop begins.
|
|
static uint32_t splitAt(const SampleT *s, uint32_t start, uint32_t loopLen) {
|
|
uint32_t best;
|
|
uint32_t bestDistance;
|
|
uint32_t distance;
|
|
uint32_t i;
|
|
uint8_t v;
|
|
|
|
best = start;
|
|
bestDistance = DOC_CENTER + 1u;
|
|
for (i = start; i < start + loopLen; i++) {
|
|
v = docByte(s, i);
|
|
distance = v >= DOC_CENTER ? v - DOC_CENTER : DOC_CENTER - v;
|
|
if (distance < bestDistance) {
|
|
best = i;
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
|
|
// Which tracks play an instrument that needs two oscillators, and which
|
|
// play a streamed one: every cell of every pattern in the file.
|
|
static void tracksScan(ModT *m) {
|
|
const uint8_t *cell;
|
|
uint32_t p;
|
|
uint32_t r;
|
|
uint32_t t;
|
|
uint32_t smp;
|
|
|
|
memset(m->twoOsc, 0, sizeof(m->twoOsc));
|
|
memset(m->plays, 0, sizeof(m->plays));
|
|
m->tracksSeen = 0u;
|
|
for (p = 0; p < m->patterns; p++) {
|
|
for (r = 0; r < MOD_ROWS; r++) {
|
|
for (t = 0; t < m->tracks; t++) {
|
|
cell = m->cells + (((size_t)p * MOD_ROWS + r) * m->tracks + t) * MOD_CELL_BYTES;
|
|
smp = (cell[0] & SAMPLE_HIGH_MASK) | (uint32_t)(cell[2] >> NIBBLE_BITS);
|
|
if (smp == 0u) {
|
|
continue;
|
|
}
|
|
if (m->samples[smp - 1u].length == 0u) {
|
|
toolFail("pattern %u, line %u, track %u plays sample %u, which is empty", p, r, t + 1u, smp);
|
|
}
|
|
if (m->plays[t] == 0u) {
|
|
m->trackOrder[m->tracksSeen++] = t;
|
|
}
|
|
m->twoOsc[t] = m->twoOsc[t] || needsTwo(m, &m->samples[smp - 1u]);
|
|
m->plays[t] |= 1u << (smp - 1u);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
ModT *m;
|
|
SampleT *longest;
|
|
uint32_t i;
|
|
|
|
toolFileName(TOOL_NAME);
|
|
if (argc != 3) {
|
|
fprintf(stderr, "usage: %s IN.MOD OUT.NTP\n", TOOL_NAME);
|
|
return USAGE_EXIT;
|
|
}
|
|
m = (ModT *)calloc(1u, sizeof(ModT));
|
|
if (m == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
modRead(m, argv[1]);
|
|
for (;;) {
|
|
instruments(m);
|
|
tracksScan(m);
|
|
if (place(m)) {
|
|
break;
|
|
}
|
|
// Stream the longest sample not streamed yet (the first of equals).
|
|
longest = NULL;
|
|
for (i = 0; i < MOD_SAMPLES; i++) {
|
|
if (m->samples[i].length > 0u && !m->samples[i].streamed && (longest == NULL || m->samples[i].length > longest->length)) {
|
|
longest = &m->samples[i];
|
|
}
|
|
}
|
|
if (longest == NULL) {
|
|
toolFail("%s does not fit in DOC RAM even streamed", argv[1]);
|
|
}
|
|
longest->streamed = true;
|
|
}
|
|
ntpWrite(m, argv[2]);
|
|
return 0;
|
|
}
|