Music, effects and speech

Start the driver for the card that is installed. Divide its voices between music and effects, so that one effect table works with six or twelve voices. Write or arrange music, and add spoken lines with captions.

Start the driver

A game cannot ask the card for its personality. Thus let the player choose, as the STARFALL title screen does with M and P. A request for a Phasor on a card that operates as a Mockingboard also works. The driver finds that the second pair of chips is missing, and it serves six voices.

static uint8_t music_voices;

static ti_error start_audio(uint8_t personality)
{
    ti_audio_config config;
    ti_error e;
    config.personality = personality;   /* TI_AUDIO_MOCKINGBOARD or TI_AUDIO_PHASOR */
    config.speech = 1;
    config.ducking = 1;                 /* music three steps quieter during speech */
    config.via_hz = 0;                  /* nominal 1 MHz VIA clock */
    config.voices = 0;                  /* all voices of the card */
    e = ti_audio_init(&config);
    if (e) return e;
    /* Eight song tracks on a Phasor; two on a Mockingboard. */
    music_voices = ti_audio_voices() == 12 ? 8 : 2;
    return TI_OK;
}

A game that needs few voices sets voices. The starter sets 3, which is one AY chip. Each update then compares and writes only that chip.

Divide the voices between music and effects

ti_audio_reserve takes a mask of music voices. The other voices play effects. Use ti_audio_effect_voice to number the effect voices. It counts from the first voice that the music does not use.

On a Mockingboard with two music voices, effect voice 0 is voice 2. On a Phasor with eight music voices, effect voice 0 is voice 8. Thus one table works on the two cards.

static void reserve_music(void)
{
    ti_audio_reserve((1U << music_voices) - 1);   /* 0x03 or 0xFF */
}

/* One row per game event: period, effect voice, volume, ticks, priority. */
static const uint16_t sfx_period[4]   = {150, 950, 1700, 2000};
static const uint8_t  sfx_voice[4]    = {0, 1, 2, 3};
static const uint8_t  sfx_volume[4]   = {7, 12, 15, 15};
static const uint8_t  sfx_ticks[4]    = {3, 10, 20, 30};
static const uint8_t  sfx_priority[4] = {1, 2, 4, 5};

static void sound(uint8_t event)
{
    ti_audio_effect(ti_audio_effect_voice(sfx_voice[event]), sfx_period[event],
                    sfx_volume[event], sfx_ticks[event], sfx_priority[event]);
}

An effect replaces the effect on its voice only if its priority is equal or higher. Thus a shot cannot stop an explosion. An effect stops automatically after its duration in ticks. On a Mockingboard with two music voices, the four effect voices are 2-5.

Event scores

An event score is a list of tone changes. It is easy to write by hand for short tunes. In JSON:

{
  "voices": 6,
  "events": [
    {"duration": 0,  "period": 956,  "voice": 0, "volume": 7},
    {"duration": 25, "period": 1517, "voice": 1, "volume": 5},
    {"duration": 25, "period": 851,  "voice": 0, "volume": 7}
  ]
}
python tools/music.py jingle.json --output build/jingle.h --symbol jingle --report build/jingle.json
#include "jingle.h"      /* jingle[], JINGLE_COUNT */
ti_music_start(jingle, JINGLE_COUNT, 0);

Each event sets one voice and then waits duration ticks. A duration of 0 lets the next event change a voice at the same time.

Periods are AY periods for the nominal 1 MHz clock. The frequency is 1,000,000 / (16 × period) Hz. Thus 956 gives approximately 65 Hz, which is C2.

The compiler accepts 1-512 events. The total duration must be more than zero. The compiler rejects 32 events in sequence with a duration of 0, because one update cannot play them.

Pattern songs

A pattern song is much smaller than an event score for long music. Each track plays a sequence of patterns, and the song stores each repeated bar one time. Write the song in JSON:

{"song": {"bpm": 152, "rows_per_beat": 8, "loop": 0,
  "tracks": [
    {"name": "lead", "volume": 12, "decay": 4, "floor": 8, "order": ["a", "a", "b"]},
    {"name": "bass", "volume": 12, "decay": 2, "floor": 6, "order": ["c", "c", "c"]}
  ],
  "patterns": {
    "a": "D#5:2 F#5:2 -:4 G#5:8",
    "b": "v10 B5:12 ^:4",
    "c": "D#3:8 D#3:8"
  }}}
python tools/music.py song.json --preview
python tools/music.py song.json --output build/song.h --symbol song --report build/song.json
#include "song.h"        /* song[], SONG_SIZE */
ti_audio_reserve(0x03);   /* track 0 on voice 0, track 1 on voice 1 */
ti_song_start(song, SONG_SIZE, 1);

Track i plays on the ith reserved voice. Put the most important tracks first. A Mockingboard game that reserves two voices plays only the first two tracks of a song for a Phasor.

The song player has 1,120 bytes of code and 195 bytes of state. In the full-game layout, most of the player runs from low memory.

Arrange a MIDI file

tools/midi.py converts a Standard MIDI File into a song document. An arrangement file selects the bars. It gives each track one MIDI channel and one note of a chord:

{
  "credit": "Music arranged from tune.mid, Copyright (C) its author",
  "bars": [26, 49], "loop_bar": 26, "rows_per_beat": 8,
  "tracks": [
    {"name": "lead", "channel": 4, "slot": 0, "volume": 11, "decay": 4, "floor": 7},
    {"name": "bass", "channel": 8, "slot": 0, "volume": 12, "decay": 2, "floor": 6},
    {"name": "gtr_hi", "channel": 2, "slot": 0, "volume": 8, "decay": 1, "floor": 3},
    {"name": "gtr_lo", "channel": 2, "slot": 1, "volume": 9, "decay": 1, "floor": 3}
  ]
}
python tools/midi.py tune.mid arrangement.json --output song.json

The build compiles the song document, not the MIDI file. Thus the repository can keep an arrangement without the original file. The generated header contains the credit line. Read the terms of the music before you release an arrangement. The STARFALL notes in assets/starfall/music/README.md give an example.

Hardware envelopes

ti_audio_envelope gives the AY envelope to reserved music voices, for bells, sweeps and buzzes. The three voices of a chip use one envelope. Thus a call must include each voice of that chip that already uses the envelope.

On a Mockingboard with speech, use voices 3-5. The speech chip uses the port of the first AY chip and can reset it.

Speech

Write the phrases as text in JSON. Add pronunciations for words that the compiler does not know:

{
  "phrases": {
    "SAY_READY": "Ready.",
    "SAY_POWER": "Power up!",
    "SAY_OVER": "Game over."
  },
  "pronunciations": {
    "UP": ["UH", "P"],
    "GAME": ["G", "AY", "M"]
  }
}
python tools/speech.py speech.json --preview
python tools/speech.py speech.json --output build/my_speech.h --symbol my_speech

The header defines each phrase name as its number. It also defines a bank with the name that --symbol gives. Bind the bank one time after ti_audio_init. Then say phrases by name:

#include "my_speech.h"
ti_speech_bind(&my_speech);
ti_speech_say(SAY_READY, TI_SPEECH_INTERRUPT | 2);   /* immediately, over less important speech */
if (!ti_speech_busy()) ti_speech_say(SAY_POWER, 1);   /* only if no other phrase plays */

Show each spoken line as a caption also. The speech chip can be missing or silent. In standalone ONE//e mode, speech needs Appletini firmware F1.3.2 or later. STARFALL shows a caption for each line. When the driver refuses the repair phrase, STARFALL shows HULL REPAIRED.

The clock of the music

Durations, rows and effect lengths are in device ticks, 100 a second with the default via_hz. If the VIA clock of a card has a different rate, set via_hz to that rate. A clock gap stops the music. After a pause, start the music again from the first bar, as STARFALL does.