Protracker Player, part 2: Conducting the Orchestra

In my last post, I talked about how I approached my kotlin protracker player. The first thing I needed to figure out was how to resample and how to keep track of timing. Now that I had a basic grasp of that, I had to get it to actually play a song.

My design was fairly straightforward: I decided to create a single class to manage generating audio for the entire song. This class - AudioGenerator - would in turn have four instances of another new class, ChannelAudioGenerator, which each is responsible for generating audio for a specific channel. As you recall, ProTracker modules have four channels, meaning up to four "voices" that can be played at the same time (later module formats went beyond this: ScreamTracker supported 16 channels, FastTracker supported 32, and Impulse Tracker supported up to 64 regular channels and even more "virtual" channels).

All the main AudioGenerator needed to do was keep track of the position in the song, apply global effects, and make sure each ChannelAudioGenerator had the information it needed to generate audio for its channel. Think of the AudioGenerator as the conductor, and each ChannelAudioGenerator as the individual orchestra members.


Keeping track of position in the song wasn't too hard: You just start from zero and start counting up. When you reach the end of a row, move to the next row. When you reach the end of a pattern, move to the next position in the order list. When you reach the end of the order list, you're done.

The first important job was to make sure that each ChannelAudioGenerator has the information it needs to play a sound correctly. That's effectively three things: the pitch, the instrument, and the effect. The pitch is the simplest thing, as all it's used for is determining what frequency to resample the instrument. The instrument includes the byte array representing the instrument's audio data (aka the sample data), but also the default volume and fine tune values (though fine tuning wasn't used in Space Debris).

One quirk is that any single row could contain any combination of that data - sometimes there's nothing at all, sometimes the pitch, instrument, and effect are all set, sometimes you get a pitch value with no instrument number, sometimes you get an instrument number and effect but no pitch, etc. There's no official manual for the format, so I pretty much just had to listen to how other players handle each scenario to know what I needed to do.

Whenever the AudioGenerator encounters a new row, it sends the new row information to the channel audio generators. So, every row they can potentially get a new pitch, a new instrument, and/or a new effect. When there is a new instrument, for example, the channel needs to immediately stop playing whatever instrument was in there already, and start playing the new one.

Implementing effects also varies widely, and I didn't end up implementing them all. There are global effects, which are handled by the AudioGenerator. Those are effects such as breaking out of a pattern early, switching to a different position in the order list, and changing the tempo of the song.

The ChannelAudioGenerators handles channel-specific effects, such as volume slides, pitch slides, and vibrato. That being said, the generators still need to be "told" when to apply the effects, and some of them are applied differently from each other. For example, a fine volume slide effect is applied exactly once, once per row, at the start of the row. But a regular volume slide effect is applied once per tick, except for the first tick in the row. I added several methods to the ChannelAudioGenerator to apply effects, namely applyStartOfRowEffects and applyPerTickEffects. These are invoked by the AudioGenerator at the appropriate point - once at the start of each new row, and once per tick, respectively (by default, there are six ticks per row).

The outcome of this is that the ChannelAudioGenerators really have no idea what the position in the song is, all they know is what they're supposed to be playing. They just do what they're told.

In my next post about this project, I'll talk about specific effects, mixing, stereo panning, and putting it all together.

Comments