reinerh / loopertrx

import/export audio data from some looper pedals
GNU General Public License v2.0
30 stars 5 forks source link

Rowin Twin Looper - different USB IDs, and appears to use Midi/SysEx #6

Open mungewell opened 1 year ago

mungewell commented 1 year ago

As per title, I've picked up the Rowin Twin Looper pedal (similar construction to other brands).

It identifies as

Jan 23 20:59:14 thevoid kernel: [ 6483.720688] usb 1-2: new full-speed USB device number 8 using xhci_hcd
Jan 23 20:59:14 thevoid kernel: [ 6483.870145] usb 1-2: New USB device found, idVendor=0416, idProduct=5555, bcdDevice= 0.01
Jan 23 20:59:14 thevoid kernel: [ 6483.870167] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
Jan 23 20:59:14 thevoid kernel: [ 6483.870176] usb 1-2: Product: DFU̒
Jan 23 20:59:14 thevoid kernel: [ 6483.870183] usb 1-2: Manufacturer: Rowin
Jan 23 20:59:14 thevoid mtp-probe: checking bus 1, device 8: "/sys/devices/pci0000:00/0000:00:14.0/usb1/1-2"
Jan 23 20:59:14 thevoid mtp-probe: bus: 1, device: 8 was not an MTP device
Jan 23 20:59:14 thevoid upowerd[1416]: unhandled action 'bind' on /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.1
Jan 23 20:59:14 thevoid systemd-udevd[3369]: Process '/usr/sbin/alsactl -E HOME=/run/alsa restore 2' failed with exit code 99.
Jan 23 20:59:14 thevoid upowerd[1416]: unhandled action 'bind' on /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0
Jan 23 20:59:14 thevoid upowerd[1416]: unhandled action 'bind' on /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2
Jan 23 20:59:14 thevoid pulseaudio[1380]: [pulseaudio] module-alsa-card.c: Failed to find a working profile.
Jan 23 20:59:14 thevoid pulseaudio[1380]: [pulseaudio] module.c: Failed to load module "module-alsa-card" (argument: "device_id="2" name="usb-Rowin_DFU__-00" card_name="alsa_card.usb-Rowin_DFU__-00" namereg_fail=false tsched=yes fixed_latency_range=no ignore_dB=no deferred_volume=yes use_ucm=yes card_properties="module-udev-detect.discovered=1""): initialization failed.

With the UDEV and script changed to different ID, running the script fails and causes the pedal to detach it's midi port.

~/loopertrx-github$ amidi -l
Dir Device    Name
IO  hw:2,0,0  DFU̒ MIDI 1
~/loopertrx-github$ python3 loopertrx.py rx test.wav
[Errno 16] Resource busy
~/loopertrx-github$ amidi -l
Dir Device    Name
~/loopertrx-github$

Prior to runnig the script, it DOES respond to the same midi as noted for the AP-09 (bug #1).

$ amidi -p hw:2,0,0 -S 'F0 00 32 45 00 00 00 40 7f F7' -t 2 -r test.bin
41 bytes read

$ hexdump -C test.bin
00000000  f0 00 32 45 58 01 00 40  30 62 46 11 2b 66 6c 19  |..2EX..@0bF.+fl.|
00000010  34 61 44 0d 23 56 4c 59  33 68 02 00 28 20 62 04  |4aD.#VLY3h..( b.|
00000020  0a 15 2c 5c 40 11 23 01  f7                       |..,\@.#..|
00000029
mungewell commented 1 year ago

It is the self.dev.set_configuration() call which is failing.

mungewell commented 1 year ago

After seeing the note in #2 I blacklisted usb_snd_audio and then set_configuration() completed, however i then got another error.

$ python3 loopertrx.py rx test.wav
parsed arg
created Cli
Device found
Detached
configuration
found USB device
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 223, in get_interface_and_endpoint
KeyError: 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "loopertrx.py", line 297, in <module>
    main()
  File "loopertrx.py", line 289, in main
    dev.receive_file(args.filename)
  File "loopertrx.py", line 130, in receive_file
    size = self.get_size()
  File "loopertrx.py", line 85, in get_size
    self.dev.write(self.ENDPOINT_OUT, header)
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 940, in write
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 102, in wrapper
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 215, in setup_request
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 102, in wrapper
  File "/usr/local/lib/python3.6/dist-packages/pyusb-1.0.2-py3.6.egg/usb/core.py", line 231, in get_interface_and_endpoint
ValueError: Invalid endpoint address 0x1

It seems that the Rowin pedal uses a different endpoint (0x02). lsusb_vv.txt

Changing this and I got a timeout.... we're making progress.

$ python3 loopertrx.py rx test.wav
parsed arg
created Cli
found USB device
[Errno 110] Operation timed out
mungewell commented 1 year ago

As noted in title, it appears that this pedal uses the Midi/SysEx protocol (not the Mass-Storage protocol). So there would be some reverse engineering required to make it work...

mungewell commented 1 year ago

The biggest challenge here is working out how the official code packs the Midi data. As midi is 7-bit, the 8-bit bytes need to be split in some way.

We know that previously the code used 1024 (1000??) byte blocks of audio data, this seems to fit with the 1173 (7-bit) blocks we see. 1173 * 7 / 8 = 1026.735

Looking at some of the Midi data, it looks like (and makes sense) that there is a length field sent before the data, this might be counted in bits...

f000320d493f0040007127627b107e00000000000000000000000000000000000000000000000000000000000
        LLLLLLMr<--
        L = 00,3f,49 = (63 * 128) + 73 = 8137 bits

f000320d4100004000622f627b604b00001e01f7
        LLLLLLMr<--20c,10b=70bits-->Cs
        L = 00,00,41 = 65 bits
f000320d3126004000622f627b604b00000000000000000000000000000000000000000000000000000000000
        LLLLLLMr<--
        L = 00,26,31 = (38 * 128) + 49 = 4913 bits

f0003245000000407ff7
        LLLLLLMrCs
        L = 0
f000324558010040306246112b666c193461440d23564c5933680200282062040a152c5c40112301f7
        LLLLLLMr<--- 62 chars, 31 bytes, each 7bit -> 217 bits           ---->Cs 
        L = 00,01,58 (7 bit) = 88 + (1 * 128) = 216 bits?

The next question is how they pack the data.. although this could be several schemes, given they appear to count bits this might just be a sequential bit field pack.

ie

8-bit packed to 7bit (LSB)
11111111  -->                          _1111111
22222222                       __2222221
33333333               ___3333322
44444444       ____4444333
            ...4444
mungewell commented 1 year ago

Well using this idea for the packing, it looks like:

$ amidi -p hw:2,0,0 -S 'F0 00 32 45 00 00 00 40 7f F7' -t 2 -r test.bin
41 bytes read

$ hexdump -C test.bin
00000000  f0 00 32 45 58 01 00 40  30 62 46 11 2b 66 6c 19  |..2EX..@0bF.+fl.|
00000010  34 61 44 0d 23 56 4c 59  33 68 02 00 28 20 62 04  |4aD.#VLY3h..( b.|
00000020  0a 15 2c 5c 40 11 23 01  f7                       |..,\@.#..|
00000029

Decodes into something plausible.

$ python3 pack_test.py 
216
00000000: 30 B1 31 B2 32 B3 33 B4  30 B1 31 B2 32 B3 33 B4  0.1.2.3.0.1.2.3.
00000010: 00 80 02 89 09 8A 0A 8B  0B 8C 8C                 ...........
None

pack_test.py.txt

mungewell commented 1 year ago

What about audio packets? From WireShark log on #1 we have PC->Pedal packet(s) with 2358 bytes each. If we unpack these it looks like the first few bytes might be something different (ie not audio). But output[7:] results in 2048 bytes per packet and gives us a nice clear output waveform.

$ python3 pack_test.py | grep -e "000000[0123]"
00000000: 00 00 BC 96 02 FF 03 00  80 BC 7F 00 BF 7F 80 C2  ................
00000010: 7F 00 C6 7F 80 C8 7F 80  CC 7F 80 CF 7F 80 D2 7F  ................
00000020: 80 D5 7F 80 D8 7F 80 DB  7F 00 DE 7F 00 E1 7F 00  ................
00000030: E4 7F 00 E6 7F 80 E9 7F  00 ED 7F 00 F0 7F 00 F2  ................
$ python3 pack_test.py | tail                  
00000780: 1A 00 00 17 00 00 12 00  00 0F 00 80 0B 00 80 07  ................
00000790: 00 00 03 00 80 FF 7F 00  FB 7F 80 F6 7F 00 F3 7F  ................
000007A0: 00 EE 7F 80 EA 7F 80 E5  7F 00 E1 7F 80 DD 7F 80  ................
000007B0: D9 7F 80 D5 7F 00 D2 7F  80 CE 7F 80 CB 7F 80 C8  ................
000007C0: 7F 80 C5 7F 80 C2 7F 80  BF 7F 80 BD 7F 00 BB 7F  ................
000007D0: 00 B9 7F 80 B7 7F 80 B4  7F 00 B2 7F 00 B0 7F 00  ................
000007E0: AE 7F 00 AC 7F 80 AA 7F  00 A8 7F 00 A7 7F 80 A4  ................
000007F0: 7F 80 A3 7F 80 A2 7F 00  A1 7F 80 9E 7F 00 9E 7F  ................
00000800: 80 9D 7F 80 9C 7F 0D                              .......
None
$ ls -al stream.raw 
-rw-rw-r-- 1 simon simon 2048 Jan 25 12:02 stream.raw
$ sox -r 48k -e signed -b 24 -c 1 --endian little stream.raw stream.wav

converted_audio

Next step is to see if I can script up grabbing packets from log (rather than manually copying/processing) and chain them together to recover whole audio clip.

mungewell commented 1 year ago

Not programmatic, but I was able to tshark to dump all the packets are text and then grep out the ones I wanted. A bit of substitution and I have them as Python b"string".

They stitch together OK, but there is something wrong with the way bytes are being interpreted as audio - causing high level signals to be clipped/inverted... maybe I just have things miss-aligned. messed_up_audio

For reference I used:

$ sox -r 48k -e signed -b 24 -c 1 --endian big stream.raw stream.wav

$ soxi stream.wav 

Input File     : 'stream.wav'
Channels       : 1
Sample Rate    : 48000
Precision      : 24-bit
Duration       : 00:00:03.52 = 169136 samples ~ 264.275 CDDA sectors
File Size      : 507k
Bit Rate       : 1.15M
Sample Encoding: 24-bit Signed Integer PCM
mungewell commented 1 year ago

Basically it looks like the data is just plain weird... if you use 3bytes (for 24bit) the 3rd byte is just 00 or 01 depending on whether the 2nd byte is +ve or -ve.

Never seen this this before, and can't bend Sox to decode it...

$ hexdump -v -e '/1 "%02X\n"' stream.raw | awk '!(NR%3){print q,p,$0}{q=p;p=$0}'           
9E FD 01
86 FD 01
6A FD 01
4C FD 01
36 FD 01
1E FD 01
0E FD 01
FA FC 01
E8 FC 01
D8 FC 01
CA FC 01
BC FC 01
B2 FC 01
...
0C FF 01
26 FF 01
42 FF 01
62 FF 01
7E FF 01
98 FF 01
B4 FF 01
CA FF 01
E2 FF 01
02 00 00
18 00 00
32 00 00
46 00 00
64 00 00
7A 00 00
90 00 00
A6 00 00

So if we don't output the 3rd byte and treat the raw data as 16bit signed, we get a usable audio file. better_audio stream.wav.zip

mungewell commented 1 year ago
$ python3 pack_test.py                           
$ sox -r 48k -e signed -b 16 -c 1 --endian little stream.raw stream.wav

pack_test.py.zip

mungewell commented 1 year ago

I got the code 'talking' to the pedal, and it can perform the 'search' part - looking for recording to download.

search_download.py.txt

$ python3 search_download.py 
Found pedal:
00000000: 30 B1 31 B2 32 B3 33 B4  30 B1 31 B2 32 B3 33 B4  0.1.2.3.0.1.2.3.
00000010: 00 80 02 89 09 8A 0A 8B  0B 8C 8C                 ...........
None
Unpacked Response:
00000000: 80 21 00 00 10 00 00 01  00 00 00 16 27 00 00 0B  .!..........'...
00000010: 28 DF F8 E4 16 DF F8 15                           (.......
None
Checking Address: 0xbcf8
Unpacked Response:
00000000: 00 00 F8 BC 07 01 00 00  07 00 0B                 ...........
None
Checking Address: 0xbcf0
Unpacked Response:
00000000: 00 00 F0 BC 07 01 00 80  31 80 08                 ........1..
None
...

You may need to change the midiname that the code looks for. For me the Rowin Twin Looper reports as DFU.

I found that initially my pedal didn't report any 'addresses', giving response with 0xFF or 0x7F. Then I filled the whole 10mins up with audio, and now it reports different values.

I have attempted to code the next stage, of downloading the sample block. But this is not working yet... It's also worth noting that the official app doesn't work for me, giving an error whenever it is started.

mungewell commented 1 year ago

Finally got the official Software working, the "Data Length Error" seems to be that it's trying to communicate on the wrong Midi interface. I had a Akai Midi/Network driver installed, when I disabled that the software would see the pedal.

Given that I have not seen others comment, and I have an increasing amount of code... I might spin up my own project to cover this pedal. Thoughts...?

mungewell commented 1 year ago

This is where I'm at. I am able to 'search' and then download the first block. This is very rough code, lots to be tested and understood.

For example the data should be a 1Khz wave, which I can't pull out of the data...

$ python3 search_download.py 
Found pedal:
00000000: 30 B1 31 B2 32 B3 33 B4  30 B1 31 B2 32 B3 33 B4  0.1.2.3.0.1.2.3.
00000010: 00 80 02 89 09 8A 0A 8B  0B 8C 8C                 ...........
None
00000000: 00 32 0D 41 00 00 00 00  00 43 00 00 00 02 00 00  .2.A.....C......
00000010: 4E 00                                             N.
None
00000000: 00 C0 10 00 00 08 00 00  27                       ........'
None
Packed response:
00000000: 00 00 43 00 00 00 02 00  00 01 00 00 00 60 62 09  ..C..........`b.
00000010: 00 00 16 20 79 0D 1F 39  0B 5F 71 57              ... y..9._qW
None
Unpacked Response:
00000000: 00 C0 10 00 00 08 00 80  00 00 00 00 8B 13 00 80  ................
00000010: 05 94 6F 7C 72 8B 6F FC  0A                       ..o|r.o..
None
Checking Address: 0xbcf8
Unpacked Response:
00000000: 00 00 F8 BC 07 01 00 00  07 00 0B                 ...........
None
Download Address: 0xbcf8
Unpacked Response:
00000000: 00 00 F8 BC 87 F8 01 00  07 00 80 0F 00 80 1F 00  ................
00000010: 00 2B 00 00 BA FF FF 56  00 00 A5 FF 7F 70 00 00  .+.....V.....p..
00000020: E9 FF FF B7 FF FF E0 FF  FF 0B 00 00 22 00 80 29  ............"..)
00000030: 00 80 F0 FF FF B5 FF FF  2B 00 80 25 00 00 86 00  ........+..%....
00000040: 80 03 00 00 30 00 00 26  00 00 E9 FF FF 24 00 80  ....0..&.....$..
00000050: C4 FF 7F 02 00 80 DD FF  7F 21 00 00 D4 FF FF 1C  .........!......
00000060: 00 00 17 00 80 02 00 00  1E 00 00 08 00 80 68 00  ..............h.
00000070: 80 E9 FF FF 0D 00 00 E1  FF 7F 07 00 00 B1 FF FF  ................
00000080: 4E 00 80 1C 00 80 27 00  00 16 00 00 EC FF FF ED  N.....'.........
00000090: FF 7F EF FF 7F 0A 00 80  F3 FF FF FA FF FF D5 FF  ................
000000A0: FF 2B 00 80 FA FF 7F F8  FF FF FC FF 7F D5 FF FF  .+..............
000000B0: 35 00 00 FB FF FF C1 FF  7F 08 00 80 2C 00 80 CE  5...........,...
000000C0: FF 7F 99 FF FF BB FF FF  F3 FF FF 3E 00 80 4B 00  ...........>..K.
000000D0: 00 0D 00 80 C3 FF 7F 25  00 00 4C 00 00 0F 00 00  .......%..L.....
000000E0: C2 FF FF 1F 00 80 DA FF  FF 16 00 80 08 00 00 B7  ................
000000F0: FF FF 03 00 80 29 00 80  40 00 00 BA FF 7F 1E 00  .....)..@.......
00000100: 00 BE FF FF ED FF FF 1C  00 80 2A 00 80 E6 FF FF  ..........*.....
00000110: 05 00 80 1E 00 00 C6 FF  7F B2 FF 7F EC FF 7F 1B  ................
00000120: 00 80 E6 FF FF 5C 00 80  23 00 80 F3 FF FF 34 00  .....\..#.....4.
00000130: 80 EF FF FF 63 FF 7F 60  00 00 DC FF 7F 07 00 00  ....c..`........
00000140: 1C 00 00 1F 00 80 06 00  00 08 00 00 C9 FF 7F 48  ...............H
00000150: 00 00 CE FF 7F 0F 00 00  0E 00 80 01 00 80 0B 00  ................
00000160: 80 01 00 00 F5 FF 7F 21  00 80 3F 00 00 D0 FF FF  .......!..?.....
00000170: D3 FF 7F EE FF FF 13 00  80 5B 00 80 48 00 00 FB  .........[..H...
00000180: FF FF 31 00 00 C9 FF 7F  E6 FF 7F 2E 00 80 FD FF  ..1.............
00000190: 7F A1 FF FF DA FF 7F 89  FF 7F FF FF 7F 3D 00 00  .............=..
000001A0: A5 FF FF 08 00 00 1E 00  00 02 00 80 CE FF 7F 0C  ................
000001B0: 00 00 BA FF FF F9 FF 7F  CE FF FF 9C FF 7F 05 00  ................
000001C0: 00 FC FF FF 4C 00 00 E3  FF 7F EF FF FF BA FF 7F  ....L...........
000001D0: 22 00 00 34 00 80 03 00  80 DD FF FF D7 FF 7F 31  "..4...........1
000001E0: 00 80 C1 FF FF 0C 00 00  09 00 00 3B 00 80 BD FF  ...........;....
000001F0: 7F EF FF 7F DC FF FF D5  FF FF 10 00 00 0B 00 00  ................
00000200: C0 FF FF AE FF FF 35 00  80 BC FF 7F AD FF FF 34  ......5........4
00000210: 00 80 1E 00 00 EA FF FF  D4 FF FF 1C 00 80 10 00  ................
00000220: 80 D1 FF FF 24 00 00 D0  FF 7F F6 FF 7F 88 FF FF  ....$...........
00000230: FA FF 7F C5 FF FF 15 00  80 1A 00 80 DD FF FF 30  ...............0
00000240: 00 80 C7 FF FF 33 00 00  E6 FF FF A8 FF FF 56 00  .....3........V.
00000250: 00 39 00 80 EB FF FF 31  00 00 EF FF FF F5 FF FF  .9.....1........
00000260: E4 FF 7F BF FF 7F 4D 00  80 E1 FF 7F E7 FF 7F 07  ......M.........
00000270: 00 00 E8 FF FF A0 FF 7F  C9 FF 7F 24 00 80 55 00  ...........$..U.
00000280: 80 FF FF FF CA FF FF 3B  00 80 03 00 80 D5 FF 7F  .......;........
00000290: FF FF 7F 10 00 00 F6 FF  7F 28 00 00 10 00 80 06  .........(......
000002A0: 00 80 D8 FF FF EA FF 7F  15 00 00 60 00 80 07 00  ...........`....
000002B0: 80 FA FF FF AC FF FF B2  FF FF B3 FF 7F 0A 00 80  ................
000002C0: C7 FF FF F8 FF FF 68 FF  FF D4 FF 7F CB FF 7F D9  ......h.........
000002D0: FF FF 1B 00 80 B9 FF 7F  D1 FF 7F 0C 00 00 E9 FF  ................
000002E0: 7F EF FF FF F3 FF FF 3F  00 00 B5 FF 7F 6B 00 80  .......?.....k..
000002F0: 06 00 80 FA FF 7F 3A 00  80 E4 FF 7F DA FF FF D7  ......:.........
00000300: FF FF 1A 00 00 00 00 80  FE FF FF 1A 00 00 BA FF  ................
00000310: 7F 83 00 00 F4 FF 7F 97  00 00 FA FF FF 14 00 00  ................
00000320: E6 FF 7F F9 FF FF 2B 00  00 0C 00 80 51 00 80 B8  ......+.....Q...
00000330: FF FF 70 FF 7F B8 FF FF  A1 FF 7F EC FF FF C3 FF  ..p.............
00000340: 7F 28 00 80 17 00 00 EC  FF FF F6 FF FF 1C 00 80  .(..............
00000350: C7 FF 7F 0A 00 80 06 00  00 CC FF FF B8 FF FF D4  ................
00000360: FF FF 19 00 00 50 00 00  07 00 00 FD FF FF 1F 00  .....P..........
00000370: 80 95 FF 7F 17 00 00 04  00 80 ED FF 7F 0A 00 80  ................
00000380: 35 00 00 F0 FF 7F F0 FF  7F F9 FF 7F F8 FF FF E0  5...............
00000390: FF FF D1 FF FF 58 00 00  FF FF FF 04 00 00 20 00  .....X........ .
000003A0: 00 CC FF 7F 48 00 00 34  00 80 4C 00 80 EA FF FF  ....H..4..L.....
000003B0: 3B 00 00 F0 FF 7F 27 00  00 E3 FF 7F D6 FF 7F CF  ;.....'.........
000003C0: FF FF 25 00 00 01 00 80  A5 FF 7F B3 FF FF 9B FF  ..%.............
000003D0: FF 2F 00 80 57 00 80 F0  FF FF E4 FF FF BE FF FF  ./..W...........
000003E0: FD FF FF 1A 00 00 BA FF  FF 01 00 00 C5 FF 7F 89  ................
000003F0: 00 00 0C 00 00 CD FF 7F  F2 11                    ..........
None

search_download.py.txt

mungewell commented 1 year ago

I've put my code here: https://github.com/mungewell/twinlooper

Please come and make it better ;-)