// SoundPatch.c // Turns a soundThing patch into a table the sound device can be handed. // // ---- Why this exists ---- // // soundThing is where a patch gets DESIGNED, because it has a screen, a keyboard and a pair // of ears attached to it. Voyager's sound device is the same voice engine with the editor // taken off, so the sound a patch makes is the same sound - but its settings arrive as bytes // through a selector, and a byte is not seconds or hertz. // // The first sound written for a game here was guessed at in bytes: a cutoff of 40 looked // small and is 57 Hz, so the bang came out as a low gurgle. That is what this is for. Design // it where it can be heard, convert it, and the numbers stop being a matter of opinion. // // ---- What a byte means ---- // // Every parameter the device takes is a documented function of a natural value, and all of // them invert. Times are SQUARED into four seconds, because the difference between five and // fifty milliseconds is the whole character of a percussive sound and the difference between // three and four seconds is nothing anybody can hear. Cutoff and LFO rate are EXPONENTIAL, // because pitch is logarithmic and so is where a filter sounds like it is. Depths and detune // are CENTRED on 128, so half of nothing is no change and either side of it is a direction. // // Written by Anachronaut #include #include #include #include // How a natural value becomes a byte. The inverse of soundParameter in sound.c, and the // reason that file and this one have to be read together if either changes. typedef enum { AS_DIRECT, // already a byte: a waveform, a routing, a filter type AS_BOOL, // nought or one AS_RANGE, // linear from low to high AS_SQUARED, // times, so that the short end has the resolution AS_EXPONENTIAL, // cutoff and rate, because hearing is logarithmic AS_SIGNED, // centred on 128, reaching `high` either way AS_OCTAVE // a small signed number, offset by 128 } Shape; typedef struct { const char *field; // what soundThing calls it unsigned char parameter; // what the device calls it Shape shape; double low, high; const char *note; } Mapping; // ---- The whole of the correspondence ---- // // Oscillator one is oscillator nought's parameters plus 0x10, and the modulation envelope is // the amplitude one plus 0x10, which is why the parameter numbers are laid out the way they // are. Written out in full anyway: a table that has to be understood before it can be read is // worse than a long one. static const Mapping mappings[] = { { "osc0_waveform", 0x00, AS_DIRECT, 0, 0, "oscillator 0, waveform" }, { "osc0_gain", 0x01, AS_RANGE, 0, 4.0, "gain" }, { "osc0_dutyCycle", 0x02, AS_RANGE, 0.05, 0.95, "duty" }, { "osc0_detune", 0x03, AS_SIGNED, 0, 1200, "detune, in cents" }, { "osc0_octave", 0x04, AS_OCTAVE, 0, 0, "octave" }, { "osc0_active", 0x05, AS_BOOL, 0, 0, "on" }, { "osc0_modRouting0", 0x06, AS_DIRECT, 0, 0, "what moves its width" }, { "osc0_modDepth0", 0x07, AS_SIGNED, 0, 0.5, "and how far" }, { "osc0_modRouting1", 0x08, AS_DIRECT, 0, 0, "what moves its pitch" }, { "osc0_modDepth1", 0x09, AS_SIGNED, 0, 1200, "and how far" }, { "osc0_modRouting2", 0x0A, AS_DIRECT, 0, 0, "what moves its gain" }, { "osc0_modDepth2", 0x0B, AS_SIGNED, 0, 4.0, "and how far" }, { "osc1_waveform", 0x10, AS_DIRECT, 0, 0, "oscillator 1, waveform" }, { "osc1_gain", 0x11, AS_RANGE, 0, 4.0, "gain" }, { "osc1_dutyCycle", 0x12, AS_RANGE, 0.05, 0.95, "duty" }, { "osc1_detune", 0x13, AS_SIGNED, 0, 1200, "detune, in cents" }, { "osc1_octave", 0x14, AS_OCTAVE, 0, 0, "octave" }, { "osc1_active", 0x15, AS_BOOL, 0, 0, "on" }, { "osc1_modRouting0", 0x16, AS_DIRECT, 0, 0, "what moves its width" }, { "osc1_modDepth0", 0x17, AS_SIGNED, 0, 0.5, "and how far" }, { "osc1_modRouting1", 0x18, AS_DIRECT, 0, 0, "what moves its pitch" }, { "osc1_modDepth1", 0x19, AS_SIGNED, 0, 1200, "and how far" }, { "osc1_modRouting2", 0x1A, AS_DIRECT, 0, 0, "what moves its gain" }, { "osc1_modDepth2", 0x1B, AS_SIGNED, 0, 4.0, "and how far" }, { "ampEnv_attack", 0x20, AS_SQUARED, 0, 4.0, "amplitude envelope, attack" }, { "ampEnv_decay", 0x21, AS_SQUARED, 0, 4.0, "decay" }, { "ampEnv_sustain", 0x22, AS_RANGE, 0, 1.0, "sustain" }, { "ampEnv_release", 0x23, AS_SQUARED, 0, 4.0, "release" }, { "modEnv_attack", 0x30, AS_SQUARED, 0, 4.0, "modulation envelope, attack" }, { "modEnv_decay", 0x31, AS_SQUARED, 0, 4.0, "decay" }, { "modEnv_sustain", 0x32, AS_RANGE, 0, 1.0, "sustain" }, { "modEnv_release", 0x33, AS_SQUARED, 0, 4.0, "release" }, { "filter_active", 0x40, AS_BOOL, 0, 0, "filter, on" }, { "filter_type", 0x41, AS_DIRECT, 0, 0, "type: 0 low, 1 high, 2 band" }, { "filter_cutoff", 0x42, AS_EXPONENTIAL, 20.0, 20000.0, "cutoff, in hertz" }, { "filter_resonance", 0x43, AS_RANGE, 0, 0.99, "resonance" }, { "filter_modRouting", 0x44, AS_DIRECT, 0, 0, "what moves the cutoff" }, { "filter_modDepth", 0x45, AS_SIGNED, 0, 8000.0, "and how far, in hertz" }, { "filter_resModRouting", 0x46, AS_DIRECT, 0, 0, "what moves the resonance" }, { "filter_resModDepth", 0x47, AS_SIGNED, 0, 0.99, "and how far" }, { "lfo0_active", 0x60, AS_BOOL, 0, 0, "LFO 0, on" }, { "lfo0_waveform", 0x61, AS_DIRECT, 0, 0, "waveform" }, { "lfo0_rate", 0x62, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" }, { "lfo1_active", 0x70, AS_BOOL, 0, 0, "LFO 1, on" }, { "lfo1_waveform", 0x71, AS_DIRECT, 0, 0, "waveform" }, { "lfo1_rate", 0x72, AS_EXPONENTIAL, 0.05, 20.0, "rate, in hertz" }, }; #define MAPPING_COUNT ((int)(sizeof(mappings) / sizeof(mappings[0]))) // ---- What soundThing has and the device does not ---- // // Named rather than ignored, so a field that is simply new shows up as unknown and a field // that is deliberately dropped does not. Master volume is the editor's own output level and // the device has one of its own; pitch bend has no wheel to come from. static const char *ignored[] = { "master_volume", "master_pitchBendRange" }; #define IGNORED_COUNT ((int)(sizeof(ignored) / sizeof(ignored[0]))) static int clampByte(double raw) { long v = lround(raw); if (v < 0) return 0; if (v > 255) return 255; return (int)v; } static int toByte(const Mapping *m, double value) { switch (m->shape) { case AS_DIRECT: return clampByte(value); case AS_BOOL: return value != 0.0 ? 1 : 0; case AS_RANGE: return clampByte(255.0 * (value - m->low) / (m->high - m->low)); // The inverse of part*part*high, so the root comes back out. case AS_SQUARED: return clampByte(255.0 * sqrt(value / m->high)); // And of low * (high/low)^part. case AS_EXPONENTIAL: if (value < m->low) value = m->low; return clampByte(255.0 * log(value / m->low) / log(m->high / m->low)); case AS_SIGNED: return clampByte(128.0 + 128.0 * value / m->high); case AS_OCTAVE: return clampByte(128.0 + value); } return 0; } int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "Usage: %s