zoom-zt2
zoom-zt2 copied to clipboard
G1on/G1xon
Does this work with G1on/G1xon and, in general, with pedals that use the ZDL format?
Thanks
In theory the parse might work, but as you noted the pedal uses the (older) ZDL and ZDT formats and these appear quite different.
In theory the parsing could be changed to work with the older format, but I don't have one of these pedals to test against.... and I'm not sure the same uploading MIDI commands could be used.
The G1Four's ZT2 file starts
G1_FOUR_v1.10_Win_E/zt2$ hexdump -C FLST_SEQ.ZT2 | head
00000000 3e 3e 3e 00 00 00 00 00 00 00 00 00 00 00 00 00 |>>>.............|
00000010 00 00 00 00 00 00 00 00 00 00 42 59 50 41 53 53 |..........BYPASS|
00000020 2e 5a 44 32 00 00 00 00 00 00 00 00 01 00 00 00 |.ZD2............|
00000030 00 00 00 00 3c 3c 3c 00 00 00 00 00 00 00 00 00 |....<<<.........|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3e 3e |..............>>|
00000050 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 |>...............|
00000060 00 00 00 00 00 00 00 00 43 4f 4d 50 2e 5a 44 32 |........COMP.ZD2|
00000070 00 00 00 00 00 31 2e 32 30 00 01 10 00 00 01 00 |.....1.20.......|
00000080 00 00 52 41 43 4b 43 4f 4d 50 2e 5a 44 32 00 31 |..RACKCOMP.ZD2.1|
00000090 2e 32 30 00 01 20 00 00 01 00 00 00 53 4c 57 41 |.20.. ......SLWA|
Whereas the G1on's starts```
G1on_v1.21_Win_E$ hexdump -C FLST_SEQ.ZDT | head
00000000 3e 3e 3e 00 00 00 00 00 00 00 00 00 00 00 00 00 |>>>.............|
00000010 00 00 00 00 00 00 00 00 00 00 3c 3c 3c 00 00 00 |..........<<<...|
00000020 00 00 00 00 00 00 00 3e 3e 3e 00 01 00 00 00 00 |.......>>>......|
00000030 00 00 00 00 43 4f 4d 50 2e 5a 44 4c 00 00 00 00 |....COMP.ZDL....|
00000040 00 4f 50 54 43 4f 4d 50 2e 5a 44 4c 00 00 31 36 |.OPTCOMP.ZDL..16|
00000050 30 5f 43 4f 4d 50 2e 5a 44 4c 00 53 4c 57 41 54 |0_COMP.ZDL.SLWAT|
00000060 4b 2e 5a 44 4c 00 00 00 5a 4e 52 2e 5a 44 4c 00 |K.ZDL...ZNR.ZDL.|
00000070 00 00 00 00 00 4e 4f 49 53 45 47 54 45 2e 5a 44 |.....NOISEGTE.ZD|
00000080 4c 00 3c 3c 3c 00 01 00 00 00 00 00 00 00 00 3e |L.<<<..........>|
00000090 3e 3e 00 02 00 00 00 00 00 00 00 00 47 45 51 2e |>>..........GEQ.|
Why not also check out https://github.com/SysExTones/g1on
I am seriously thinking about getting a g1on - the particles reverb sounds amazing. I have an eBAY bid right now ... if I look I will lose it ... wish me luck.
Also the g1on seems to be able to hold 100 patches cf 50 on g1x four.
Hi,
Regarding the G1Xon, has anyone figured out how the effects are arranged in the response you get with the command F0 52 00 64 29 F7
?
I started rearranging effects, or replace them with bypass, but it's still too complicated.
My first goal is to be able to tell how many effects there are in a patch and if they are enabled or not.
This is the original A0 patch:
F0 52 00 64 28 40 21 00 00 02 03 08 00 00 64 00 00 00 00 00 00 04 00 00 00 00 21 00 00 01 16 00 50 00 00 00 00 00 0C 00 00 00 00 00 00 25 00 41 00 00 06 03 20 00 01 64 00 00 00 00 00 00 00 00 00 00 00 11 00 28 00 10 65 30 00 10 40 20 01 00 0C 00 00 00 00 02 00 00 61 02 00 48 04 4D 70 00 57 20 66 65 66 09 06 26 00 00 00 00 20 00 64 22 00 00 00 20 03 00 00 00 00 4D 55 53 45 00 55 4D 20 20 20 20 00 F7
I would appreciate any help. Thanks.
@dkts2000 My memory is a little fuzzy, but I do see the '0x29 - download current patch' command in my midi_notes which i posted here: https://github.com/Barsik-Barbosik/Zoom-Firmware-Editor/files/5662857/midi_notes.txt
I also did some work with uploading/downloading specific patches in the form of a 'PTCF' file. I have a script on my local machine which interpreted these, but it doesn't look like it got published yet.... I'll see if I can get those uploaded in the next couple of days. https://github.com/mungewell/zoom-zt2/blob/master/zoomzt2.py#L368
packet = bytearray(b"\x52\x00\x6e\x09\x00")
packet.append(int(location/10)-1)
packet.append(location % 10)
Looking at your hex dump there does not seem to be ASCII name(s) at the start, even so this is a similar format... but without the text descriptors.
@dkts2000 You may also benefit looking into the alternate way of reading the Patch set-up. https://github.com/mungewell/zoom-zt2/issues/13
This was more like reading the contents of the display, rather than reading the patch setup (CMD 0x29 and CMD 0x09).
@dkts2000 Also note that the string "4D 55 53 45 00 55 4D 20 20 20 20" has a '00' in the middle, this suggests that this is packed (8bit -> 7bit, for transmission over Midi). You may need to unpack it before the block of data makes sense.
https://github.com/mungewell/zoom-zt2/blob/master/zoomzt2.py#L163
@dkts2000 unpack.py.txt
$ python3 unpack.py
00000000: A1 00 00 02 03 08 00 64 00 00 00 00 00 00 00 00 .......d........
00000010: 00 00 A1 00 00 16 00 50 00 00 00 80 0C 00 00 00 .......P........
00000020: 00 00 00 00 C1 00 00 86 03 A0 01 64 00 00 00 00 ...........d....
00000030: 00 00 00 00 00 00 11 00 00 90 65 B0 00 10 40 01 ..........e...@.
00000040: 80 0C 00 00 00 00 00 00 61 02 00 C8 04 F0 00 57 ........a......W
00000050: A0 E6 65 E6 06 26 00 80 00 00 A0 64 22 00 00 00 ..e..&.....d"...
00000060: 20 03 00 00 00 4D 55 53 45 55 4D 20 20 20 20 00 ....MUSEUM .
Thanks for info! It seems i can't fully understand this 8bit to 7bit convertion. I tried to "interpret" your unpack function to c++ but i don't get the same results.
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
void unpack (uint8_t * sysex, uint8_t * data, uint8_t len)
{
// Unpack data 7bit to 8bit, MSBs in first byte
//data = bytearray(b"")
int loop = -1;
uint8_t hibits = 0;
for (int i = 0; i < len; i++) { //byte in packet:
uint8_t byt = sysex[i];
if (loop != -1)
{
uint8_t p = pow (2, loop);
if (hibits & p) {
byt = 0x80 + byt; //data.append(128 + byte)
//} else {
//data.append(byte)
}
data[i] = byt;
loop = loop - 1;
} else {
hibits = byt;
// do we need to acount for short sets (at end of block block)?
loop = 6;
}
}
}
int main()
{
uint8_t dataBuffer[] =
{ 0x40, 0x21, 0x00, 0x00, 0x02, 0x03, 0x08, 0x00, 0x00, 0x64, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x01, 0x16,
0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x41, 0x00, 0x00,
0x06, 0x03, 0x20, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x28,
0x00, 0x10, 0x65, 0x30, 0x00, 0x10, 0x40, 0x20,
0x01, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x61, 0x02,
0x00, 0x48, 0x04, 0x4D, 0x70, 0x00, 0x57, 0x20,
0x66, 0x65, 0x66, 0x09, 0x06, 0x26, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0x64, 0x22, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00,
0x00, 0x00, 0x00,
0x4D, 0x55, 0x53, 0x45, 0x00, 0x55, 0x4D, 0x20, 0x20, 0x20, 0x20, 0x00
};
uint8_t outputBuffer[200] = { 0 };
int bufsize = sizeof (dataBuffer) / sizeof (dataBuffer[0]);
unpack (dataBuffer, outputBuffer, bufsize);
}
this code gives me:
00 A1 00 00 02 03 08 00 00 64 00 00 00 00 00 00 00 00 00 00 00 A1 00 00 00 16 00 50 00 00 00 80 00 0C 00 00 00 00 00 00 00 00 C1 00 00 86 03 A0 00 01 64 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 90 65 B0 00 10 40 00 01 80 0C 00 00 00 00 00 00 00 61 02 00 C8 04 00 F0 00 57 A0 E6 65 E6 00 06 26 00 80 00 00 A0 00 64 22 00 00 00 20 03 00 00 00 00 4D 55 53 45 00 55 4D 20 20 20 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
but obviously something is wrong.
are you in a EU timezone @dkts2000 ? if so maybe we can also look at this together? My plan (see my YouTube) is to get a Ctrlr panel working for these pedals with a 21.5" touch screen for the gigs I play.
I recently got a B1on for the Particle reverb - other than that I have the G1XFour and B1XFour [[sure before I realised the great work done here that makes them near identical!]].
Anyhow have you considered adding some debug to Mungewell's python to allow you to cross check against your C++? I also ported to Lua and am using on Ctrlr running on a Raspberry Pi 4. The Pi 4 is also capable of running Mungewell's code, once you hack it a little because of import changes.
I will be making more YT and posting to these forums as I find more about the B1On which is pretty darn similar to the G1on. I strongly suspect we can use the same "get file content from midi" on these devices.
WHAT DID I HACK TO CHECK ALGORITHMS?
In the zoomzt2.py I added couple of routines locally:
def printhex(direct, msg):
print(direct)
l = []
numchar=0
for n in msg:
l.append(n)
numchar=numchar+1
if numchar % 8 == 0:
print(" ".join( "{0:#0{1}x}".format(int( m ), 4) for m in l))
l = []
if l is not None:
print(" ".join( "{0:#0{1}x}".format(int( m ), 4) for m in l))
def printExtrahex(direct, msg):
print(direct + " 0xf0 ")
printhex(direct, msg)
print(direct + " 0xf7 ")
def sniffMidiOut(mtype, data, printme = False):
if printme == True:
print("sniffMidiOut")
if mtype == "sysex":
printExtrahex("===>\t", data)
else:
printhex("===>\t", data)
return mido.Message(mtype, data = data)
def sniffMidiIn(self, printme = False):
msg = self.inport.receive()
if printme == True:
print("sniffMidiIn")
if msg.type == "sysex":
printExtrahex("<====\t", msg.data)
else:
printhex("<====\t", msg.data)
return msg
then added some more annotations to print the blocks:
def file_download(self, name):
# download file from pedal to PC
print("In file_download {}".format(name))
packet = bytearray(b"\x52\x00\x6e\x60\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00")
print("packet")
head, tail = os.path.split(name)
self.filename(packet, tail)
msg = sniffMidiOut("sysex", data = packet)
# msg = mido.Message("sysex", data = packet)
self.outport.send(msg); sleep(0); msg = sniffMidiIn(self)
# Read parts 1 through 17 - refers to FLST_SEQ, possibly larger
data = bytearray(b"")
while True:
sData = [0x52, 0x00, 0x6e, 0x60, 0x05, 0x00]
msg = sniffMidiOut("sysex", data=sData)
#msg = mido.Message("sysex", data = [0x52, 0x00, 0x6e, 0x60, 0x05, 0x00])
self.outport.send(msg); sleep(0); msg = sniffMidiIn(self)
#sData = [0x52, 0x00, 0x6e, 0x60, 0x22, 0x14, 0x2f, 0x60, 0x00, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x00]
sData = [0x52, 0x00, 0x6e, 0x60, 0x22, 0x14, 0x2f, 0x60, 0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, 0x00]
msg = sniffMidiOut("sysex", data=sData)
#msg = mido.Message("sysex", data = [0x52, 0x00, 0x6e, 0x60, 0x22, 0x14, 0x2f, 0x60, 0x00, 0x0c, 0x00, 0x04, 0x00, 0x00, 0x00])
self.outport.send(msg); sleep(0); msg = sniffMidiIn(self)
sData = [0x52, 0x00, 0x6e, 0x60, 0x05, 0x00]
msg = sniffMidiOut("sysex", data=sData)
#msg = mido.Message("sysex", data = [0x52, 0x00, 0x6e, 0x60, 0x05, 0x00])
self.outport.send(msg); sleep(0); msg = sniffMidiIn(self)
#decode received data
packet = msg.data
length = int(packet[9]) * 128 + int(packet[8])
if length == 0:
print("WE GOT ZERO LEN BACK")
break
print("WE GOT {} LEN BACK".format(length))
block = self.unpack(packet[10:10 + length + int(length/7) + 1])
print("HERE IS THE BLOCK!! {} from {}".format(len(block), len(packet)+2))
printhex("BLOCK ", block)
# confirm checksum (last 5 bytes of packet)
# note: mido packet does not have SysEx prefix/postfix
checksum = packet[-5] + (packet[-4] << 7) + (packet[-3] << 14) \
+ (packet[-2] << 21) + ((packet[-1] & 0x0F) << 28)
if (checksum ^ 0xFFFFFFFF) == binascii.crc32(block):
data = data + block
else:
print("Checksum error", hex(checksum))
break
return(data)
This was especially handy when comparing Ctrlr - turns out some Linux low level Midi has a 512 byte max buffer. So I found some of the SYSEX has a buffer size 0x04 ... so I dropped it to 0x02 and went round twice ... got the data.
Well, my c++ and python knowledge is minimal, so i won't be much of a help. I try to make an arduino based controller for the G1Xon. I can receive the SysEx responses from the g1xon but i can't decode them. My first goal is to be able to tell how many effects there are in a patch and if they are enabled or not.
OK so now I see why you want C/C++ for Arduino. I also do a lot of Arduino and more recently ESP32 but then I realized the Pi 4 has much more power, has GPIO if one needs it BUT with a touchscreen well ... much easier than playing with a load of IC4067.
Let me see if I can check your C/C++. Will try at the weekend.
I found another piece of code for decoding :
uint8_t sysex_to_data(uint8_t *sysex, uint8_t *data, uint8_t len) {
uint8_t cnt;
uint8_t cnt2 = 0;
uint8_t bits = 0;
for (cnt = 0; cnt < len; cnt++) {
if ((cnt % 8) == 0) {
bits = sysex[cnt];
} else {
data[cnt2++] = sysex[cnt] | ((bits & 1) << 7);
bits >>= 1;
}
}
return cnt2;
}
this gives:
21 00 00 02 03 08 80 64 00 00 00 00 00 00 00 00 80 00 21 00 00 96 00 50 00 00 00 00 0C 00 00 00 00 00 00 80 41 80 00 06 83 20 01 64 00 00 00 00 00 00 00 00 00 00 11 00 00 10 65 B0 00 90 40 01 00 0C 00 00 80 00 00 80 61 02 00 48 04 F0 00 D7 A0 66 65 E6 86 26 00 80 00 00 20 64 22 00 00 00 20 03 00 00 00 4D 55 53 45 55 4D 20 20 20 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
if that helps
I was going to say I hacked your original code to pront out the into.
#include <iostream>
#include <iomanip>
#include <string>
#include <cmath>
using namespace std;
void unpack (uint8_t * sysex, uint8_t * data, uint8_t len)
{
// Unpack data 7bit to 8bit, MSBs in first byte
//data = bytearray(b"")
int loop = -1;
uint8_t hibits = 0;
for (int i = 0; i < len; i++) { //byte in packet:
uint8_t byt = sysex[i];
if (loop != -1)
{
uint8_t p = pow (2, loop);
if (hibits & p) {
byt = 0x80 + byt; //data.append(128 + byte)
//} else {
//data.append(byte)
}
data[i] = byt;
loop = loop - 1;
} else {
hibits = byt;
// do we need to acount for short sets (at end of block block)?
loop = 6;
}
std::cout << std::setfill('0') << std::setw(2) << std::hex << (0xff & (u
nsigned int)data[i]) << " ";
if (i % 8 == 0) std::cout << std::endl;
}
}
int main()
{
uint8_t dataBuffer[] =
{ 0x40, 0x21, 0x00, 0x00, 0x02, 0x03, 0x08, 0x00, 0x00, 0x64, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x01, 0x16,
0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x41, 0x00, 0x00,
0x06, 0x03, 0x20, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x28,
0x00, 0x10, 0x65, 0x30, 0x00, 0x10, 0x40, 0x20,
0x01, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x61, 0x02,
0x00, 0x48, 0x04, 0x4D, 0x70, 0x00, 0x57, 0x20,
0x66, 0x65, 0x66, 0x09, 0x06, 0x26, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0x64, 0x22, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00,
0x00, 0x00, 0x00,
0x4D, 0x55, 0x53, 0x45, 0x00, 0x55, 0x4D, 0x20, 0x20, 0x20, 0x20, 0x00
};
uint8_t outputBuffer[200] = { 0 };
int bufsize = sizeof (dataBuffer) / sizeof (dataBuffer[0]);
unpack (dataBuffer, outputBuffer, bufsize);
}
What I will do now (sorry in the middle of day job) is compare with an existing decode. I dont know why you use i for input AND output? Shouldnt the output be 7/8's as large - sorry I not looked at the code too deeply. Just added on some output diagnostics.
i'm sure my interpretation of the python code is wrong. Try the other piece of code i posted earlier. i found it here: https://blogs.bl0rg.net/netzstaub/2008/08/14/encoding-8-bit-data-in-midi-sysex/
So I think you are wrong iterator. Something like this in your original code - notice we track input with "i" and output with "j"
pi@raspberrypi:~/Software/zoom-zt2 $ more test1.cpp
#include <iostream>
#include <iomanip>
#include <string>
#include <cmath>
using namespace std;
void
unpack (uint8_t * sysex, uint8_t * data, uint8_t len)
{
// Unpack data 7bit to 8bit, MSBs in first byte
//data = bytearray(b"")
int loop = -1;
uint8_t hibits = 0;
int j = 0;
for (int i = 0; i < len; i++)
{ //byte in packet:
uint8_t byt = sysex[i];
if (loop != -1)
{
uint8_t p = pow (2, loop);
if (hibits & p)
{
byt = 0x80 + byt; //data.append(128 + byte)
//} else {
//data.append(byte)
}
data[j++] = byt;
loop = loop - 1;
}
else
{
hibits = byt;
// do we need to acount for short sets (at end of block block)?
loop = 6;
}
std::cout << std::setfill ('0') << std::
setw (2) << std::hex << (0xff & (unsigned int) data[j-1]) << " ";
if (i % 8 == 0)
std::cout << std::endl;
}
}
int
main ()
{
uint8_t dataBuffer[] =
{ 0x40, 0x21, 0x00, 0x00, 0x02, 0x03, 0x08, 0x00, 0x00, 0x64, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x01, 0x16,
0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x41, 0x00, 0x00,
0x06, 0x03, 0x20, 0x00, 0x01, 0x64, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x28,
0x00, 0x10, 0x65, 0x30, 0x00, 0x10, 0x40, 0x20,
0x01, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x61, 0x02,
0x00, 0x48, 0x04, 0x4D, 0x70, 0x00, 0x57, 0x20,
0x66, 0x65, 0x66, 0x09, 0x06, 0x26, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0x64, 0x22, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00,
0x00, 0x00, 0x00,
0x4D, 0x55, 0x53, 0x45, 0x00, 0x55, 0x4D, 0x20, 0x20, 0x20, 0x20, 0x00
};
uint8_t outputBuffer[200] = { 0 };
int bufsize = sizeof (dataBuffer) / sizeof (dataBuffer[0]);
unpack (dataBuffer, outputBuffer, bufsize);
}
pi@raspberrypi:~/Software/zoom-zt2 $ ./test1
b6
a1 00 00 02 03 08 00 00
64 00 00 00 00 00 00 00
00 00 00 00 a1 00 00 00
16 00 50 00 00 00 80 80
0c 00 00 00 00 00 00 00
00 c1 00 00 86 03 a0 a0
01 64 00 00 00 00 00 00
00 00 00 00 00 11 00 00
00 90 65 b0 00 10 40 40
01 80 0c 00 00 00 00 00
00 00 61 02 00 c8 04 04
f0 00 57 a0 e6 65 e6 e6
06 26 00 80 00 00 a0 a0
64 22 00 00 00 20 03 03
00 00 00 4d 55 53 45 45
55 4d 20 20 20 20 00
you are right! if you ommit the cout part:
std::cout << std::setfill ('0') << std::
setw (2) << std::hex << (0xff & (unsigned int) data[j-1]) << " ";
if (i % 8 == 0)
std::cout << std::endl;
which i think adds some 00s between the parts, and outpout the outputBuffer you get :
A1 00 00 02 03 08 00 64 00 00 00 00 00 00 00 00 00 00 A1 00 00 16 00 50 00 00 00 80 0C 00 00 00 00 00 00 00 C1 00 00 86 03 A0 01 64 00 00 00 00 00 00 00 00 00 00 11 00 00 90 65 B0 00 10 40 01 80 0C 00 00 00 00 00 00 61 02 00 C8 04 F0 00 57 A0 E6 65 E6 06 26 00 80 00 00 A0 64 22 00 00 00 20 03 00 00 00 4D 55 53 45 55 4D 20 20 20 20 00
which is the same as the one mungewell gave
00000000: A1 00 00 02 03 08 00 64 00 00 00 00 00 00 00 00 .......d........
00000010: 00 00 A1 00 00 16 00 50 00 00 00 80 0C 00 00 00 .......P........
00000020: 00 00 00 00 C1 00 00 86 03 A0 01 64 00 00 00 00 ...........d....
00000030: 00 00 00 00 00 00 11 00 00 90 65 B0 00 10 40 01 ..........e...@.
00000040: 80 0C 00 00 00 00 00 00 61 02 00 C8 04 F0 00 57 ........a......W
00000050: A0 E6 65 E6 06 26 00 80 00 00 A0 64 22 00 00 00 ..e..&.....d"...
00000060: 20 03 00 00 00 4D 55 53 45 55 4D 20 20 20 20 00 ....MUSEUM .
Thanks! i'll try to implement the pack function as well and then experiment with different patches to try to figure out the patch parts. I'm curious though, why the other code i found gives different result.
Hey! I left the cout there to show you what it was doing :-)
I even wrote a better version that produces this:
Input:
40 21 00 00 02 03 08 00
Output:
a1 00 00 02 03 08 00
Input:
00 64 00 00 00 00 00 00
Output:
64 00 00 00 00 00 00
Input:
04 00 00 00 00 21 00 00
Output:
00 00 00 00 a1 00 00
Input:
01 16 00 50 00 00 00 00
...
so you can see the transformation. You might like to do the same on the other code you found to understand it... or just accept the unpack is taking 8 bytes and transforming to 7 ... so you cannot use "i" for both input and output stream.
Anyhow I am starting to play with the B1On as well as the other B1XFour/G1XFour. Would be cool to collaborate some how.
BTW you notice - of the special BYTE is 00 you can just drain next 8 chars and move to next chunk. You do have to be careful when there are insufficient input bytes to encode 8 -> 7.
I can't convert the pack function to c++.
def pack(self, data):
# Pack 8bit data into 7bit, MSB's in first byte followed
# by 7 bytes (bits 6..0).
packet = bytearray(b"")
encode = bytearray(b"\x00")
for byte in data:
encode[0] = encode[0] + ((byte & 0x80) >> len(encode))
encode.append(byte & 0x7f)
if len(encode) > 7:
packet = packet + encode
encode = bytearray(b"\x00")
# don't forget to add last few bytes
if len(encode) > 1:
packet = packet + encode
return(packet)
packet and encode are uint8_t arrays?
EDIT: finally i made a working function in c++. I leave it here in case someone wants it:
std::vector<uint8_t> encodePacket(std::vector<uint8_t> data)
{
// # Pack 8bit data into 7bit, MSB's in first byte followed
//# by 7 bytes (bits 6..0).
auto len = data.size();//is this right??? or iterate using for (size_t i=0;i<data.size()....)?
std::vector<uint8_t> packet; //packet = bytearray(b"")
std::vector<uint8_t> encode(1); //encode = bytearray(b"\x00")
encode[0] = 0;
for (int i = 0; i < len; i++) { //for byte in data:
uint8_t b = data[i];
auto len_encode=encode.size();//???. code also works if i use >> encode.size() for the bit shifting
encode[0] = encode[0] + ((b & 0x80) >> len_encode); //encode[0] = encode[0] + ((byte & 0x80) >> len(encode))
encode.push_back(b & 0x7f); //encode.append(byte & 0x7f)
if (encode.size() > 7) { //if len(encode) > 7:
packet.insert(packet.end(), encode.begin(), encode.end()); //packet = packet + encode
encode.clear(); //encode = bytearray(b"\x00")
encode.push_back(0);
}
}
//# don't forget to add last few bytes
if (encode.size() > 1) { //if len(encode) > 1:
packet.insert(packet.end(), encode.begin(), encode.end()); //packet = packet + encode
}
return packet;
}
can u be a little more specific? so all it is doing is the reverse of the unpack. Previous 8 sysex bytes get turned into 7 8-bit bytes for processing in your local computer (in your case Arduino but I more or less gave up on them for reasonably processing tasks because they hit nasty limits - and since I used to use embedded processors professionally I know when to quit with them).
Now your mission is to take 7 8-bit bytes and turn it into 8 7 bit bytes.
In C++ you have Vectors, Lists etc. Arduino has a Standard Library clone (I used it to create arpegiator unit for a Midi controller).
So create a blank vector of bytes (unsigned chars). read in 7 bytes from the packed data. create a new byte from top bits of next 7 bytes. that I think is the first. Then push_back the 7 bytes & 0x7F to mask off the top bits.
And you can see that is what Mungwell is doing - but in Python. I suspect the new phantom byte is (pseudo code)
pb = 0
for i = 0 .. 6
# check if the byte[0] top bit is top bit of first byte
pb |= (byte[i]&128)>>(6 - i)
# but if byte[0] top bit is least significant bit it might be
# pb |= (byte[i]&128)>>i
# actually look at Mungwell's code. The encode starts empty list.
# then he uses len(encode) as the shift - and he check BEFORE he expands.
# so I think we should use pb |= (byte[i]&128)>>i
end
That would encode the top bit
then we would push the pb first.
Then iterator 0 ... 6 and push back byte[i] & 0x7F
But - check out how elegant Mungewell's code is ... the does it in one pass. Then when he gets to the next "pb" see how he seeds it. I look at a LOT of code and this is really neat stuff.
BTW I added SYSEX for controlling parameters on a B1ON ... my guess is change 65 to come other string on the G1ON and it should work. Will be checking some more and publishing as I find them.
Ok I think I sort of got it. What I notice on the B1On cf the B1X is often times it is prudent to resend the "EditorOn" - this was also pointed out in the other link I added above. It's only now I have on that I realise way. But in the meantime I also got into the Zoom-Firmware-Editor that Mungewell also worked on. And, other than it being WAY too cold to go into garage to test firmware upgrade on PC, seems to allow me to take a B1XFour's firmware, pull out Particles and other FX from a Zoom 70 and inject them. Let's see whether they are all in the Guard blocker or now.
Back to the B1ON.
I am using the following helper scripts on the Raspberry Pi.
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more EditorOn.sh
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
amidi -p ${MIDI_DEV} -S "f0 52 00 65 50 f7" -r temp.bin -t 1 ; hexdump -C temp.bin
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more GetMoreData.sh
#!/bin/bash
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
probeString="F0 52 00 65 60 05 00 F7"
echo ${probeString}
theFile=temp.$$
amidi -p ${MIDI_DEV} -S ${probeString} -r ${theFile} -t 1 ; hexdump -C ${theFile}; rm ${theFile}
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more GetCurrentPatch.sh
#!/bin/bash
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
# F0 52 00 65 29 00 F7
probeString="F0 52 00 65 29 F7"
echo ${probeString}
theFile=temp.$$
amidi -p ${MIDI_DEV} -S ${probeString} -r ${theFile} -t 1 ; hexdump -C ${theFile}; rm ${theFile}
Why the Pi4? Eventually that is the brain I want to use to control it. It is basically Linux and I can run Ctrlr and normal amidi on it to check things out without having to run my Windows box in garage - but tomorrow I will use it to check the state of injected firmware since updating needs a Windows box.
What I did was to go to some patch, empty it all out. Grab the sysex. Then systematically add the first FX on a B1On into the next slot, capturing in between. And I used the above program to decode.
After a while I see what I think is the pattern. Let me give a "for example" with the repeated FX.
61 00 00 c2 01 b0 01 32 00 00 00 00 00 00 00 00 00 00
61 00 00 c2 01 b0 01 32 00 00 00 00 00 00 00 00 00 00
61 00 00 c2 01 b0 01 32 00 00 00 00 00 00 00 00 00 00
61 00 00 c2 01 b0 01 32 00 00 00 00 00 00 00 00 00 00
61 00 00 c2 01 b0 01 32 00 00 00 00 00 00 00 00 00 00
a0 64 04 00 00 00 20 03 00 00 00 54 43 20 43 4c 45 41 4e 20 20 00
T C C L E A N
So see you were passing all the bytes EXCEPT the F7 to unpack AFTER the 28. 0x29 was used to run the command. On a Zoom often times one less is the command response, in this case 0x28
So the above is the decoded. And it seems 18 bytes are in the FX for slots 1, 2, 3, 4, 5 then some more cruft. Then the 10 chars at the end for a name. In this case TC CLEAN. So my guess is the first param is some ID. Then likely some group.
So now I do a similar trick on "SLAP".
41 00 00 c2 08 30 00 64 a0 00 00 00 00 00 00 00 00 00
61 00 00 84 07 40 01 64 00 00 00 00 00 00 00 00 00 00
31 00 00 c4 03 70 00 0d 80 a1 c1 e1 c1 08 00 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
S L A P
60 64 04 00 00 00 20 03 00 00 00 53 4c 41 50 20 20 20 20 20 20 00
Now SLAP only has slots 1 to 3 in use. So we notice "BYPASS" must be the last two rows. I see see if the others are some FXID, GroupID and maybe some number per parameter.
So I will see if I can hack some of Mungewell's construct in Python to parse these B1On's
Lots of information here, and other bugs, on the G1on - so it looks like you are making progress, even if I don't have the capacity to follow all of it....
When I was looking at the patch files from the G1Four it took me a while to figure out that the values for each parameter are stored in n-bit values spread across byte boundaries. The first parameters had more bits than the later... it may be the same for the G1on. The Construct code is a little funky because I couldn't figure out how to describe these split values....
I wrote up a lot here: https://github.com/mungewell/zoom-zt2/issues/7#issuecomment-630428690
Good luck, happy to help where I can ;-)
Thanks. I am taking a break to watch FA cuo football. I have written an automation to generate parameter changes then unpack.
So fx1 P1 spans 2 bytes and seems to be low high - I keep forgetting it is an 8 bit when unpacked.
0 - 02 1 - 42 so shift right 6 Sounds familiar? 2 - 82 3 - c2 4 - 02 01 = 102 5 - 42 01 = 142 6 - 82 01 = 182 7 - c2 01 = 1c2 8 - 02 02 = 202 9 - 42 02 = 242
242 = 1001000010 shift right 6 1001 = 9
Looks promising.
On Sun, 24 Jan 2021 16:53 mungewell, [email protected] wrote:
Lots of information here, and other bugs, on the G1on - so it looks like you are making progress, even if I don't have the capacity to follow all of it....
When I was looking at the patch files from the G1Four it took me a while to figure out that the values for each parameter are stored in n-bit values spread across byte boundaries. The first parameters had more bits than the later... it may be the same for the G1on. The Construct code is a little funky because I couldn't figure out how to describe these split values....
I wrote up a lot here: #7 (comment) https://github.com/mungewell/zoom-zt2/issues/7#issuecomment-630428690
Good luck, happy to help where I can ;-)
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/mungewell/zoom-zt2/issues/6#issuecomment-766393326, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFO7EVZHVUWSA6PPX75X6VTS3RGB5ANCNFSM4M5FBJ5A .
Nice hints - I will look at that tomorrow. Dinner time.
In the meantime @dkts2000 maybe you already have a methodology but here's what I am doing (I NEED to change my summary to make it clear what I am seeing - sorry I just filter thru tons of info really quick - then assume because I can see it so can others - Asperger's can be a curse).
ExerciseFXParam.sh
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more ExerciseFXParam.sh
#!/bin/bash
#
# Set your current patch to all Bypass.
# Then load an FX.
# modify the parameter by one each time
# store output in a file
exerFX=1
exerParam=2
./FXM_PN.sh ${exerFX} ${exerParam} 0
# first FX param let's try on to 10 as most are not on/off
theFile=FXInvestigation.txt
rm ${theFile}
for i in {1..10}
do
./EditorOn.sh
./GetMoreData.sh
./GetCurrentPatch.sh >> ${theFile}
./B1OnPatchUnpack000 currentPatch.bin >> ${theFile}
./FXM_PN.sh ${exerFX} ${exerParam} ${i} >> ${theFile}
done
# what changes for JUST FX P1 moving from 0 to 10?
So the above copies crap systematically to a file. Then over a tea or coffee you see the info. zB (this is the summary I NEED to make!!!)
OUTPUT
p1 = ([4]*256+[3])>>6
0 1 2 3 4 5 6
61 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 08 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 10 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 18 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 20 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 28 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 30 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 38 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 40 00 00 00 00 00 00 00 00 00 00 00 00
61 00 00 02 00 48 00 00 00 00 00 00 00 00 00 00 00 00
4 5
0 -> 00 00 0
1 -> 08 00 01000
2 -> 10 00 10000
3 -> 18 00 11000
4 -> 20 00 100000
5 -> 28 00 101000
6 -> 30 00 110000
7 -> 38 00 111000 so right shit 3
8 -> 40
9 -> 48
10 ->
And hence I adjust my C++ to write out more summary. I modified your port of Mungewells code to use C++ vector. Then we can push back like our Python buddies.
B1OnPatchUnpack000.cpp
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more B1OnPatchUnpack000.cpp
#include <iostream>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <string>
#include <cmath>
#include <vector>
using namespace std;
typedef unsigned char BYTE;
vector<BYTE> unpack (vector<BYTE> &vi);
vector<BYTE> unpack ( vector<BYTE> &sysex )
{
// Unpack data 7bit to 8bit, MSBs in first byte
//data = bytearray(b"")
int loop = -1;
uint8_t hibits = 0;
int j = 0;
vector<BYTE> unpacked;
// We expect this unpacked sysex to start from byte 5 (0 bias)
for (int i = 5; i < sysex.size() - 5 - 1; i++)
{
//byte in packet:
uint8_t byt = sysex[i];
if (loop != -1)
{
uint8_t p = pow (2, loop);
if (hibits & p)
{
byt = 0x80 + byt; //data.append(128 + byte)
unpacked.push_back(byt);
}
else
{
unpacked.push_back(byt);
}
loop = loop - 1;
}
else
{
hibits = byt;
// do we need to acount for short sets (at end of block
block)?
loop = 6;
}
}
// Start to summarize what we know
cout << "FX1 P1 = " << ( ( (unpacked[4] << 8) + (unpacked[3]) ) >> 6) <<
endl;
cout << "FX1 P2 = " << ( ( (unpacked[6] << 8) + (unpacked[5]) ) >> 3) <<
endl;
cout << endl;
return unpacked;
}
vector<BYTE> readFile(char* filename)
{
// open the file:
ifstream file(filename, ios::binary | ios::in);
// Stop eating new lines in binary mode!!!
file.unsetf(ios::skipws);
// get its size:
streampos fileSize;
file.seekg(0, ios::end);
fileSize = file.tellg();
file.seekg(0, ios::beg);
vector<BYTE> vec;
// reserve capacity
vec.reserve(fileSize);
// read the data:
vec.insert(vec.begin(),
istream_iterator<BYTE>(file),
istream_iterator<BYTE>());
// cout << "vec was " << vec.size() << endl;
file.close();
return vec;
}
int
main (int argc, char **argv)
{
/* read the file in, strip off header and F7 */
cout << "Infile: " << argv[1] << endl;
ifstream in_f(argv[1], ios::in | ios::binary);
// ofstream out_f(argv[1], ios::out | ios::binary);
vector<BYTE> vo;
vector<BYTE> vi = readFile(argv[1]);
cout << "INPUT\n";
int ctr = 0;
for(auto i: vi)
{
if (ctr == 5) cout << endl;
if (ctr > 5 && ((ctr - 5) % 16 == 0)) cout << endl;
ctr++;
int value = i;
cout << setfill ('0') << setw (2) << hex << (0xff & (BYTE) value
) << " ";
}
cout << endl;
// cout << "file size: " << vi.size() << endl;
vo = unpack (vi);
ctr = 0;
// we expect 5 x 18 bytes then rest.
cout << "OUTPUT\n";
for (auto i: vo)
{
if (ctr % 18 == 0) cout << endl;
ctr++;
int value = i;
cout << setfill ('0') << setw (2) << hex << (0xff & (BYTE) value
) << " ";
}
cout << endl;
return 0;
}
So this effectively closes the loop on generating candidate sysex strings. It's similar to how I do it in other synths (except ****** AKAI Miniak where you cannot get it to generate such strings!!).
FXM_PN.sh
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more FXM_PN.sh
#!/bin/bash
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
#
# seems to also dump, sometimes, state of the effect?
# but not consistently - likely based on EditorOn
#
# expect FX Param value
# ie 1 1 10
# 2 3 20
theFX=$1
theParam=$2
theValue=$3
theFXMod=$(($theFX-1))
theParamMod=$(($theParam+1))
hexFX=`printf "%02x" $theFXMod`
hexParam=`printf "%02x" $theParamMod`
theVlow=$(($theValue & 127))
theVhigh=$(( ($theValue / 128) & 127 ))
hexValueLow=`printf "%02x" $theVlow`
hexValueHigh=`printf "%02x" $theVhigh`
echo $theValue
echo $hexValueLow
echo $hexValueHigh
probeString="F0 52 00 65 31 ${hexFX} ${hexParam} ${hexValueLow} ${hexValueHigh}
F7"
echo ${probeString}
theFile=temp.$$
amidi -p ${MIDI_DEV} -S ${probeString} -r ${theFile} -t 1 ; hexdump -C ${theFile
}; rm ${theFile}
EditorOn.sh
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more EditorOn.sh
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
amidi -p ${MIDI_DEV} -S "f0 52 00 65 50 f7" -r temp.bin -t 1 ; hexdump -C temp.b
in
GetMoreData.sh
This seems to clear the buffer - no idea if it is correct but I am engineering with it.
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more GetMoreData.sh
#!/bin/bash
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
probeString="F0 52 00 65 60 05 00 F7"
echo ${probeString}
theFile=temp.$$
amidi -p ${MIDI_DEV} -S ${probeString} -r ${theFile} -t 1 ; hexdump -C ${theFile
}; rm ${theFile}
GetCurrentPatch.sh
This gets current patch
pi@raspberrypi:~/Software/ZoomPedalB1ON $ more GetCurrentPatch.sh
#!/bin/bash
export MIDI_DEV=`amidi -l | grep ZOOM | awk '{print $2}'`
# F0 52 00 65 29 00 F7
probeString="F0 52 00 65 29 F7"
echo ${probeString}
theFile=currentPatch.bin
rm ${theFile}
amidi -p ${MIDI_DEV} -S ${probeString} -r ${theFile} -t 1 ; hexdump -C ${theFile
}
As you can see in the C++ I found the first 2 param for FX1. And it looks like it will be an offset to find the others?
I will deffo need Mungewell's insights to find the FX and checksums.
My guess is 12 bit packed across byte boundaries? In the unpacked, starting a 0 offset it seems like for FX1.
// Start to summarize what we know
cout << "FX1 P1 = " << ( ( (unpacked[ 4] << 8) + (unpacked[ 3]) ) >> 6 ) << endl;
cout << "FX1 P2 = " << ( ( (unpacked[ 6] << 8) + (unpacked[ 5]) ) >> 3 ) << endl;
// might need some masking? So looks like we have a byte shared. Maybe 12 bit values?
// so I probably should also mask when shifting right except 4 is < 5 so the value self masks?
cout << "FX1 P3 = " << ( ( ((unpacked[ 8] & 0xF) << 8) + (unpacked[ 7]) ) ) << endl;
cout << "FX1 P4 = " << ( ( ((unpacked[ 9] & 0xF) << 8) + (unpacked[ 8]) ) >> 5 ) << endl;
cout << "FX1 P5 = " << ( ( ((unpacked[10] & 0xF) << 8) + (unpacked[ 9]) ) >> 5 ) << endl;
cout << "FX1 P6 = " << ( ( ((unpacked[11] & 0xF) << 8) + (unpacked[10]) ) >> 5 ) << endl;
cout << "FX1 P7 = " << ( ( ((unpacked[12] & 0xF) << 8) + (unpacked[11]) ) >> 5 ) << endl;
cout << "FX1 P8 = " << ( ( ((unpacked[13] & 0xF) << 8) + (unpacked[12]) ) >> 5 ) << endl;
And it looks like we add 18 x (FX - 1) for the others to those offsets (to be detemined) I think I dont need to mask top/bottom nybble in all cases base on my comment above. If bottom 4 bits dont count but we right shift more ... it's academic.
What I am going to do is see if I can find a 9 parameter FX. Ba GEQ is 8. And of course 10 = 0xa ... makes me wonder if that is why volume is at 10?
Then I will write a random number generate and do some Monte Carlo over the parameter space. So I will look for FX1 P1 - 9 set to 9 randoms. Then check for each value do I get a bingo? If so great. If not .. have I hit a limiter due to the TMS enforcing a max/min? I did generate a JSON file on the B1XFour for the parameters - that was tedious. I was hoping to use something like this to trial sysex 0 ... increment up in 10s say. If stops increasing then go back to previous 10, go up in 5, etc etc .. find the max midi value for a given FX and param.
Oh and in the above ... my guess is 3, 4, 5, 6 might also be shared with FX/Group. More as I find it.
I hope they have the ASCII format output like on B1XFour. That would make it easier to match Sysex value to displayed value.
I put dumps of 1.30 patches, their zeroed out versions (with my decode) so one can read off the FXID/GID in the patch - I need to do a bit more work to map to the specific FX. You can find it here - https://github.com/shooking/ZoomPedalFun/tree/main/B1ON/DerivedData/1.30
Equally, if you look up a few directories you get the current summary of what I know.
Taking a break to play out some of the new Sysex I found from a GCE-3 on a B1Xfour. Finding kinky ways to do similar things.