Programming the MP3 controller/player: playing tracks sequentially

Skip to main content (skip navigation menu)






Playing tracks sequentially

 

The play() function of the H0420 MP3 controller/player is non-blocking. This means that the function starts play-back of the selected track and then returns immediately. It does not wait for the track to finish playing. The rationale for this design is that it allows the H0420 to execute other commands and to react to events while the track is playing.

If you wish to play a few files in sequence, the code snippet below does not work:

@reset()
    {
    play "track1.mp3"
    play "track2.mp3"
    play "track3.mp3"
    }

Instead of playing "track1.mp3", "track2.mp3" and "track3.mp3" in sequence, it plays only track 3. What happens in the code is that the @reset() function first starts play-back of track1.mp3. Then, when function play() returns, it immediately starts playing track2.mp3, thereby aborting track1.mp3. Similarly, the subsequent command to start playing track3.mp3 aborts track2.mp3. The first two tracks were started and aborted immediately thereafter. Only the last track gets a chance of actually producing audio.

What we were actually trying to do is to start playback of the next track as soon as the previous track starts. With the "event-driven" architecture of the H0420, you do so by handling the event that catches changes in the audio status. When a track starts or stops playing, the audio status changes. This in turn fires the @audiostatus() event function. In this function (if you add it to your script) you can launch the next track.

The following code snippet plays tracks in random order. The @reset() function starts playing the first track. The function @audiostatus() starts a new track is soon as the status changes to "Stopped". It is important to test for the audio status, because you would not want to launch a new track when the status changes to "Playing", for example.

@reset()
    {
    playrandom
    }

@audiostatus(AudioStat: status)
    {
    if (status == Stopped)
        playrandom
    }

playrandom()
    {
    new count = fexist("*.mp3")
    new track = random(count)

    new filename[100 char]
    fmatch filename, "*.mp3", track

    play filename
    }

All the real code in this script is in function playrandom(). That function first counts how many files there are in the root directory with the extension ".mp3". Then it chooses a pseudo-random number between zero and that file count. It uses that randomly selected track number to convert it back to a full filename, using function fmatch(). Once it has a filename, it plays it.

A queue of tracks

The above script plays tracks in random order, and this may not be what you want. If you have a short series of tracks to play in sequence, you can instead implement a "waiting queue" of tracks. The first track plays immediately, all other tracks are inserted in the waiting queue and get popped off this queue as soon as the previous track ends.

The goal is that we can create a script that contains the following:

@reset()
    {
    enqueue "track1.mp3"
    enqueue "track2.mp3"
    enqueue "track3.mp3"
    }

This function must then play the tracks in sequence. When you compare the above code to the snippet at the top of this article, you will notice that function enqueue() has replaced play(), but that the functions are otherwise identical.

The magic is in the function enqueue() and a compagnion function dequeue(). If no track is currently playing, enqueue() plays a file immediately; otherwise (if a track is already playing), it adds it to a circular queue. Then, when the track stops, @audiostatus() event function calls dequeue(), which in turn removes the first track from the queue and plays it.

Below is the implementation of the queue and the associated functions, in a complete program. The maximum number of tracks in the queue and the maximum length of a track filename can be configured.

const QueueSize = 3
const MaxName = 64

new Queue[QueueSize][MaxName char]
new QueuePos

dequeue()
    {
    if (Queue[QueuePos][0] != EOS)
        {
        play Queue[QueuePos]            /* play the file */
        Queue[QueuePos][0] = EOS        /* remove from the queue */
        QueuePos = (QueuePos + 1) % QueueSize
        }
    }

enqueue(const name[])
    {
    if (audiostatus() == Stopped)
        play name
    else
        {
        new item = QueuePos
        if (Queue[item][0] != EOS)
            {
            /* find the first available slot */
            do
                item = (item + 1) % QueueSize
            while (Queue[item][0] != EOS && item != QueuePos)
            if (item == QueuePos)
                return  /* no slot available */
            }
        strpack Queue[item], name
        }
    }

@audiostatus(AudioStat: status)
    {
    if (status == Stopped)
        dequeue         /* play until queue is empty */
    }

@reset()
    {
    enqueue !"track1.mp3"
    enqueue !"track2.mp3"
    enqueue !"track3.mp3"
    }

The above routines are suitable for short sequences of tracks. If you need to play a long sequence, it may be better to have the script walk through a playlist. This is beyond the scope of this article, but an example script for playlists comes with the software development kit of the H0420.

Minimizing the gap between tracks

The script presented so far works, but there is a gap between two consecutive tracks. How big this gap is, depends on a few factors: the speed of the CompactFlash card, and the number of files on it, for example. You can minimize the gap by preparing some of the work in playrandom()

The next script contains the above improvements. The @reset() counts the number of MP3 files, and also chooses the first track to run. In playrandom(), the order of the operations is swapped: it first plays the file whose name was determined earlier, and then chooses a new name for the next track.

new TrackCount
new Filename[100 char]

@reset()
    {
    TrackCount = fexist("*.mp3")
    fmatch Filename, "*.mp3", random(TrackCount)
    playrandom
    }

@audiostatus(AudioStat: status)
    {
    if (status == Stopped)
        playrandom
    }

playrandom()
    {
    play Filename
    fmatch Filename, "*.mp3", random(TrackCount)
    }

One more improvement is possible with version 1.6 (and later) of the firmware of the H0420 MP3 controller. When calling play(), that function must still browse through the FAT directory on the CompactFlash card to look up the track. When you have a lot of tracks on the card, this takes a little time. However, this low-level FAT look-up can also happen in the background.

The H0420 controller provides function fstat(), which returns various kinds of data for a file, among which the file length in bytes and its "inode" number. These two parameters are all what function play() needs to play a file, without looking it up. What we need to do, is to set up a "resource id" for the file, and use that instead of the filename. The next script has this improvement.

new TrackCount
new TrackResource[3]

@reset()
    {
    TrackCount = fexist("*.mp3")
    selecttrack TrackResource, TrackCount
    playrandom
    }

@audiostatus(AudioStat: status)
    {
    if (status == Stopped)
        playrandom
    }

playrandom()
    {
    play TrackResource
    selecttrack TrackResource, TrackCount
    }

selecttrack(resource[3], count)
    {
    new filename[100 char]
    fmatch filename, "*.mp3", random(count)

    resource[0] = 0
    fstat filename, .inode = resource[1], .size = resource[2]
    }

In this example, it is easy to spot that the only significant change is that fmatch() is replaced by a new user function selecttrack(). Function selecttrack() first calls fmatch(), and then proceeds to make a "resource" that represents the file by calling fstat() on it. As the H0420 "Reference Guide", it is required that the first element in the array for the track resource (resource[0] in the above example) must always be zero.

Avoiding tracks to be repeated too soon

This article focuses on playing sequences of tracks quickly after another and with random selection. With random playing of tracks, a kind of "track separation" is often desirable. That is, when the script has picked a track called, say, "The Mistuned Piano.mp3" from a collection of 50 tracks on the CompactFlash card, you will want to avoid that the track that is picked to play after that track is, again, "The Mistuned Piano.mp3". In fact, you will want to make sure that at least five or ten other tracks play before "The Mistuned Piano" sounds again.

This, and a further improvement called "artist separation" are covered in detail in a separate application note: "Track and artist separation".

End notes

The scripts presented in this application note only play back tracks in the root directory of the CompactFlash card. If you wish to play back tracks from a subdirectory, you need to use "/audio/*.mp3" instead of just "*.mp3" in the parameters to fexist() and fmatch() (assuming that the subdirectory is called "audio", of course). The H0420 does not have a concept of an "active" directory, so the directory path must be present in every call.

If the subdirectory is variable as well, then you need to do some string processing. The H0420 functions like strpack(), strcat() and strformat() may be helpful for this task.