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"
}}}
NOTE:ROWSplays a note for that number of rows. Examples of notes areC4(MIDI note 60),F#3andBb5.-:ROWSis a rest.^:ROWSholds the current sound.vN,dNandfNset the volume, decay and floor of the track, 0-15.- A note starts at the volume. It decreases by one level every
decayticks, until it reaches the floor. Thus repeated notes stay separate. A decay of 0 keeps the volume. - All tracks must have the same number of order positions and the same total number of rows.
loopis the order position where a looped song starts again. - A row lasts
100 * 60 / (bpm * rows_per_beat)ticks, 1-100. The value has 8.8 fixed-point format, so fractional rows keep the tempo.
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
- Bar numbers start at 0 at the start of the file. The range of bars must have one tempo and one meter.
channelis the MIDI channel, 1-16.slot0 takes the highest note that starts on a row.slot1 takes the next lower note, and so on.- A track plays one note at a time. A later note stops an earlier note.
transposemoves a track up or down by semitones. - The arranger rounds notes to the nearest row. It moves notes by octaves into the AY range. A note that continues across a bar line becomes a hold. Identical bars become one pattern.
- For each channel, the report gives the number of notes that no track uses.
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 */
- The compiler has a small English vocabulary: the number words and fifty other words, for example READY, POWER, GAME, OVER, SCORE and RECORD. An unknown word stops the build, and the message asks for a pronunciation. The compiler is not a general text-to-speech system.
- Pronunciations use the phoneme names
PA IY Y AY IH EH AE AA AO OW OY UW UH ER R L W B D G P T K CH J TH DH Z S ZH SH V F HH M N NG.AWuses the same sound asAO. Listen to each pronunciation on the hardware. - Digits in a phrase become number words. At run time,
ti_speech_numbersays numbers. It needs the thirty number words in the bank.--no-numbersremoves them and saves memory. --previewprints the phonemes and an estimated length of each phrase. It does not make sound.
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.