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()
-
Obviously, instead of counting the number of tracks each time again, you can
call
fexist()
only once and keep that number in a global variable. -
Instead of searching for a new track once the H0420 has stopped playing (and hence
is silent), you can run the procedure for selecting the next track while a
track plays. Then, when the
@audiostatus()
event arrives, you have the new filename already.
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.