Reverse engineering Midi Quest SQZ files

I’ve recently acquired a few Roland synths, one of them being the Roland D-20. It has some nice sounds, and you can make your own patches, so I decided to give it a go. However, editing parameters on the D-20 itself is quite a chore, so I looked for a way to do it via the computer. There are basically two ways of doing this: one is to send MIDI SysEx messages, which are documented in its manual, and the other is to use a software that does this for you, and has a more usable GUI.

I’ve come across Midi Quest by SoundQuest, which seems to be quite an old software company, since I’ve heard it mentioned in a late ’80’s instructional video. Anyway, Midi Quest seems to do at least part of what I need, so i figured I’d give it a try. However, the cheapest version is $150, and I’m still not sure if it’s worth the purchase. There’s a cracked pro version floating around, but I haven’t managed to get it working without the demo restrictions.

Recently I’ve come to the conclusion that a lot of audio related stuff is way overpriced. An example is the hardware synth Waldorf Streichfett, which you can find (as of this writing) for around $250 on eBay. I’ve looked at a teardown of the device, and it seems it’s a STM32F3 series microcontroller, and some knobs and buttons in a box. If you do some quick math, it seems really overpriced.

Anyway, Midi Quest has a device definition file for each MIDI device it supports. These are .sqz files. Let’s begin by looking at the .sqz file for Roland GR-1, which is a simpler file:

00000000:  53 51 5a 20 0e 00 00 00 0f 52 6f 6c 61 6e 64 20  SQZ .....Roland 
00000010:  47 52 2d 31 2e 69 6e 69 9f 01 00 00 1f 8b 08 00  GR-1.ini........
00000020:  00 00 00 00 00 0b 75 52 cb 8e d4 30 10 bc 47 ca  ......uR...0..G.
00000030:  3f cc 0f 38 f2 6b 93 f8 e0 43 1c 3b 43 0e b3 82  ?..8.k...C.;C...
00000040:  09 8b 90 10 07 93 31 6c 84 f3 50 ec 20 f1 f7 38  ......1l..P. ..8
00000050:  ce 00 23 21 4e 5d dd 5d ed 2e 75 f9 53 3b 39 bf  ..#!N].]..u.S;9.
00000060:  6e a3 99 fc e7 34 79 d6 a3 e1 d7 d9 ea e9 76 3a  n....4y.......v:
00000070:  5f 01 4a 13 39 ac fc 40 cf db 28 d7 e1 87 59 1d  _.J.9..@..(...Y.
00000080:  a7 69 72 35 ae 19 ac e1 97 77 e1 01 31 2e d9 cd  .ir5.....w..1...
00000090:  da 58 0d 34 ce 48 9a a4 49 2b 21 7f ab 7d ff 9a  .X.4.H..I+!..}..
000000a0:  89 e9 fb 9e a2 23 dd 21 e6 dd 3c 7d db 11 e1 dd  .....#.!..<}....
000000b0:  4f e7 cd 18 27 26 07 39 8a 11 71 18 23 be e7 64  O...'&.9..q.#..d
000000c0:  8f 69 22 06 3f ea 25 2a ca be 8c 4b 54 75 5e e7  .i".?.%*...KTu^.
000000d0:  6d 71 7b 3f 22 c8 a5 f9 aa 37 eb 4f 9d f1 bf 6b  mq{?"....7.O...k
000000e0:  19 fc db cf d0 03 c6 0f 98 dc 77 dd ef d1 f6 f3  ..........w.....
000000f0:  c4 31 0e ed 97 56 5e 9a 8f 9c 95 b9 50 35 2b 40  .1...V^.....P5+@
00000100:  2e f0 13 a0 a4 54 80 e5 35 04 45 c3 9e 0a 89 08  .....T..5.E.....
00000110:  c2 b2 fe c3 56 b7 c1 cf 2b 47 12 4a 55 30 0a 14  ....V...+G.JU0..
00000120:  c6 0d a0 b4 09 33 0a e5 a0 81 98 12 9c 0b 86 72  .....3.........r
00000130:  74 cc bc 9f bd b6 57 d3 6b 6b 79 81 1a ac 68 5e  t.....W.kky...h^
00000140:  03 56 2a 02 68 2e 05 10 10 56 80 36 48 49 51 d4  .V*.h....V.6HIQ.
00000150:  48 61 f2 cf d4 7d 63 51 d2 12 86 45 a0 64 a2 00  Ha...}cQ...E.d..
00000160:  14 33 14 54 b2 06 08 84 61 55 09 21 30 0b 2a bb  .3.T....aU.!0.*.
00000170:  4b d5 fb e0 e6 79 d5 cb eb d0 3f 9a 7e 52 93 ce  K....y....?.~R..
00000180:  96 dd 9d ee d2 4e fa bf 34 39 b8 83 f6 21 fc 89  .....N..49...!..
00000190:  21 dc 0a 65 30 1a 12 5d 36 8e 1f 59 70 21 c2 7a  !..e0..]6..Yp!.z
000001a0:  73 7e 1e df f8 d1 f2 34 a9 5e ea f9 66 38 44 a2  s~.....4.^..f8D.
000001b0:  48 93 5f 04 1e 82 4b 84 02 00 00 0d 47 52 2d 31  H._...K.....GR-1
000001c0:  5c 47 52 2d 31 2e 70 6e 67 70 1e 00 00 1f 8b 08  \GR-1.pngp......
000001d0:  00 00 00 00 00 00 0b 01 59 1e a6 e1 89 50 4e 47  ........Y....PNG
000001e0:  0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 52  ........IHDR...R
000001f0:  00 00 00 37 08 02 00 00 00 10 77 8a 25 00 00 00  ...7......w.%...
00000200:  09 70 48 59 73 00 00 2e 20 00 00 2e 20 01 d5 1c  .pHYs... ... ...

I’ve made the interesting strings bold. We can see at the start of the file that there is an SQZ  signature, next we have what seems to be the version of the file, and following that, we have multiple records that are each made of:

  • filename length (8 bits)
  • filename
  • data length (32 bits)
  • data

We can also see that the data itself begins with 1f 8b, which is just the gzip header signature, and this means the data is gzipped. This also correlates to the extension, which ends in ‘z’, indicating zlib compression.

So, these files are just a simple archive format. Let’s write a simple C program to extract them:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <zlib.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))

// http://stackoverflow.com/questions/2336242/recursive-mkdir-system-call-on-unix
static void _mkdir(const char *dir) {
	char tmp[256];
	char *p = NULL;
	size_t len;

	snprintf(tmp, sizeof(tmp),"%s",dir);
	len = strlen(tmp);
	if(tmp[len - 1] == '/' || tmp[len - 1] == '\\')
		tmp[len - 1] = 0;
	for(p = tmp + 1; *p; p++)
		if(*p == '/' || *p == '\\') {
			*p = 0;
			mkdir(tmp, S_IRWXU);
			*p = '/';
		}
	mkdir(tmp, S_IRWXU);
}

int main(int argc, char **argv) {
	for(int i = 1; i < argc; i++) {
		FILE *f = fopen(argv[i], "rb");
		if(!f) {
			perror("fopen");
			continue;
		}
		char buf[4];
		fread(buf, 4, 1, f);
		if(strncmp(buf, "SQZ ", 4)) {
			fprintf(stderr, "%s: File does not start with \"SQZ \": \"%c%c%c%c\"\n", argv[i], buf[0], buf[1], buf[2], buf[3]);
			fclose(f);
			continue;
		}
		uint32_t version;
		fread(&version, 4, 1, f);
		printf("Version: %d\n", version);
		while(!feof(f)) {
			uint8_t filenamelen = 0;
			fread(&filenamelen, 1, 1, f);
			if(filenamelen == 0) break;
			char filename[256];
			memset(filename, 0, 256);
			fread(filename, filenamelen, 1, f);
			printf("Filename: \"%s\"\n", filename);
			uint32_t filesize;
			fread(&filesize, 4, 1, f);
			printf("Size: %d\n", filesize);
			if(filesize == 0) break;
			uint8_t *filedata = malloc(filesize);
			if(!filedata) {
				fprintf(stderr, "Could not allocate %d bytes\n", filesize);
				break;
			}
			fread(filedata, 1, filesize, f);
			char *rc = strrchr(filename, '/');
			if(!rc) rc = strrchr(filename, '\\');
			if(rc && rc > filename) {
				int oc = *rc;
				*rc = 0;
				_mkdir(filename);
				*rc = oc;
			}
			FILE *of = fopen(filename, "wb");
			if(!of) {
				perror("opening file for writing");
				fseek(f, filesize, SEEK_CUR);
			} else {
				z_stream z;
				z.next_in = filedata;
				z.avail_in = filesize;
				z.zalloc = Z_NULL;
				z.zfree = Z_NULL;
				inflateInit2(&z, 16+MAX_WBITS);
				uint8_t inflatebuf[1024];
				while(1) {
					z.next_out = inflatebuf;
					z.avail_out = sizeof(inflatebuf);
					int r = inflate(&z, Z_SYNC_FLUSH);
					if(r == Z_OK || r == Z_STREAM_END)
						fwrite(inflatebuf, 1, sizeof(inflatebuf) - z.avail_out, of);
					if(r != Z_OK)
						break;

				}
				fclose(of);
			}
		}
		fclose(f);
	}
	return 0;
}

This seems to work, and it extracts the files properly. Some of them are text, some are png images, and some are binary: