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:
- Size of the file
- Duration of the track
- Audio channels (stereo/mono)
- Whether there’s an image associated
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_t
s, 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:
- The file starts with
MUSICARRAY SAVEFILE 01.00.0
- Then, it’s a series of entries, each consisting of:
- 0x200 bytes of UTF-16 encoded path name
- It’s a Windows path, beginning with
E:\
for my SD card - Presumably
D:\
is the phone’s built-in memory - The area is null-padded at the end
- It’s a Windows path, beginning with
- A little-endian encoded uint16 count of UTF-16 characters in the path
- The bytes
030e 2907 0000 2607 0b00
- A little-endia encoded uint32 size of the MP3 file, in bytes
- 0x200 bytes of UTF-16 encoded path name
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!