• Updated 2023-07-12: Hello, Guest! Welcome back, and be sure to check out this follow-up post about our outage a week or so ago.

Found a sound in Quadra 700/900 ROM, but can't extract.

dougg3

Well-known member
-223 means "invalid compression type" according to MacErrors.h. Probably because that compressionID was only used for startup sounds and never actually done in software in the Sound Manager...

 

Dennis Nedry

Well-known member
The BootBeep control panel actually sends the compressed audio from ROM to the Enhanced Apple Sound Chip, still compressed. I think that with my very limited 68k disassembly skills, I can hack the cdev resource in this control panel to inject additional compressed data to the sound chip right after playing the boot sound from ROM, for example, one extra packet - 15 bytes. This can be easily and quickly modified and I think we can get some good info from it by making changes and studying the effects on the audio output from the Mac.

Resorcerer has a built-in 68k disassembler, and I think that if I go to offset 0x27E, I can just start adding op-codes there. There's only one reference after this and Resorcerer fixes references as they get bumped around, so that should be okay. D0 is set up to receive encoded data bytes (so 2 nibbles at a time) at this point, and this little number checks and waits for a status OK from the sound chip:

move.b $0804(a6),d0

btst #$02,d0

beq.s +$C0 ; branches back to the move.b line.

So for a really filthy, dirty hack, which I apparently seem to be good at lately, I can load each of 15 bytes with a:

moveq #$byte,d0

followed by the status wait/ok thing.

Then make an audio recording inspecting the little blip of sound at the end of the startup sound, modifying, making another recording, repeat. I believe that this compression is simple enough that it can be cracked this way.

 

Dennis Nedry

Well-known member
I don't have my Quadra 700 handy at this exact moment but I think this cdev ASM hack should inject an extra 15-byte compressed audio packet. The packet itself is highlighted in yellow in the image.

Code:
286F0004266F00082A2F000C3C2F00123E2F
00149E46302F0016D040303B001667044EBB
00102F4B00182F570014DEFC00144E75001E
006601340000017000000000000001980000
000000000000000000004E56FFF242A72F3C
426F6F744267A9A0265F24537002D01AD032
00FED03200FFD4C0204A203C00010089A051
70010812000167025240610002182F2EFFF4
3F3C0001A9634E5E4E754E56FFF20C470003
670000846D504AA72F0FA87420780D2C4868
00102F08A873A87B207802AE203C0004F19C
0C28001500126706203C0004F1ACD1C04E90
A9374AA73F3C0010AA0CAA472F0C2F3809EE
A90DA873203C0000FFFFA032606A30076100
01A64A672F2EFFF4A9604A5F661E2F2EFFF4
3F3C0001A9637001B0476602524061000184
2F2EFFF44267A96342A73F3C0004A9B9205F
2F10A8517001610001684A672F2EFFF4A960
207802AED1FC000C35504A5F660A207802AE
D1FC000BE2F06100008AA8504E5E4E750000
4E56FFF270016100013224537002D01AD032
00FED03200FFD4C0089200014A672F2EFFF4
A9604A5F660408D20001204A203C00010089
A0524E5E4E754E56FFF27005610000F6487A
0014A89D486EFFF8A8A1A89E2F0CA9814E5E
4E75AA55AA55AA55AA5597CB207802AE0C28
0015001267080C2800170012660447F80001
4E7548E7FFFE2C780CC0D0FC001470003028
003AE648222800164C001001242800083628
0038D0FC00404C3C24000000FFFF4C7C2400
AC4400003C2E0F043E2E0F243D420F043D42
0F240C4300056606363C0001600E0C430006
6606363C00026002760000030080182E0F08
1A2E0F281D430F081D430F28240148426012
102E08040800000267F610181C801D400400
51C9FFEC51CAFFE8702C1C801D400400102E
08040800000267F670011C801D400400102E
08040800000267F670231C801D400400102E
08040800000267F670451C801D400400102E
08040800000267F670671C801D400400102E
08040800000267F670891C801D400400102E
08040800000267F670AB1C801D400400102E
08040800000267F670CD1C801D400400102E
08040800000267F670EF1C801D400400102E
08040800000267F670FF1C801D400400102E
08040800000267F670001C801D400400102E
08040800000267F670771C801D400400102E
08040800000267F670881C801D400400102E
08040800000267F670881C801D400400102E
08040800000267F670781C801D400400102E
08040800000267F6102E08040800000367F6
1D440F081D450F283D460F043D470F24422E
08044CDF7FFF4E752F0CD0463F00486EFFF2
486EFFF4486EFFF8A98D4E75
Here is the extra data disassembled. I'm not sure why Resorcerer throws in the negative stuff when disassembling. I would think that numbers are generally more common to be unsigned, but whatever.

Code:
moveq     #$2C,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$E2

moveq     #$01,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$F4

moveq     #$23,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0106

moveq     #$45,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0118

moveq     #$67,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$012A

moveq     #-$77,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$013C

moveq     #-$55,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$014E

moveq     #-$33,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0160

moveq     #-$11,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0172

moveq     #-$01,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0184

moveq     #$00,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$0196

moveq     #$77,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$01A8

moveq     #-$78,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$01BA

moveq     #-$78,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$01CC

moveq     #$78,d0
move.b    d0,(a6)
move.b    d0,$0400(a6)
move.b    $0804(a6),d0
btst      #$02,d0
beq.s     +$01DE
Picture-4.gif

 

dougg3

Well-known member
Nice! I'm assuming the control panel turns off interrupts while it's writing to the chip too...am I correct? (On the IIci, if I left interrupts enabled the OS took over and screwed things up sometimes)

I just realized that the sound header says the original uncompressed samples are 16 bits each. Whether that header is telling the truth or not, who knows...but if the header is telling the truth, that would mean this compression is actually 4:1. Now, I'm really starting to wonder if this is a close variant of IMA 4:1, which turns 4-bit nibbles into 16 bit sound samples and uses a lookup table for values. It also works based on accumulating deltas (although they're looked up from a table rather than just used directly as samples), so that might explain why you are saying the sound seems kind of like a derivative.

http://wiki.multimedia.cx/index.php?title=IMA_ADPCM

In particular, QuickTime IMA looks like this:

http://wiki.multimedia.cx/index.php?title=Apple_QuickTime_IMA_ADPCM

Clearly it isn't QuickTime IMA since each packet would be 34 bytes with a 2-byte header. But I'm wondering if we can figure something out based on how IMA encoding works. I don't know if IMA encoding was around before the Quadra 700 came out, though, but whether it was or not, it could be a similar idea.

Edit: Another question that might be worth checking into. Is each packet completely independent? E.g. can you start at the beginning of a packet halfway through the sound, feed all of the bytes starting at that point into the sound chip, and will the second half of the sound play correctly? That might be worth testing in the Quadra. It would give us more clues about the algorithm. My initial guess is that yes, each chunk is probably independent. But who knows...

 

dougg3

Well-known member
Just wanted to fix this list:

  • Known valid values for the upper header nibble: 0 to 3 inclusive.
  • Known valid values for the lower header nibble: 1 to 12 inclusive (NOT 0 to 12).
  • The data nibbles are known to use up the entire range of possible values: 0 to 15.


My code had a bug that actually processes one byte more than it should, so it was processing a junk header byte at the end. Probably wasn't affecting anything, but it was messing up my counts of known valid values. I forgot that feof() does not return an error until an error has actually occurred due to fread() or a similar function. Do this instead of feof() to fix it:

Code:
while (fread(&s, 1, 1, f) > 0)
Anyway, I've been playing around some more and getting nowhere with the IMA algorithm.

 

Dennis Nedry

Well-known member
I'm not too particularly savvy on 68k assembly language. What does it look like to disable/enable interrupts? I've seen it where there's a dedicated instruction for this - is that the case here?

I just tried BootBeep on a real Quadra 700 and it worked great, but my modified version doesn't work - it crashes the Mac when I launch it, though recoverable with interrupt + "G FINDER". I'll look at it more closely. Resorcerer gave me a warning about increasing the size of the cdev resource past its original size, so I'm not sure what that's all about.

BootBeep's assembly code is VERY small in size, so this should theoretically be pretty easy to hack.

 

dougg3

Well-known member
You can disable interrupts with:

Code:
ori #$0700, sr
And re-enable interrupts by restoring the original value of sr.

On a side note, I went back and read this thread from the beginning and noticed that I was very confused and wasn't following what was going on. I didn't realize you already knew what each sound was...*smacks myself on the forehead*

 

Dennis Nedry

Well-known member
haha that's okay. I went back trough the ASM and realized that Resorcerer only updates jump and branch addresses that come AFTER the changes you make. So I went back through and manually updated all of the instructions that jump to after the hack. Now it works! We can see an extra little blip from the extra packet. This is a sound recording, so there is some low-pass filtering and noise happening, but we can at least characterize the effects when tweaking come of these compressed audio packets now.

The flat line was produced form an unmodified BootBeep, and the squiggle came from the extra packet. This is the same packet shown highlighted in an image above.

Picture-1.gif

 

Dennis Nedry

Well-known member
I no longer have any particular reason to believe that packets play backwards. However, I do have reason to believe that the nibbles ARE stored in derivative (delta) form. When I sent the following packet:

26:77 77 77 77 77 77 77 77 77 77 77 77 77 77

I got the attached response. I will try 8 next, that should be similar but negative.

Edit

Indeed, 8 is negative!

I'm not so sure, but would this method be considered a trojan horse? }:) This may be a rare non-destructive example of such a thing.

Edit again

Changing the most significant nibble to 1 (i.e. 0x26 -> 0x16) resulted in a dramatic reduction in amplitude, it was approx. 0.218 times the original.

Picture 2.png

Picture 3.png

Picture-5.gif

 
Last edited by a moderator:

dougg3

Well-known member
Fascinating stuff, so it looks like the deltas are signed...so do packets like 333333... do the same thing as 777777... but with a flatter slope? And what happens if you change the header byte?

This is a really cool setup you have! Are you just recording the Quadra 700 sound output and displaying the waveform in Audacity/a similar app?

 

Dennis Nedry

Well-known member
Yep, I have a little Quadra 700 sitting here next to my MDD G4, a stereo analog audio cable connecting them, and I'm using Sound Studio to record. I wish there was a way to turn off the automatic low-pass, but I think it's in hardware. I added an edit to the previous post where I changed the first header nibble.

So far the everything is supporting the characteristics that we found by experimenting before. This tool allows us to verify the characteristics and potentially help quantify them.

 

Dennis Nedry

Well-known member
As I test more, I have discovered that the first nibble determines the "type" of packet. Types include:

0: direct samples (non-cumulative)

1: Dull Slope (more research needed)

2: S-Curve Slope (more research needed)

3: Sinc pulse (cumulative)

And as a matter of fact, this only uses 2 bits, leaving the most significant 2 bits in the header byte unknown, probably unused. It loops through the types, for example, using type 4 starts back in with direct samples again, 5 = dull slope, etc.

I have done some work on the Sinc pulse. When a packet is converted to plaintext (i'll refer to sound output as plaintext), for each data nibble, a constant sinc wave is added to the remaining plaintext samples in the packet. The value of the nibble is multiplied by the constant sinc wave and then divided by 5 before it is added to each sample. At the beginning of the packet, I am unsure if the samples start at 0 or at the previous packet's final sample. I have, however, done my very best to grab strategic points, forming all sorts of crazy trendlines in Excel that all comes back to a real sine function. This is certainly an area that might not be easily perfected, but I think I'm pretty close. We'll have to listen to see for sure.

So at this point, I'm going to poke at types 1 and 2. First I'll see if they're cumulative, and then I'll figure out their wave constants.

 

Dennis Nedry

Well-known member
There is still more work to be done on wave types 1 and 2. I treated them just like type 3 in some respects without testing. I can hear more bass in the output already, though.

Code:
#include 
#include 
#include 
using namespace std;

int main(int argc, char *argv[])
{
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");

int8_t packet[15];

int16_t sinc_wave[28] = {0, 17162, 29319, 34628, 32767, 24841, 13019, 0, -11581, -19657, -23071, -21701, -16360, -8532, 0, 7530, 12751, 14951, 14072, 10636, 5573, 0, -5009, -8602, -10268, -9880, -7668, -4145};
int16_t dull_wave[28] = {32767, 31343, 29962, 28623, 27326, 26072, 24860, 23691, 22564, 21479, 20437, 19437, 18480, 17565, 16692, 15862, 15074, 14329, 13626, 12965, 12347, 11771, 11237, 10746, 10298, 9891, 9527, 9206};
int16_t s_wave[28] = {0, 19660, 31311, 32403, 32767, 32767, 32039, 30947, 30218, 29126, 27670, 26214, 25121, 23665, 22209, 20752, 19296, 17840, 16748, 15291, 14199, 13107, 12015, 10922, 9830, 8738, 8010, 6917};

int16_t output_data[28];

int8_t nibble;

while (!feof(f))
{
	// Read in entire packet.
	for(int i = 0; i < 15; i++)
	{
		fread(&packet[i], 1, 1, f);
	}

	// Switch for first nibble (sound characteristic)
	switch(packet[0] & 0x30)
	{
		case 0x00:  // Direct samples
			for(int i = 1; i < 15; i++) // for each packet data byte
			{
				// FIRST NIBBLE
				nibble = (packet[i] & 0xF0) >> 4;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}

				output_data[2 * (i-1)] = (nibble * 512) >> ((packet[0] & 0x0F) - 3);

				// SECOND NIBBLE
				nibble = (packet[i] & 0xF0) >> 4;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}

				output_data[(2 * (i-1)) + 1] = (nibble * 512) >> ((packet[0] & 0x0F) - 3);
			}

			break;

		case 0x10:  // Dull slope samples
			for (int i = 0; i < 28; i++)
			{
				output_data[i] = 0;//output_data[27];  // Clear output data.
				// Note: PROBABLY needs to start at the previous raw from the previous packet, not 0.
			}

			for(int i = 1; i < 15; i++) // for each packet data byte
			{
				// FIRST NIBBLE
				nibble = (packet[i] & 0xF0) >> 4;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 1; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((dull_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}

				// SECOND NIBBLE
				nibble = (packet[i] & 0x0F) >> 0;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 0; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((dull_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}
			}

			break;

		case 0x20:  // S-Curve samples
			for (int i = 0; i < 28; i++)
			{
				output_data[i] = 0;//output_data[27];  // Clear output data.
				// Note: PROBABLY needs to start at the previous raw from the previous packet, not 0.
			}

			for(int i = 1; i < 15; i++) // for each packet data byte
			{
				// FIRST NIBBLE
				nibble = (packet[i] & 0xF0) >> 4;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 1; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((s_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}

				// SECOND NIBBLE
				nibble = (packet[i] & 0x0F) >> 0;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 0; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((s_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}
			}

			break;

		case 0x30:  // Sinc pulse samples
			for (int i = 0; i < 28; i++)
			{
				output_data[i] = 0;//output_data[27];  // Clear output data.
				// Note: PROBABLY needs to start at the previous raw from the previous packet, not 0.
			}

			for(int i = 1; i < 15; i++) // for each packet data byte
			{
				// FIRST NIBBLE
				nibble = (packet[i] & 0xF0) >> 4;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 1; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((sinc_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}

				// SECOND NIBBLE
				nibble = (packet[i] & 0x0F) >> 0;
				if(nibble & 0x08)
				{
					nibble |= 0xF0;  // Sign extend.
				}
				if(nibble > 5) nibble = 5;  // nibble max is 5 with sinc.
				if(nibble < -5) nibble = -5;  // This line is not verified.

				for(int j = i - 0; j < 28; j++)  // follow through to each remaining sample.
				{
					output_data[j] = output_data[j] + ((sinc_wave[j] * nibble) / 5) >> ((packet[0] & 0x0F) - 3);
				}
			}

			break;

		default:
			cerr << "Error when parsing sound characteristic nibble.\n";
			break;
	}

	for(int i = 0; i < 28; i++)
	{
		fwrite(&output_data[i], 2, 1, w);
	}
}

fclose(f);
fclose(w);
}
 

Dennis Nedry

Well-known member
More progress! It clearly needs a lot more work. I have been focusing on type 3 (sinc wave). I will go back and test for the behavior of types 1 and 2. It does appear that they shift differently using the second header nibble. Also, my source waveforms probably need some work. It's more difficult than you might think to get a perfect sample of a little blip through analog audio hardware. I have a 192kHz audio card in my computer but I don't seem to get any more clarity at those higher rates.

https://sites.google.com/site/benboldt/files/Q700DataOut2.aif

 

dougg3

Well-known member
Wow, awesome progress! I tried looking up "sinc" to understand what's going on, but the wikipedia article is way over my head. This is so awesome that you're basically reverse engineering hardware by doing this :-D

 

Dennis Nedry

Well-known member
It's not exactly a sinc wave, it doesn't trail off as quickly. There are 3 parts to its function:

Base sine wave:

Sin(4*Pi*i/14)

Amplitude of sine wave:

.3718i^2 - 11.168i + 107.69 (Approximate)

Normalization from pixel count in Sound Studio at my particular window size to signed 16-bit number:

23767/89.474

Then you multiply all 3 of these things together to get the wave.

i represents the numbers from 0 to 13.5, inclusive, in increments of 0.5. I used Excel to solve from each i value.

I have not done anything like this for the other 2 waves yet. With the sinc wave, I can tell what it's supposed to look like, but with the other two, I'm not sure. There is some distortion on the ends that I'm not sure if it's intentional. Also, I have skewed each image in Photoshop to approximately compensate for the high-pass audio hardware. In fact, the high pass distorts on a curve, not a line like the skew function. So there are multiple issues here. The green lines represent the 28 samples per packet. The waves themselves appear to be the same length as a packet. These lines are spaced based on 22kHz audio I think. (The image is 1:1 88 kHz)

Picture-12.gif

Picture-10.gif

 

Dennis Nedry

Well-known member
The little tiny click that you hear at the very end of the latest audio sample is in fact an extra packet, exactly like what I am sneakily sending to the audio chip in the real Quadra 700. If you zoom all the way in with an audio program, you'll see that it's the sinc type of packet.

 
Top