198 lines
6.5 KiB
C
198 lines
6.5 KiB
C
|
|
/**
|
||
|
|
* @file audio_tables.c
|
||
|
|
* @brief The SOUND, ENVELOPE and PLAY conversions, laid out as the tables they are.
|
||
|
|
*
|
||
|
|
* BASIC 7.0's sound verbs speak in SID register values: SOUND takes a frequency
|
||
|
|
* *register*, not a pitch, and ENVELOPE takes the SID's 0-15 rate numbers, whose
|
||
|
|
* real durations are tabulated in silicon rather than computed. The backend takes
|
||
|
|
* hertz and milliseconds. These are the conversions between the two, and they are
|
||
|
|
* kept in one file because each is somewhere a wrong constant produces a
|
||
|
|
* plausible wrong pitch rather than an error somebody would notice.
|
||
|
|
*
|
||
|
|
* Two of the three are transcriptions. The third -- TEMPO -- is a calibration
|
||
|
|
* choice and is labelled as one where it is made.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <math.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akbasic/audio.h>
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The SID's attack rates, register value to milliseconds.
|
||
|
|
*
|
||
|
|
* Non-linear, and not a curve anybody would guess: the steps are small at the
|
||
|
|
* bottom and enormous at the top. This is the 6581/8580 table.
|
||
|
|
*/
|
||
|
|
static const int ATTACK_MS[16] = {
|
||
|
|
2, /* 0 */
|
||
|
|
8, /* 1 */
|
||
|
|
16, /* 2 */
|
||
|
|
24, /* 3 */
|
||
|
|
38, /* 4 */
|
||
|
|
56, /* 5 */
|
||
|
|
68, /* 6 */
|
||
|
|
80, /* 7 */
|
||
|
|
100, /* 8 */
|
||
|
|
250, /* 9 */
|
||
|
|
500, /* 10 */
|
||
|
|
800, /* 11 */
|
||
|
|
1000, /* 12 */
|
||
|
|
3000, /* 13 */
|
||
|
|
5000, /* 14 */
|
||
|
|
8000 /* 15 */
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The SID's decay and release rates, register value to milliseconds.
|
||
|
|
*
|
||
|
|
* Exactly three times the attack table -- that is a property of the chip's
|
||
|
|
* envelope generator, not a coincidence, so it is written out rather than
|
||
|
|
* computed. A reader checking this against a datasheet should see the datasheet's
|
||
|
|
* numbers.
|
||
|
|
*/
|
||
|
|
static const int DECAY_MS[16] = {
|
||
|
|
6, /* 0 */
|
||
|
|
24, /* 1 */
|
||
|
|
48, /* 2 */
|
||
|
|
72, /* 3 */
|
||
|
|
114, /* 4 */
|
||
|
|
168, /* 5 */
|
||
|
|
204, /* 6 */
|
||
|
|
240, /* 7 */
|
||
|
|
300, /* 8 */
|
||
|
|
750, /* 9 */
|
||
|
|
1500, /* 10 */
|
||
|
|
2400, /* 11 */
|
||
|
|
3000, /* 12 */
|
||
|
|
9000, /* 13 */
|
||
|
|
15000, /* 14 */
|
||
|
|
24000 /* 15 */
|
||
|
|
};
|
||
|
|
|
||
|
|
/*
|
||
|
|
* SOUND's frequency register to hertz.
|
||
|
|
*
|
||
|
|
* The SID computes Fout = (Fn * Fclk) / 2^24. Fclk is the system clock, which is
|
||
|
|
* 1022730 Hz on an NTSC machine and 985248 on a PAL one -- so the same listing
|
||
|
|
* plays very slightly sharp or flat depending on which side of the Atlantic it
|
||
|
|
* was typed on. NTSC is used here; the difference is about 0.6%, well under a
|
||
|
|
* semitone, and picking one is unavoidable.
|
||
|
|
*/
|
||
|
|
#define SID_CLOCK_HZ 1022730.0
|
||
|
|
#define SID_ACCUMULATOR 16777216.0
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A whole note at TEMPO 1, in milliseconds.
|
||
|
|
*
|
||
|
|
* This one is a **calibration choice, not a transcription.** BASIC 7.0 documents
|
||
|
|
* TEMPO as a relative duration from 1 to 255 with a default of 8 and does not
|
||
|
|
* publish the constant. 16000 puts a default-tempo quarter note at 500 ms, which
|
||
|
|
* is 120 beats per minute -- a moderate tempo, which is what a default should be.
|
||
|
|
* If somebody turns up the real constant, this is the one line to change.
|
||
|
|
*/
|
||
|
|
#define WHOLE_NOTE_AT_TEMPO_1 16000
|
||
|
|
|
||
|
|
/** @brief Concert pitch, and the note the equal-tempered scale is derived from. */
|
||
|
|
#define A4_HZ 440.0
|
||
|
|
/** @brief Semitone offset of A within an octave that starts at C. */
|
||
|
|
#define A_SEMITONE 9
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_audio_register_to_hz(int value, double *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL destination in register_to_hz");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (value >= 0 && value <= 65535), AKBASIC_ERR_BOUNDS,
|
||
|
|
"SOUND frequency %d out of range (0 to 65535)", value);
|
||
|
|
*dest = ((double)value * SID_CLOCK_HZ) / SID_ACCUMULATOR;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_audio_rate_to_ms(int rate, bool isattack, int *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL destination in rate_to_ms");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (rate >= 0 && rate <= 15), AKBASIC_ERR_BOUNDS,
|
||
|
|
"ENVELOPE rate %d out of range (0 to 15)", rate);
|
||
|
|
*dest = isattack ? ATTACK_MS[rate] : DECAY_MS[rate];
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_audio_note_hz(int semitone, int octave, double *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int steps = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL destination in note_hz");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (semitone >= 0 && semitone <= 11), AKBASIC_ERR_BOUNDS,
|
||
|
|
"Note %d out of range (0 to 11)", semitone);
|
||
|
|
FAIL_ZERO_RETURN(errctx, (octave >= 0 && octave <= 6), AKBASIC_ERR_BOUNDS,
|
||
|
|
"PLAY octave %d out of range (0 to 6)", octave);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Equal temperament from A4. Computed rather than tabulated because a table
|
||
|
|
* of 84 frequencies is 84 chances to fat-finger one, and this is exact to
|
||
|
|
* the precision anybody can hear.
|
||
|
|
*/
|
||
|
|
steps = ((octave - 4) * 12) + (semitone - A_SEMITONE);
|
||
|
|
*dest = A4_HZ * pow(2.0, (double)steps / 12.0);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_audio_whole_note_ms(int tempo, int *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL destination in whole_note_ms");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (tempo >= AKBASIC_TEMPO_MIN && tempo <= AKBASIC_TEMPO_MAX),
|
||
|
|
AKBASIC_ERR_BOUNDS, "TEMPO %d out of range (%d to %d)",
|
||
|
|
tempo, AKBASIC_TEMPO_MIN, AKBASIC_TEMPO_MAX);
|
||
|
|
*dest = WHOLE_NOTE_AT_TEMPO_1 / tempo;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_audio_state_init(akbasic_AudioState *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int i = 0;
|
||
|
|
int wholems = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL audio state in init");
|
||
|
|
|
||
|
|
obj->head = 0;
|
||
|
|
obj->count = 0;
|
||
|
|
obj->nextms = 0;
|
||
|
|
obj->sounding = false;
|
||
|
|
obj->tempo = AKBASIC_TEMPO_DEFAULT;
|
||
|
|
obj->voice = 0;
|
||
|
|
obj->octave = 4;
|
||
|
|
obj->envelope = 0;
|
||
|
|
obj->level = 1.0;
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_audio_whole_note_ms(obj->tempo, &wholems));
|
||
|
|
obj->notems = wholems / 4; /* PLAY starts on quarter notes */
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Every preset starts as a plain square wave held at full level. A zeroed
|
||
|
|
* envelope would have a sustain of 0.0, which is silence with nothing to
|
||
|
|
* explain it -- the same reasoning libakgl gives for its own voice defaults.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < AKBASIC_ENVELOPES; i++ ) {
|
||
|
|
obj->envelopes[i].attack = 0;
|
||
|
|
obj->envelopes[i].decay = 0;
|
||
|
|
obj->envelopes[i].sustain = 1.0;
|
||
|
|
obj->envelopes[i].release = 0;
|
||
|
|
obj->envelopes[i].waveform = AKBASIC_WAVE_SQUARE;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|