• 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
Interesting observations! So maybe we just have to integrate it...however, the weird "every 15" bytes still have to play into the equation somewhere...

The uncompressed LC III startup sound (which is the same sound the Quadra 700 makes at startup according to Mactracker) is 31,178 bytes. The compressed sounds in the Quadra 700 ROM are 20,985 bytes and 16,440 bytes.

No idea which one is which, but assuming it's the first (longer) one, it's looking like a compression ratio of about 3:2. I suppose someone with a Quadra 700 could put the smaller sound in place of the bigger one. If a different sound plays, we'd know what the smaller one sounds like...if the same sound plays, we'd know the second smaller sound is the Q700's startup chime and then we could figure out a way to play the bigger sound to find out what it contains. I believe my ROM SIMM is compatible with the Quadra 700...

It's possible that in the Quadra 630 ROM they just left a bunch of old code that happened to include the older compressed version of the same sound. I already know they leave a lot of backwards-compatible code in their ROMs -- for example, if I boot a IIci with the Quadra 700 ROM, it plays the IIci's synthesized startup sound (and boots OK). True, it would still be kind of strange to leave both the compressed and uncompressed versions in the ROM though, considering they could just patch the code to use the uncompressed version anyway (the sound player code they include would automatically handle everything correctly if they just pointed to the uncompressed sound header).

 

Dennis Nedry

Well-known member
I created a remarkable white noise generator with the integration approach, patents pending. Don't touch! :eek:)

I have a feeling that this isn't terribly complicated, but also that they did something obscure that we might not easily think of. This is a real shame because it may very well be that the startup sound is the only thing that ever used this cool hardware audio decompression feature.

There is a possibility that it was cheaper for Apple to build this decompression into their custom audio chip specifically for the startup sound than it was to store that extra chunk of data in ROM. The extra data may have bumped them up in ROM capacity whereas the audio chip was probably the exact same price with this feature after the engineering was complete.

 

dougg3

Well-known member
LOL...well it was worth a try! Yeah, I hope it's not too complicated. This is really starting to bug me that we haven't figured it out!

I just thought of a few other things. Just pulling ideas out here, not necessarily facts. Since every 15th byte is presumably some control byte or extra info that's not necessarily audio data...

The first sound *really* contains 20985*(14/15) = 19586 bytes and the second sound contains 16440*(14/15) = 15344 bytes.

Also, I forgot that the uncompressed sound is actually labeled as 22.254-ish KHz while the compressed ones are labeled as 22.050 KHz. If we correct for that when comparing across different sample rates (so thinking of the uncompressed startup sound as having 31178*(22.050/22.2545454) = 30891.4 "equivalent" bytes if it were 22.050 KHz, that's EXTREMELY close to being 2:1 compression for the second smaller sound which would go along with your thoughts about the data possibly being in nibbles...

Edit: The second smaller one is DEFINITELY the Quadra 700 startup chime. Check this WAV (and the code I used to generate it) out...

All that's missing is figuring out WHAT the byte at the beginning of each 15 byte chunk MEANS! Maybe it's some kind of an amplitude or offset or something? I'm almost thinking an amplitude value. Because if you listen to the end of my WAV, the sound cuts off abruptly, but on the uncompressed version of it, it kind of fades out at the end.

Code:
#include 
#include 

int main(int argc, char *argv[])
{
FILE *f = fopen("/home/doug/Desktop/Q700RawSoundData2", "rb");
FILE *w = fopen("/home/doug/Desktop/Q700DataOut", "wb");
int counter = 0;
uint8_t headerByte;

while (!feof(f))
{
	uint8_t s;
	fread(&s, 1, 1, f);

	if ((counter % 15) != 0)
	{
		uint8_t s1 = s >> 4;
		uint8_t s2 = s & 0x0F;

		// TODO: What to do with headerByte?
		s1 *= 16;
		s2 *= 16;

		fwrite(&s1, 1, 1, w);
		fwrite(&s2, 1, 1, w);
	}
	else
	{
		headerByte = s;
	}

	counter++;
}

fclose(f);
fclose(w);
}
 

Dennis Nedry

Well-known member
I did the exact same thing with the second sound but failed to share! In fact, if I feed the extra byte through with the equation that I posted a ways back, it sounds even better. The uncompressed sound fades in amplitude at the end and converting this one without the extra byte does NOT fade out, to my ears when I listen and to my eyes when I look at the waveform. I believe what I sense here and this is a very good clue if it's true.

I'm beginning to think that this is related to u-Law compression. From what I can tell, the 4-bit nibbles are signed (unlike ulaw) and act as u-Law's interval code, and the extra byte acts like u-Law's range code. This unknown compression seems roughly to structure its 15-byte packets in one of the following ways:

1 range nibble and 29 interval nibbles

1 range byte and 28 interval nibbles

1 range nibble, 1 unknown nibble, and 28 interval nibbles

u-Law:

http://en.wikipedia.org/wiki/Μ-law_algorithm

I have not tried playing this 'snd ' resource as a System 7 sound file on my real Quadra 700. This may be a required test. If we have a way to play these files, then we can easily make slight modifications and note the effects.

 

Dennis Nedry

Well-known member
Instead of:

Code:
       // TODO: What to do with headerByte?
        s1 *= 16;
        s2 *= 16;
Try this:

Code:
       // TODO: Admittedly I still don't know what to do with headerByte!!
        s1 *= 16 * (0x2C - headerByte);
        s2 *= 16 * (0x2C - headerByte);
In theory, we should be able to modify the sound slightly in ROM and compare analog recordings of the startup chime. I'll have to look and see if the Q700 has a ROM slot - I think it does.

 

dougg3

Well-known member
Interesting! I'll check out the uLaw and aLaw stuff. I thought it was mostly meant for voice recordings...

I tried the mod you gave, but it doesn't work--I end up with something that doesn't look like a sound waveform anymore.

The headerByte values I get are between 0x01 and 0x35. On the longer sound, the range is from 0x00 to 0x3C. I'm almost thinking it's a 6-bit value of some kind (between 0 and 0x3F). Looks like most of the time it's about 0x24 or 0x25, but sometimes you see a spike up or down.

Edit: I like the idea of changing it slightly to see how the analog recording changes! I'm also pretty sure the Q700 has a ROM SIMM slot, and I'm also 99% sure it's compatible with the standard II series pinout. I almost want to say someone has tried the programmable SIMM in one already...

 

Dennis Nedry

Well-known member
I'll have to look at what I did more closely, I know that I ended up writing out 16-bit sound because of some overflowage issues.

I know that numbers greater than 0x2C exist, so the calculation will come out negative at times, but this value produces the best result for me for some reason. I'm not sure why. I'll post my code. It is a COMPLETE hack job, please excuse me in advance. Heck, it spits hex out to the terminal that you have to manually copy into a hex editor program.

This program produces raw 16-bit, 22k, mono, signed audio. I'll post it in a moment.

 

Dennis Nedry

Well-known member
This is REALLY embarrassing. What a complete mess.

Code:
#include 
#include 
using namespace std;

signed int get_sample(signed char four_bit, unsigned char cur_vol);
void print_sample(signed int sample);

int main (int argc, char * const argv[]) {
signed char data[0x4038] = {INSERT RAW COMPRESSED AUDIO DUMP HERE};

unsigned char cur_vol = 0;
signed char sample = 0;

for(int i = 0; i < 0x4038; i++)
{
	if(0 != i % 15)
	{
		//cout << hex << setw(4) << setfill('0') << (short int)((short int)data[i] * (short int)(0x35 - cur_vol) * 16) << " ";


		sample = get_sample( (data[i] & 0xF0) >> 4, cur_vol );
		//sample += get_sample( (data[i] & 0xF0) >> 4, cur_vol );
		print_sample(sample);

		sample = get_sample( (data[i] & 0x0F) >> 0, cur_vol );
		//sample += get_sample( (data[i] & 0x0F) >> 0, cur_vol );
		print_sample(sample);

		//print_sample(  get_sample( (data[i] & 0x0F) >> 0, cur_vol )  );



		//cout << hex << setw(4) << setfill('0') << (short int)( get_sample((data[i] & 0xF0) >> 4, cur_vol) ) << " ";
		//cout << hex << setw(4) << setfill('0') << (short int)( get_sample((data[i] & 0x0F), cur_vol) ) << " ";

		// 0x0C ~ loud
		// 0x2C ~ quiet

		//40 (*
		//30 (*16) Best
		//2C (*16) ?
		//20 (*16) Bad
	}
	else
	{
//			if(data[i] > cur_vol)
//			{
//				cur_vol = data[i];
//			}
		cur_vol = data[i];
//			cout << hex << setw(2) << setfill('0') << (short int)data[i] << " ";
	}
}

//	cout << "\n\nMax: " << hex << setw(2) << setfill('0') << (short int)cur_vol;
   return 0;
}

signed int get_sample(signed char four_bit, unsigned char cur_vol)
{
signed int sample;

if(four_bit > 0x07) // Sign Extend
{
	four_bit |= 0xF0;
}

//2C is better than 20, 26.

cur_vol = 0x2C - cur_vol;

sample = four_bit * cur_vol;

return sample;
}

void print_sample(signed int sample)
{
cout << hex << setw(4) << setfill('0') << (short int)(sample <<  << " ";

return;
}
 

Dennis Nedry

Well-known member
I took a moment to go through and apply my changes to your code. I changed the file paths at the top to suit my setup, everything bases off of the build directory for me on my old Mac for some reason.

The reason I used 16 bit was not because of overflowing, it was limitation of the "cout" function I was using. 8-bit couts print out as ASCII characters, I'm not sure if there's any quick way around that. This code (below) produces 8-bit output like it should.

This produces a more poppy sound, but the dynamics are better and there is less white noise, which makes me think it's headed in the right direction.

Code:
#include 
#include 

int main(int argc, char *argv[])
{
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");
int counter = 0;
uint8_t headerByte = 0;
int8_t sample;

while (!feof(f))
{
	uint8_t s;
	fread(&s, 1, 1, f);

	if ((counter % 15) != 0)
	{
		int8_t s1 = s >> 4;  // Most significant nibble
		int8_t s2 = s & 0x0F; // Least significant nibble

		// Sign extend nibbles into full bytes.
		if(0 != (s1 & 0x08))
		{
			s1 |= 0xF0;
		}
		if(0 != (s2 & 0x08))
		{
			s2 |= 0xF0;
		}

		sample = (0x2C - headerByte) * s1;
		fwrite(&sample, 1, 1, w);

		sample = (0x2C - headerByte) * s2;
		fwrite(&sample, 1, 1, w);
	}
	else
	{
		headerByte = s;
	}

	counter++;
}

fclose(f);
fclose(w);
}
 

Dennis Nedry

Well-known member
It seems like headerByte relies more on the least significant nibble for range. The most significant nibble tends to alternate between 1 - 3 with no regard to the other nibble. I think we're dealing with an unknown nibble here, possibly used for transition between packets. Neither of these nibbles act like the other "interval" nibbles.

I'm throwing terms out there that come with implicit assumptions due to lack of better terms...

Packet structure:

xy ii ii ii ii ii ii ii ii ii ii ii ii ii ii

x = most significant header nibble

y = least significant header nibble

i = interval nibble

 

Dennis Nedry

Well-known member
Definite progress again. This modification effectively strips the most significant header nibble, but was written in such a way to be easily tweaked. We're back to not knowing what in TARNATION the least significant nibble does.

Code:
#include 
#include 

int main(int argc, char *argv[])
{
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");
int counter = 0;
uint8_t headerByte = 0;  // This will get reassigned when counter = 0 so init still isn't necessary.

while (!feof(f))
{
	uint8_t s;
	fread(&s, 1, 1, f);

	if ((counter % 15) != 0)
	{
		int8_t s1 = s >> 4;  // Most significant nibble
		int8_t s2 = s & 0x0F; // Least significant nibble

		// Sign extend nibbles into full bytes.
		if(0 != (s1 & 0x08))
		{
			s1 |= 0xF0;
		}
		if(0 != (s2 & 0x08))
		{
			s2 |= 0xF0;
		}

		switch(headerByte & 0xF0)
		{
			case 0x00:
				s1 = 0;
				s2 = 0;
				break;
			case 0x10:
				s1 = ( (0x1C - headerByte) * s1 ) + 0x00;
				s2 = ( (0x1C - headerByte) * s2 ) + 0x00;
				break;
			case 0x20:
				s1 = ( (0x2C - headerByte) * s1 ) + 0x00;
				s2 = ( (0x2C - headerByte) * s2 ) + 0x00;
				break;
			case 0x30:
				s1 = ( (0x3C - headerByte) * s1 ) + 0x00;
				s2 = ( (0x3C - headerByte) * s2 ) + 0x00;
				break;
			default:
				s1 = 0;
				s2 = 0;
				break;					
		}

		fwrite(&s1, 1, 1, w);
		fwrite(&s2, 1, 1, w);
	}
	else
	{
		headerByte = s;
	}

	counter++;
}

fclose(f);
fclose(w);
}
 

Dennis Nedry

Well-known member
I know I'm blowing these pages way out by posting all of this code, but I may be on to something by using the most significant nibble as a bit shifter.

Code:
#include 
#include 

int main(int argc, char *argv[])
{
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");
int counter = 0;
uint8_t headerByte = 0;  // This will get reassigned when counter = 0 so init still isn't necessary.

while (!feof(f))
{
	uint8_t s;
	fread(&s, 1, 1, f);

	if ((counter % 15) != 0)
	{
		int8_t s1 = s >> 4;  // Most significant nibble
		int8_t s2 = s & 0x0F; // Least significant nibble

		// Sign extend nibbles into full bytes.
		if(0 != (s1 & 0x08))
		{
			s1 |= 0xF0;
		}
		if(0 != (s2 & 0x08))
		{
			s2 |= 0xF0;
		}

		int8_t velocity = 0x0C - (headerByte & 0x0F);
		int8_t bit_shift = ((headerByte & 0xF0) >> 4) - 1;

		s1 = ( velocity * s1 ) << bit_shift;
		s2 = ( velocity * s2 ) << bit_shift;

		fwrite(&s1, 1, 1, w);
		fwrite(&s2, 1, 1, w);
	}
	else
	{
		headerByte = s;
	}

	counter++;
}

fclose(f);
fclose(w);
}
It produces these sounds.

 

dougg3

Well-known member
Wow, nice progress! Hmm...you're right. There's more going on here in regard to that first byte and how it's probably split into two nibbles. I think the first sound with the zeros at the end may end up being a very useful clue as to what's going on with the nibbles.

So we now have a pretty good idea of what that first chime sounds like. I don't recognize it from anywhere...maybe it's a hidden sound that they were considering using but never actually used?

 

dougg3

Well-known member
Ha, cool! Now that I think about it, I think I have heard that sound before. Isn't it the startup chime for Macs that have PowerPC upgrades from Apple? I swear I've heard it somewhere other than through an easter egg.

The ROM has a code path for playing it as a startup chime. It depends on the result it gets from reading some register of the sound chip.

 

Dennis Nedry

Well-known member
I'm pretty sure that this is completely its own sound, not heard by default on any Mac.

slomacuser posted about it here, even with a good audio recording:

viewtopic.php?f=8&t=13618&start=0

I have a collection of recorded and lossless startup sounds, death chimes, etc. I shared it at one point but I can upload it somewhere again if you don't have it.

 

dougg3

Well-known member
Ah, ok...never mind. After hearing the higher-quality sound, it doesn't sound familiar...very cool trivia though!

I'll definitely pay closer attention to the startup code to figure out what conditions are necessary in order for it to play.

 

dougg3

Well-known member
The "header byte" as I've been calling it definitely doesn't contain an extra sample -- if I try to use any of its data as another sample, the sound slows down just enough to be the wrong pitch. We definitely have the correct number of samples at this point. It's all about what that first byte (probably as you said, pair of nibbles) does. Gah!

More info:

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

 

Dennis Nedry

Well-known member
I attempted packing both of the compressed sounds into a system 7 sound file and playing in Basilisk II running on the Quadra 700/900 ROM and got the attached message. The OS clearly intervenes. Attempting to play the startup sound with the BootBeep control panel results in a crash, somewhat as expected.

Picture 3.png

 
Top