Audio

#include "libtini/audio.h"

One driver controls the slot-4 Mockingboard or Phasor. It gives the device clock, music, sound effects and AY envelopes. It also plays SSI-263 phrases, which the speech page describes.

FunctionPurpose
ti_audio_init, ti_audio_shutdownGet and release the card
ti_audio_updateAdvance music, effects and speech; call it frequently
ti_audio_ticksThe device clock, nominally 100 ticks each second
ti_audio_voices, ti_audio_clock_valid, ti_audio_source_hz, ti_audio_tick_divisorThe hardware that the driver found, and its clock settings
ti_audio_mark_gap, ti_audio_resume_clockEnclose work that blocks and stops interrupts
ti_audio_reserve, ti_audio_effect_voiceDivide the voices between music and effects
ti_music_start, ti_song_start, ti_music_stopPlay an event score or a pattern song
ti_audio_effectPlay a tone on an effect voice, by priority
ti_audio_envelope, ti_audio_envelope_stopUse the AY hardware envelope on music voices

Ownership

ti_audio_init gets exclusive control of the slot-4 card: the two 6522 VIAs and their timers, all AY chips and the two SSI-263 sockets. Do not start it while a different driver uses the card. Do not change the personality or slot of the card while the driver runs.

The driver installs a ProDOS interrupt handler with the standard MLI ALLOC_INTERRUPT call. ti_audio_shutdown removes it with DEALLOC_INTERRUPT. Call ti_audio_shutdown before other software uses the card. Audio does not need ti_init.

The interrupt handler is in assembly. It obeys the ProDOS rules: CLD first, and carry clear when it accepts the interrupt. It counts clock ticks, records Phasor speech completion and acknowledges the timer. It never calls C, uses zero page, maps memory or calls ProDOS. All other work occurs in ti_audio_update, in the foreground.

Voices and the clock

A Mockingboard has two AY chips with six voices. Voices 0-2 are on the first chip and voices 3-5 are on the second chip. A Phasor in native mode has four chips with twelve voices. Voice v is channel v mod 3 of chip v / 3.

A program can request a Phasor on a card where the second chip pair is an alias of the first pair. Then the driver uses six Mockingboard voices. ti_audio_voices tells which mode is active.

Tone periods are AY periods for a nominal 1 MHz base clock, 0-2047. In native Phasor mode, the driver doubles the periods. Thus the pitch does not change.

VIA-B timer 1 runs freely at the clock rate of the card, independent of the CPU. It causes an interrupt nominally 100 times each second. The handler counts these ticks in 32 bits. The actual rate is ti_audio_source_hz() / ti_audio_tick_divisor(). The default configuration uses a 1,000,000 Hz source and a divisor of 10,000. To use a calibrated source, set it in via_hz. On the development emulator, the Apple bus runs at approximately 1.020484 MHz. There the default gives approximately 102 ticks for each emulated second.

Constants and types

NameValueMeaning
TI_AUDIO_MOCKINGBOARD1Personality: six voices
TI_AUDIO_PHASOR2Personality: native Phasor, twelve voices when present
TI_AUDIO_TICKS_PER_SECOND100Nominal rate of the device clock

ti_audio_config

typedef struct ti_audio_config {
    uint8_t personality, speech, ducking;
    uint32_t via_hz;   /* 0 assumes nominal 1 MHz; calibrate on the board. */
    uint8_t voices;    /* 0 uses every voice */
} ti_audio_config;
personality
TI_AUDIO_MOCKINGBOARD or TI_AUDIO_PHASOR.
speech
Nonzero to control the SSI-263. With 0, speech calls return TI_E_UNAVAILABLE.
ducking
Nonzero to decrease the music volume by three steps while speech plays. This also mutes envelope voices. Effects keep their volume.
via_hz
The VIA clock in hertz, 100,000 to 4,000,000. A value of 0 means 1,000,000.
voices
The number of voices that the driver serves, rounded up to full chips of three voices. Init silences all chips. After init, each update compares and writes only the chips of these voices, which decreases the time of each update. Voices above the limit are out of range. A value of 0 uses all six or twelve voices.
/* Mockingboard, speech with music ducking, nominal clock, all six voices. */
static ti_audio_config audio = {TI_AUDIO_MOCKINGBOARD, 1, 1, 0, 0};

ti_music_event

typedef struct ti_music_event { uint16_t duration, period; uint8_t voice, volume; } ti_music_event;

One step of an event score. It sets voice to tone period (0-2047) at volume (0-15). Then the player waits duration device ticks before the next event. A duration of 0 lets several voices change at the same time. tools/music.py compiles a JSON score into an array of these events.

Start and stop

ti_audio_init

ti_error ti_audio_init(const ti_audio_config *config);

This function first checks that no VIA interrupt is enabled, because an enabled interrupt shows that a different driver owns the card. It also checks that a VIA answers. Then it does these steps:

  • It saves the VIA port, direction and control registers.
  • It installs the interrupt handler and selects the personality.
  • It silences all chips.
  • When the configuration enables speech, it sets preset 0, pitch 0x40 and rate 10. The speech chip stays powered down until the first phoneme.
  • It starts the clock at tick 0. The default music reservation is voices 0-2.

During a video session, the function lends the AUX workspace of ProDOS back to ProDOS while it installs the handler. For the duration of the call, this needs a free slot and 32,768 resource bytes.

Returns

TI_OKAudio and the clock run.
TI_E_STATEAudio is already initialized.
TI_E_ARGUMENTThe function cannot read the configuration, or the personality is not known.
TI_E_RANGEvia_hz is outside 100,000-4,000,000.
TI_E_BUSYA VIA interrupt is enabled: a different driver owns the card.
TI_E_UNAVAILABLENo VIA answered, or ProDOS did not accept the interrupt handler.
TI_E_NOMEMDuring a video session, no slot or space is available for the temporary loan.

ti_audio_shutdown

ti_error ti_audio_shutdown(void);

This function stops speech and music, silences all voices and powers down the speech chips. It stops the clock and removes the interrupt handler. Then it sets the Mockingboard personality and restores the saved VIA registers. Without active audio, it returns TI_OK. ti_shutdown calls this function.

Returns

TI_OK; TI_E_IO when ProDOS does not remove the handler; TI_E_NOMEM during a video session when no space is available for the loan. After TI_E_IO, the hardware is silent and its interrupts are off, but the driver keeps control and its code. The only permitted audio call is then a new ti_audio_shutdown. After TI_E_NOMEM, the driver keeps control of the card.

ti_audio_update

ti_error ti_audio_update(void);

This function is the foreground service of the driver. It ends expired effects and plays all score events and song rows that are due. It sends a maximum of one speech phoneme. Then it writes the AY registers that changed.

When no device tick occurred since the last call and nothing requested a write, the function writes nothing. Thus frequent calls cost little time. Call it one time in each tick or more to keep the music regular. During a long draw, call it from the service function of a scene, or between draw calls.

Returns

TI_OKThe service is complete.
TI_E_STATEAudio is not initialized.
TI_E_TIMINGMore than 100 ticks occurred since the last update, or more than 32 score events or song rows were due at the same time. The driver then marks a gap and stops music and speech. The function also returns this code for all calls while the clock is not valid, until ti_audio_resume_clock. It also returns this code one time when a speech phoneme did not report completion in two seconds; see speech.

ti_audio_ticks

uint32_t ti_audio_ticks(void);

This function returns the number of device ticks since ti_audio_init. It reads the count atomically. It is a ti_tick_source for ti_steps_begin and ti_gfx_budget. The count does not increase during a declared gap.

ti_audio_voices

uint8_t ti_audio_voices(void);

This function returns the number of voices that the driver serves. This is 12 on a native Phasor, and 6 on a Mockingboard or on a Phasor that answers as a Mockingboard. With a voices limit, it is the rounded limit. Without active audio, it is 0.

ti_audio_clock_valid

uint8_t ti_audio_clock_valid(void);

This function returns 1 while audio runs and no gap made the clock not valid since the last resume. A game uses it to find when to pause.

ti_audio_source_hz, ti_audio_tick_divisor

uint32_t ti_audio_source_hz(void);
uint16_t ti_audio_tick_divisor(void);

The first function returns the VIA clock rate that the driver uses. The second function returns the number of VIA clocks in one tick, which is the timer latch plus 2. Calibrated seconds are ticks * divisor / source_hz. Use 32-bit arithmetic. Without active audio, the two functions return 0.

Gaps

A VIA can record one missed timer period, but not more. Some work keeps interrupts masked for more than one tick, or blocks in slot firmware. During that work, the driver loses ticks and cannot know it. The file and AMEM calls of libtini enclose their work in these two functions. A game must do the same around other blocking work.

ti_audio_mark_gap

void ti_audio_mark_gap(void);

This function makes the clock not valid and stops the timer interrupt. It powers down speech and cancels the queued phrases. It cancels effects and mutes all voices. During the pause, the interrupt handler declines interrupts before it touches slot I/O. Without active audio, the function only marks a display-timing gap.

ti_audio_resume_clock

ti_error ti_audio_resume_clock(void);

This function starts the timer again for a full period and does not clear the count. It moves the music and speech deadlines to the current tick and restores the music volumes. The ticks did not increase during the gap, thus they do not measure its length. Also call ti_steps_reset for a fixed-step loop.

Returns

TI_OK, or TI_E_STATE without active audio.

Music voices and effect voices

ti_audio_reserve

ti_error ti_audio_reserve(uint16_t music_voices);

This function sets the music voices as a bit mask, with bit v for voice v. The other voices are effect voices. Scores can play only on music voices, and effects only on effect voices. After init, the mask is 7: voices 0-2.

Returns

TI_OK; TI_E_STATE without audio; TI_E_RANGE for a bit at or above ti_audio_voices(); TI_E_BUSY when the change removes a voice that the score or an envelope uses, or reserves a voice that plays an effect. Stop these users first.

ti_audio_effect_voice

uint8_t ti_audio_effect_voice(uint8_t n);

This function returns the voice number of effect voice n, counted from 0. When fewer effect voices are available, it returns 255, which ti_audio_effect rejects.

With music on voices 0-1 of a Mockingboard, effect voice 0 is voice 2. With music on voices 0-7 of a Phasor, effect voice 0 is voice 8. Thus one table of effect voices is correct for the two cards.

Music

There are two types of score. An event score is an array of ti_music_event. It is easy to write by hand. A pattern song repeats bars, as tracker music does. For real music it is much smaller. tools/music.py compiles a song from JSON, and tools/midi.py makes a song from a MIDI file.

The two players are separate archive modules. A program links only the player that it starts. The player uses the score data in near read-only memory until the score stops, a new score starts or audio stops.

ti_music_start

ti_error ti_music_start(const ti_music_event *events, uint16_t count, uint8_t loop);

This function stops the current score and starts to play events immediately. When loop is nonzero, the score starts again at the first event after the last event. The function checks all events first.

Returns

TI_OK; TI_E_STATE without audio; TI_E_ARGUMENT for a count of 0 or more than 512, or for events that are not readable near memory; TI_E_RANGE for an event with a voice out of range or not reserved, a volume above 15 or a period above 2047.

ti_song_start

ti_error ti_song_start(const uint8_t *song, uint16_t size, uint8_t loop);

This function plays a pattern song from tools/music.py. Track i plays on reserved voice i, counted from voice 0 when the song starts. The function does not play tracks above the number of reserved voices. Thus a Mockingboard can play the first tracks of a song for a Phasor. When loop is 0, the song stops after its last order position. Otherwise it goes back to its loop position.

The function checks the full song before it stops the current score. It checks the header ranges, the table limits, each order entry and the bytes of each pattern. When the song is not valid, the old score continues. Data formats describes the bytes.

Returns

TI_OK; TI_E_STATE without audio; TI_E_ARGUMENT for a song shorter than 8 bytes or outside readable near memory; TI_E_RANGE when no voice is reserved or the song is not valid.

#include "starfall_music.h"            /* sf_song[], SF_SONG_SIZE from tools/music.py */

/* Eight tracks on a Phasor; only the lead and bass on a Mockingboard. */
ti_audio_reserve(ti_audio_voices() == 12 ? 0xFF : 0x03);
ti_song_start(sf_song, SF_SONG_SIZE, 1);

ti_music_stop

void ti_music_stop(void);

This function stops the two types of score and silences their voices. It keeps the reservation.

Effects

ti_audio_effect

ti_error ti_audio_effect(uint8_t voice, uint16_t period, uint8_t volume,
                         uint16_t duration, uint8_t priority);

This function plays a tone on an effect voice immediately, for duration device ticks. A new effect replaces the current effect of the voice when its priority is the same or higher.

Returns

TI_OK; TI_E_STATE without audio; TI_E_RANGE for a voice out of range (255 from ti_audio_effect_voice included), a period above 2047, a volume above 15, or a zero duration or priority; TI_E_BUSY when the voice is a music voice or plays an effect of higher priority.

/* A short laser on the first effect voice, priority 2. */
ti_audio_effect(ti_audio_effect_voice(0), 150, 12, 3, 2);

Hardware envelopes

Each AY chip has one envelope generator, and its three voices share it. The envelope functions control it on music voices. They are a separate archive module, which the linker adds only when a program calls them.

ti_audio_envelope

ti_error ti_audio_envelope(uint16_t voices, uint16_t period, uint8_t shape);

This function sets the voices in the mask to envelope amplitude. It writes the period and shape to each chip of these voices. Each call writes registers 11, 12 and 13, also when the shape does not change, because a write to register 13 starts the envelope again. Ducking mutes envelope voices during speech, and the envelope continues.

Returns

TI_OK; TI_E_STATE without audio; TI_E_RANGE for an empty mask or a mask out of range, a zero period, a shape above 15, or a period above 32,767 on a native Phasor (the driver doubles it); TI_E_BUSY for these conditions:

  • A voice in the mask is not a music voice.
  • The call changes a chip but does not include a voice that already uses the envelope of that chip.
  • The mask contains voices 0-2 on a Mockingboard with speech enabled. The SSI shares the port of the first chip and can reset it. Use voices 3-5 for envelopes there.

ti_audio_envelope_stop

ti_error ti_audio_envelope_stop(uint16_t voices);

This function sets the voices in the mask back to their score volume.

Returns

TI_OK; TI_E_STATE without audio; TI_E_RANGE for a mask bit out of range.