ListTracks() doesn't sort alphabetically

I’m messing around with the code for Fett263’s track player and I’ve noticed that when I use the ListTracks() function in order to figure out which file to play when changing tracks, the list doesn’t appear to be in alphabetical order, as evidenced by the fact that I have the tracks named alphabetically, but they play out of order when I progress to the next track. In fact, I can’t figure out exactly what order it’s listing them in. Tracks aren’t sorted alphabetically or by file size, but the function does return the same order every time. Is this intentional? Is it simply caused by how the Iterator class works?

They are ordered by when they are “found”. I believe it is the order they were added to the directory if I recall.

1 Like

Makes sense. I even tried touching the files in order to see if they were sorted by modification date, but apparently not.

So I either need to figure out how to sort the output of ListTracks() in my own prop file, or I need to patch the function itself to return a sorted list. Is the latter something anyone other than me would want?

Just add them to the font/tracks/ directory on your SD in the order you want them to play in, that’s what I do if there’s a specific order needed.

That’s a fine workaround, but I’d prefer to make the experience as foolproof as possible.

Not sure what you mean, you can just turn the dial control to roll through all of them anyway.

I mean I expect the first track in the list to be the default track, so that whoever’s using my control set gets the expected results regardless of what order they copy the tracks to the card.

Also, I’m not using your dial control for the track player… I built my own track player by looking at yours. Yours is great for a large number of tracks, mine is meant for just a few per font. EVENT_FIRST_SAVED_CLICK turns the player on or off, EVENT_FIRST_HELD_MEDIUM advances to the next track if the player is turned on. You can double-click and hold to change the player mode: single playthrough, loop, rotate, or random. I’m assuming that if “rotate” is selected, the user will want the tracks to play in a certain order.

I am using your dial control for the volume menu, though, and it works great. You were right about having found the sweet spot for sensitivity.

The trick to getting the tracks sorted isn’t to modify ListTracks(), but instead to create a new function instead of RunCommandAndGetSingleLine which gets the next or previous track in alphabetical order.

I can write that if there people want it. It will only take space if people use it.

1 Like

Could be worth adding, then just enable via define for users that want it.

*Opinion: Add it. We already have the option to roll through the fonts in EditMode and they list alphabetically. Heck, even changing the font folder name makes it sort that way. Might as well make it an option.

Fonts do not list alphabetically, they list in the order found on the SD card, which may or may not be alphabetical.

RunCommandAndFindNextSortedLine function added, and can now be used by props that want things in sorted order.


@profezzorn I should have clarified that part, yes I sort by name as well as kind in how I format all SD’s I handle. It makes it quick and easy to click to where I need to go when need be. A bonus is I can easily tell if someone has messed w stuff.

Also, thanks!

How exactly do I use it? It looks like it needs a completely different set of parameters from RunCommandAndGetSingleLine(), so it’s not just a drop-in replacement?

This is the function I need to alter:

  void PlayTrack() {
    char playtrack[128];
    RunCommandAndGetSingleLine("list_current_tracks", nullptr, track_num_, playtrack, sizeof(playtrack));
    track_player_ = GetFreeWavPlayer();
    if (track_player_) {
    } else {
      STDOUT.println("No available WAV players.");

My understanding of how this works is that track_num_ is used to select the specific line from the list_current_tracks output. That command is defined here:

  bool Parse(const char *cmd, const char* arg) override {
    if (PropBase::Parse(cmd, arg)) return true;
    if (!strcmp(cmd, "list_current_tracks")) {
// Tracks must be in: font/tracks/*.wav
      for (const char* dir = current_directory; dir; dir = next_current_directory(dir)) {
        PathHelper path(dir, "tracks");
      return true;
    return false;

This would be why I was focused on ListTracks() as the source of the issue. It just spits a list out and RunCommandAndGetSingleLine uses track_num_ to choose a particular item from that list. If the list isn’t in order, the wrong item is chosen. How would you suggest I use the new function?

Yes, it’s not a drop-in replacement.
(It’s also not tested yet, so if you’re the first to use it, please beware that it might not work properly…)

Anyways, with RunCommandAndGetSingleLine, you can use a single integer to keep track of which track is being played. That integer just tells it which line to return. It still has to walk through all the lines to find that one line though.

RunCommandAndFindNextSortedLine() is slightly more limiting, because we have to keep track of what the previous line was. From there we can find the next or previous one. So instead of int track_num_; we need something like char track_[128];. We could then use it something like this:

void NextTrack(bool reverse) {
   char next_track[128];
   next_track[0] = 0;
   if (!RunCommandAndFindNextSortedLine<128>("list_current_tracks", nullptr, track_, next_track, reverse)) {
      RunCommandAndFindNextSortedLine<128>("list_current_tracks", nullptr, nullptr, next_track, reverse);
   strcpy(track_, next_track);

Note, we need to add track_[0] = 0 to the constructor to make sure it’s a valid, null-terminated string.
Once we’ve called NextTrack, the filename we need to play will be in track_.

Note that this will sort tracks based on their directory name AND their file name. It’s entirely possible to make something that sorts the tracks based only on their base name (so A/B.wav would come after B/A.wav) it just takes a few modifications to the compare function in GetNextLineCommandOutput.

1 Like

Got it, so instead of using an int for the position of the item in the list, I use a char to store the item itself. Makes sense, thanks. I’ll play around with it and see what I get.

One question: What happens if the current selected track is at the end of the list? Is there logic in RunCommandAndFindNextSortedLine to go back to the beginning of the list, or do I need to build that myself?

nit: an array of char

This is why my example code calls RunCommandAndFindNextSortedLine twice.
It will return false if nothing is found, so then you call it again, but without a previous parameter to make it find the first one. (or last one if “reverse” is true.)

Is it possible to set track_ to the default track for the current preset in the constructor?

Not in the constructor, because the default track isn’t known when the prop constructor runs. It can be done at the end of SetPreset though. (by overriding SetPreset, then after calling PropBase::SetPreset, you can copy current_preset_.track to track_.

1 Like

Okay, I’ve got it compiled with the new code, but it doesn’t seem to be working. Based on output from the serial monitor, it looks like RunCommandAndFindNextSortedLine() concatenates the previous line and next line into a single output string. So now it’s looking for Caiwyn/tracks/01.wavCaiwyn/tracks/02.wav

Note: I did not add current_track_[0] = [0]; to the constructor, because it throws an error on compile, but that shouldn’t be necessary because I’ve overridden SetPreset() and added:

strcpy(current_track_, current_preset_.track.get());

And that copies a valid null-terminated string before the track player is ever invoked.
This works as intended, and after I boot the saber I can play the default track. I just can’t switch to the next track.