Nokia Playlists

2025-Sep-11

Yesterday I started using a Nokia 225 4G (2024). I’ll probably do a write-up about this phone, but for the moment, I’m curious about whether I can script something to leverage the “playlist” feature in the music player.

To see what it’s doing, I created a playlist on the device first. I put two songs in it, and saved it as a new playlist Moo. Then I mounted the filesystem on my laptop to see if I could find Moo, and there it was: System/Mp3_res/Moo.lst. I copied it over.

The playlist file

penguin:/home/neale/Downloads % cat Moo.lst | hd
00000000  4d 55 53 49 43 41 52 52  41 59 20 53 41 56 45 46  MUSICARRAY SAVEF
00000010  49 4c 45 20 30 31 2e 30  30 2e 30 45 00 3a 00 5c  ILE 01.00.0E·:·\
00000020  00 4d 00 75 00 73 00 69  00 63 00 5c 00 4f 00 73  ·M·u·s·i·c·\·O·s
00000030  00 63 00 61 00 72 00 20  00 50 00 65 00 74 00 65  ·c·a·r· ·P·e·t·e
00000040  00 72 00 73 00 6f 00 6e  00 5c 00 54 00 68 00 65  ·r·s·o·n·\·T·h·e
00000050  00 20 00 53 00 6f 00 6e  00 67 00 20 00 42 00 6f  · ·S·o·n·g· ·B·o
00000060  00 6f 00 6b 00 73 00 20  00 28 00 32 00 30 00 31  ·o·k·s· ·(·2·0·1
00000070  00 37 00 29 00 5c 00 31  00 30 00 31 00 20 00 2d  ·7·)·\·1·0·1· ·-
00000080  00 20 00 49 00 6e 00 20  00 74 00 68 00 65 00 20  · ·I·n· ·t·h·e·
00000090  00 53 00 74 00 69 00 6c  00 6c 00 20 00 6f 00 66  ·S·t·i·l·l· ·o·f
000000a0  00 20 00 74 00 68 00 65  00 20 00 4e 00 69 00 67  · ·t·h·e· ·N·i·g
000000b0  00 68 00 74 00 2e 00 6d  00 70 00 33 00 00 00 00  ·h·t·.·m·p·3····
000000c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ················
⋮
00000210  00 00 00 00 00 00 00 00  00 00 00 51 00 0e 03 e9  ···········Q·♫♥Θ
00000220  07 00 00 26 07 0b 00 c8  e1 2e 00 45 00 3a 00 5c  •··&•♂·╚ß.·E·:·\
00000230  00 4d 00 75 00 73 00 69  00 63 00 5c 00 4f 00 73  ·M·u·s·i·c·\·O·s
00000240  00 63 00 61 00 72 00 20  00 50 00 65 00 74 00 65  ·c·a·r· ·P·e·t·e
00000250  00 72 00 73 00 6f 00 6e  00 5c 00 54 00 68 00 65  ·r·s·o·n·\·T·h·e
00000260  00 20 00 53 00 6f 00 6e  00 67 00 20 00 42 00 6f  · ·S·o·n·g· ·B·o
00000270  00 6f 00 6b 00 73 00 20  00 28 00 32 00 30 00 31  ·o·k·s· ·(·2·0·1
00000280  00 37 00 29 00 5c 00 31  00 30 00 32 00 20 00 2d  ·7·)·\·1·0·2· ·-
00000290  00 20 00 49 00 74 00 73  00 20 00 41 00 6c 00 6c  · ·I·t·s· ·A·l·l
000002a0  00 72 00 69 00 67 00 68  00 74 00 20 00 77 00 69  ·r·i·g·h·t· ·w·i
000002b0  00 74 00 68 00 20 00 4d  00 65 00 2e 00 6d 00 70  ·t·h· ·M·e·.·m·p
000002c0  00 33 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ·3··············
000002d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ················
⋮
00000420  00 00 00 00 00 00 00 00  00 00 00 4c 00 0e 03 e9  ···········L·♫♥Θ
00000430  07 00 00 26 07 0b 00 7a  5f 2f 00                 •··&•♂·z_/·
0000043b

This looks pretty approachable. We’ve got something like a header, MUSICARRAY SAVEFILE 01.00.0, then what looks like a UTF-16 encoded path: E:\Music\Oscar Peterson\The Song Books (2017)\101 - In The Still of the Night.mp3, a ton of nulls, some kind of footer, then another path and footer.

It’s unusual to find a file header with no length or terminator, so I guess it’s expecting a fixed-length string there. Easy enough to write that out. The UTF-16 encoding will be simple enough, as well.

The Nulls

The nulls indicate to me that the path is a fixed-length string, but I can test that with the second one using a bit of math.

The first path begins at offset 0000001b, and it’s nothing but nulls until offset 0000021b. If my assumption were true, the strings here have a maximum length of 00000200, a nice round number!

Let’s check the next path: it begins at 0000022b, and ends at 0000042b, another block of length 00000200.

This looks even more obvious after stripping the file header:

oscar:/home/neale/tmp % cat Moo.lst | slice 0x1b | hd
00000000  45 00 3a 00 5c 00 4d 00  75 00 73 00 69 00 63 00  E·:·\·M·u·s·i·c·
00000010  5c 00 4f 00 73 00 63 00  61 00 72 00 20 00 50 00  \·O·s·c·a·r· ·P·
00000020  65 00 74 00 65 00 72 00  73 00 6f 00 6e 00 5c 00  e·t·e·r·s·o·n·\·
00000030  54 00 68 00 65 00 20 00  53 00 6f 00 6e 00 67 00  T·h·e· ·S·o·n·g·
00000040  20 00 42 00 6f 00 6f 00  6b 00 73 00 20 00 28 00   ·B·o·o·k·s· ·(·
00000050  32 00 30 00 31 00 37 00  29 00 5c 00 31 00 30 00  2·0·1·7·)·\·1·0·
00000060  31 00 20 00 2d 00 20 00  49 00 6e 00 20 00 74 00  1· ·-· ·I·n· ·t·
00000070  68 00 65 00 20 00 53 00  74 00 69 00 6c 00 6c 00  h·e· ·S·t·i·l·l·
00000080  20 00 6f 00 66 00 20 00  74 00 68 00 65 00 20 00   ·o·f· ·t·h·e· ·
00000090  4e 00 69 00 67 00 68 00  74 00 2e 00 6d 00 70 00  N·i·g·h·t·.·m·p·
000000a0  33 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  3···············
000000b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ················
⋮
00000200  51 00 0e 03 e9 07 00 00  26 07 0b 00 c8 e1 2e 00  Q·♫♥Θ•··&•♂·╚ß.·
00000210  45 00 3a 00 5c 00 4d 00  75 00 73 00 69 00 63 00  E·:·\·M·u·s·i·c·
00000220  5c 00 4f 00 73 00 63 00  61 00 72 00 20 00 50 00  \·O·s·c·a·r· ·P·
00000230  65 00 74 00 65 00 72 00  73 00 6f 00 6e 00 5c 00  e·t·e·r·s·o·n·\·
00000240  54 00 68 00 65 00 20 00  53 00 6f 00 6e 00 67 00  T·h·e· ·S·o·n·g·
00000250  20 00 42 00 6f 00 6f 00  6b 00 73 00 20 00 28 00   ·B·o·o·k·s· ·(·
00000260  32 00 30 00 31 00 37 00  29 00 5c 00 31 00 30 00  2·0·1·7·)·\·1·0·
00000270  32 00 20 00 2d 00 20 00  49 00 74 00 73 00 20 00  2· ·-· ·I·t·s· ·
00000280  41 00 6c 00 6c 00 72 00  69 00 67 00 68 00 74 00  A·l·l·r·i·g·h·t·
00000290  20 00 77 00 69 00 74 00  68 00 20 00 4d 00 65 00   ·w·i·t·h· ·M·e·
000002a0  2e 00 6d 00 70 00 33 00  00 00 00 00 00 00 00 00  .·m·p·3·········
000002b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ················
⋮
00000410  4c 00 0e 03 e9 07 00 00  26 07 0b 00 7a 5f 2f 00  L·♫♥Θ•··&•♂·z_/·
00000420

Hopefully you too can see how everything lines up nicely at 16-byte boundaries.

We can start mapping out what an entry looks like at this point:

type Entry struct {
	Path [0x200]byte // utf-16le
  unknown [16]byte
}

The rest of the data

What else could the music player want to know about entries, that would fit into the 16 remaining bytes? My first guess:

File Size

File size is easy to check; let’s get the length of those two tracks:

penguin:/home/neale/Downloads % ls -l *.mp3
-rw-r--r-- 1 neale neale 3072456 Mar 14 11:07 '101 - In the Still of the Night.mp3'
-rw-r--r-- 1 neale neale 3104634 Mar 14 11:07 '102 - Its Allright with Me.mp3'
penguin:/home/neale/Downloads % python3
Python 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(3072456)
'0x2ee1c8'
>>> hex(3104634)
'0x2f5f7a'
>>>

So that’s 002ee1c8 for the first one, and 002f5f7a for the second one. Let’s look back at the two sections:

00000200  51 00 0e 03 e9 07 00 00  26 07 0b 00 c8 e1 2e 00  Q·♫♥Θ•··&•♂·╚ß.·

00000410  4c 00 0e 03 e9 07 00 00  26 07 0b 00 7a 5f 2f 00  L·♫♥Θ•··&•♂·z_/·

Hey, look at that, the file size is the last four bytes, endian-swapped. So the first one is c81e2e00, and the second one is 7a5f2f00.

type Entry struct {
	Path [0x200]byte // utf-16le
  unknown [12]byte
  FileSize uint32_t // little-endian
}

The Middle

I don’t have any initial guesses for what that stuff in the middle might be, and since it doesn’t change, we’ll just copy it and see what happens. I’m also going to make a wild guess that the first value is a 2-byte int, and we haven’t seen it go past 255.

type Entry struct{
	Path [0x200]byte // utf-16le
  unknown uint16_t // little-endian
  Junk [10]byte // 00e03 e907 0000 2607 0b00
  FileSize uint32_t // little-endian
}

It’s important to note here, the only reason I’m going to ignore the junk is because it never changes. That means nothing is being communicated in this section, after it’s first been stated. It’s just “same as last time”.

The beginning

So we only have two values here: 51 and 4c. Those aren’t big enough to be durations of the tracks, and they’re too big to be number of channels.

That header wasn’t NUL-terminated, so maybe the fixed-length string isn’t NUL-terminated either. If that’s true, the file would need to record the length of the path somewhere. I wonder what the path lengths are for those two tracks…

oscar:/home/neale/tmp % printf "%02x\n" $(echo -n 'E:\Music\Oscar Peterson\The Song Books (2017)\101 - In The Still of the Night.mp3' | wc -c)
51
oscar:/home/neale/tmp % printf "%02x\n" $(echo -n 'E:\Music\Oscar Peterson\The Song Books (2017)\101 - Its Allright With Me.mp3' | wc -c)
4c

BAM! Gottem. Since it’s UTF-16, we’d need to double the length to get the length in bytes, but this is clearly the path length, which makes sense looking at how our structure is unfolding.

type Entry struct{
	Path [0x200]byte // utf-16le
  PathLen uint16_t // little-endian, length of UTF-16 characters
  Junk [10]byte // 0e03 e907 0000 2607 0b00
  FileSize uint32_t // little-endian
}

I’ve got a hunch that the Junk is five uint16_ts, which is why I grouped them that way in the comment

For now, it’s time to make an initial attempt at creating my own playlist file!

Making a Playlist File

The rules I know about so far are:

This took me about 40 minutes:

#! /bin/sh

# All files will be listed as living here.
# Override with -pfx.
prefix='E:\Music'

case "$1" in
  -pfx|--pfx)
    shift
    prefix="${1%\\}" shift
    ;;
esac

uint32_le () {
  printf "%02x%02x%02x%02x" \
    $((($1 / 1) % 256)) \
    $((($1 / 256) % 256)) \
    $((($1 / 65536) % 256)) \
    $((($1 / 16777216) % 256))
}

uint16_le () {
  uint32_le $1 | slice 0 4
}

printf "%s" "MUSICARRAY SAVEFILE 01.00.0"
for fn in "$@"; do
  size=$(stat -c'%s' "$fn")
  ofn=$(printf '%s\%s' "$prefix" "$fn")
  ofn_len=$(printf '%s' "$ofn" | wc -m)  # XXX: length in glyphs, or half the length in bytes?

  printf '%s' "$ofn" | iconv -f utf-8 -t utf-16le | dd bs=512 count=1 conv=sync status=none
  uint16_le "$ofn_len" | unhex
  printf '0e03 e907 0000 2607 0b00' | unhex
  uint32_le "$size" | unhex
done

One final check to ensure we’ve got it:

oscar:/home/neale/tmp % diff <(cat Moo.lst | hd) <(./mklist.sh -pfx 'E:\Music\Oscar Peterson\The Song Books (2017)' *.mp3 | hd)
oscar:/home/neale/tmp %

No differences!

The test

I’m concerned that I don’t know what the Junk is, but let’s try loading up a whole directory and see if it works:

oscar:/srv/media/music/Oscar Peterson/The Song Books (2017) % ~/tmp/mklist.sh -pfx 'E:\Music\Oscar Peterson\The Song Books (2017)' * > ~/tmp/Song-Books.lst
oscar:/srv/media/music/Oscar Peterson/The Song Books (2017) %

I copied this over to the phone and… nothing. It only shows the Moo playlist.

Okay, fine, I’ll overwrite Moo.lst.

That works! Success! The whole playlist plays!

Next steps

I may twiddle this script to make it work better for my situation, but this works as written. You will need slice and unhex from fluffy. You can probably write shell functions to do the same thing with dd and xxd if you like a challenge and/or don’t want to compile my 50-line C programs.

I’ll also probably try to get this making audiobooks!