/** * @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 #include #include #include /** * @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); }