Sequence Player
Play MIDI-style sequenced music using the N64's audio hardware.
import "github.com/drpaneas/gosprite64/audio/sequence"
How sequences work
N64 games do not stream audio like modern platforms. Instead, music is stored as a compact sequence of note events (similar to MIDI) that drive instrument samples loaded into audio RAM. The sequence player reads these events and produces sound through the RSP audio microcode.
This approach uses very little ROM space - a full song might be only a few kilobytes of sequence data plus shared instrument samples.
Creating a player
player := sequence.NewPlayer()
NewPlayer returns a player with sensible defaults:
- Tempo: 120 BPM
- Master volume: 127 (maximum)
- All 16 channel volumes: 127
Loading sequence data
Assign raw sequence bytes to the player's Data field before calling Play:
player.Data = sequenceBytes
The sequence data is typically loaded from a ROM asset at startup.
Playback controls
Play
Starts (or restarts) playback from the beginning:
player.Play()
If the player is already playing, this resets to position 0 and starts over.
Stop
Halts playback and resets position to the beginning:
player.Stop()
Pause and Resume
Pause freezes playback at the current position. Resume continues from where it left off:
player.Pause()
// ...later...
player.Resume()
Calling Pause when not playing has no effect. Calling Resume when not paused has no effect.
IsPlaying
Returns true if the player is actively producing audio (playing and not paused):
if player.IsPlaying() {
drawMusicIcon()
}
Tempo
SetTempo controls playback speed in beats per minute:
player.SetTempo(140) // faster
player.SetTempo(80) // slower
bpm := player.Tempo() // read current tempo
The default is 120 BPM. The internal tick resolution is 48 ticks per beat.
Volume
Master volume
SetVolume sets the overall output level (0-127):
player.SetVolume(100) // slightly quieter
player.SetVolume(0) // muted
vol := player.Volume() // read current volume
Per-channel volume
SetChannelVolume controls individual channels (0-15). Useful for muting or emphasizing specific instrument parts:
player.SetChannelVolume(9, 0) // mute channel 9 (often drums)
player.SetChannelVolume(0, 80) // soften the melody channel
vol := player.ChannelVolume(0) // read channel volume
Looping
SetLoop configures loop behavior. The first parameter is the byte position to loop back to. The second is the loop count: use -1 for infinite looping, or a positive number for a fixed repeat count.
player.SetLoop(0, -1) // loop the whole song forever
player.SetLoop(1024, 3) // after reaching the end, jump to byte 1024, repeat 3 times
player.SetLoop(0, 0) // no looping (play once and stop)
Advancing playback
Call Tick once per audio frame in your game loop, passing the audio sample rate. It advances the internal position based on the current tempo and returns any note events generated during the tick:
func (g *Game) Update() {
events := g.musicPlayer.Tick(32000) // N64 typical: 32000 Hz
for _, ev := range events {
// Feed events to the audio mixer
if ev.On {
g.mixer.NoteOn(ev.Channel, ev.Note, ev.Velocity)
} else {
g.mixer.NoteOff(ev.Channel, ev.Note)
}
}
}
NoteEvent
Each event returned by Tick is a NoteEvent:
type NoteEvent struct {
Channel uint8 // MIDI channel (0-15)
Note uint8 // MIDI note number (0-127)
Velocity uint8 // Strike intensity (0-127)
On bool // true = note on, false = note off
}
Complete example
func NewGame() *Game {
g := &Game{}
g.music = sequence.NewPlayer()
g.music.Data = loadAsset("overworld.seq")
g.music.SetLoop(0, -1)
g.music.SetVolume(100)
g.music.Play()
return g
}
func (g *Game) Update() {
events := g.music.Tick(32000)
for _, ev := range events {
handleNoteEvent(ev)
}
// Pause music when game is paused
if gs.IsButtonJustPressed(gs.ButtonStart) {
if g.paused {
g.music.Resume()
} else {
g.music.Pause()
}
g.paused = !g.paused
}
}