SoundPatch: design a sound where it can be heard, then convert it
The bang was guessed at directly in bytes and came out as a low gurgle, which is what guessing at bytes gets you: a cutoff of 40 looks small and is 57 Hz, so the filter sweep ended almost shut. Voyager's sound device is soundThing's voice engine with the editor taken off, so a patch designed in soundThing - where there is a screen, a keyboard and a pair of ears - makes the same sound here. What differs is only how it arrives. SoundPatch converts one into the other. Every parameter the device takes is a documented function of a natural value and all of them invert: times are squared into four seconds, cutoff and rate are exponential, depths and detune are centred on 128. It writes a table of a count and that many parameter and value pairs, and playPatch hands it to the device - so every sound after this costs a table and a call rather than forty lines of its own. Two things the conversion has to say out loud. soundThing has no field for the level routing, because there it is always the amplitude envelope, so the table says so explicitly - a channel keeps its patch between notes and a leftover from a previous one would otherwise be carried in. And a field the tool does not recognise STOPS it: a patch format that has moved on would otherwise produce a table that quietly means something else. THE BUILD DOES NOT DEPEND ON IT. soundThing lives in its own repository and is not needed to build anything here; the tables are checked in and the tool is for when a sound is being changed. Lander's crash now plays Kick808 as a stand-in until a bang is designed for it, and the difference is the point: the hand-guessed patch wandered between 1600 and 8200 for eight tenths of a second, and this decays 3492, 2743, 2037, 1515, 1040, 614, 87, nothing. The docs check caught the tool count in two manuals, which is what it is for.
This commit is contained in:
@@ -2661,10 +2661,18 @@ hideSprite:
|
||||
|
||||
; ---- The bang, set up once and played in two writes ----
|
||||
;
|
||||
; A patch is twenty odd writes and a note is two, which is the whole point of the selector and
|
||||
; A patch is forty odd writes and a note is two, which is the whole point of the selector and
|
||||
; value registers - so the instrument is built at startup like the tiles are, and a crash only
|
||||
; has to say "this channel, this note".
|
||||
;
|
||||
; ---- And the numbers came out of soundThing ----
|
||||
;
|
||||
; The first version of this was guessed at in bytes, and a byte is not seconds or hertz: a
|
||||
; cutoff of 40 looked small and is 57 Hz, which made the bang a low gurgle. Voyager's sound
|
||||
; device is soundThing's voice engine with the editor taken off, so a patch designed there -
|
||||
; where it can be HEARD - means the same thing here. SoundPatch converts one into the table
|
||||
; below. Change the sound in soundThing and convert it again; do not edit the numbers.
|
||||
;
|
||||
; ---- Channel three, counting down ----
|
||||
;
|
||||
; Effects take the top channel and work downwards, so music, if it ever arrives, can take
|
||||
@@ -2673,87 +2681,31 @@ hideSprite:
|
||||
putBoom:
|
||||
INIA 0x03
|
||||
OUTA 0x41 ; Channel three. Every parameter write below lands on it.
|
||||
SETD.0 BoomPatch
|
||||
CALL playPatch
|
||||
RET
|
||||
|
||||
; ---- Noise, which is what an explosion is ----
|
||||
;
|
||||
; Every other waveform here has a pitch, and a pitched bang is a note. Writing a port leaves
|
||||
; A alone, so the three that selected the channel would select parameter three; the wave is
|
||||
; parameter nought and has to be said.
|
||||
RSTA
|
||||
; ---- A whole patch, out of a table ----
|
||||
;
|
||||
; DP0 names a count and then that many parameter and value pairs. Every sound after this one
|
||||
; costs a table and a call rather than forty lines of its own.
|
||||
playPatch:
|
||||
LDA.0
|
||||
SETD.1 PatchLeft
|
||||
STA.1
|
||||
INCD.0
|
||||
playPatchOne:
|
||||
LDA.0
|
||||
OUTA 0x42
|
||||
INIA 0d5 ; Noise.
|
||||
OUTA 0x43
|
||||
INIA 0x01 ; Oscillator nought, gain.
|
||||
OUTA 0x42
|
||||
INIA 0xFF
|
||||
OUTA 0x43
|
||||
|
||||
; ---- Instant, then a long way down ----
|
||||
;
|
||||
; No attack at all, because a bang that fades in is not a bang. A sustain of nothing does NOT
|
||||
; end the note - a voice holding at nothing is what a held key is - so the gate still has to
|
||||
; be dropped afterwards, which the explosion does once its pieces have gone.
|
||||
INIA 0x20 ; Amplitude envelope, attack.
|
||||
OUTA 0x42
|
||||
RSTA
|
||||
OUTA 0x43
|
||||
INIA 0x21 ; ---- Decay: about half a second ----
|
||||
OUTA 0x42 ;
|
||||
; A byte is not seconds. It is squared and scaled to four of them,
|
||||
; so 200 is two and a half - which over the eight tenths of a
|
||||
; second the pieces are in the air is not a bang fading, it is the
|
||||
; first third of one, and it read as a flat wash of noise.
|
||||
INIA 0d90
|
||||
OUTA 0x43
|
||||
INIA 0x22 ; Sustain: nothing.
|
||||
OUTA 0x42
|
||||
RSTA
|
||||
OUTA 0x43
|
||||
INIA 0x23 ; Release.
|
||||
OUTA 0x42
|
||||
INIA 0d40
|
||||
OUTA 0x43
|
||||
|
||||
; ---- And a filter closing, which is what makes it a boom and not a hiss ----
|
||||
;
|
||||
; Noise on its own is a cymbal. The modulation envelope shuts the low pass over the same
|
||||
; moment the level falls, so the bright part is only at the front of it.
|
||||
INIA 0x40 ; Filter, on.
|
||||
OUTA 0x42
|
||||
INIA 0x01
|
||||
OUTA 0x43
|
||||
INIA 0x41 ; Type: low pass.
|
||||
OUTA 0x42
|
||||
RSTA
|
||||
OUTA 0x43
|
||||
INIA 0x42 ; Cutoff, low, which is where it ends up.
|
||||
OUTA 0x42
|
||||
INIA 0d40
|
||||
OUTA 0x43
|
||||
INIA 0x43 ; A little resonance, so the sweep can be heard.
|
||||
OUTA 0x42
|
||||
INIA 0d120
|
||||
OUTA 0x43
|
||||
INIA 0x44 ; What opens it: the modulation envelope.
|
||||
OUTA 0x42
|
||||
INIA 0d2
|
||||
OUTA 0x43
|
||||
INIA 0x45 ; And how far above where it sits.
|
||||
OUTA 0x42
|
||||
INIA 0d200
|
||||
OUTA 0x43
|
||||
INIA 0x30 ; That envelope: open at once,
|
||||
OUTA 0x42
|
||||
RSTA
|
||||
OUTA 0x43
|
||||
INIA 0x31 ; shut over about a third of a second,
|
||||
OUTA 0x42
|
||||
INIA 0d90
|
||||
OUTA 0x43
|
||||
INIA 0x32 ; and stay shut.
|
||||
OUTA 0x42
|
||||
RSTA
|
||||
INCD.0
|
||||
LDA.0
|
||||
OUTA 0x43
|
||||
INCD.0
|
||||
SETD.1 PatchLeft
|
||||
LDA.1
|
||||
DECA
|
||||
STA.1
|
||||
BNA playPatchOne
|
||||
RET
|
||||
|
||||
; Two writes, which is what a note is meant to cost.
|
||||
@@ -3948,6 +3900,8 @@ CopyLeft:
|
||||
0x00
|
||||
HideSlot:
|
||||
0x00
|
||||
PatchLeft:
|
||||
0x00
|
||||
|
||||
; What a flame is being asked to be, before it is written out.
|
||||
FlameSlot:
|
||||
@@ -4132,3 +4086,5 @@ LanderArtAt:
|
||||
#Reserve 0d2
|
||||
|
||||
#Include math.asm
|
||||
|
||||
#Include boom.asm
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
; BoomPatch, converted from External/soundThing/patches/Drums/Kick808.json by SoundPatch.
|
||||
; Designed in soundThing, where it can be heard. Do not edit the numbers
|
||||
; here: change the patch and convert it again.
|
||||
|
||||
#Data
|
||||
|
||||
BoomPatch:
|
||||
0d47 ; how many pairs follow
|
||||
0x50 0x01 ; the level is the amplitude envelope
|
||||
0x00 0d0 ; oscillator 0, waveform
|
||||
0x01 0d255 ; gain
|
||||
0x02 0d128 ; duty
|
||||
0x03 0d128 ; detune, in cents
|
||||
0x04 0d127 ; octave
|
||||
0x05 0d1 ; on
|
||||
0x06 0d0 ; what moves its width
|
||||
0x07 0d128 ; and how far
|
||||
0x08 0d2 ; what moves its pitch
|
||||
0x09 0d197 ; and how far
|
||||
0x0A 0d0 ; what moves its gain
|
||||
0x0B 0d128 ; and how far
|
||||
0x10 0d5 ; oscillator 1, waveform
|
||||
0x11 0d146 ; gain
|
||||
0x12 0d128 ; duty
|
||||
0x13 0d128 ; detune, in cents
|
||||
0x14 0d128 ; octave
|
||||
0x15 0d1 ; on
|
||||
0x16 0d0 ; what moves its width
|
||||
0x17 0d128 ; and how far
|
||||
0x18 0d0 ; what moves its pitch
|
||||
0x19 0d128 ; and how far
|
||||
0x1A 0d0 ; what moves its gain
|
||||
0x1B 0d128 ; and how far
|
||||
0x20 0d4 ; amplitude envelope, attack
|
||||
0x21 0d79 ; decay
|
||||
0x22 0d0 ; sustain
|
||||
0x23 0d31 ; release
|
||||
0x30 0d4 ; modulation envelope, attack
|
||||
0x31 0d35 ; decay
|
||||
0x32 0d0 ; sustain
|
||||
0x33 0d13 ; release
|
||||
0x40 0d1 ; filter, on
|
||||
0x41 0d0 ; type: 0 low, 1 high, 2 band
|
||||
0x42 0d92 ; cutoff, in hertz
|
||||
0x43 0d10 ; resonance
|
||||
0x44 0d0 ; what moves the cutoff
|
||||
0x45 0d128 ; and how far, in hertz
|
||||
0x46 0d0 ; what moves the resonance
|
||||
0x47 0d128 ; and how far
|
||||
0x60 0d0 ; LFO 0, on
|
||||
0x61 0d0 ; waveform
|
||||
0x62 0d127 ; rate, in hertz
|
||||
0x70 0d0 ; LFO 1, on
|
||||
0x71 0d0 ; waveform
|
||||
0x72 0d127 ; rate, in hertz
|
||||
@@ -24,8 +24,10 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
|
||||
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
|
||||
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
|
||||
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
|
||||
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
|
||||
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
|
||||
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
|
||||
| [`Programs/Sounds`](Programs/Sounds) | Patch tables, converted from soundThing and checked in so the build never needs it |
|
||||
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
|
||||
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
|
||||
| [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives |
|
||||
@@ -114,7 +116,7 @@ Manual.
|
||||
|
||||
## Getting Started:
|
||||
|
||||
Clone it and build the four tools. You need gcc and make, or similar:
|
||||
Clone it and build the five tools. You need gcc and make, or similar:
|
||||
|
||||
```
|
||||
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
|
||||
@@ -304,6 +306,32 @@ this is what turns such a count into a list of routine names.
|
||||
|
||||
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
|
||||
|
||||
## Making A Patch: SoundPatch
|
||||
|
||||
```
|
||||
./SoundPatch <patch.json> <label> [output.asm]
|
||||
```
|
||||
|
||||
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
|
||||
in soundThing makes the same sound here. What differs is how it arrives: soundThing writes
|
||||
seconds and hertz, and the device takes bytes through a selector. SoundPatch converts one into
|
||||
the other and writes a table a program can hand to the device.
|
||||
|
||||
A byte is not a number of seconds, and that is the whole reason this exists. Times are squared
|
||||
into four seconds, because the difference between five and fifty milliseconds is the 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 hearing is logarithmic. Depths and detune
|
||||
are centred on 128, so half of nothing is no change. The first sound written for a game here
|
||||
was guessed at directly in bytes: a cutoff of 40 looks small and is 57 Hz, and the bang came
|
||||
out as a low gurgle.
|
||||
|
||||
A field the tool does not recognise stops it rather than being skipped, because a patch format
|
||||
that has moved on would otherwise produce a table that quietly means something else.
|
||||
|
||||
**The build does not depend on it.** soundThing lives in its own repository and is not
|
||||
required to build anything here: the tables it produces are checked into `Programs/Sounds`,
|
||||
and the tool is for when a sound is being changed.
|
||||
|
||||
## Checking Assembly: SplitLint
|
||||
|
||||
```
|
||||
@@ -611,7 +639,7 @@ $(BUILD)/%.bin: %.asm
|
||||
make test
|
||||
```
|
||||
|
||||
Builds the four tools, checks they build clean under strict ISO C, and runs nine scripts.
|
||||
Builds the five tools, checks they build clean under strict ISO C, and runs nine scripts.
|
||||
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
|
||||
against recorded output; six more ask the questions a recorded file cannot answer. Between
|
||||
them they check the two assemblers against each other byte for byte, the two SBFS
|
||||
@@ -630,7 +658,7 @@ correct.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
Rebuilds all four tools with the address and undefined behaviour sanitizers and runs the
|
||||
Rebuilds all five tools with the address and undefined behaviour sanitizers and runs the
|
||||
whole suite under them. It catches reads and writes past the end of an array, use after
|
||||
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
|
||||
binaries back when it finishes.
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,226 @@
|
||||
// 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
// 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 <patch.json> <label> [output.asm]\n\n"
|
||||
"Turns a soundThing patch into a table for the sound device: a count, then that\n"
|
||||
"many parameter and value pairs. Hand the label to playPatch.\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char *path = argv[1];
|
||||
const char *label = argv[2];
|
||||
|
||||
FILE *in = fopen(path, "r");
|
||||
if (!in) { fprintf(stderr, "SoundPatch: cannot read %s\n", path); return 1; }
|
||||
|
||||
int values[MAPPING_COUNT];
|
||||
int seen[MAPPING_COUNT];
|
||||
memset(seen, 0, sizeof(seen));
|
||||
|
||||
char line[512];
|
||||
int unknown = 0;
|
||||
while (fgets(line, sizeof(line), in)) {
|
||||
char field[128];
|
||||
double value;
|
||||
// Every line of one of these is `"name": number`, with or without a trailing comma.
|
||||
if (sscanf(line, " \"%127[^\"]\" : %lf", field, &value) != 2) continue;
|
||||
int found = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (strcmp(field, mappings[i].field) == 0) {
|
||||
values[i] = toByte(&mappings[i], value);
|
||||
seen[i] = 1;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) continue;
|
||||
for (int i = 0; i < IGNORED_COUNT; i++) {
|
||||
if (strcmp(field, ignored[i]) == 0) { found = 1; break; }
|
||||
}
|
||||
if (!found) {
|
||||
fprintf(stderr, "SoundPatch: %s has a field this does not know: %s\n", path, field);
|
||||
unknown++;
|
||||
}
|
||||
}
|
||||
fclose(in);
|
||||
|
||||
// A field this does not know is a patch format that has moved on, and writing a table
|
||||
// that quietly leaves it out would make a sound nobody could account for.
|
||||
if (unknown) {
|
||||
fprintf(stderr, "SoundPatch: %d unknown field%s, so nothing was written.\n",
|
||||
unknown, unknown == 1 ? "" : "s");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
||||
// The level routing is soundThing's one blind spot: it has no field for it because there
|
||||
// it is always the amplitude envelope. Said out loud here so a channel that was left set
|
||||
// some other way by a previous patch does not carry it into this one.
|
||||
count++;
|
||||
|
||||
FILE *out = stdout;
|
||||
if (argc > 3) {
|
||||
out = fopen(argv[3], "w");
|
||||
if (!out) { fprintf(stderr, "SoundPatch: cannot write %s\n", argv[3]); return 1; }
|
||||
}
|
||||
|
||||
fprintf(out, "; %s, converted from %s by SoundPatch.\n", label, path);
|
||||
fprintf(out, "; Designed in soundThing, where it can be heard. Do not edit the numbers\n");
|
||||
fprintf(out, "; here: change the patch and convert it again.\n\n");
|
||||
// Data, and no base: a table of bytes belongs wherever the program including it has got
|
||||
// to, the way every other included table does.
|
||||
fprintf(out, "#Data\n\n");
|
||||
fprintf(out, "%s:\n", label);
|
||||
fprintf(out, " 0d%-24d; how many pairs follow\n", count);
|
||||
fprintf(out, " 0x50 0x01 ; the level is the amplitude envelope\n");
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (!seen[i]) continue;
|
||||
fprintf(out, " 0x%02X 0d%-3d ; %s\n",
|
||||
mappings[i].parameter, values[i], mappings[i].note);
|
||||
}
|
||||
if (out != stdout) fclose(out);
|
||||
return 0;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
|
||||
make test
|
||||
```
|
||||
|
||||
Builds the four tools - and Voyager, where Raylib is installed - checks they compile under
|
||||
Builds the five tools - and Voyager, where Raylib is installed - checks they compile under
|
||||
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
|
||||
level of the suite, only tests that record an expected failure of the assembler.
|
||||
|
||||
@@ -45,7 +45,7 @@ level of the suite, only tests that record an expected failure of the assembler.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
The same suite with the four tools rebuilt under AddressSanitizer and
|
||||
The same suite with the five tools rebuilt under AddressSanitizer and
|
||||
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
||||
|
||||
Individual scripts can be run on their own, from anywhere:
|
||||
@@ -540,7 +540,7 @@ which stops the markers outliving the code they were about.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
Rebuilds all four tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||
Rebuilds all five tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||
under them. What it reliably catches is invalid access: reads and writes off the end of an
|
||||
array, use after free, leaks, and arithmetic the standard does not define.
|
||||
|
||||
@@ -552,7 +552,7 @@ deliberate `calloc`. The machine's Program and Data memories are static arrays,
|
||||
sanitizers neither fill nor bound-check - which is the same fact, seen from a
|
||||
different side, as the overrun blind spot below.
|
||||
|
||||
It runs everything because it used to not. It built all four tools sanitized and then ran
|
||||
It runs everything because it used to not. It built all five tools sanitized and then ran
|
||||
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
|
||||
exercised, and `native.sh` - which drives the assembler and the emulator harder than
|
||||
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk
|
||||
|
||||
@@ -25,7 +25,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -32,7 +32,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -108,7 +108,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -27,7 +27,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -15,7 +15,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -13,7 +13,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
@@ -22,7 +22,7 @@ Mode.sbx 48
|
||||
Flip.sbx 173
|
||||
Sprite.sbx 442
|
||||
Depth.sbx 672
|
||||
Lander.sbx 8388
|
||||
Lander.sbx 8412
|
||||
Pad.sbx 264
|
||||
Crash.sbx 632
|
||||
vars.script 50
|
||||
|
||||
+2
-1
@@ -146,7 +146,8 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
|
||||
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Depth.sbx" >/dev/null
|
||||
# Lander.sbx is Lunar Porter, and the biggest program on the disk. It takes the screen, the
|
||||
# mode and the map, so it is also the one that says a program can have all three back.
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/Libraries" \
|
||||
-I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Lander.asm" -o "$WORK/Lander.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Lander.sbx" >/dev/null
|
||||
# Pad.sbx says what the controllers are doing, which is the only way to tell a pad nobody
|
||||
|
||||
@@ -49,6 +49,7 @@ SRC_DIR_EMU = Source/Emulator
|
||||
SRC_DIR_ASM = Source/Assembler
|
||||
SRC_DIR_DSK = Source/DiskTool
|
||||
SRC_DIR_LINT = Source/Linter
|
||||
SRC_DIR_PATCH = Source/Patch
|
||||
OBJ_DIR = Object
|
||||
|
||||
# Source files
|
||||
@@ -63,12 +64,14 @@ VOY_SRCS = voyager.c $(MACHINE_SRCS)
|
||||
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
|
||||
DSK_SRCS = SplitDisk.c
|
||||
LINT_SRCS = Linter.c
|
||||
PATCH_SRCS = SoundPatch.c
|
||||
|
||||
EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
VOY_OBJS = $(VOY_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
DSK_OBJS = $(DSK_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
LINT_OBJS = $(LINT_SRCS:%.c=$(OBJ_DIR)/%.o) $(OBJ_DIR)/assembly.o
|
||||
PATCH_OBJS = $(PATCH_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
|
||||
# Output binary names
|
||||
EMU_TARGET = SplitBit
|
||||
@@ -76,6 +79,7 @@ VOY_TARGET = Voyager
|
||||
ASM_TARGET = Assembler
|
||||
DSK_TARGET = SplitDisk
|
||||
LINT_TARGET = SplitLint
|
||||
PATCH_TARGET = SoundPatch
|
||||
|
||||
# ---- Whether this machine can build Voyager ----
|
||||
#
|
||||
@@ -147,7 +151,7 @@ DISKTOOL = ./$(DSK_TARGET)
|
||||
|
||||
# Libraries are included by bare name, so the assembler is told where to find them. CosmOS
|
||||
# owns the filesystem library and the service names, so it is a place to look too.
|
||||
INCLUDES = -I $(PROG_DIR)/Libraries -I $(PROG_DIR)/CosmOS/Source
|
||||
INCLUDES = -I $(PROG_DIR)/Sounds -I $(PROG_DIR)/Libraries -I $(PROG_DIR)/CosmOS/Source
|
||||
|
||||
# The programs worth building. Every one lives in a directory that says what kind it is:
|
||||
# Examples/ is what you read to learn, Loader/ is the standalone loader CosmOS grew out of,
|
||||
@@ -208,7 +212,7 @@ SCRATCH_BLOCKS = 2048
|
||||
# linter, the whole test suite - has to build on a machine with no graphics library at all,
|
||||
# because a project about a small understandable CPU should not need OpenGL to run its
|
||||
# tests. Where Raylib is missing, 'make' says so once and builds everything else.
|
||||
TOOLS = $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET)
|
||||
TOOLS = $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(PATCH_TARGET)
|
||||
|
||||
# ---- What 'make' builds ----
|
||||
#
|
||||
@@ -291,6 +295,20 @@ $(DSK_TARGET): $(DSK_OBJS)
|
||||
$(LINT_TARGET): $(LINT_OBJS)
|
||||
$(CC) $(CFLAGS) -o $(LINT_TARGET) $(LINT_OBJS)
|
||||
|
||||
# ---- SoundPatch, which needs the maths library and nothing else ----
|
||||
#
|
||||
# It converts a soundThing patch into a table of bytes for the sound device. Nothing in the
|
||||
# build depends on it, and that is deliberate: soundThing lives in another repository and the
|
||||
# tables it produces are CHECKED IN, so this tree builds on a machine that has never heard of
|
||||
# it. The tool is here for when a sound is being changed.
|
||||
$(PATCH_TARGET): $(PATCH_OBJS)
|
||||
$(CC) $(CFLAGS) -o $(PATCH_TARGET) $(PATCH_OBJS) -lm
|
||||
|
||||
# Compile patch tool source files to object files
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR_PATCH)/%.c
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||
|
||||
$(OBJ_DIR)/Linter.o: $(SRC_DIR_LINT)/Linter.c
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||
@@ -311,7 +329,7 @@ $(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
||||
# disagreed with everything else about how big the struct was, and smashed its stack on every
|
||||
# run. A clean build hid it, which is why 'make sanitize' would never have found it either.
|
||||
# Tests/voyager.sh did, by failing all 115 tests that start the machine.
|
||||
-include $(EMU_OBJS:.o=.d) $(VOY_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d) $(LINT_OBJS:.o=.d)
|
||||
-include $(EMU_OBJS:.o=.d) $(VOY_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d) $(LINT_OBJS:.o=.d) $(PATCH_OBJS:.o=.d)
|
||||
|
||||
# ---- The strict build the README promises ----
|
||||
#
|
||||
@@ -425,7 +443,7 @@ clean:
|
||||
rm -rf Tests/build
|
||||
rm -rf $(PROG_BUILD)
|
||||
rm -f $(SRC_DIR_EMU)/rom.c
|
||||
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(VOY_TARGET)
|
||||
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(VOY_TARGET) $(PATCH_TARGET)
|
||||
|
||||
# Install compiled binaries
|
||||
install: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET)
|
||||
|
||||
Reference in New Issue
Block a user