Instrument Banks

Define and load custom instrument samples for the sequence player.

import "github.com/drpaneas/gosprite64/audio/bank"

What is an instrument bank?

An instrument bank is a collection of instrument definitions paired with raw audio sample data. When the sequence player triggers a note event, it looks up the instrument assigned to that channel, selects the appropriate sample based on the note's pitch, and plays it through the audio hardware.

This is the same approach used by classic N64 games: a compact bank of instrument definitions stored alongside compressed sample data in the ROM. One bank might contain a piano, strings, drums, and bass - everything needed for a song.

Types

Instrument

Each instrument has metadata and one or more sounds that cover different key ranges:

type Instrument struct {
    ID         uint8
    Volume     uint8    // default volume (0-127)
    Pan        uint8    // stereo panning (0=left, 64=center, 127=right)
    Priority   uint8    // voice allocation priority
    SampleRate uint16   // native sample rate in Hz
    KeyLow     uint8    // lowest MIDI note this instrument responds to
    KeyHigh    uint8    // highest MIDI note this instrument responds to
    Sounds     []Sound  // sample entries for different key ranges
}

Sound

A sound is a single audio sample within an instrument. Instruments with multiple sounds use key-splitting to play different samples depending on the note pitch:

type Sound struct {
    SampleAddr uint32   // offset into sample data
    SampleLen  uint32   // sample length in bytes
    LoopStart  uint32   // loop region start
    LoopEnd    uint32   // loop region end
    LoopCount  int32    // -1 for infinite, 0 for no loop
    KeyBase    uint8    // the note at which this sample plays at native pitch
    Tuning     float32  // fine-tuning adjustment
}

Creating a bank

Empty bank

b := bank.NewBank()

Creates a bank with no instruments. You can populate it manually by appending to b.Instruments and setting b.SampleData.

Loading from binary data

b, err := bank.LoadBank(bankData)
if err != nil {
    // handle invalid bank data
}

LoadBank parses a binary bank file and returns a fully populated Bank. The binary format stores instrument definitions followed by per-instrument sound entries, all in big-endian byte order (matching N64 hardware).

The bank.ErrInvalidBank error is returned if the data is truncated or malformed.

Querying a bank

GetInstrument

Retrieve an instrument by its zero-based index:

inst := b.GetInstrument(0)
if inst == nil {
    // index out of range
}
fmt.Printf("Instrument: rate=%d, sounds=%d\n", inst.SampleRate, len(inst.Sounds))

InstrumentCount

Returns the total number of instruments in the bank:

count := b.InstrumentCount()
for i := 0; i < count; i++ {
    inst := b.GetInstrument(uint8(i))
    fmt.Printf("[%d] keys %d-%d\n", inst.ID, inst.KeyLow, inst.KeyHigh)
}

Complete example

func NewGame() *Game {
    g := &Game{}

    // Load the instrument bank from ROM
    bankData := loadAsset("instruments.bnk")
    b, err := bank.LoadBank(bankData)
    if err != nil {
        panic("failed to load instrument bank: " + err.Error())
    }
    g.bank = b

    // Set up the sequence player
    g.music = sequence.NewPlayer()
    g.music.Data = loadAsset("overworld.seq")
    g.music.SetLoop(0, -1)
    g.music.Play()

    return g
}

Bank binary format reference

The binary format parsed by LoadBank:

FieldTypeDescription
instrumentCountu8Number of instruments

Per instrument:

FieldTypeDescription
idu8Instrument ID
volumeu8Default volume
panu8Stereo panning
priorityu8Voice priority
sampleRateu16 BENative sample rate
keyLowu8Lowest responding key
keyHighu8Highest responding key
soundCountu8Number of sounds

Per sound (28 bytes):

FieldTypeDescription
sampleAddru32 BEOffset into sample data
sampleLenu32 BESample length in bytes
loopStartu32 BELoop start offset
loopEndu32 BELoop end offset
loopCounts32 BELoop count (-1 = infinite)
keyBaseu8Note at native pitch
(padding)3 bytesAlignment padding
tuningf32 BEFine-tuning factor