Skip to content

A JVM-based kotlin application that loads an Amiga Protracker mod file, converts into a PCM audio stream, and plays it.

Notifications You must be signed in to change notification settings

mabersold/kotlin-protracker-demo

Repository files navigation

Kotlin Protracker Demo

by Mark Abersold

https://www.linkedin.com/in/markabersold/

Description

Loads and plays a ProTracker mod file, written in Kotlin and running on the JVM. This is specifically designed to play the song "Space Debris" by Captain, aka Markus Kaarlonen, which he composed in the early 1990's. It is a classic song of the Demoscene era, and I consider it to be the best song written in the Protracker mod format.

The song is included in this repository, so all you need to do is execute the application and it will play successfully. No command-line arguments or additional downloads necessary.

Note that I only implemented enough to play this specific song. Other songs may not play successfully because I did not implement every Protracker feature, such as fine tuning or the arpeggio effect. This is not intended to be an exhaustive demo of every Protracker feature: It is only intended to showcase a possible implementation of a Protracker player in Kotlin.

How to run

If you have gradle installed on your system, simply use the default build and run tasks.

gradle build
gradle run

If you do not have gradle installed, you can instead use gradlew or gradlew.bat (if you are on a Windows system) included in the root directory.

gradlew build
gradlew run

How it works

Simplified Workflow

Main ---> ProTrackerLoader.loadModule
Main <--- Module loaded from disk
Main ---> Get AudioGenerator for module

Repeat the next steps until the end of the song is reached

Main ---> AudioGenerator.generateSample
    AudioGenerator ---> ChannelAudioGenerator[].generateNextSample
        ChannelAudioGenerator ---> Resampler.getInterpolatedSample
        ChannelAudioGenerator <--- returns interpolated sample
    AudioGenerator <--- returns sample adjusted for volume and stereo panning
Main <--- Returns mixed samples
Main ---> AudioPlayer.playAudio
    AudioPlayer ---> Sends sampled audio to output device

Loader

The loader is responsible for loading the mod file from disk into memory. It is stored in the data classes contianed in the model package. Note that it only will be able to successfully load a ProTracker mod with the identifier "M.K." - any other variety of mod file will not likely be loaded or played successfully.

Audio generators

The pcm package contains the classes that do the bulk of the work: rendering the module to a PCM audio stream. There are three classes in this package, each with a different responsibility.

AudioGenerator: The main controller. Has four channel audio generators. While it does not actually do any of the audio rendering, its role is to maintain the overall state of the audio generators. It is responsible for keeping track of the position in the song, sending data to the channel audio generators, applying global effects, and making sure channel effects are applied at the appropriate times.

ChannelAudioGenerator: Manages the state of individual channels. Each instance has a resampler instance. Is aware of the state of the current channel, but is unaware of the song position, or of the content of the other channel audio generators. Only has information sent to it from the main AudioGenerator. Responsible for the pitch, volume, and effects to be applied to the currently playing instrument. Only applies effects when instructed to by the main audio generator (except vibrato, which is applied when generating a sample).

Resampler: Responsible for producing the individual samples of the PCM stream. Only aware of the instrument it is supposed to play, and how quickly it is supposed to step through the instrument's audio data. Only has two externally facing functions: getInterpolatedSample (to generate the next sample) and recalculateStep (needs to be called every time the pitch changes so that the step is accurate).

Player

The AudioPlayer class is the smallest: All it does is accept a ByteArray of samples (which are generated by the classes in the pcm package), and sends those samples to the audio device.

Resampling algorithm

Semantic disclaimer: Because the word "sample" has more than one meaning in this context, I chose to only use sample to refer to the individual numbers within a PCM audio stream or collection. "Sample" is typically also used to refer to the individual instruments within a module, but in this documentation and in my code, I either refer to them as "instruments" or "audio data."

The heart of any mod player is in the resampling algorithm, as it is required to be able to play the instrument audio data at varying pitches.

The audio data for the instruments is a simple collection of signed bytes. All Protracker audio data is 8-bit. A note command in a Protracker mod has several pieces of information, including the pitch and the instrument number. The instrument number is simply used to determine the correct audio data to play. The pitch tells us how we should modify the waveform's period for playback. From this information, we should be able to derive how to resample the audio data.

In my implementation, I convert all the sample data for the instruments into floating point and keep the values within a range if -1.0 to 1.0. All the resampling is done in floating point. I convert it to a signed short (16-bit) before sending it to audio output - in some sound systems this is not necessary and can be left as floating point, but in my implementation it's needed because the Java audio system doesn't allow me to select PCM_FLOAT encoding.

What is Interpolation?

Most, if not all ProTracker instruments store audio data at a much higher frequency than it is to actually be played. This means we will need to down-sample the audio data and interpolate values in between the original samples. For example, let's say we have the following two samples in our original PCM data:

10 18

Let's also say we need to downsample to the point that there are three additional samples in between the original samples. A poorly implemented algorithm might do this with no interpolation, like this:

10 10 10 10 18

But a better algorithm might do linear interpolation:

10 12 14 16 18

My implementation

My algorithm aims to do linear interpolation. Here's how it works. First, we perform a calculation to determine the number of samples per second for a given pitch with the following formula:

samplesPerSecond = 7093789.2 / (pitch * 2)

(7093789.2 is the clock rate of a PAL Amiga computer - we could also implement this with the NTSC clock rate, which I could include as an option later)

The instrument audio data is an array of bytes. Typically, we start from the beginning of the audio data. Let's say that's at array index 0. Each time we generate a sample, we will step through the array. We will need to continually add a step value to a counter to track our position. In this example, we initialize the counter at our index. We calculate our step value with the following formula:

step = samplesPerSecond / 44100

44100 is our sampling rate. So now as we generate samples, we continually add the step value to our counter. We then retrieve the sample from our audio data simply by doing this:

sample = audioData[floor(counter)]

Now, this will not interpolate correctly - if our sample at index 0 is 10, and our step is 0.25, we will just get four values of ten. We need to also calculate our slope and interpolate properly. The formula is:

rise = nextSample - currentSample
stepsPassed = floor((counter - floor(counter)) / step)
stepsRemaining = floor((floor(counter) + 1 - counter) / step)
run = stepsRemaining + stepsPassed + 1
slope = rise / run

Finally, we can use the slope to determine our interpolated sample.

interpolatedSample = sample + (slope * stepsPassed)

You can see the implementation of this in ChannelAudioGenerator.getInterpolatedSample.

I realize there may be better ways to resample but this is how I'm implementing it for the purposes of this demo, and so far it seems to work.

About

A JVM-based kotlin application that loads an Amiga Protracker mod file, converts into a PCM audio stream, and plays it.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages