mirror of
https://github.com/zeldaret/oot.git
synced 2024-12-29 08:16:11 +00:00
df5d4cb467
* Introduce afile_sizes, generate headers of sizes for soundfonts and sequences * Initial tools/audio README * Versioning for samplebank extraction * Clean up the disassemble_sequence.py runnable interface * Add static assertions for maximum bank sizes * Boost optimization for audio tools * Samplebank XML doc * Soundfont XML doc * More docs in sampleconv for vadpcm * Various tools fixes/cleanup * VADPCM doc * Try to fix md formatting * VADPCM doc can come later * Fix merge with PR 9 * Fix blobs from MM * Try to fix bss * Try fix bss round 2 * Fix sampleconv memset bug * Suggested documentation tweaks
1818 lines
68 KiB
C
1818 lines
68 KiB
C
/**
|
|
* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
#include <alloca.h>
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "xml.h"
|
|
#include "aifc.h"
|
|
#include "samplebank.h"
|
|
#include "soundfont.h"
|
|
#include "util.h"
|
|
|
|
static_assert(sizeof(float) == sizeof(uint32_t), "Float is assumed to be 32-bit");
|
|
|
|
static float
|
|
i2f(uint32_t i)
|
|
{
|
|
union {
|
|
float f;
|
|
uint32_t i;
|
|
} fi;
|
|
|
|
fi.i = i;
|
|
return fi.f;
|
|
}
|
|
|
|
static uint32_t
|
|
f2i(float f)
|
|
{
|
|
union {
|
|
float f;
|
|
uint32_t i;
|
|
} fi;
|
|
|
|
fi.f = f;
|
|
return fi.i;
|
|
}
|
|
|
|
static int
|
|
midinote_to_z64note(int note)
|
|
{
|
|
// Converts from MIDI note number (middle C = 60) to Z64 note number (middle C = 39)
|
|
int z64note = note - 21;
|
|
if (z64note < 0) // % 128
|
|
z64note += 128;
|
|
return z64note;
|
|
}
|
|
|
|
/**
|
|
* Calculate the tuning value from a given samplerate and basenote.
|
|
*
|
|
* Uses a lookup table (gPitchFrequencies from the audio driver source) to compute the result of `2^(basenote / 12)`
|
|
* (with appropriate shifting such that the index for C4 results in 1.0)
|
|
*/
|
|
static float
|
|
calc_tuning(float sample_rate, int basenote)
|
|
{
|
|
static const float playback_sample_rate = 32000.0f; // Target samplerate in-game is 32KHz
|
|
static const float pitch_frequencies[] = {
|
|
// gPitchFrequencies in audio driver source
|
|
/* 0x00 */ 0.105112f, // PITCH_A0
|
|
/* 0x01 */ 0.111362f, // PITCH_BF0
|
|
/* 0x02 */ 0.117984f, // PITCH_B0
|
|
/* 0x03 */ 0.125f, // PITCH_C1
|
|
/* 0x04 */ 0.132433f, // PITCH_DF1
|
|
/* 0x05 */ 0.140308f, // PITCH_D1
|
|
/* 0x06 */ 0.148651f, // PITCH_EF1
|
|
/* 0x07 */ 0.15749f, // PITCH_E1
|
|
/* 0x08 */ 0.166855f, // PITCH_F1
|
|
/* 0x09 */ 0.176777f, // PITCH_GF1
|
|
/* 0x0A */ 0.187288f, // PITCH_G1
|
|
/* 0x0B */ 0.198425f, // PITCH_AF1
|
|
/* 0x0C */ 0.210224f, // PITCH_A1
|
|
/* 0x0D */ 0.222725f, // PITCH_BF1
|
|
/* 0x0E */ 0.235969f, // PITCH_B1
|
|
/* 0x0F */ 0.25f, // PITCH_C2
|
|
/* 0x10 */ 0.264866f, // PITCH_DF2
|
|
/* 0x11 */ 0.280616f, // PITCH_D2
|
|
/* 0x12 */ 0.297302f, // PITCH_EF2
|
|
/* 0x13 */ 0.31498f, // PITCH_E2
|
|
/* 0x14 */ 0.33371f, // PITCH_F2
|
|
/* 0x15 */ 0.353553f, // PITCH_GF2
|
|
/* 0x16 */ 0.374577f, // PITCH_G2
|
|
/* 0x17 */ 0.39685f, // PITCH_AF2
|
|
/* 0x18 */ 0.420448f, // PITCH_A2
|
|
/* 0x19 */ 0.445449f, // PITCH_BF2
|
|
/* 0x1A */ 0.471937f, // PITCH_B2
|
|
/* 0x1B */ 0.5f, // PITCH_C3
|
|
/* 0x1C */ 0.529732f, // PITCH_DF3
|
|
/* 0x1D */ 0.561231f, // PITCH_D3
|
|
/* 0x1E */ 0.594604f, // PITCH_EF3
|
|
/* 0x1F */ 0.629961f, // PITCH_E3
|
|
/* 0x20 */ 0.66742f, // PITCH_F3
|
|
/* 0x21 */ 0.707107f, // PITCH_GF3
|
|
/* 0x22 */ 0.749154f, // PITCH_G3
|
|
/* 0x23 */ 0.793701f, // PITCH_AF3
|
|
/* 0x24 */ 0.840897f, // PITCH_A3
|
|
/* 0x25 */ 0.890899f, // PITCH_BF3
|
|
/* 0x26 */ 0.943875f, // PITCH_B3
|
|
/* 0x27 */ 1.0f, // PITCH_C4 (Middle C)
|
|
/* 0x28 */ 1.059463f, // PITCH_DF4
|
|
/* 0x29 */ 1.122462f, // PITCH_D4
|
|
/* 0x2A */ 1.189207f, // PITCH_EF4
|
|
/* 0x2B */ 1.259921f, // PITCH_E4
|
|
/* 0x2C */ 1.33484f, // PITCH_F4
|
|
/* 0x2D */ 1.414214f, // PITCH_GF4
|
|
/* 0x2E */ 1.498307f, // PITCH_G4
|
|
/* 0x2F */ 1.587401f, // PITCH_AF4
|
|
/* 0x30 */ 1.681793f, // PITCH_A4
|
|
/* 0x31 */ 1.781798f, // PITCH_BF4
|
|
/* 0x32 */ 1.887749f, // PITCH_B4
|
|
/* 0x33 */ 2.0f, // PITCH_C5
|
|
/* 0x34 */ 2.118926f, // PITCH_DF5
|
|
/* 0x35 */ 2.244924f, // PITCH_D5
|
|
/* 0x36 */ 2.378414f, // PITCH_EF5
|
|
/* 0x37 */ 2.519842f, // PITCH_E5
|
|
/* 0x38 */ 2.66968f, // PITCH_F5
|
|
/* 0x39 */ 2.828428f, // PITCH_GF5
|
|
/* 0x3A */ 2.996615f, // PITCH_G5
|
|
/* 0x3B */ 3.174803f, // PITCH_AF5
|
|
/* 0x3C */ 3.363586f, // PITCH_A5
|
|
/* 0x3D */ 3.563596f, // PITCH_BF5
|
|
/* 0x3E */ 3.775498f, // PITCH_B5
|
|
/* 0x3F */ 4.0f, // PITCH_C6
|
|
/* 0x40 */ 4.237853f, // PITCH_DF6
|
|
/* 0x41 */ 4.489849f, // PITCH_D6
|
|
/* 0x42 */ 4.756829f, // PITCH_EF6
|
|
/* 0x43 */ 5.039685f, // PITCH_E6
|
|
/* 0x44 */ 5.33936f, // PITCH_F6
|
|
/* 0x45 */ 5.656855f, // PITCH_GF6
|
|
/* 0x46 */ 5.993229f, // PITCH_G6
|
|
/* 0x47 */ 6.349606f, // PITCH_AF6
|
|
/* 0x48 */ 6.727173f, // PITCH_A6
|
|
/* 0x49 */ 7.127192f, // PITCH_BF6
|
|
/* 0x4A */ 7.550996f, // PITCH_B6
|
|
/* 0x4B */ 8.0f, // PITCH_C7
|
|
/* 0x4C */ 8.475705f, // PITCH_DF7
|
|
/* 0x4D */ 8.979697f, // PITCH_D7
|
|
/* 0x4E */ 9.513658f, // PITCH_EF7
|
|
/* 0x4F */ 10.07937f, // PITCH_E7
|
|
/* 0x50 */ 10.6787205f, // PITCH_F7
|
|
/* 0x51 */ 11.31371f, // PITCH_GF7
|
|
/* 0x52 */ 11.986459f, // PITCH_G7
|
|
/* 0x53 */ 12.699211f, // PITCH_AF7
|
|
/* 0x54 */ 13.454346f, // PITCH_A7
|
|
/* 0x55 */ 14.254383f, // PITCH_BF7
|
|
/* 0x56 */ 15.101993f, // PITCH_B7
|
|
/* 0x57 */ 16.0f, // PITCH_C8
|
|
/* 0x58 */ 16.95141f, // PITCH_DF8
|
|
/* 0x59 */ 17.959395f, // PITCH_D8
|
|
/* 0x5A */ 19.027315f, // PITCH_EF8
|
|
/* 0x5B */ 20.15874f, // PITCH_E8
|
|
/* 0x5C */ 21.35744f, // PITCH_F8
|
|
/* 0x5D */ 22.62742f, // PITCH_GF8
|
|
/* 0x5E */ 23.972918f, // PITCH_G8
|
|
/* 0x5F */ 25.398422f, // PITCH_AF8
|
|
/* 0x60 */ 26.908691f, // PITCH_A8
|
|
/* 0x61 */ 28.508766f, // PITCH_BF8
|
|
/* 0x62 */ 30.203985f, // PITCH_B8
|
|
/* 0x63 */ 32.0f, // PITCH_C9
|
|
/* 0x64 */ 33.90282f, // PITCH_DF9
|
|
/* 0x65 */ 35.91879f, // PITCH_D9
|
|
/* 0x66 */ 38.05463f, // PITCH_EF9
|
|
/* 0x67 */ 40.31748f, // PITCH_E9
|
|
/* 0x68 */ 42.71488f, // PITCH_F9
|
|
/* 0x69 */ 45.25484f, // PITCH_GF9
|
|
/* 0x6A */ 47.945835f, // PITCH_G9
|
|
/* 0x6B */ 50.796845f, // PITCH_AF9
|
|
/* 0x6C */ 53.817383f, // PITCH_A9
|
|
/* 0x6D */ 57.017532f, // PITCH_BF9
|
|
/* 0x6E */ 60.40797f, // PITCH_B9
|
|
/* 0x6F */ 64.0f, // PITCH_C10
|
|
/* 0x70 */ 67.80564f, // PITCH_DF10
|
|
/* 0x71 */ 71.83758f, // PITCH_D10
|
|
/* 0x72 */ 76.10926f, // PITCH_EF10
|
|
/* 0x73 */ 80.63496f, // PITCH_E10
|
|
/* 0x74 */ 85.42976f, // PITCH_F10
|
|
/* 0x75 */ 0.055681f, // PITCH_BFNEG1
|
|
/* 0x76 */ 0.058992f, // PITCH_BNEG1
|
|
/* 0x77 */ 0.0625f, // PITCH_C0
|
|
/* 0x78 */ 0.066216f, // PITCH_DF0
|
|
/* 0x79 */ 0.070154f, // PITCH_D0
|
|
/* 0x7A */ 0.074325f, // PITCH_EF0
|
|
/* 0x7B */ 0.078745f, // PITCH_E0
|
|
/* 0x7C */ 0.083427f, // PITCH_F0
|
|
/* 0x7D */ 0.088388f, // PITCH_GF0
|
|
/* 0x7E */ 0.093644f, // PITCH_G0
|
|
/* 0x7F */ 0.099213f, // PITCH_AF0
|
|
};
|
|
|
|
return (sample_rate / playback_sample_rate) * pitch_frequencies[basenote];
|
|
}
|
|
|
|
void
|
|
read_envelopes_info(soundfont *sf, xmlNodePtr envelopes)
|
|
{
|
|
static const xml_attr_spec spec_env = {
|
|
{"Name", false, xml_parse_c_identifier, offsetof(envelope_data, name) },
|
|
{ "Release", false, xml_parse_u8, offsetof(envelope_data, release)},
|
|
};
|
|
static const xml_attr_spec spec_env_pt = {
|
|
{"Delay", false, xml_parse_s16, offsetof(envelope_point, delay)},
|
|
{ "Arg", false, xml_parse_s16, offsetof(envelope_point, arg) },
|
|
};
|
|
static const xml_attr_spec spec_env_goto = {
|
|
{"Index", false, xml_parse_s16, offsetof(envelope_point, arg)},
|
|
};
|
|
|
|
LL_FOREACH(xmlNodePtr, env, envelopes->children) {
|
|
if (env->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(env->name);
|
|
if (!strequ(name, "Envelope"))
|
|
error("Unexpected element node %s in envelopes list (line %d)", name, env->line);
|
|
|
|
envelope_data *envdata;
|
|
|
|
if (env->children == NULL) {
|
|
// Empty envelopes for mm
|
|
envdata = (envelope_data *)malloc(sizeof(envelope_data));
|
|
envdata->name = NULL;
|
|
envdata->points = NULL;
|
|
envdata->release = 0;
|
|
envdata->n_points = 0;
|
|
} else {
|
|
size_t points_cap = 4;
|
|
size_t points_num = 0;
|
|
|
|
void *envelopes_data = malloc(sizeof(envelope_data) + points_cap * sizeof(envelope_point));
|
|
envdata = (envelope_data *)envelopes_data;
|
|
|
|
xml_parse_node_by_spec(envdata, env, spec_env, ARRAY_COUNT(spec_env));
|
|
|
|
// Ensure name is unique
|
|
LL_FOREACH(envelope_data *, envdata2, sf->envelopes) {
|
|
if (envdata2->name != NULL && strequ(envdata->name, envdata2->name))
|
|
error("Duplicate envelope name %s (second occurrence on line %d)", envdata->name, env->line);
|
|
}
|
|
|
|
envelope_point *pts = (envelope_point *)(envdata + 1);
|
|
|
|
LL_FOREACH(xmlNodePtr, env_pt, env->children) {
|
|
if (points_num >= points_cap) {
|
|
points_cap *= 2;
|
|
envelopes_data =
|
|
realloc(envelopes_data, sizeof(envelope_data) + points_cap * sizeof(envelope_point));
|
|
envdata = (envelope_data *)envelopes_data;
|
|
pts = (envelope_point *)(envdata + 1);
|
|
}
|
|
|
|
envelope_point *pt = &pts[points_num];
|
|
|
|
if (env_pt->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *pt_name = XMLSTR_TO_STR(env_pt->name);
|
|
|
|
if (strequ(pt_name, "Point")) {
|
|
xml_parse_node_by_spec(pt, env_pt, spec_env_pt, ARRAY_COUNT(spec_env_pt));
|
|
} else if (strequ(pt_name, "Disable")) {
|
|
pt->delay = ADSR_DISABLE;
|
|
pt->arg = 0;
|
|
} else if (strequ(pt_name, "Goto")) {
|
|
pt->delay = ADSR_GOTO;
|
|
xml_parse_node_by_spec(pt, env_pt, spec_env_goto, ARRAY_COUNT(spec_env_goto));
|
|
} else if (strequ(pt_name, "Restart")) {
|
|
pt->delay = ADSR_RESTART;
|
|
pt->arg = 0;
|
|
} else if (strequ(pt_name, "Hang")) {
|
|
pt->delay = ADSR_HANG;
|
|
pt->arg = 0;
|
|
// TODO force end here and don't emit an extra hang
|
|
} else {
|
|
error("Unexpected element node %s in envelope definition (line %d)", name, env->line);
|
|
}
|
|
|
|
points_num++;
|
|
}
|
|
envdata->points = pts;
|
|
envdata->n_points = points_num;
|
|
}
|
|
|
|
envdata->used = false;
|
|
|
|
// link
|
|
if (sf->envelopes == NULL) {
|
|
sf->envelopes = envdata;
|
|
sf->envelope_last = envdata;
|
|
} else {
|
|
sf->envelope_last->next = envdata;
|
|
sf->envelope_last = envdata;
|
|
}
|
|
envdata->next = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
read_instrs_info(soundfont *sf, xmlNodePtr instrs)
|
|
{
|
|
static const xml_attr_spec instr_spec = {
|
|
{"ProgramNumber", true, xml_parse_uint, offsetof(instr_data, program_number) },
|
|
{ "Name", true, xml_parse_c_identifier, offsetof(instr_data, name) },
|
|
{ "Envelope", false, xml_parse_c_identifier, offsetof(instr_data, envelope_name) },
|
|
{ "Release", true, xml_parse_u8, offsetof(instr_data, release) },
|
|
|
|
{ "Sample", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_mid) },
|
|
{ "BaseNote", true, xml_parse_note_number, offsetof(instr_data, base_note_mid) },
|
|
{ "SampleRate", true, xml_parse_double, offsetof(instr_data, sample_rate_mid) },
|
|
|
|
{ "RangeLo", true, xml_parse_note_number, offsetof(instr_data, sample_low_end) },
|
|
{ "SampleLo", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_low) },
|
|
{ "BaseNoteLo", true, xml_parse_note_number, offsetof(instr_data, base_note_lo) },
|
|
{ "SampleRateLo", true, xml_parse_double, offsetof(instr_data, sample_rate_lo) },
|
|
|
|
{ "RangeHi", true, xml_parse_note_number, offsetof(instr_data, sample_high_start)},
|
|
{ "SampleHi", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_high) },
|
|
{ "BaseNoteHi", true, xml_parse_note_number, offsetof(instr_data, base_note_hi) },
|
|
{ "SampleRateHi", true, xml_parse_double, offsetof(instr_data, sample_rate_hi) },
|
|
};
|
|
|
|
LL_FOREACH(xmlNodePtr, instr_node, instrs->children) {
|
|
if (instr_node->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(instr_node->name);
|
|
|
|
bool is_instr = strequ(name, "Instrument");
|
|
bool is_instr_unused = strequ(name, "InstrumentUnused");
|
|
|
|
if (!is_instr && !is_instr_unused)
|
|
error("Unexpected element node %s in instrument list (line %d)", name, instr_node->line);
|
|
|
|
instr_data *instr = malloc(sizeof(instr_data));
|
|
|
|
instr->program_number = (unsigned)-1;
|
|
instr->name = NULL;
|
|
instr->sample_name_low = NULL;
|
|
instr->sample_name_mid = NULL;
|
|
instr->sample_name_high = NULL;
|
|
instr->sample_low_end = INSTR_LO_NONE;
|
|
instr->sample_low = NULL;
|
|
instr->sample_high_start = INSTR_HI_NONE;
|
|
instr->sample_high = NULL;
|
|
instr->base_note_mid = NOTE_UNSET;
|
|
instr->base_note_lo = NOTE_UNSET;
|
|
instr->base_note_hi = NOTE_UNSET;
|
|
instr->sample_rate_mid = -1.0;
|
|
instr->sample_rate_lo = -1.0;
|
|
instr->sample_rate_hi = -1.0;
|
|
instr->release = RELEASE_UNSET;
|
|
instr->unused = is_instr_unused;
|
|
|
|
xml_parse_node_by_spec(instr, instr_node, instr_spec, ARRAY_COUNT(instr_spec));
|
|
|
|
if (!is_instr_unused) {
|
|
// Check program number, midi program number range is 0-127 but the audio driver reserves 126 and 127 for
|
|
// sfx and percussion so the range we allow is 0-125
|
|
if (instr->program_number >= 126)
|
|
error("Program numbers must be in the range 0-125 (got %u on line %d)", instr->program_number,
|
|
instr_node->line);
|
|
|
|
// Ensure program number is unique
|
|
unsigned upper = instr->program_number >> 5 & 3;
|
|
unsigned lower = instr->program_number & 0x1F;
|
|
if (sf->program_number_bitset[upper] & (1 << lower))
|
|
error("Duplicate program number %u (second occurrence on line %d)", instr->program_number,
|
|
instr_node->line);
|
|
sf->program_number_bitset[upper] |= (1 << lower);
|
|
|
|
if (instr->program_number >= sf->info.num_instruments)
|
|
sf->info.num_instruments = instr->program_number + 1;
|
|
|
|
// Check name
|
|
if (instr->name == NULL)
|
|
error("Instrument must be named (line %d)", instr_node->line);
|
|
}
|
|
|
|
// Check envelope
|
|
instr->envelope = sf_get_envelope(sf, instr->envelope_name);
|
|
if (instr->envelope == NULL)
|
|
error("Bad envelope name %s (line %d)", instr->envelope_name, instr_node->line);
|
|
|
|
// Validate optionals
|
|
if (instr->release == RELEASE_UNSET)
|
|
instr->release = instr->envelope->release;
|
|
|
|
if (instr->sample_name_mid == NULL) {
|
|
// For a used instrument to have no sample path, it must have sample children and have specified at least
|
|
// one of RangeLo or RangeHi
|
|
|
|
if (instr->sample_low_end == INSTR_LO_NONE && instr->sample_high_start == INSTR_HI_NONE)
|
|
error("Instrument has no mid sample but also does not define a low or high sample (line %d)",
|
|
instr_node->line);
|
|
|
|
if (instr_node->children == NULL)
|
|
error("Instrument sample list is empty, must specify at least one sample (line %d)", instr_node->line);
|
|
|
|
bool seen_low = false;
|
|
bool seen_mid = false;
|
|
bool seen_high = false;
|
|
|
|
LL_FOREACH(xmlNodePtr, instr_sample_node, instr_node->children) {
|
|
if (instr_sample_node->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(instr_sample_node->name);
|
|
if (!strequ(name, "Sample"))
|
|
error("Unexpected element node %s in instrument sample list (line %d)", name,
|
|
instr_sample_node->line);
|
|
|
|
if (instr_sample_node->properties == NULL)
|
|
error("Expected a Low/Mid/High sample path (line %d)", instr_sample_node->line);
|
|
|
|
xmlAttrPtr attr = instr_sample_node->properties;
|
|
if (attr->next != NULL)
|
|
error("Instrument sample should have exactly one attribute (line %d)", instr_sample_node->line);
|
|
|
|
const char *attr_name = XMLSTR_TO_STR(attr->name);
|
|
|
|
bool *seen;
|
|
const char **name_ptr;
|
|
|
|
if (strequ(attr_name, "Low")) {
|
|
seen = &seen_low;
|
|
name_ptr = &instr->sample_name_low;
|
|
|
|
if (instr->sample_low_end == INSTR_LO_NONE)
|
|
error("Useless Low sample specified (RangeLo is 0) (line %d)", instr_sample_node->line);
|
|
} else if (strequ(attr_name, "Mid")) {
|
|
seen = &seen_mid;
|
|
name_ptr = &instr->sample_name_mid;
|
|
} else if (strequ(attr_name, "High")) {
|
|
seen = &seen_high;
|
|
name_ptr = &instr->sample_name_high;
|
|
|
|
if (instr->sample_high_start == INSTR_HI_NONE)
|
|
error("Useless High sample specified (RangeHi is 127) (line %d)", instr_sample_node->line);
|
|
} else {
|
|
error("Unexpected attribute name for instrument sample (line %d)", instr_sample_node->line);
|
|
}
|
|
|
|
if (*seen)
|
|
error("Duplicate \"%s\" sample specifier in instrument sample (line %d)", attr_name,
|
|
instr_sample_node->line);
|
|
*seen = true;
|
|
|
|
xmlChar *xvalue = xmlNodeListGetString(instr_sample_node->doc, attr->children, 1);
|
|
const char *value = XMLSTR_TO_STR(xvalue);
|
|
xml_parse_c_identifier(value, name_ptr);
|
|
}
|
|
|
|
if (!seen_mid && instr->sample_low_end != instr->sample_high_start)
|
|
error("Unset-but-used Mid sample (line %d)", instr_node->line);
|
|
if (!seen_low && instr->sample_low_end != 0)
|
|
error("Unset-but-used Low sample (line %d)", instr_node->line);
|
|
if (!seen_high && instr->sample_high_start != 0)
|
|
error("Unset-but-used High sample (line %d)", instr_node->line);
|
|
}
|
|
|
|
if (instr->sample_name_low != NULL) {
|
|
instr->sample_low = sample_data_forname(sf, instr->sample_name_low);
|
|
if (instr->sample_low == NULL)
|
|
error("Bad sample name \"%s\" for LOW sample (line %d). Is it defined in <Samples>?",
|
|
instr->sample_name_low, instr_node->line);
|
|
|
|
if (instr->base_note_lo == NOTE_UNSET)
|
|
instr->base_note_lo = instr->sample_low->base_note;
|
|
|
|
if (instr->sample_rate_lo < 0.0)
|
|
instr->sample_rate_lo = instr->sample_low->sample_rate;
|
|
|
|
instr->sample_low_tuning = calc_tuning(instr->sample_rate_lo, instr->base_note_lo);
|
|
}
|
|
|
|
instr->sample_mid = sample_data_forname(sf, instr->sample_name_mid);
|
|
if (instr->sample_mid == NULL)
|
|
error("Bad sample name \"%s\" for MID sample (line %d). Is it defined in <Samples>?",
|
|
instr->sample_name_mid, instr_node->line);
|
|
|
|
if (instr->base_note_mid == NOTE_UNSET)
|
|
instr->base_note_mid = instr->sample_mid->base_note;
|
|
|
|
if (instr->sample_rate_mid < 0.0)
|
|
instr->sample_rate_mid = instr->sample_mid->sample_rate;
|
|
|
|
instr->sample_mid_tuning = calc_tuning(instr->sample_rate_mid, instr->base_note_mid);
|
|
|
|
// Some tuning values don't decompose properly into a samplerate and basenote, they must be accounted for here
|
|
// for matching. So far this has only been seen for an Instrument mid sample.
|
|
// NOTE: Keep in sync with the BAD_FLOATS list in extraction/tuning.py
|
|
if (f2i(instr->sample_mid_tuning) == 0x3E7319DF /* 0.237403377 */) // diff = 2^-24
|
|
instr->sample_mid_tuning = i2f(0x3E7319E3 /* 0.237403437 */);
|
|
|
|
if (instr->sample_name_high != NULL) {
|
|
instr->sample_high = sample_data_forname(sf, instr->sample_name_high);
|
|
if (instr->sample_high == NULL)
|
|
error("Bad sample name \"%s\" for HIGH sample (line %d). Is it defined in <Samples>?",
|
|
instr->sample_name_high, instr_node->line);
|
|
|
|
if (instr->base_note_hi == NOTE_UNSET)
|
|
instr->base_note_hi = instr->sample_high->base_note;
|
|
|
|
if (instr->sample_rate_hi < 0.0)
|
|
instr->sample_rate_hi = instr->sample_high->sample_rate;
|
|
|
|
instr->sample_high_tuning = calc_tuning(instr->sample_rate_hi, instr->base_note_hi);
|
|
}
|
|
|
|
// link
|
|
if (sf->instruments == NULL) {
|
|
sf->instruments = instr;
|
|
sf->instrument_last = instr;
|
|
} else {
|
|
sf->instrument_last->next = instr;
|
|
sf->instrument_last = instr;
|
|
}
|
|
instr->next = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
read_drums_info(soundfont *sf, xmlNodePtr drums)
|
|
{
|
|
static const xml_attr_spec drum_spec = {
|
|
{"Name", false, xml_parse_c_identifier, offsetof(drum_data, name) },
|
|
{ "Note", true, xml_parse_note_number, offsetof(drum_data, note) },
|
|
{ "NoteStart", true, xml_parse_note_number, offsetof(drum_data, note_start) },
|
|
{ "NoteEnd", true, xml_parse_note_number, offsetof(drum_data, note_end) },
|
|
{ "Pan", false, xml_parse_int, offsetof(drum_data, pan) },
|
|
{ "Envelope", false, xml_parse_c_identifier, offsetof(drum_data, envelope_name)},
|
|
{ "Release", true, xml_parse_u8, offsetof(drum_data, release) },
|
|
{ "Sample", false, xml_parse_c_identifier, offsetof(drum_data, sample_name) },
|
|
{ "SampleRate", true, xml_parse_double, offsetof(drum_data, sample_rate) },
|
|
{ "BaseNote", true, xml_parse_note_number, offsetof(drum_data, base_note) },
|
|
};
|
|
|
|
LL_FOREACH(xmlNodePtr, drum_node, drums->children) {
|
|
if (drum_node->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(drum_node->name);
|
|
if (!strequ(name, "Drum"))
|
|
error("Unexpected element node %s in drums list (line %d)", name, drum_node->line);
|
|
|
|
drum_data *drum = malloc(sizeof(drum_data));
|
|
drum->note = NOTE_UNSET;
|
|
drum->note_start = NOTE_UNSET;
|
|
drum->note_end = NOTE_UNSET;
|
|
drum->sample_rate = -1;
|
|
drum->base_note = NOTE_UNSET;
|
|
drum->release = RELEASE_UNSET;
|
|
|
|
if (drum_node->properties == NULL) {
|
|
// <Drum/>
|
|
drum->name = NULL;
|
|
drum->envelope = NULL;
|
|
drum->sample_name = NULL;
|
|
drum->sample = NULL;
|
|
goto link_drum;
|
|
}
|
|
|
|
xml_parse_node_by_spec(drum, drum_node, drum_spec, ARRAY_COUNT(drum_spec));
|
|
|
|
drum->envelope = sf_get_envelope(sf, drum->envelope_name);
|
|
if (drum->envelope == NULL)
|
|
error("Bad envelope name %s (line %d)", drum->envelope_name, drum_node->line);
|
|
|
|
// validate optionals
|
|
if (drum->release == RELEASE_UNSET)
|
|
drum->release = drum->envelope->release;
|
|
|
|
if (drum->note == NOTE_UNSET) {
|
|
if (drum->note_start == NOTE_UNSET || drum->note_end == NOTE_UNSET)
|
|
error("Incomplete note range specification (line %d)", drum_node->line);
|
|
} else {
|
|
if (drum->note_start != NOTE_UNSET || drum->note_end != NOTE_UNSET)
|
|
error("Overspecified note range (line %d)", drum_node->line);
|
|
|
|
drum->note_start = drum->note_end = drum->note;
|
|
}
|
|
|
|
if (drum->note_end < drum->note_start)
|
|
error("Invalid drum note range: [%d - %d] (line %d)", drum->note_start, drum->note_end, drum_node->line);
|
|
|
|
drum->sample = sample_data_forname(sf, drum->sample_name);
|
|
if (drum->sample == NULL)
|
|
error("Bad sample name \"%s\" (line %d). Is it defined in <Samples>?", drum->sample_name, drum_node->line);
|
|
|
|
// set final samplerate if not overridden
|
|
if (drum->sample_rate == -1) {
|
|
drum->sample_rate = drum->sample->sample_rate;
|
|
}
|
|
|
|
// set basenote if not overridden
|
|
if (drum->base_note == NOTE_UNSET) {
|
|
if (drum->sample->aifc.has_inst) {
|
|
drum->base_note = drum->sample->base_note;
|
|
} else {
|
|
error("No basenote for drum (line %d)", drum_node->line);
|
|
}
|
|
}
|
|
|
|
// link
|
|
link_drum:
|
|
if (sf->drums == NULL) {
|
|
sf->drums = drum;
|
|
sf->drums_last = drum;
|
|
} else {
|
|
sf->drums_last->next = drum;
|
|
sf->drums_last = drum;
|
|
}
|
|
drum->next = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
read_sfx_info(soundfont *sf, xmlNodePtr effects)
|
|
{
|
|
static const xml_attr_spec sfx_spec = {
|
|
{"Name", false, xml_parse_c_identifier, offsetof(sfx_data, name) },
|
|
{ "Sample", false, xml_parse_c_identifier, offsetof(sfx_data, sample_name)},
|
|
{ "SampleRate", true, xml_parse_double, offsetof(sfx_data, sample_rate)},
|
|
{ "BaseNote", true, xml_parse_note_number, offsetof(sfx_data, base_note) },
|
|
};
|
|
|
|
LL_FOREACH(xmlNodePtr, sfx_node, effects->children) {
|
|
if (sfx_node->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(sfx_node->name);
|
|
if (!strequ(name, "Effect"))
|
|
error("Unexpected element node %s in effects list (line %d)", name, sfx_node->line);
|
|
|
|
sf->info.num_effects++;
|
|
|
|
sfx_data *sfx = malloc(sizeof(sfx_data));
|
|
|
|
if (sfx_node->properties == NULL) {
|
|
sfx->sample = NULL;
|
|
} else {
|
|
sfx->sample_rate = -1;
|
|
sfx->base_note = NOTE_UNSET;
|
|
xml_parse_node_by_spec(sfx, sfx_node, sfx_spec, ARRAY_COUNT(sfx_spec));
|
|
|
|
sfx->sample = sample_data_forname(sf, sfx->sample_name);
|
|
if (sfx->sample == NULL)
|
|
error("Bad sample name \"%s\" (line %d). Is it defined in <Samples>?", sfx->sample_name,
|
|
sfx_node->line);
|
|
|
|
if (sfx->base_note == NOTE_UNSET)
|
|
sfx->base_note = sfx->sample->base_note;
|
|
|
|
if (sfx->sample_rate == -1)
|
|
sfx->sample_rate = sfx->sample->sample_rate;
|
|
|
|
sfx->tuning = calc_tuning(sfx->sample_rate, sfx->base_note);
|
|
}
|
|
|
|
// link
|
|
if (sf->sfx == NULL) {
|
|
sf->sfx = sfx;
|
|
sf->sfx_last = sfx;
|
|
} else {
|
|
sf->sfx_last->next = sfx;
|
|
sf->sfx_last = sfx;
|
|
}
|
|
sfx->next = NULL;
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
bool is_dd;
|
|
bool cached;
|
|
} sample_data_defaults;
|
|
|
|
void
|
|
read_samples_info(soundfont *sf, xmlNodePtr samples)
|
|
{
|
|
static const xml_attr_spec samples_spec = {
|
|
{"IsDD", true, xml_parse_bool, offsetof(sample_data_defaults, is_dd) },
|
|
{ "Cached", true, xml_parse_bool, offsetof(sample_data_defaults, cached)},
|
|
};
|
|
static const xml_attr_spec sample_spec = {
|
|
{"Name", false, xml_parse_c_identifier, offsetof(sample_data, name) },
|
|
{ "SampleRate", true, xml_parse_double, offsetof(sample_data, sample_rate)},
|
|
{ "BaseNote", true, xml_parse_note_number, offsetof(sample_data, base_note) },
|
|
{ "IsDD", true, xml_parse_bool, offsetof(sample_data, is_dd) },
|
|
{ "Cached", true, xml_parse_bool, offsetof(sample_data, cached) },
|
|
};
|
|
|
|
sample_data_defaults defaults;
|
|
defaults.is_dd = false;
|
|
defaults.cached = false;
|
|
xml_parse_node_by_spec(&defaults, samples, samples_spec, ARRAY_COUNT(samples_spec));
|
|
|
|
LL_FOREACH(xmlNodePtr, sample_node, samples->children) {
|
|
if (sample_node->type != XML_ELEMENT_NODE)
|
|
continue;
|
|
|
|
const char *name = XMLSTR_TO_STR(sample_node->name);
|
|
if (!strequ(name, "Sample"))
|
|
error("Unexpected element node %s in samples list (line %d)", name, sample_node->line);
|
|
|
|
sample_data *sample = malloc(sizeof(sample_data));
|
|
|
|
sample->sample_rate = -1.0;
|
|
sample->base_note = NOTE_UNSET;
|
|
sample->is_dd = defaults.is_dd;
|
|
sample->cached = defaults.cached;
|
|
|
|
xml_parse_node_by_spec(sample, sample_node, sample_spec, ARRAY_COUNT(sample_spec));
|
|
|
|
samplebank *sb = (sample->is_dd) ? &sf->sbdd : &sf->sb;
|
|
|
|
const char *sample_path = samplebank_path_forname(sb, sample->name);
|
|
if (sample_path == NULL)
|
|
error("Bad sample name %s, does it exist in the samplebank? (line %d)", sample->name, sample_node->line);
|
|
|
|
aifc_read(&sample->aifc, sample_path, NULL, NULL);
|
|
|
|
if (sample->sample_rate == -1.0)
|
|
sample->sample_rate = sample->aifc.sample_rate;
|
|
|
|
if (sample->base_note == NOTE_UNSET) {
|
|
if (sample->aifc.has_inst)
|
|
sample->base_note = midinote_to_z64note(sample->aifc.basenote);
|
|
else
|
|
error("No basenote for sample %s (line %d)", sample->name, sample_node->line);
|
|
}
|
|
|
|
if (!sample->aifc.has_book)
|
|
error("No vadpcm codebook for sample %s (line %d)", sample->name, sample_node->line);
|
|
|
|
// link
|
|
if (sf->samples == NULL) {
|
|
sf->samples = sample;
|
|
sf->sample_last = sample;
|
|
} else {
|
|
sf->sample_last->next = sample;
|
|
sf->sample_last = sample;
|
|
}
|
|
sample->next = NULL;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
is_hex(char c)
|
|
{
|
|
return ('0' <= c && c <= '9') || ('A' <= c && c <= 'F');
|
|
}
|
|
|
|
static int
|
|
from_hex(char c)
|
|
{
|
|
if ('0' <= c && c <= '9')
|
|
return c - '0';
|
|
if ('A' <= c && c <= 'F')
|
|
return c - 'A' + 10;
|
|
assert(false);
|
|
return -0xABABABAB;
|
|
}
|
|
|
|
void
|
|
read_match_padding(soundfont *sf, xmlNodePtr padding_decl)
|
|
{
|
|
if (padding_decl->properties != NULL)
|
|
error("Unexpected properties for MatchPadding declaration (line %d)", padding_decl->line);
|
|
|
|
if (padding_decl->children == NULL || padding_decl->children->content == NULL)
|
|
error("No data declared for MatchPadding (line %d)", padding_decl->line);
|
|
|
|
if (padding_decl->children->next != NULL)
|
|
error("Unexpected layout for MatchPadding declaration (line %d)", padding_decl->line);
|
|
|
|
const char *data_str = XMLSTR_TO_STR(padding_decl->children->content);
|
|
size_t data_len = strlen(data_str);
|
|
|
|
// We expect padding to be bytes like 0xAB separated by comma or whitespace, so string length / 5 is the upper bound
|
|
uint8_t *padding = malloc(data_len / 5);
|
|
|
|
size_t k = 0;
|
|
bool must_be_delimiter = false;
|
|
|
|
for (size_t i = 0; i < data_len - 4; i++) {
|
|
if (isspace(data_str[i]) || data_str[i] == ',') {
|
|
must_be_delimiter = false;
|
|
continue;
|
|
}
|
|
|
|
if (must_be_delimiter)
|
|
error("Malformed padding data, expected a space or comma at position %ld", i);
|
|
|
|
if (data_str[i + 0] != '0' || data_str[i + 1] != 'x')
|
|
error("Malformed padding data, expected an 0x prefix at position %ld", i);
|
|
|
|
char c1 = toupper(data_str[i + 2]);
|
|
char c2 = toupper(data_str[i + 3]);
|
|
|
|
if (!is_hex(c1) || !is_hex(c2))
|
|
error("Malformed padding data, expected hexadecimal digits at position %ld", i + 2);
|
|
|
|
padding[k++] = (from_hex(c1) << 4) | from_hex(c2);
|
|
must_be_delimiter = true;
|
|
i += 3;
|
|
}
|
|
|
|
sf->match_padding = padding;
|
|
sf->match_padding_num = k;
|
|
}
|
|
|
|
/**
|
|
* Emit a padding statement that pads to the next 0x10 byte boundary. Assumes that `pos` measures from an 0x10-byte
|
|
* aligned location.
|
|
*/
|
|
static void
|
|
emit_padding_stmt(FILE *out, unsigned pos)
|
|
{
|
|
switch (ALIGN16(pos) - pos) {
|
|
case 0:
|
|
// Already aligned, pass silently
|
|
break;
|
|
case 4:
|
|
fprintf(out, "SF_PAD4();\n");
|
|
break;
|
|
case 8:
|
|
fprintf(out, "SF_PAD8();\n");
|
|
break;
|
|
case 0xC:
|
|
fprintf(out, "SF_PADC();\n");
|
|
break;
|
|
default:
|
|
// We don't expect to need to support alignment from anything less than word-aligned.
|
|
error("[Internal] Bad alignment generated");
|
|
break;
|
|
}
|
|
}
|
|
|
|
size_t
|
|
emit_c_header(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
fprintf(out, "// HEADER\n\n");
|
|
|
|
// Generate externs for use in the header.
|
|
|
|
if (sf->drums != NULL)
|
|
fprintf(out, "extern Drum* SF%d_DRUMS_PTR_LIST[];\n\n", sf->info.index);
|
|
|
|
if (sf->sfx != NULL)
|
|
fprintf(out, "extern SoundEffect SF%d_SFX_LIST[];\n\n", sf->info.index);
|
|
|
|
if (sf->instruments != NULL) {
|
|
// Externs are emitted in struct order
|
|
LL_FOREACH(instr_data *, instr, sf->instruments) {
|
|
if (instr->unused)
|
|
continue;
|
|
fprintf(out, "extern Instrument SF%d_%s;\n", sf->info.index, instr->name);
|
|
}
|
|
fprintf(out, "\n");
|
|
}
|
|
|
|
// Generate the header itself: drums -> sfx -> instruments.
|
|
|
|
// We always need to write pointers for drums and sfx even if they are NULL.
|
|
|
|
uint32_t pos = 0;
|
|
|
|
if (sf->drums != NULL)
|
|
fprintf(out, "NO_REORDER SECTION_DATA Drum** SF%d_DRUMS_PTR_LIST_PTR = SF%d_DRUMS_PTR_LIST;\n", sf->info.index,
|
|
sf->info.index);
|
|
else
|
|
fprintf(out, "NO_REORDER SECTION_DATA Drum** SF%d_DRUMS_PTR_LIST_PTR = NULL;\n", sf->info.index);
|
|
|
|
pos += 4;
|
|
size += 4;
|
|
|
|
if (sf->sfx != NULL)
|
|
fprintf(out, "NO_REORDER SECTION_DATA SoundEffect* SF%d_SFX_LIST_PTR = SF%d_SFX_LIST;\n", sf->info.index,
|
|
sf->info.index);
|
|
else
|
|
fprintf(out, "NO_REORDER SECTION_DATA SoundEffect* SF%d_SFX_LIST_PTR = NULL;\n", sf->info.index);
|
|
|
|
pos += 4;
|
|
size += 4;
|
|
|
|
if (sf->instruments != NULL) {
|
|
const char **instr_names = calloc(sf->info.num_instruments, sizeof(const char *));
|
|
|
|
// The instrument pointer table is indexed by program number. Since sf->instruments is sorted by struct index
|
|
// we must first sort by program number.
|
|
LL_FOREACH(instr_data *, instr, sf->instruments) {
|
|
if (instr->unused)
|
|
continue; // Unused instruments are not included in the table and have no meaningful program number
|
|
instr_names[instr->program_number] = instr->name;
|
|
}
|
|
|
|
fprintf(out, "NO_REORDER SECTION_DATA Instrument* SF%d_INSTRUMENT_PTR_LIST[] = {\n", sf->info.index);
|
|
|
|
for (unsigned i = 0; i < sf->info.num_instruments; i++) {
|
|
if (instr_names[i] == NULL)
|
|
fprintf(out, " NULL,\n");
|
|
else
|
|
fprintf(out, " &SF%d_%s,\n", sf->info.index, instr_names[i]);
|
|
pos += 4;
|
|
size += 4;
|
|
}
|
|
fprintf(out, "};\n");
|
|
|
|
free(instr_names);
|
|
}
|
|
|
|
// Pad the header to the next 0x10-byte boundary.
|
|
emit_padding_stmt(out, pos);
|
|
fprintf(out, "\n");
|
|
|
|
return ALIGN16(size);
|
|
}
|
|
|
|
/**
|
|
* Convert the compression type as indicated in the AIFC to the corresponding SampleCodec enum value.
|
|
* These must be kept in sync with the SampleCodec definition!
|
|
*/
|
|
static const char *
|
|
codec_enum(uint32_t compression_type, const char *origin_file)
|
|
{
|
|
switch (compression_type) {
|
|
case CC4('A', 'D', 'P', '9'):
|
|
return "CODEC_ADPCM";
|
|
|
|
case CC4('H', 'P', 'C', 'M'):
|
|
return "CODEC_S8";
|
|
|
|
case CC4('A', 'D', 'P', '5'):
|
|
return "CODEC_SMALL_ADPCM";
|
|
|
|
case CC4('R', 'V', 'R', 'B'):
|
|
return "CODEC_REVERB";
|
|
|
|
case CC4('N', 'O', 'N', 'E'):
|
|
return "CODEC_S16";
|
|
}
|
|
error("Bad compression type in aifc file %s", origin_file);
|
|
__builtin_unreachable();
|
|
}
|
|
|
|
static unsigned int
|
|
codec_frame_size(uint32_t compression_type)
|
|
{
|
|
switch (compression_type) {
|
|
case CC4('A', 'D', 'P', '9'):
|
|
return 9;
|
|
|
|
case CC4('A', 'D', 'P', '5'):
|
|
return 5;
|
|
|
|
default: // TODO should any others not use 16?
|
|
return 16;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare the codebooks of two samples. Returns true if they are identical.
|
|
*/
|
|
static bool
|
|
samples_books_equal(sample_data *s1, sample_data *s2)
|
|
{
|
|
int32_t s1_order = s1->aifc.book.order;
|
|
int32_t s1_npredictors = s1->aifc.book.npredictors;
|
|
int32_t s2_order = s1->aifc.book.order;
|
|
int32_t s2_npredictors = s1->aifc.book.npredictors;
|
|
|
|
if (s1_order != s2_order || s1_npredictors != s2_npredictors)
|
|
return false;
|
|
return !memcmp(*s1->aifc.book_state, *s2->aifc.book_state, 8 * (unsigned)s1_order * (unsigned)s1_npredictors);
|
|
}
|
|
|
|
/**
|
|
* Writes all samples, their codebooks and their loops to C structures.
|
|
*/
|
|
size_t
|
|
emit_c_samples(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
if (sf->samples == NULL)
|
|
return size;
|
|
|
|
int i = 0;
|
|
LL_FOREACH(sample_data *, sample, sf->samples) {
|
|
// Determine if we need to write a new book structure. If we've already emitted a book structure with the
|
|
// same contents we use that instead.
|
|
|
|
bool new_book = true;
|
|
const char *bookname = sample->name;
|
|
|
|
LL_FOREACH(sample_data *, sample2, sf->samples) {
|
|
if (sample2 == sample)
|
|
// Caught up to our current position, we need to write a new book.
|
|
break;
|
|
|
|
if (samples_books_equal(sample, sample2)) {
|
|
// A book that we've already seen is the same as this one. Since the book we are comparing to here is
|
|
// the first such book, this is guaranteed to have already been written and we move the reference to
|
|
// this one.
|
|
new_book = false;
|
|
bookname = sample2->name;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fprintf(out, "// SAMPLE %d\n\n", i);
|
|
|
|
// Write the sample header
|
|
|
|
samplebank *sb = (sample->is_dd) ? &sf->sbdd : &sf->sb;
|
|
|
|
// Note: We could skip writing the book extern if new_book is false, but it's probably not worth the extra code
|
|
fprintf(out,
|
|
// clang-format off
|
|
"extern u8 %s_%s_Off[];" "\n"
|
|
"extern AdpcmBook SF%d_%s_BOOK;" "\n"
|
|
"extern AdpcmLoop SF%d_%s_LOOP;" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sb->name, sample->name, sf->info.index, bookname, sf->info.index, sample->name);
|
|
|
|
const char *codec_name = codec_enum(sample->aifc.compression_type, sample->aifc.path);
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"NO_REORDER SECTION_DATA ALIGNED(16) Sample SF%d_%s_HEADER = {" "\n"
|
|
" "
|
|
#ifdef SFC_MM
|
|
// MM has an extra unused field in the sample structure compared to OoT
|
|
"%d, "
|
|
#endif
|
|
"%s, %d, %s, %s," "\n"
|
|
" 0x%06lX," "\n"
|
|
" %s_%s_Off," "\n"
|
|
" &SF%d_%s_LOOP," "\n"
|
|
" &SF%d_%s_BOOK," "\n"
|
|
"};" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf->info.index, sample->name,
|
|
#ifdef SFC_MM
|
|
0,
|
|
#endif
|
|
codec_name, sample->is_dd, BOOL_STR(sample->cached), BOOL_STR(false), sample->aifc.ssnd_size, sb->name,
|
|
sample->name, sf->info.index, sample->name, sf->info.index, bookname);
|
|
size += 0x10;
|
|
|
|
// Write the book if it hasn't been deduplicated.
|
|
|
|
if (new_book) {
|
|
// Since books are variable-size structures and we want to support a C89 compiler, we first write the
|
|
// header as one structure and the book state as an array. We then declare a weak symbol for the book
|
|
// header to alias it to the correct type without casts, avoiding potential type conflicts with externs.
|
|
size_t book_size = 0;
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"NO_REORDER SECTION_DATA ALIGNED(16) AdpcmBookHeader SF%d_%s_BOOK_HEADER = {" "\n"
|
|
" %d, %d," "\n"
|
|
"};" "\n"
|
|
"NO_REORDER SECTION_DATA AdpcmBookData SF%d_%s_BOOK_DATA = {" "\n",
|
|
// clang-format on
|
|
sf->info.index, bookname, sample->aifc.book.order, sample->aifc.book.npredictors, sf->info.index,
|
|
bookname);
|
|
book_size += 8;
|
|
|
|
for (size_t j = 0; j < (unsigned)sample->aifc.book.order * (unsigned)sample->aifc.book.npredictors; j++) {
|
|
fprintf(
|
|
out,
|
|
// clang-format off
|
|
" (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, "
|
|
"(s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X,\n",
|
|
// clang-format on
|
|
(uint16_t)(*sample->aifc.book_state)[j * 8 + 0], (uint16_t)(*sample->aifc.book_state)[j * 8 + 1],
|
|
(uint16_t)(*sample->aifc.book_state)[j * 8 + 2], (uint16_t)(*sample->aifc.book_state)[j * 8 + 3],
|
|
(uint16_t)(*sample->aifc.book_state)[j * 8 + 4], (uint16_t)(*sample->aifc.book_state)[j * 8 + 5],
|
|
(uint16_t)(*sample->aifc.book_state)[j * 8 + 6], (uint16_t)(*sample->aifc.book_state)[j * 8 + 7]);
|
|
}
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"};" "\n"
|
|
"#pragma weak SF%d_%s_BOOK = SF%d_%s_BOOK_HEADER" "\n",
|
|
// clang-format on
|
|
sf->info.index, bookname, sf->info.index, bookname);
|
|
|
|
// We assume here that book structures begin on 0x10-byte boundaries. Book structures are always
|
|
// `4 + 4 + 8 * order * npredictors` large, emit a padding statement to the next 0x10-byte boundary.
|
|
book_size += 2 * 8 * (unsigned)sample->aifc.book.order * (unsigned)sample->aifc.book.npredictors;
|
|
emit_padding_stmt(out, book_size);
|
|
fprintf(out, "\n");
|
|
|
|
size += ALIGN16(book_size);
|
|
}
|
|
|
|
// Write the loop
|
|
|
|
// Can't use sample->aifc.num_frames directly, the original vadpcm_enc tool occasionally got the number
|
|
// of frames wrong (off-by-1) which we must reproduce here for matching (rather than reproducing it in the
|
|
// aifc and wav/aiff files themselves)
|
|
uint32_t frame_count = (sample->aifc.ssnd_size * 16) / codec_frame_size(sample->aifc.compression_type);
|
|
|
|
// We cannot deduplicate or skip writing loops in general as the audio driver assumes that at least a loop
|
|
// header exists for every sample. We could deduplicate on the special case that two samples have the same
|
|
// frame count? TODO
|
|
|
|
if (!sample->aifc.has_loop || sample->aifc.loop.count == 0) {
|
|
// No loop present, or a loop with a count of 0 was explicitly written into the aifc.
|
|
// Write a header only, using the same weak symbol trick as with books.
|
|
|
|
uint32_t start;
|
|
uint32_t end;
|
|
uint32_t count;
|
|
|
|
if (!sample->aifc.has_loop) {
|
|
// No loop, write a loop header that spans the entire sample with a count of 0.
|
|
// The audio driver expects that a loop structure always exists for a sample.
|
|
start = 0;
|
|
end = frame_count;
|
|
count = 0;
|
|
} else {
|
|
// There is a count=0 loop in the aifc file, trust it.
|
|
start = sample->aifc.loop.start;
|
|
end = sample->aifc.loop.end;
|
|
count = sample->aifc.loop.count;
|
|
}
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"NO_REORDER SECTION_DATA ALIGNED(16) AdpcmLoopHeader SF%d_%s_LOOP_HEADER = {" "\n"
|
|
" %u, %u, %u, 0," "\n"
|
|
"};" "\n"
|
|
"#pragma weak SF%d_%s_LOOP = SF%d_%s_LOOP_HEADER" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf->info.index, sample->name, start, end, count, sf->info.index, sample->name, sf->info.index,
|
|
sample->name);
|
|
size += 0x10;
|
|
} else {
|
|
// With state, since loop states are a fixed size there is no need for a weak alias.
|
|
|
|
// Some soundfonts include the total frame count of the sample, but not all of them.
|
|
// Set the frame count to 0 here to inhibit writing it into the loop structure if this is
|
|
// a soundfont that does not include it.
|
|
if (!sf->info.loops_have_frames)
|
|
frame_count = 0;
|
|
|
|
char count_str[12];
|
|
|
|
if (sample->aifc.loop.count == 0xFFFFFFFF)
|
|
snprintf(count_str, sizeof(count_str), "0x%08X", sample->aifc.loop.count);
|
|
else
|
|
snprintf(count_str, sizeof(count_str), "%u", sample->aifc.loop.count);
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"NO_REORDER SECTION_DATA ALIGNED(16) AdpcmLoop SF%d_%s_LOOP = {" "\n"
|
|
" { %u, %u, %s, %u }," "\n"
|
|
" {" "\n"
|
|
" (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n"
|
|
" (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n"
|
|
" (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n"
|
|
" (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n"
|
|
" }," "\n"
|
|
"};" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf->info.index, sample->name, sample->aifc.loop.start, sample->aifc.loop.end, count_str,
|
|
frame_count, (uint16_t)sample->aifc.loop.state[0], (uint16_t)sample->aifc.loop.state[1],
|
|
(uint16_t)sample->aifc.loop.state[2], (uint16_t)sample->aifc.loop.state[3],
|
|
(uint16_t)sample->aifc.loop.state[4], (uint16_t)sample->aifc.loop.state[5],
|
|
(uint16_t)sample->aifc.loop.state[6], (uint16_t)sample->aifc.loop.state[7],
|
|
(uint16_t)sample->aifc.loop.state[8], (uint16_t)sample->aifc.loop.state[9],
|
|
(uint16_t)sample->aifc.loop.state[10], (uint16_t)sample->aifc.loop.state[11],
|
|
(uint16_t)sample->aifc.loop.state[12], (uint16_t)sample->aifc.loop.state[13],
|
|
(uint16_t)sample->aifc.loop.state[14], (uint16_t)sample->aifc.loop.state[15]);
|
|
size += 0x30;
|
|
}
|
|
i++;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
/**
|
|
* Write envelope structures.
|
|
*/
|
|
size_t
|
|
emit_c_envelopes(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
if (sf->envelopes == NULL)
|
|
return size;
|
|
|
|
fprintf(out, "// ENVELOPES\n\n");
|
|
|
|
size_t empty_num = 0;
|
|
|
|
LL_FOREACH(envelope_data *, envdata, sf->envelopes) {
|
|
if (sf->matching && envdata->name == NULL) {
|
|
// For MM: write 16 bytes of 0 when matching
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"NO_REORDER SECTION_DATA ALIGNED(16) EnvelopePoint SF%d_ENV_EMPTY_%lu[] = {" "\n"
|
|
" { 0, 0, }," "\n"
|
|
" { 0, 0, }," "\n"
|
|
" { 0, 0, }," "\n"
|
|
" { 0, 0, }," "\n"
|
|
"};" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf->info.index, empty_num);
|
|
|
|
empty_num++;
|
|
size += 0x10;
|
|
} else {
|
|
fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) EnvelopePoint SF%d_%s[] = {\n", sf->info.index,
|
|
envdata->name);
|
|
|
|
// Write all points
|
|
for (size_t j = 0; j < envdata->n_points; j++) {
|
|
envelope_point *pt = &envdata->points[j];
|
|
|
|
switch (pt->delay) {
|
|
case ADSR_DISABLE:
|
|
fprintf(out, " ENVELOPE_DISABLE(),\n");
|
|
break;
|
|
case ADSR_GOTO:
|
|
fprintf(out, " ENVELOPE_GOTO(%d),\n", pt->arg);
|
|
break;
|
|
case ADSR_HANG:
|
|
fprintf(out, " ENVELOPE_HANG(),\n");
|
|
break;
|
|
case ADSR_RESTART:
|
|
fprintf(out, " ENVELOPE_RESTART(),\n");
|
|
break;
|
|
default:
|
|
fprintf(out, " ENVELOPE_POINT(%5d, %5d),\n", pt->delay, pt->arg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Automatically add a HANG command at the end
|
|
fprintf(out, " ENVELOPE_HANG(),\n"
|
|
"};\n");
|
|
|
|
// Pad to 0x10-byte boundary
|
|
size_t env_size = 4 * (envdata->n_points + 1);
|
|
emit_padding_stmt(out, env_size);
|
|
fprintf(out, "\n");
|
|
|
|
size += ALIGN16(env_size);
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
|
|
#define F32_FMT "%.22f"
|
|
|
|
size_t
|
|
emit_c_instruments(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
fprintf(out, "// INSTRUMENTS\n\n");
|
|
|
|
size_t unused_instr_num = 0;
|
|
|
|
LL_FOREACH(instr_data *, instr, sf->instruments) {
|
|
if (instr->unused) {
|
|
fprintf(out, "NO_REORDER SECTION_DATA Instrument SF%d_INSTR_UNUSED_%lu = {\n", sf->info.index,
|
|
unused_instr_num);
|
|
unused_instr_num++;
|
|
} else {
|
|
fprintf(out, "NO_REORDER SECTION_DATA Instrument SF%d_%s = {\n", sf->info.index, instr->name);
|
|
}
|
|
|
|
char nlo[5];
|
|
snprintf(nlo, sizeof(nlo), "%3d", instr->sample_low_end);
|
|
char nhi[5];
|
|
snprintf(nhi, sizeof(nhi), "%3d", instr->sample_high_start);
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
" false," "\n"
|
|
" %s," "\n"
|
|
" %s," "\n"
|
|
" %d," "\n"
|
|
" SF%d_%s," "\n",
|
|
// clang-format on
|
|
(instr->sample_low_end == INSTR_LO_NONE) ? "INSTR_SAMPLE_LO_NONE" : nlo,
|
|
(instr->sample_high_start == INSTR_HI_NONE) ? "INSTR_SAMPLE_HI_NONE" : nhi, instr->release,
|
|
sf->info.index, instr->envelope_name);
|
|
|
|
if (instr->sample_low != NULL)
|
|
fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_low,
|
|
instr->sample_low_tuning);
|
|
else
|
|
fprintf(out, " INSTR_SAMPLE_NONE,\n");
|
|
|
|
fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_mid,
|
|
instr->sample_mid_tuning);
|
|
|
|
if (instr->sample_high != NULL)
|
|
fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_high,
|
|
instr->sample_high_tuning);
|
|
else
|
|
fprintf(out, " INSTR_SAMPLE_NONE,\n");
|
|
|
|
fprintf(out, "};\n\n");
|
|
|
|
size += 0x20;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
size_t
|
|
emit_c_drums(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
if (sf->drums == NULL)
|
|
return size;
|
|
|
|
fprintf(out, "// DRUMS\n\n");
|
|
|
|
// Prepare pointer table data to be filled in while writing the drum structures. Init to 0 so if any low notes are
|
|
// not covered by any drum group the name will be NULL.
|
|
struct {
|
|
const char *name;
|
|
int n;
|
|
} ptr_table[64];
|
|
memset(ptr_table, 0, sizeof(ptr_table));
|
|
|
|
// While writing the drum structures we record the maximum note covered by this soundfont. Some "oddball" soundfonts
|
|
// like soundfont 0 do not have an array entry for all 64 notes. We use this to know when to stop writing entries in
|
|
// the pointer table.
|
|
int max_note = -1;
|
|
|
|
LL_FOREACH(drum_data *, drum, sf->drums) {
|
|
if (drum->name == NULL) {
|
|
max_note++;
|
|
continue;
|
|
}
|
|
|
|
if (drum->note_end > max_note)
|
|
max_note = drum->note_end;
|
|
|
|
size_t length = drum->note_end - drum->note_start + 1;
|
|
|
|
// Drum structures are duplicated for each note in the range they cover, the basenote for each is incremented
|
|
// by one but the data is otherwise identical. We write a preprocessor definition to make the resulting source
|
|
// more compact for easier inspection.
|
|
|
|
fprintf(out,
|
|
// clang-format off
|
|
"#define SF%d_%s_ENTRY(tuning) \\" "\n"
|
|
" { \\" "\n"
|
|
" %d, \\" "\n"
|
|
" %d, \\" "\n"
|
|
" false, \\" "\n"
|
|
" { &SF%d_%s_HEADER, (tuning) }, \\" "\n"
|
|
" SF%d_%s, \\" "\n"
|
|
" }" "\n"
|
|
"NO_REORDER SECTION_DATA Drum SF%d_%s[%lu] = {" "\n",
|
|
// clang-format on
|
|
sf->info.index, drum->name, drum->release, drum->pan, sf->info.index, drum->sample->name,
|
|
sf->info.index, drum->envelope->name, sf->info.index, drum->name, length);
|
|
|
|
// Write each structure while building the drum pointer table
|
|
|
|
if (drum->note_end + 1 > 64)
|
|
error("Bad drum range for drum spanning %d to %d, should be within 0 to 63", drum->note_start,
|
|
drum->note_end);
|
|
|
|
for (size_t note_offset = 0; note_offset < length; note_offset++) {
|
|
size_t ptr_offset = drum->note_start + note_offset;
|
|
|
|
ptr_table[ptr_offset].name = drum->name;
|
|
ptr_table[ptr_offset].n = note_offset;
|
|
|
|
// wrap note on overflow
|
|
int note = drum->base_note + note_offset;
|
|
if (note > 127)
|
|
note -= 128;
|
|
|
|
float tuning = calc_tuning(drum->sample_rate, note);
|
|
|
|
fprintf(out, " SF%d_%s_ENTRY(" F32_FMT "f),\n", sf->info.index, drum->name, tuning);
|
|
}
|
|
|
|
fprintf(out, "};\n\n");
|
|
size += 0x10 * length;
|
|
}
|
|
|
|
// Write the drum pointer table. Always start at 0 and end at the maximum used note. If any low notes are not used,
|
|
// NULL is written into the array.
|
|
|
|
size_t table_len = max_note + 1;
|
|
if (table_len > 64)
|
|
error("Bad drum pointer table length %lu, should be at most 64", table_len);
|
|
|
|
fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) Drum* SF%d_DRUMS_PTR_LIST[%lu] = {\n", sf->info.index, table_len);
|
|
|
|
for (size_t i = 0; i < table_len; i++) {
|
|
if (ptr_table[i].name == NULL) {
|
|
fprintf(out, " NULL,\n");
|
|
continue;
|
|
}
|
|
|
|
if (i != 0 && ptr_table[i].n == 0) // Add some space between different drum groups
|
|
fprintf(out, "\n");
|
|
fprintf(out, " &SF%d_%s[%d],\n", sf->info.index, ptr_table[i].name, ptr_table[i].n);
|
|
}
|
|
|
|
sf->info.num_drums = table_len;
|
|
|
|
fprintf(out, "};\n");
|
|
emit_padding_stmt(out, table_len * 4);
|
|
fprintf(out, "\n");
|
|
|
|
size += ALIGN16(table_len * 4);
|
|
return size;
|
|
}
|
|
|
|
size_t
|
|
emit_c_effects(FILE *out, soundfont *sf)
|
|
{
|
|
size_t size = 0;
|
|
|
|
if (sf->sfx == NULL)
|
|
return size;
|
|
|
|
fprintf(out, "// EFFECTS\n\n");
|
|
|
|
// Effects are all contained in the same array. We write empty <Effect/> entries as NULL entries in this array.
|
|
|
|
fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) SoundEffect SF%d_SFX_LIST[] = {\n", sf->info.index);
|
|
|
|
LL_FOREACH(sfx_data *, sfx, sf->sfx) {
|
|
if (sfx->sample != NULL)
|
|
fprintf(out, " { { &SF%d_%s_HEADER, " F32_FMT "f } },\n", sf->info.index, sfx->sample->name,
|
|
sfx->tuning);
|
|
else
|
|
fprintf(out, " { { NULL, 0.0f } },\n");
|
|
|
|
size += 8;
|
|
}
|
|
|
|
fprintf(out, "};\n\n");
|
|
|
|
return size;
|
|
}
|
|
|
|
void
|
|
emit_c_match_padding(FILE *out, soundfont *sf, size_t size)
|
|
{
|
|
if (sf->match_padding != NULL && sf->match_padding_num != 0) {
|
|
// Sometimes a soundfont will have non-zero padding at the end, add these values manually
|
|
size_t expected = sf->match_padding_num;
|
|
|
|
// Don't pad any further than the next 0x10 byte boundary
|
|
size_t remaining = ALIGN16(size) - size;
|
|
size_t amount = (expected > remaining) ? remaining : expected;
|
|
|
|
fprintf(out, "// MATCH PADDING\n\n");
|
|
|
|
fprintf(out, "NO_REORDER SECTION_DATA u8 SF%d_MATCH_PADDING[] = {\n", sf->info.index);
|
|
for (size_t i = 0; i < amount; i++)
|
|
fprintf(out, " 0x%02X,\n", sf->match_padding[i]);
|
|
fprintf(out, "};\n\n");
|
|
|
|
size += amount;
|
|
}
|
|
|
|
if (sf->info.pad_to_size != 0) {
|
|
if (sf->info.pad_to_size <= size) {
|
|
warning("PadToSize directive ignored.");
|
|
} else {
|
|
fprintf(out, "// MATCH SIZE PADDING\n\n");
|
|
|
|
// pad to given size
|
|
size_t amount = sf->info.pad_to_size - size;
|
|
fprintf(out, "NO_REORDER SECTION_DATA u8 SF%d_MATCH_PADDING_TO_SIZE[%lu] = { 0 };\n", sf->info.index,
|
|
amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
emit_h_instruments(FILE *out, soundfont *sf)
|
|
{
|
|
if (sf->instruments == NULL)
|
|
return;
|
|
|
|
// Example output:
|
|
// #define FONT{Index}_INSTR_{EnumName} {EnumValue}
|
|
|
|
LL_FOREACH(instr_data *, instr, sf->instruments) {
|
|
if (!instr->unused) {
|
|
fprintf(out, "#define SF%d_%s %d\n", sf->info.index, instr->name, instr->program_number);
|
|
}
|
|
}
|
|
fprintf(out, "\n");
|
|
}
|
|
|
|
static const char *
|
|
z64_note_name(int note_num)
|
|
{
|
|
static const char *const note_names[] = {
|
|
"A0", "BF0", "B0", "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1",
|
|
"C2", "DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2", "C3", "DF3", "D3",
|
|
"EF3", "E3", "F3", "GF3", "G3", "AF3", "A3", "BF3", "B3", "C4", "DF4", "D4", "EF4", "E4", "F4",
|
|
"GF4", "G4", "AF4", "A4", "BF4", "B4", "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5",
|
|
"A5", "BF5", "B5", "C6", "DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6",
|
|
"C7", "DF7", "D7", "EF7", "E7", "F7", "GF7", "G7", "AF7", "A7", "BF7", "B7", "C8", "DF8", "D8",
|
|
"EF8", "E8", "F8", "GF8", "G8", "AF8", "A8", "BF8", "B8", "C9", "DF9", "D9", "EF9", "E9", "F9",
|
|
"GF9", "G9", "AF9", "A9", "BF9", "B9", "C10", "DF10", "D10", "EF10", "E10", "F10", "BFNEG1", "BNEG1", "C0",
|
|
"DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0",
|
|
};
|
|
return note_names[note_num];
|
|
}
|
|
|
|
void
|
|
emit_h_drums(FILE *out, soundfont *sf)
|
|
{
|
|
if (sf->drums == NULL)
|
|
return;
|
|
|
|
// Emit drum defines in groups, named like [DrumName]_[NoteName]
|
|
// e.g. a drum called "MY_DRUM" with a sample basenote of C4 covering a note range of 0..3 looks like
|
|
// #define MY_DRUM_C4 0
|
|
// #define MY_DRUM_DF4 1
|
|
// #define MY_DRUM_D4 2
|
|
// #define MY_DRUM_EF4 3
|
|
|
|
LL_FOREACH(drum_data *, drum, sf->drums) {
|
|
if (drum->name == NULL)
|
|
continue;
|
|
|
|
int length = drum->note_end - drum->note_start + 1;
|
|
|
|
for (int note_offset = 0; note_offset < length; note_offset++) {
|
|
// wrap note on overflow
|
|
int note = drum->base_note + note_offset;
|
|
if (note > 127)
|
|
note -= 128;
|
|
|
|
fprintf(out, "#define SF%d_%s_%s %d\n", sf->info.index, drum->name, z64_note_name(note),
|
|
drum->note_start + note_offset);
|
|
}
|
|
|
|
fprintf(out, "\n");
|
|
}
|
|
}
|
|
|
|
void
|
|
emit_h_effects(FILE *out, soundfont *sf)
|
|
{
|
|
if (sf->sfx == NULL)
|
|
return;
|
|
|
|
int i = 0;
|
|
LL_FOREACH(sfx_data *, sfx, sf->sfx) {
|
|
if (sfx->sample != NULL)
|
|
fprintf(out, "#define SF%d_%s %d\n", sf->info.index, sfx->name, i);
|
|
i++;
|
|
}
|
|
fprintf(out, "\n");
|
|
}
|
|
|
|
NORETURN static void
|
|
usage(const char *progname)
|
|
{
|
|
fprintf(stderr, "Usage: %s [--matching] <filename.xml> <out.c> <out.h> <out.name>\n", progname);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
char *filename_in = NULL;
|
|
char *filename_out_c = NULL;
|
|
char *filename_out_h = NULL;
|
|
char *filename_out_name = NULL;
|
|
const char *mdfilename = NULL;
|
|
FILE *mdfile;
|
|
xmlDocPtr document;
|
|
soundfont sf;
|
|
|
|
sf.matching = false;
|
|
|
|
// parse args
|
|
|
|
#define arg_error(fmt, ...) \
|
|
do { \
|
|
fprintf(stderr, fmt "\n", ##__VA_ARGS__); \
|
|
usage(argv[0]); \
|
|
} while (0)
|
|
|
|
int argn = 0;
|
|
for (int i = 1; i < argc; i++) {
|
|
if (argv[i][0] == '-') {
|
|
// Optional args
|
|
|
|
if (strequ(argv[i], "--matching")) {
|
|
if (sf.matching)
|
|
arg_error("Received --matching option twice");
|
|
|
|
sf.matching = true;
|
|
continue;
|
|
}
|
|
if (strequ(argv[i], "--makedepend")) {
|
|
if (mdfilename != NULL)
|
|
arg_error("Received --makedepend option twice");
|
|
if (i + 1 == argc)
|
|
arg_error("--makedepend missing required argument");
|
|
|
|
mdfilename = argv[++i];
|
|
continue;
|
|
}
|
|
arg_error("Unknown option \"%s\"", argv[i]);
|
|
} else {
|
|
// Required args
|
|
|
|
switch (argn) {
|
|
case 0:
|
|
filename_in = argv[i];
|
|
break;
|
|
case 1:
|
|
filename_out_c = argv[i];
|
|
break;
|
|
case 2:
|
|
filename_out_h = argv[i];
|
|
break;
|
|
case 3:
|
|
filename_out_name = argv[i];
|
|
break;
|
|
default:
|
|
arg_error("Unknown positional argument \"%s\"", argv[i]);
|
|
break;
|
|
}
|
|
argn++;
|
|
}
|
|
}
|
|
if (argn != 4)
|
|
arg_error("Not enough positional arguments");
|
|
|
|
#undef arg_error
|
|
|
|
document = xmlReadFile(filename_in, NULL, XML_PARSE_NONET);
|
|
if (document == NULL)
|
|
return EXIT_FAILURE;
|
|
|
|
xmlNodePtr root = xmlDocGetRootElement(document);
|
|
if (!strequ(XMLSTR_TO_STR(root->name), "Soundfont"))
|
|
error("Root node must be <Soundfont>");
|
|
read_soundfont_info(&sf, root);
|
|
|
|
sf.envelopes = sf.envelope_last = NULL;
|
|
|
|
// read all envelopes first irrespective of their positioning in the xml
|
|
LL_FOREACH(xmlNodePtr, node, root->children) {
|
|
const char *name = XMLSTR_TO_STR(node->name);
|
|
|
|
if (strequ(name, "Envelopes"))
|
|
read_envelopes_info(&sf, node);
|
|
}
|
|
|
|
// read all samples
|
|
sf.samples = NULL;
|
|
LL_FOREACH(xmlNodePtr, node, root->children) {
|
|
const char *name = XMLSTR_TO_STR(node->name);
|
|
|
|
if (strequ(name, "Samples"))
|
|
read_samples_info(&sf, node);
|
|
}
|
|
|
|
// read all instruments
|
|
memset(sf.program_number_bitset, 0, sizeof(sf.program_number_bitset));
|
|
sf.instruments = NULL;
|
|
sf.drums = NULL;
|
|
sf.sfx = NULL;
|
|
LL_FOREACH(xmlNodePtr, node, root->children) {
|
|
const char *name = XMLSTR_TO_STR(node->name);
|
|
|
|
if (strequ(name, "Instruments"))
|
|
read_instrs_info(&sf, node);
|
|
if (strequ(name, "Drums"))
|
|
read_drums_info(&sf, node);
|
|
if (strequ(name, "Effects"))
|
|
read_sfx_info(&sf, node);
|
|
}
|
|
|
|
// read match padding if it exists
|
|
sf.match_padding = NULL;
|
|
LL_FOREACH(xmlNodePtr, node, root->children) {
|
|
const char *name = XMLSTR_TO_STR(node->name);
|
|
|
|
if (strequ(name, "MatchPadding"))
|
|
read_match_padding(&sf, node);
|
|
}
|
|
|
|
// emit C source
|
|
|
|
FILE *out_c = fopen(filename_out_c, "w");
|
|
fprintf(out_c, "#include \"soundfont_file.h\"\n\n");
|
|
|
|
size_t size = 0;
|
|
size += emit_c_header(out_c, &sf);
|
|
size += emit_c_samples(out_c, &sf);
|
|
size += emit_c_envelopes(out_c, &sf);
|
|
size += emit_c_instruments(out_c, &sf);
|
|
size += emit_c_drums(out_c, &sf);
|
|
size += emit_c_effects(out_c, &sf);
|
|
emit_c_match_padding(out_c, &sf, size);
|
|
|
|
fclose(out_c);
|
|
|
|
// emit C header
|
|
|
|
FILE *out_h = fopen(filename_out_h, "w");
|
|
fprintf(out_h,
|
|
// clang-format off
|
|
"#ifndef SOUNDFONT_%d_H_" "\n"
|
|
"#define SOUNDFONT_%d_H_" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf.info.index, sf.info.index);
|
|
|
|
fprintf(out_h,
|
|
// clang-format off
|
|
"#ifdef _LANGUAGE_ASEQ" "\n"
|
|
".pushsection .note.fonts, \"\", @note" "\n"
|
|
" .byte %d /*sf id*/" "\n"
|
|
".popsection" "\n"
|
|
"#endif" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf.info.index);
|
|
|
|
fprintf(out_h,
|
|
// clang-format off
|
|
"#define %s_ID %d" "\n"
|
|
"\n"
|
|
"#define SF%d_NUM_INSTRUMENTS %d" "\n"
|
|
"#define SF%d_NUM_DRUMS %d" "\n"
|
|
"#define SF%d_NUM_SFX %d" "\n"
|
|
"\n",
|
|
// clang-format on
|
|
sf.info.name, sf.info.index, sf.info.index, sf.info.num_instruments, sf.info.index, sf.info.num_drums,
|
|
sf.info.index, sf.info.num_effects);
|
|
|
|
emit_h_instruments(out_h, &sf);
|
|
emit_h_drums(out_h, &sf);
|
|
emit_h_effects(out_h, &sf);
|
|
|
|
fprintf(out_h, "#endif\n");
|
|
fclose(out_h);
|
|
|
|
// emit name marker
|
|
|
|
FILE *out_name = fopen(filename_out_name, "wb");
|
|
// We need to emit an explicit null terminator so that we can run objcopy --add-section to include the name
|
|
// in a .note.name section in the compiled object file. This is so that the string that ends up in the .note.name
|
|
// section is null-terminated, its length may be verified by any tools that read the name out of this section.
|
|
fprintf(out_name, "%s%c", sf.info.name, '\0');
|
|
fclose(out_name);
|
|
|
|
// emit dependency file if wanted
|
|
|
|
if (mdfilename != NULL) {
|
|
mdfile = fopen(mdfilename, "w");
|
|
if (mdfile == NULL)
|
|
error("Unable to open dependency file [%s] for writing", mdfilename);
|
|
|
|
// Begin rule + depend on the soundfont xml input
|
|
fprintf(mdfile, "%s %s %s: \\\n %s", filename_out_c, filename_out_h, filename_out_name, filename_in);
|
|
|
|
// Depend on the referenced samplebank xmls
|
|
if (sf.info.bank_path != NULL)
|
|
fprintf(mdfile, " \\\n %s", sf.info.bank_path);
|
|
if (sf.info.bank_path_dd != NULL)
|
|
fprintf(mdfile, " \\\n %s", sf.info.bank_path_dd);
|
|
|
|
// Depend on the aifc files used by this soundfont
|
|
LL_FOREACH(sample_data *, sample, sf.samples) {
|
|
fprintf(mdfile, " \\\n %s", sample->aifc.path);
|
|
}
|
|
|
|
fputs("\n", mdfile);
|
|
fclose(mdfile);
|
|
}
|
|
|
|
// done
|
|
|
|
xmlFreeDoc(document);
|
|
return EXIT_SUCCESS;
|
|
}
|