Definitely not hacking a Timio player

2022-10-23 By qld

First of all, apologies, this blog post looks like a mess. It contains my raw process from 0 knowledge to achieving an arbitrary britney output primitive on the TIMIO child device. If you're not interested in the thought process, here it is:

  • rubberband the audio x2.75 to elevate pitch and speed
  • use a GeneralPlus tool to create .a18 files
  • put the files on the device SD card in the folder with a disc name
  • ???
  • profit

Directly seek the end of this page for the poc video.

Intro

We recently received a gift for toddlers, a Timio player.

It has some "disks", providing different audio contents. Heading one of these up to the light doesn't show much hidden storage device, but rather some metal-ish chips (magnets?) to detect orientation, and what I guess is a NFC tag. AFAIK NFC tags can't hold 100 minutes of audio.

Their FAQ (https://timio.co/fr/pages/faq) says:

  • Q: One of the discs is not working, what should I do?
  • A: It's possible that the RFID tag in the disc is not programmed correctly. Please contact us timio@timio.co and send us a picture of the disc in question, we'll replace it for you.
  • Q: How much disks ?
  • A: 25, totalling 4000 files and 24 of audio for the 8 languages.
  • Q: Can it play MP3 ?
  • A: No, it uses its own encoding.

AHA. That's a challenge.

Reading the NFC tags

I just used a standard NFC reader found on F-Droid, and the good old MifareClassicTool to get some raw-ish API results.

se.anyro.nfc_reader
de.syss.MifareClassicTool

Tags for disks 1-5, along with their number and title.

Topic            -  id  -  NFC id
stories          -  4   -  68e31ca01d
sleep            -  5   -  68e8dfed1d
farm             -  13  -  68e5ec0d1d
road             -  16  -  68e2dee11d
classical music  -  26  -  68e7b0861d

Oh no the warranty

So where's the audio ? The manual has an enticing instruction:

Ok, so there's an SD card, not-write-locked by default, and it's in there, just waiting to be dd'd.

Dumping the card

As it turns out, the thing auto-mounted itself, but I swear I didn't take a peek at the content, and unmounted it straight ahead.

$ lsblk
mmcblk0     179:0    0   481M  1 disk 
└─mmcblk0p1 179:1    0   481M  1 part

$ sudo dd if=/dev/mmcblk0 of=timio.img
985088+0 records in
985088+0 records out
504365056 bytes (504 MB, 481 MiB) copied, 38,176 s, 13,2 MB/s

$ sha256sum timio.img 
34a86ca29aecba03d8af8db6a468c91efece91041520413a43d9844ae3eb6543  timio.img

So, what does it hold ? A standard partition table and a single FAT16 partition.

$ file timio.img 
timio.img: DOS/MBR boot sector; partition 1 : ID=0x6, active, start-CHS (0x0,1,1), end-CHS (0x3ff,1,63), startsector 32, 985056 sectors
$ fdisk -l timio.img 
Disk timio.img: 481 MiB, 504365056 bytes, 985088 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x01b39c4b

Device     Boot Start    End Sectors  Size Id Type
timio.img1 *       32 985087  985056  481M  6 FAT16

Reading the card contents

Let's mount readonly

$ sudo kpartx -v -a timio.img -r
add map loop0p1 (253:0): 0 985056 linear 7:0 32

$ sudo mount /dev/mapper/loop0p1 mounted/

Hmmm 9 folders, eitght with the same amount of files, and a special L00 one with less files, and as it turns out, this folder contains the only files not named with a 8-character full caps filename:

$ cd mounted/
$ ls
L00  L01  L02  L03  L04  L05  L06  L07  L08
$ find . -type f | sort | head
./L00/Disc.a18
./L00/L0000L01.a18
./L00/L0000L02.a18
./L00/L0000L03.a18
./L00/L0000L04.a18
./L00/L0000L05.a18
./L00/L0000L06.a18
./L00/L0000L07.a18
./L00/L0000L08.a18
./L00/L0009S01.a18
$ find . -type f | sort | wc -l
4358
$ for i in * ; do echo -n "${i} : "; find "${i}" -type f | wc -l ; done
L00 : 22
L01 : 542
L02 : 542
L03 : 542
L04 : 542
L05 : 542
L06 : 542
L07 : 542
L08 : 542

L00 contents:

I'd say the L00 folder contains the audio for the settings of the Timio device, only usable when no disk is attached.

$ find * -type f | grep -v /L
L00/Disc.a18
L00/WEL.a18
$ ls L00 -1
Disc.a18
L0000L01.a18  ; language
L0000L02.a18  ; language
L0000L03.a18  ; language
L0000L04.a18  ; language
L0000L05.a18  ; language
L0000L06.a18  ; language
L0000L07.a18  ; language
L0000L08.a18  ; language
L0009S01.a18  ; 
L0009S02.a18  ; 
L0009S03.a18  ; 
L0009S04.a18  ; 
L0009S05.a18  ; 
L0009S06.a18  ; 
L0009S07.a18  ; 
L0009S08.a18  ; 
L0009S09.a18  ; 
L0009S10.a18  ; 
L0009S11.a18  ; 
L0009S12.a18  ; 
WEL.a18

Inspecting the files.

.a18 isn't a common file extension. Before diving in the format, now that we know it's a custom format, let's do what folks do in that case: download base materials from the provided updates.

Looks like they're distributed via Google Drive because why not lol.

$ file TM*zip
TM01-01-A-Files-2021-11-11.zip:                 Zip archive data, at least v2.0 to extract
TM02-02-A-2021-11-11.zip:                       Zip archive data, at least v2.0 to extract
TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11.zip: Zip archive data, at least v2.0 to extract
$ md5sum TM*zip
0132af169d90dae16713d326a2a75f0d  TM01-01-A-Files-2021-11-11.zip
d9e9d2d56ed454c7a23980c85fcb8696  TM02-02-A-2021-11-11.zip
ffee1c75165420774c3d9c9aefbb2ace  TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11.zip
$ ls -lh *zip
363M TM01-01-A-Files-2021-11-11.zip
377M TM02-02-A-2021-11-11.zip
380M TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11.zip

What version do I have ?

As per the timestamps and file hashes, I'd say mine is TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11.

$ find TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11 -type f -printf "%CY-%Cm-%Cd %AY-%Am-%Ad %TY-%Tm-%Td\n" | sort | uniq -c
     14 2022-10-23 2020-09-09 2020-09-09
      8 2022-10-23 2021-06-02 2021-06-02
   4315 2022-10-23 2021-08-26 2021-08-26
      1 2022-10-23 2021-09-01 2021-09-01
      2 2022-10-23 2021-09-17 2021-09-17
      5 2022-10-23 2021-09-27 2021-09-27
     12 2022-10-23 2021-11-06 2021-11-06
      1 2022-10-23 2021-11-11 2021-11-11
$ find TM02-02-A-2021-11-11 -type f -printf "%CY-%Cm-%Cd %AY-%Am-%Ad %TY-%Tm-%Td\n" | sort | uniq -c
     15 2022-10-23 2020-09-09 2020-09-09
   4195 2022-10-23 2021-05-18 2021-05-18
      2 2022-10-23 2021-05-20 2021-05-20
     21 2022-10-23 2021-05-22 2021-05-22
    104 2022-10-23 2021-05-23 2021-05-23
      8 2022-10-23 2021-06-02 2021-06-02
      1 2022-10-23 2021-10-25 2021-10-25
     11 2022-10-23 2021-11-06 2021-11-06
      1 2022-10-23 2021-11-07 2021-11-07
$ find mounted -type f -printf "%CY-%Cm-%Cd %AY-%Am-%Ad %TY-%Tm-%Td\n" | sort | uniq -c
     14 2021-12-08 2021-12-08 2020-09-09
      8 2021-12-08 2021-12-08 2021-06-02
   4315 2021-12-08 2021-12-08 2021-08-27
      1 2021-12-08 2021-12-08 2021-09-01
      2 2021-12-08 2021-12-08 2021-09-17
      5 2021-12-08 2021-12-08 2021-09-27
     12 2021-12-08 2021-12-08 2021-11-06
      1 2021-12-08 2021-12-08 2021-11-11
$ sha256sum TM*/L00/* TM*/*/L00/* | sort -k2 |sort | grep -iE '(wek|disc|L0000L07)'
261b3b7aa300f1dd9e0608d227d3ac4a3b900260040bfcbf0986e371f59bc055  TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11/L00/L0000L07.a18
261b3b7aa300f1dd9e0608d227d3ac4a3b900260040bfcbf0986e371f59bc055  TM-mine/L00/L0000L07.a18
4d3e5319a41d786cf075e1389f074e9c50623bebfd6976c884558630c58b8b60  TM01-01-A-Files-2021-11-11/TM01-01-A-Files-2021-11-11/L00/Disc.a18
4d3e5319a41d786cf075e1389f074e9c50623bebfd6976c884558630c58b8b60  TM02-02-A-2021-11-11/L00/Disc.a18
4d3e5319a41d786cf075e1389f074e9c50623bebfd6976c884558630c58b8b60  TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11/L00/Disc.a18
4d3e5319a41d786cf075e1389f074e9c50623bebfd6976c884558630c58b8b60  TM-mine/L00/Disc.a18
accb746ccfc4212ea9a5f708a5adfab2979a50cb656e4f276678799e51332ac2  TM02-02-A-2021-11-11/L00/L0000L07.a18

Diving in the files.

First of all, that "Disc.a18"

$ find TM* -type f -name Disc.a18 -exec echo {} ';' -exec hd {} ';'
TM01-01-A-Files-2021-11-11/TM01-01-A-Files-2021-11-11/L00/Disc.a18
00000000  00 00 08 00 1e 00                                 |......|
00000006
TM02-02-A-2021-11-11/L00/Disc.a18
00000000  00 00 08 00 1e 00                                 |......|
00000006
TM02-02-UK-ES-FR-DE-NL-CN-IT-PT-2021-11-11/L00/Disc.a18
00000000  00 00 08 00 1e 00                                 |......|
00000006
TM-mine/L00/Disc.a18
00000000  00 00 08 00 1e 00                                 |......|
00000006

Oh, it's just the same 6 bytes.

The rest are for sure audio files. What about the filenames ? Not that hard, since some disks have a Quizz mode, and we know there are 25 disks:

  • Filename : L0125S05.a18
  • L01 : Language 01
  • 25 : Disk number 25
  • S05 : Sound n°5 / Q01 : Question n°1
  • .a18 : extension

Let's count them. Looks like the secret/special disk 0 (surely the global settings once language is selected) has some extra C, V and W audio files.

13 Sounds for 12 positions + 1 title.

>>> filenames = list(map(str,(Path('.').glob('*a18'))))
>>> counts = defaultdict(lambda:defaultdict(int))
>>> for f in filenames:
...     counts[f[3:5]][f[5]] += 1
... 
>>> DataFrame.from_records(counts).transpose()
      C     Q    V    W     S
00  3.0   2.0  4.0  3.0   NaN
01  NaN   NaN  NaN  NaN  13.0
02  NaN   NaN  NaN  NaN  13.0
03  NaN   NaN  NaN  NaN  13.0
04  NaN   NaN  NaN  NaN  13.0
05  NaN   NaN  NaN  NaN  13.0
06  NaN   NaN  NaN  NaN  13.0
07  NaN  12.0  NaN  NaN  13.0
08  NaN  12.0  NaN  NaN  13.0
09  NaN  12.0  NaN  NaN  13.0
10  NaN  12.0  NaN  NaN  13.0
11  NaN  12.0  NaN  NaN  13.0
12  NaN  12.0  NaN  NaN  13.0
13  NaN  12.0  NaN  NaN  13.0
14  NaN  12.0  NaN  NaN  13.0
15  NaN  12.0  NaN  NaN  13.0
16  NaN  12.0  NaN  NaN  13.0
17  NaN  12.0  NaN  NaN  13.0
18  NaN  12.0  NaN  NaN  13.0
19  NaN  12.0  NaN  NaN  13.0
20  NaN  12.0  NaN  NaN  13.0
21  NaN   NaN  NaN  NaN  13.0
22  NaN   NaN  NaN  NaN  13.0
23  NaN   NaN  NaN  NaN  13.0
24  NaN  12.0  NaN  NaN  13.0
25  NaN  12.0  NaN  NaN  13.0
26  NaN   NaN  NaN  NaN  13.0

Not all disks have quizzes. And there are 26 disks !§!!. All files are unique, so it's not a reedition with some changed files. Gotta get that.

Also, the "language 0" (L00) contains the other languages names. Let's take a second peek at it:

Disc.a18     - ?
L0000L01.a18 - system - language name 
L0000L02.a18 - system - language name 
L0000L03.a18 - system - language name 
L0000L04.a18 - system - language name 
L0000L05.a18 - system - language name 
L0000L06.a18 - system - language name 
L0000L07.a18 - system - language name 
L0000L08.a18 - system - language name 
L0009S01.a18 - debug? - coordinate sound
L0009S02.a18 - debug? - coordinate sound
L0009S03.a18 - debug? - coordinate sound
L0009S04.a18 - debug? - coordinate sound
L0009S05.a18 - debug? - coordinate sound
L0009S06.a18 - debug? - coordinate sound
L0009S07.a18 - debug? - coordinate sound
L0009S08.a18 - debug? - coordinate sound
L0009S09.a18 - debug? - coordinate sound
L0009S10.a18 - debug? - coordinate sound
L0009S11.a18 - debug? - coordinate sound
L0009S12.a18 - debug? - coordinate sound
WEL.a18      - welcome ?

Maybe they used disc 9 as a debug one or smth. But that makes "disk" 0 being the system audio, and disks 1-26 being the 26 disks. I have not found a way to play a .a18 file rather than editing the SD card contents, and that process is slow, you won't have me move them all around to find which of these disks is the unreleased controversial one.

Enough metadata, show me the bytes !

Ok, let's go. At first sight, I'd say (WEL being some greetings beeps) the same wrapper format supports some MIDI-like musical notes instruction and some actual audio recording format.

$ hd L0113S09.a18 -n 512
00000000  00 ff 00 ff 47 45 4e 45  52 41 4c 50 4c 55 53 20  |....GENERALPLUS |
00000010  53 50 00 00 fe 02 60 00  10 00 80 3e 00 00 80 3e  |SP....`....>...>|
00000020  00 00 00 00 00 00 00 00  00 01 00 00 00 00 00 00  |................|
00000030  22 17 00 00 80 3e 9c 11  78 69 83 b5 cb 0e 46 fc  |"....>..xi....F.|
00000040  93 c2 21 75 51 cc 48 07  94 82 d3 ef 0a a1 10 eb  |..!uQ.H.........|
00000050  15 6a 41 2f b5 18 4a 0f  4b 9d fb 20 fe ff 9c 11  |.jA/..J.K.. ....|
00000060  6b 69 2b 7b 2f 3b 32 fb  66 4e 29 18 f5 b6 b2 5d  |ki+{/;2.fN)....]|
00000070  20 0a c8 03 f5 7b 48 58  66 77 cc 4e 11 99 8b c0  | ....{HXfw.N....|
00000080  a7 20 00 9b fe dd 9c 11  0b 68 77 9f 76 60 fd 99  |. .......hw.v`..|
00000090  9e c1 c4 d6 3d 1d 94 05  49 ea 7c 8b 73 51 30 7c  |....=...I.|.sQ0||
000000a0  64 bc 0f 69 e0 95 0c 5e  7b b9 df a8 fe 73 1e 11  |d..i...^{....s..|
000000b0  12 e1 73 ad 76 20 7f b6  00 7b 95 f1 64 35 c8 d8  |..s.v ...{..d5..|
000000c0  55 e9 c6 34 9d b6 60 8d  84 94 8e c9 20 57 af 42  |U..4..`..... W.B|
000000d0  b8 86 ff 24 fe ff 97 44  2a 62 dd bd 42 10 57 00  |...$...D*b..B.W.|
000000e0  29 f3 b3 4f ee 59 d6 80  69 24 cd df 69 5a e6 6f  |)..O.Y..i$..iZ.o|
000000f0  12 4a a8 dc 23 e2 b2 4c  1f cf 29 75 9e cb 4f 90  |.J..#..L..)u..O.|
00000100  3c 24 1d ba ee 90 90 ac  bf 95 0e c8 aa 13 ad 2a  |<$.............*|
00000110  29 9a 18 50 69 48 d2 ec  bf 3e 7b 17 cb fb 45 13  |)..PiH...>{...E.|
00000120  98 e5 b7 a7 fe ff 4f c1  2a 3a 85 ec 45 a8 20 0b  |......O.*:..E. .|
00000130  59 d9 38 84 50 42 11 12  76 dd 0d 05 9e 4e a0 36  |Y.8.PB..v....N.6|
00000140  fa a9 ea a4 6d 4f cf 24  6d cb 91 d7 d2 cc 09 e0  |....mO.$m.......|
00000150  24 09 80 84 3a c7 f2 e4  a4 0d 8d 68 43 84 87 fb  |$...:......hC...|
00000160  54 4e 7a a2 0c 3b b2 80  5c 33 94 73 c7 91 34 b3  |TNz..;..\3.s..4.|
00000170  d7 a4 52 8b 2e e0 78 d2  45 a0 5f 13 f6 19 25 61  |..R...x.E._...%a|
00000180  e0 ed 20 91 d9 68 55 e4  1e 83 00 b4 b1 e2 ff f5  |.. ..hU.........|
00000190  f0 61 ee 33 7a c0 77 ae  c1 1c 89 9c fe 09 6e de  |.a.3z.w.......n.|
000001a0  62 6a 24 79 ca 8f 32 25  2e 85 93 84 8b a8 38 a8  |bj$y..2%......8.|
000001b0  8e 04 81 17 f2 2a c2 7c  fa 18 3e 2a 63 ec 37 09  |.....*.|..>*c.7.|
000001c0  1a 94 a9 69 3e e4 2b ca  b0 a8 4f 0b db d5 58 84  |...i>.+...O...X.|
000001d0  75 7a 0d ab 41 f1 23 4c  e2 46 0d 99 06 89 10 87  |uz..A.#L.F......|
000001e0  ad f8 95 eb fd 10 e6 f1  20 35 ac 66 fe bf 2a d2  |........ 5.f..*.|
000001f0  27 6a 47 05 bc 5a 7f 35  ff 2a 98 14 6c a3 49 6a  |'jG..Z.5.*..l.Ij|
00000200
$ hd ../L00/WEL.a18 -n 512
00000000  00 ff 00 ff 47 45 4e 45  52 41 4c 50 4c 55 53 20  |....GENERALPLUS |
00000010  53 50 00 00 fe 02 0e 00  10 00 80 3e 00 00 80 3e  |SP.........>...>|
00000020  00 00 00 00 00 00 00 00  00 01 00 00 00 00 00 00  |................|
00000030  82 11 00 00 80 3e 60 b6  fd 90 05 68 58 78 24 e3  |.....>`....hXx$.|
00000040  9d 9f 8d 69 4b 82 d8 00  31 af c0 52 a4 79 ba af  |...iK...1..R.y..|
00000050  d7 91 4e d5 9b d4 52 61  1d 8a 17 6a bf 66 68 c5  |..N...Ra...j.fh.|
00000060  8b 99 76 5b 19 5c eb 4d  80 1c e2 0d 04 be 34 be  |..v[.\.M......4.|
00000070  80 9b 0d cb 7a 8f b6 f1  ef 6c 3c b4 b8 58 98 30  |....z....l<..X.0|
00000080  54 4f 26 c3 8f e5 4b cf  18 9c a3 c4 f8 90 96 9a  |TO&...K.........|
00000090  3c 5c fd a0 c4 df 8c cf  63 42 58 49 7d 96 5c 2e  |<\......cBXI}.\.|
000000a0  07 dc dd 9c 9a 81 b3 4a  3f 82 ff ff ff ff 69 c5  |.......J?.....i.|
000000b0  42 33 95 08 0c 50 97 d8  3c 8c 7f 2f 7c 10 fc 69  |B3...P..<../|..i|
000000c0  42 5d 62 e1 ea 4b c6 65  48 1d 0e ff bf e0 e2 bf  |B]b..K.eH.......|
000000d0  ff 8f ff ff ff ff a8 be  98 9c b2 6b 21 b2 15 e6  |...........k!...|
000000e0  01 8d 74 f8 f0 1a ce af  e7 93 64 cb 42 ab 77 3f  |..t.......d.B.w?|
000000f0  10 33 84 c1 62 07 24 98  7f 11 ff ff ff ff 34 ba  |.3..b.$.......4.|
00000100  03 4e d6 08 03 a4 4e ed  b9 03 61 03 ff 3c 7f 39  |.N....N...a..<.9|
00000110  5b 3f ad 2c f7 a5 94 99  07 dc 2e 98 db c5 02 85  |[?.,............|
00000120  7f dc ff ff ff ff d4 c0  01 49 ca 85 00 e9 96 f8  |.........I......|
00000130  f4 28 cf c1 cf e9 03 9c  c7 e6 f6 a8 0d f0 99 5f  |.(............._|
00000140  ce f3 d4 6e a5 9b 05 21  29 ec ff 1f ff ff 35 b2  |...n...!).....5.|
00000150  a1 99 96 04 d8 8d a9 7d  9e 8b 2f 51 e0 57 1f fe  |.......}../Q.W..|
00000160  0e fd 46 b6 93 b5 99 e6  86 94 26 0c 53 15 b5 04  |..F.......&.S...|
00000170  1f 9f ff ff ff ff a8 ae  42 93 da 1a ec c6 29 3e  |........B.....)>|
00000180  be 12 4c 99 97 79 57 d9  cf ce 16 01 69 7c 7f fd  |..L..yW.....i|..|
00000190  6e 2c 85 01 25 60 c4 5d  ff f2 ff ff ff ff 63 ad  |n,..%`.]......c.|
000001a0  04 e6 ad 11 87 49 3c 95  9f 2d 78 22 7a 14 9d 8f  |.....I<..-x"z...|
000001b0  9a 5f 5a 7f c7 bf c4 e7  b9 cc c3 b9 68 38 a3 30  |._Z.........h8.0|
000001c0  54 9c ff 5b ff ff 41 af  27 ff 4b 0b 30 55 5e f8  |T..[..A.'.K.0U^.|
000001d0  eb cb 8f 81 fe ba be 6e  78 7a f8 30 e2 cf ca bd  |.......nxz.0....|
000001e0  1e 14 18 62 7f 64 3f c1  ff ff ff ff ff ff 63 9d  |...b.d?.......c.|
000001f0  ea e6 d2 12 10 95 2d fa  dc 81 f9 a1 f6 ab 4b 1f  |......-.......K.|

Speaking of what, there seems to be a byte being either 0x0e or 0x60, maybe that's for MIDI vs audio.

$ for i in $( ls ../*/*| sort) ; do hd -n 32 $i ; done|sort|uniq -c
      1 00000000  00 00 08 00 1e 00                                 |......|
   4357 00000000  00 ff 00 ff 47 45 4e 45  52 41 4c 50 4c 55 53 20  |....GENERALPLUS |
      1 00000006
     13 00000010  53 50 00 00 fe 02 0e 00  10 00 80 3e 00 00 80 3e  |SP.........>...>|
   4344 00000010  53 50 00 00 fe 02 60 00  10 00 80 3e 00 00 80 3e  |SP....`....>...>|
   4357 00000020

I'd say a 56 bytes header followed by raw data.

$ for i in $( ls ../*/*| sort) ; do hd -n 64 $i ; done|sort|uniq -c
   1 00000000  00 00 08 00 1e 00                                 |......|
4357 00000000  00 ff 00 ff 47 45 4e 45  52 41 4c 50 4c 55 53 20  |....GENERALPLUS |
   1 00000006
  13 00000010  53 50 00 00 fe 02 0e 00  10 00 80 3e 00 00 80 3e  |SP.........>...>|
4344 00000010  53 50 00 00 fe 02 60 00  10 00 80 3e 00 00 80 3e  |SP....`....>...>|
4357 00000020  00 00 00 00 00 00 00 00  00 01 00 00 00 00 00 00  |................|
   1 00000030  02 00 0a 00 80 3e 58 11  d2 c4 e6 a2 77 86 44 fe  |.....>X.....w.D.|
   1 00000030  02 01 04 00 80 3e 58 11  d2 d2 f5 b1 97 9d 2f f8  |.....>X......./.|
   1 00000030  02 08 02 00 80 3e 1c 09  7a 69 06 63 f5 c2 d0 2a  |.....>..zi.c...*|
   1 00000030  02 0a 00 00 80 3e 1e 5b  1d 15 56 0c cd 35 f9 5b  |.....>.[..V..5.[|
   1 00000030  02 0a 00 00 80 3e 5e 11  d6 cc 0c ea bf 1b 1f f5  |.....>^.........|
   1 00000030  02 0a 00 00 80 3e 8f 32  37 73 41 3d a3 a4 28 fe  |.....>.27sA=..(.|
   1 00000030  02 0a 00 00 80 3e 9c 11  6e 69 8a 7a df 8d 53 f8  |.....>..ni.z..S.|
   1 00000030  02 0a 00 00 80 3e a3 18  67 60 0d 2b 4e 19 a2 aa  |.....>..g`.+N...|
   1 00000030  02 0f 00 00 80 3e 12 13  b4 70 41 ad b2 83 c0 ff  |.....>...pA.....|
   1 00000030  02 0f 00 00 80 3e 13 20  b2 02 06 9d 6b ed 53 22  |.....>. ....k.S"|
   1 00000030  02 0f 00 00 80 3e 1c 09  6b e1 07 75 f5 02 b6 2a  |.....>..k..u...*|

But first, before diving in the bytes, what's that GENERALPLUS SP string ?

Says:

Entertainment and Voice Solution Interactive Smart Toy

More and more toys that children play today are smart and smarter than ever. Today's toys are no longer just for fun, they educate our children. The truth is a simple button or a small speaker on a piece of toy may not be attractive to our children and parents anymore. Instead, toys with interactive action, voice recognition, and touch sensing become popular. GPCE4 is the product we design to lead this trend to the future.

Ah Taïwan, the good old Republic of China.

  • No.19, Industry E. Rd. IV, Hsinchu Science Park, Hsinchu City 30077, Taiwan, R.O.C

Looks like they're a SOC chip provider.

Hm what if they were close to selling processors ? Taïwan is really something. Let's invest in TSMC before they get.. uh.. Tibet-ed ? Uyghuristan-ed ? Ah, digressions.

Searching for "generalplus a18" online yields some interesting results * https://github.com/ctxis/Furby/blob/master/audioutils/README.md

Audio Utils

WARNING: These scripts are very rough. You'll need to customise the file/directory names to make them work.

You'll also need to download the GeneralPlus Gadget utility and extract the a1800.dll file from it.

extract_audio.py - extracts the GeneralPlus a18-encoded audio files from the DLC file
convert.py - converts a directory of a18 files to wav, using Python's ctypes to call into the a1800 DLL. You'll need to run this on Windows.

.

  • https://www.generalplus.com/1LVlangLNxxSVyySNservice_n_support_d
  • G+ Gadget is a tool set of contraptions. The flexible and fairy design methodology provide excellent adaptability for future extensions. With this gadget, users can easily make their SPI resources by automatic process, from audio converting to file packing. (2019-12-13)

Ah, I remember some fun tweets on furby hacks. Looks like it shares the same hardware or something:

Now, we learned that the A1800 audio format is some kind of proprietary FFT-based format.

Indeed. Not always 0000803e that being said.

1 00000030  fa 43 00 00 80 3e 5f 49                           |.C...>_I|
3 00000030  fa 43 00 00 80 3e 9c 11                           |.C...>..|
1 00000030  fa 43 00 00 80 3e dc 49                           |.C...>.I|
1 00000030  fa 48 00 00 80 3e 1c 11                           |.H...>..|
2 00000030  fa 48 00 00 80 3e 9c 11                           |.H...>..|
1 00000030  fa 4d 00 00 80 3e 1c 11                           |.M...>..|
1 00000030  fa 52 00 00 80 3e 9c 11                           |.R...>..|
1 00000030  fa 57 00 00 80 3e 5e 11                           |.W...>^.|
1 00000030  fa 64 02 00 80 3e 1c 23                           |.d...>.#|
1 00000030  fa 69 02 00 80 3e 58 11                           |.i...>X.|
1 00000030  fa 69 02 00 80 3e ba 67                           |.i...>.g|
1 00000030  fa 6d 0d 00 80 3e 5e 11                           |.m...>^.|
1 00000030  fa 75 00 00 80 3e 9f 11                           |.u...>..|
1 00000030  fa 77 08 00 80 3e 1c 09                           |.w...>..|

Doesn't exactly maps to our format, unless there are chunks, or some endianness issue ?

>>> size_bytes = lambda f:struct.unpack('<H', f.read_bytes()[54:56])
>>> print('\n'.join(map(lambda f:f'{f} {size_bytes(f)} {size_file(f)}', sorted(files))))
L0101S07.a18 (4511,) 180254
L0102S07.a18 (4445,) 258494
L0103S07.a18 (10172,) 478494
L0104S07.a18 (4574,) 202414
L0105S07.a18 (46712,) 604894
L0106S07.a18 (2399,) 171894
L0107Q07.a18 (6940,) 5014
L0107S07.a18 (6616,) 6414
L0108Q07.a18 (13215,) 4854
L0108S07.a18 (22958,) 6294
L0109Q07.a18 (2195,) 4934
L0109S07.a18 (22745,) 20054
L0110Q07.a18 (14468,) 4574
L0110S07.a18 (18495,) 11454
L0111Q07.a18 (9054,) 4574
L0111S07.a18 (4446,) 13694
L0112Q07.a18 (15926,) 4894
L0112S07.a18 (5555,) 21694
L0113Q07.a18 (6930,) 4334
L0113S07.a18 (4508,) 11374
L0114Q07.a18 (16659,) 3774
L0114S07.a18 (13230,) 11934
L0115Q07.a18 (16252,) 4254
L0115S07.a18 (10894,) 13014
L0116Q07.a18 (12937,) 4014
L0116S07.a18 (9048,) 13294
L0117Q07.a18 (24400,) 3694
L0117S07.a18 (14526,) 12894
L0118Q07.a18 (11027,) 4054
L0118S07.a18 (4382,) 13974
L0119Q07.a18 (15556,) 3254
L0119S07.a18 (4508,) 14214
L0120Q07.a18 (4511,) 4454
L0120S07.a18 (14112,) 30814
L0121S07.a18 (18842,) 722294
L0122S07.a18 (19356,) 926494
L0123S07.a18 (4440,) 167334
L0124Q07.a18 (22707,) 5934
L0124S07.a18 (2076,) 12934
L0125Q07.a18 (4508,) 5134
L0125S07.a18 (4444,) 12374
L0126S07.a18 (4574,) 136454

Cursed Python code from https://github.com/ctxis/Furby/blob/master/audioutils/convert.py :

decproto = ctypes.WINFUNCTYPE(ctypes.c_uint, LPCSTR, LPCSTR, ctypes.POINTER(UINT), UINT, UINT)
decparamflags = ((1, 'infile'), (1, 'outfile'), (2, 'fp'), (1, 'unk1', 16000), (1,'unk2', 0))
decfunc = decproto(('dec', a1800dll), decparamflags)a

Great.

The G+_Gadget_V1.1.5.zip file

Let's check out that G+ (General Plus) zip

$ unzip -l G+_Gadget_V1.1.5.zip 
Archive:  G+_Gadget_V1.1.5.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
 47703817  2019-12-13 14:33   G+ Gadget V1.1.5.msi
     9250  2019-12-13 14:00   ReleaseNote.txt
---------                     -------
 47713067                     2 files

Extract it and check out that .msi

$ exiftool 'G+ Gadget V1.1.5.msi' 
ExifTool Version Number         : 12.16
File Name                       : G+ Gadget V1.1.5.msi
Directory                       : .
File Size                       : 45 MiB
File Modification Date/Time     : 2019:12:13 14:33:50+01:00
File Access Date/Time           : 2019:12:13 14:33:50+01:00
File Permissions                : rw-r--r--
File Type                       : FPX
File Type Extension             : fpx
MIME Type                       : image/vnd.fpx
Code Page                       : Windows Latin 1 (Western European)
Title                           : Installation Database
Subject                         : G+ Gadget tools set
Author                          : Generalplus
Keywords                        : Installer
Comments                        : G+ Gadget tools set
Template                        : Intel;1033
Revision Number                 : {45E0229D-F20C-46CF-B2DF-6D094E02AE97}
Create Date                     : 2019:12:13 06:33:36
Modify Date                     : 2019:12:13 06:33:36
Pages                           : 200
Words                           : 2
Software                        : Windows Installer XML (3.6.3303.0)
Security                        : Read-only recommended

Let's install msitools, since the 7z l output is pretty dry. Not sure I want to know too much on the msi format internals.

$ msiinfo suminfo 'G+ Gadget V1.1.5.msi' 
Title: Installation Database
Subject: G+ Gadget tools set
Author: Generalplus
Keywords: Installer
Comments: G+ Gadget tools set
Template: Intel;1033
Revision number (UUID): {45E0229D-F20C-46CF-B2DF-6D094E02AE97}
Created: Fri Dec 13 07:33:36 2019
Last saved: Fri Dec 13 07:33:36 2019
Version: 200 (c8)
Source: 2 (2)
Application: Windows Installer XML (3.6.3303.0)
Security: 2 (2)

One msiextract later, looks like we have an editor for that curious file format:

ReleaseNote.txt:
G+ Gadget V1.1.5 - 2019.12.13
=========================================
 - [G+ Audio Batch Converter]
    - New formats: PCM_GPCE, A1801, S800, A4800 (with Event), and A1800 (with Event).
    - Format substitution: A4800 -> A4800 (with Event) and A1800 -> A1800 (with Event).
    - Support playback with S800 format on GPCE4 and GPCE4F.
    - Support playback with A1800 (with Event) and A3400Pro 4Bit formats on GPCE1/GPCE3.
    - Support playback with A3400Pro 4Bit/5Bit formats on GPC22.
    - Support enable silence frame for some formats.
    - Support .geax format, expanding the number of I/O Event from 32 to 112, to enhance .gea format.

Also, for fun and giggles, SpiFlashWriterSet.config:

<?xml version="1.0" encoding="UTF-8"?>
<!--
    *********************************************************************************************
    *                                                                                           *
    * This system config file is very important, pluse DO NOT change it by manual!              *
    *                                                                                           *
    *********************************************************************************************
-->

Notably $ zathura 'Doc/G+ AudioBatchConverter User Guide.pdf' says :

  1. A1600 -.a16
  2. A1600Pro (with event) -.a16p
  3. A1601 -.a16
  4. A1800 (with Event) -.a18
  5. A1801 -.a181
  6. A3400Pro 4Bit -.sp4
  7. A3400Pro 5Bit -.sp5
  8. A3400ProE 4Bit -.se4
  9. A3400ProE 5Bit -.se5
  10. A3600 (ADPCM36) -.a36

Aha, here's our .a18 format.

Also, this confirms our header hypothesis, except that it's 48 bytes + 8 bytes up to 56, surely the last 8 bytes are just some data stream header.

That PDF also mentions allowed input files:

4.9 Step7: Add files to encode Three ways to add files: .wav , .gea. and .mp3.

Aha, so looks like we can have custom audio in that player. This is going to be a fun home escape-game challenge.

That also means I have to click on the Microsoft Windows operating system. Oh no.

Clicking on Windows

So, that installer works fine-ish

Supported .a18 content, there's indeed some MIDI-like thing, and audio stream.

The GENERALPLUS (G+) tool knows about .mp3 and .wav (and .gea and .geax) formats:

I could convert .wav files into .a18 using their tool. However, when played, they were slowed down terribly, and were both slow and had a low pitch. I'll skip you that part, but it's not about the input .WAV (PCM) sample rate etc. (The .mp3 are just converted to .WAV 16b-le as flat files in the folder and then deleted, I could quickly copy-paste one to check its properties.)

Playing a 440Hz tone yielded a 160Hz-ish tone.

>>> 440/160
2.75

Since ffmpeg.exe builds on windows properly solve the rate/pitch/speed problem, atempo does what you'd expect. Having that nice old-school effect of distorted wav files (aka I'm using aplay to solve ctf challenges and have no idea of the bitrate) is provided by the "rubberband" lib, not compiled in by default. I don't have time to waste recompiling ffmpeg on windows. (Lol have you even tried to compile something on windows??), it turns out they have a nice website where they drop windows builds:

Get-ChildItem .\audio-in\ | % {
    $f_in=$_.fullname;
    $f_out=$f_in.replace("audio-in","audio-spedup");
    .\rubberband.exe --tempo 2.75 --frequency 2.75 $f_in $f_out
    # ffmpeg -i $f  -filter:a 'atempo=2.0' $f.Replace("audio-in","audio-spedup");
}

This, plus some clicks, some file operations to put that on the sdcard, and boom.

Profit !

Our upcoming in-house escape-game challenge will definitely imply some AI trying to escape out of that device or something.

Thanks for reading.

Bonus epilogue, content edition timestamps

Boom

find TM* -type f -printf "%CY-%Cm-%Cd-%CH-%CM-%CS\n%AY-%Am-%Ad-%AH-%AM-%AS\n%TY-%Tm-%Td-%TH-%TM-%TS\n" | grep -v '^2022-10' | awk '{print substr($0,0,19)}'| sort > timestamps.txt
cat timestamps.txt | gnuplot -c ts.gpi
eom ts.png

gnuplot code:

set terminal png size 600,400
set output "ts.png"
set timefmt "%Y-%m-%d-%H-%M-%S"
set format x "%Y-%m-%d"
set grid
set xdata time
set xtics rotate
plot "<cat" using 1:0 with linespoints ps 2 pt 7 title "ts over time"

Result:

We could plot that by disc and language, but, uh, I'm tired. Cheers.