Jump to content
Dennis Nedry

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

Recommended Posts

I just realized that this compressed sound is much higher quality than the non-compressed version. decompressing this sound results in 16-bit audio whereas the non-compressed one is only 8-bit. Even where this converter is at right now, the 16-bit one sounds better than the 8-bit, except for the popping.

 

It may be worth the extra mile to debug this so we have the highest quality startup sample in our collection!

 

Currently I am not sure about starting points of the cosine wave in type 3, and also I am not completely sure if my cosine wave is exactly the right frequency. Passing from one type to another is an area that can be verified. Also, how peaking is handled, and if calculations are overflowing their integer types in the decompression program.

Share this post


Link to post
Share on other sites

I've been beginning all my other posts with "Wow!" so I'll try to stop. But I can't resist...wow!

 

You've almost cracked it. That is sounding AWESOME.

 

Your work on the EASC compression algorithm will undoubtedly be useful in other applications too, such as emulators. I would bet that Arbee would be interested in seeing this stuff for MESS. It's definitely worth it to get it completely working!

 

Are either of you wearing a deerstalker hat while you're doing this? :)

 

LOL, I'm not! This is all Dennis Nedry. He's doing some crazy awesome compression algorithm stuff here that I wouldn't even know where to begin to play with!

Share this post


Link to post
Share on other sites

I was bored at work and tried to read this thread . . . I need to hit the reset button on my brain!

 

I lost it about the time "accumulating deltas" were mentioned. I just thought I'd pipe in to say that this exactly how a PolyLine Font Format we cracked worked back in the day. ROM space was far more precious in the 80's when this format was devised than it was in the Quadra era, or the 68030 era for that matter.

 

Delta changes with the oddball formatting codes denoting the arcs connecting notes, keys, scales or whatnot seemed like a good fit to me for what you were describing. Annotated deltas from one note to the next would be a simple, compact way to store a sheet of music, just as a simplified version without the annotation was a compact way to store the information needed to plot a letter.

 

Just thought I'd toss it out there . . . must . . . take . . . catnap . . . :simasimac:

Share this post


Link to post
Share on other sites

I have plotted out the beginning of the normal startup chime. The dark line is the one that went through my program, the lighter line is an audio recording of the Quadra 700 playing it, magenta stripes are the first nibble in a packet, green stripes are the other nibbles, and the number printed on the bottom is the header byte of the packet. The left nibble of the header byte designates the wave type for the entire packet.

 

As you can see, there are some issues here. It seems that both of the big bumps happen right at the transition from one packet to the next.

Picture-4.gif.fd5578eac1be75ffd990215770ef53c0.gif

Share this post


Link to post
Share on other sites

I have it to the point where it sounds good now, but it is not perfect by any means when you start comparing the waveform to an actual recording. It's in the very nature of this integration-type decompression to propagate the slightest error into something huge. I believe that it could be perfected, assuming that the sound chip accomplishes its decompression with a completely digital circuit, but I also believe that it's good to realize the point when something becomes good enough to consider moving on.

 

Here is the latest version of audio and code:

 

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

 

#include 
#include 
#include 
using namespace std;

// Global variable definitions
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");

unsigned int cosine_pointer;

int8_t packet[15];
int8_t prev_type;
int8_t nibble_1, nibble_2;
int16_t raster_1, raster_2;

int16_t sample;
int16_t envelope;
int16_t accumulator;

void speak(int8_t type, int16_t raster)
{
int16_t cosine_wave[21] = {27073, 32767, 27073, 11971, -7291, -24020,
	-32401, -29522, -16384, 2449, 20430,
	31311, 31311, 20430, 2449, -16384,
	-29522, -32401, -24020, -7291, 11971};

int32_t calc;

if(cosine_pointer > 20) cosine_pointer = 0;  // Loop cosine pointer as necessary.

// Filter for max raster depending on type.
if(0x20 == type) // Damp Pulse
{
	if(raster > 9238) raster = 9238;
	else if(raster < -9238) raster = -9238;
}
else if(0x30 == type) // Enveloped Cosine
{
	if(raster > 20797) raster = 20797;
	else if(raster < -20797) raster = -20797;
}

envelope += raster;
if(0 != raster)
{
	cosine_pointer = 0;
}

switch(type)
{
	case 0x00:  // Direct samples
		sample = raster;
		fwrite(&sample, 2, 1, w);
		break;

	case 0x10:  // Envelope Only
		fwrite(&envelope, 2, 1, w);
		sample = envelope;  // Record this sample for next time.
		break;

	case 0x20:  // Damp Pulse
		accumulator += raster;
		calc = sample + accumulator;
		if(calc > 32767)
		{
			cerr << "0x20 Sample Overflow!" << calc << "\n";
			//calc = 32767;
		}
		else if(calc < -32768)
		{
			cerr << "0x20 Sample Underflow!" << calc << "\n";
			//calc = -32768;
		}
		sample = calc;
		sample = (sample * 7) / 8;
		fwrite(&sample, 2, 1, w);
		break;

	case 0x30:  // Enveloped Cosine
		sample = (cosine_wave[cosine_pointer] * envelope) / 32767;
		fwrite(&sample, 2, 1, w);
		break;

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

envelope = (envelope * 15) / 16;  // Envelope degrades: 15/16 times previous value.

accumulator = (accumulator * 7) / 8;  // Accumulator degrades: 7/8 times previous value.

cosine_pointer++;  // Keep the cosine wave rolling
}

int main(int argc, char *argv[])
{
// Init variables		
prev_type = 0x00;  // Init prev_type.
cosine_pointer = 0;  // Init cosine pointer.
envelope = 0;
sample = 0;

while (fread(packet, 1, 15, f)) // Read in entire packets until EOF.
{	
	if(0x30 != prev_type) cosine_pointer = 1;  // Start at peak of cosine wave.
	envelope = sample;  // Set the envelope to the current position of the sample when changing packet types.

	for(int i = 1; i < 15; i++)  // for each packet data byte
	{
		// Separate and sign extend each nibble.
		nibble_1 = packet[i] & 0x0F;  // Least significant nibble plays first.
		nibble_2 = (packet[i] & 0xF0) >> 4;
		if(nibble_1 & 0x08) nibble_1 |= 0xF0;  // Sign extend
		if(nibble_2 & 0x08) nibble_2 |= 0xF0;

		// Determine decompressed value based on data nibble and header nibble
		raster_1 = (nibble_1 * 4096);
		for(int j = (packet[0] & 0x0F); j != 0; j--) raster_1 /= 2;  // Can't shift because of signed numbers.

		raster_2 = (nibble_2 * 4096);
		for(int j = (packet[0] & 0x0F); j != 0; j--) raster_2 /= 2;  // Can't shift because of signed numbers.

		speak(packet[0] & 0x30, raster_1);
		speak(packet[0] & 0x30, raster_2);
	}

	prev_type = packet[0] & 0x30;
}

fclose(f);
fclose(w);
}

Share this post


Link to post
Share on other sites

Yeah, I know exactly what you're saying about the accumulating error. I think the fact that you've gotten it this far is awesome enough! You basically have a perfect recording of the sound you were looking for originally, too (I don't hear any scratches or anything in that one). Congrats!

Share this post


Link to post
Share on other sites

Most of the remaining issues come from where one packet hands off to the next, even if the packet type remains the same from one to the next.

 

I think that you have to partially calculate the next state in the current state.

Share this post


Link to post
Share on other sites

I've cleaned up the code and it actually ended up making an extremely slight improvement to the sound. But the code is simpler now.

 

In the image the I posted that shows each packet with magenta lines - it looks pretty much like those magenta lines are supposed to move two spaces to the left. I'm not sure why. Maybe the sound program I'm using snips off the first 2 samples.

 

My latest theory is that type 3 should not use a table - somehow it should take a similar approach to the other waves where they are actually calculated out.

 

#include 
#include 
#include 
using namespace std;

// Global variable definitions
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");

unsigned int cosine_pointer;

int8_t packet[15];
int8_t prev_type;
int8_t nibble_1, nibble_2;
int16_t raster_1, raster_2;

int16_t sample;
int16_t envelope;
int16_t delta;

void speak(int8_t type, int16_t raster)
{
int16_t cosine_wave[21] = {27073, 32767, 27073, 11971, -7291, -24020,
	-32401, -29522, -16384, 2449, 20430,
	31311, 31311, 20430, 2449, -16384,
	-29522, -32401, -24020, -7291, 11971};

if(cosine_pointer > 20) cosine_pointer = 0;  // Loop cosine pointer as necessary.

// Filter for max raster depending on type.
if(0x20 == type) // Damp Pulse
{
	if(raster > 9238) raster = 9238;
	else if(raster < -9238) raster = -9238;
}
else if(0x30 == type) // Enveloped Cosine
{
	if(raster > 20797) raster = 20797;
	else if(raster < -20797) raster = -20797;
}

if(0 != raster)  // Reset cosine wave with any non-zero nibble.
{
	cosine_pointer = 0;
}

switch(type)
{
	case 0x00:  // Direct samples
		envelope = raster;
		fwrite(&envelope, 2, 1, w);

		delta = 0;
		break;

	case 0x10:  // Envelope Only
		delta = -(raster);
		envelope -= delta;
		fwrite(&envelope, 2, 1, w);

		envelope -= envelope / 16;
		break;

	case 0x20:  // Damp Pulse
		delta -= raster;
		envelope = envelope - delta;
		fwrite(&envelope, 2, 1, w);

		delta -= delta / 8;
		envelope -= envelope / 8;
		break;

	case 0x30:  // Enveloped Cosine
		// I suspect that this type should calculate on-the-fly without a table.
		delta = -(raster);
		envelope -= delta;
		sample = (cosine_wave[cosine_pointer] * envelope) / 32767;
		fwrite(&sample, 2, 1, w);

		envelope -= envelope / 16;
		break;

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

cosine_pointer++;  // Keep the cosine wave rolling
}

int main(int argc, char *argv[])
{
// Init variables		
prev_type = 0x00;  // Init prev_type.
cosine_pointer = 0;  // Init cosine pointer.
envelope = 0;
sample = 0;
delta = 0;

while (fread(packet, 1, 15, f)) // Read in entire packets until EOF.
{	
	if(0x30 != prev_type)
	{
		cosine_pointer = 0;  // Start at peak of cosine wave.
	}
	else if(0x30 != packet[0] & 0x30)  // prev type was 30 and this type is NOT 30.
	{
		envelope = sample;
	}

	for(int i = 1; i < 15; i++)  // for each packet data byte
	{
		// Separate and sign extend each nibble.
		nibble_1 = packet[i] & 0x0F;  // Least significant nibble plays first.
		nibble_2 = (packet[i] & 0xF0) >> 4;
		if(nibble_1 & 0x08) nibble_1 |= 0xF0;  // Sign extend
		if(nibble_2 & 0x08) nibble_2 |= 0xF0;

		// Determine decompressed value based on data nibble and header nibble
		raster_1 = (nibble_1 * 4096);
		raster_1 /= 1 << (packet[0] & 0x0F);  // Can't shift directly because of signed numbers.

		raster_2 = (nibble_2 * 4096);
		raster_2 /= 1 << (packet[0] & 0x0F);  // Can't shift directly because of signed numbers.

		speak(packet[0] & 0x30, raster_1);
		speak(packet[0] & 0x30, raster_2);
	}

	prev_type = packet[0] & 0x30;
}

fclose(f);
fclose(w);
}

Share this post


Link to post
Share on other sites

Actually it improved it more than I thought! I keep track of the "delta" variable better when switching between packet types now. I still don't know what the HECK is going on in the 5th packet though.

Picture-4.gif.040e529c1e144e44137ca9562aa95785.gif

Share this post


Link to post
Share on other sites

Well, if I was a good engineer, I would know when to stop. :p But hey, this isn't work, it's for fun, so too bad. ;)

 

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

 

I can no longer hear any problems in the sound with my ears.

 

Some of the differences are probably because this is an analog recording with a high-pass, but there are clearly some spots that are logical issues.

 

The big change that I made was that I calculate the sinc wave directly using deltas now instead of scaling a cosine table. There are no longer any tables in the converter at all.

 

I'm betting that the remaining issues have to do with using previous/next state of variables, i.e. updating them before or after using them to modify the output. Maybe rounding could be an issue too, who knows.

 

I can't believe how concise the code has gotten! That's a really good sign for its accuracy.

 

#include 
#include 
#include 
using namespace std;

// Global variable definitions
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");

int8_t packet[15];
int8_t nibble_1, nibble_2;
int16_t raster_1, raster_2;

int16_t sample;
int16_t envelope;
int16_t delta;

void speak(int8_t type, int16_t raster)
{	
// Filter for max raster depending on type.
if(0x20 == type) // Damp Pulse
{
	if(raster > 9238) raster = 9238;
	else if(raster < -9238) raster = -9238;
}
else if(0x30 == type) // Enveloped Cosine
{
	if(raster > 20797) raster = 20797;
	else if(raster < -20797) raster = -20797;
}

switch(type)
{
	case 0x00:  // Direct samples
		envelope = raster;
		fwrite(&envelope, 2, 1, w);
		delta = 0;
		break;

	case 0x10:  // Envelope Only
		delta = -(raster);
		envelope -= delta;
		fwrite(&envelope, 2, 1, w);
		envelope -= envelope / 16;
		break;

	case 0x20:  // Damp Pulse
		delta -= raster;
		envelope -= delta;
		fwrite(&envelope, 2, 1, w);
		delta -= delta / 8;
		envelope -= envelope / 8;
		break;

	case 0x30:  // Enveloped Cosine
		delta -= raster;
		delta += (envelope / 3);
		envelope = (envelope - delta) - (envelope - delta) / 8;
		fwrite(&envelope, 2, 1, w);
		break;

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

int main(int argc, char *argv[])
{
// Init variables		
envelope = 0;
sample = 0;
delta = 0;

while (fread(packet, 1, 15, f)) // Read in entire packets until EOF.
{	
	for(int i = 1; i < 15; i++)  // for each packet data byte
	{
		// Separate and sign extend each nibble.
		nibble_1 = packet[i] & 0x0F;  // Least significant nibble plays first.
		nibble_2 = (packet[i] & 0xF0) >> 4;
		if(nibble_1 & 0x08) nibble_1 |= 0xF0;  // Sign extend
		if(nibble_2 & 0x08) nibble_2 |= 0xF0;

		// Determine decompressed values based on data nibbles and header nibble
		raster_1 = (nibble_1 * 4096) / (1 << (packet[0] & 0x0F));
		raster_2 = (nibble_2 * 4096) / (1 << (packet[0] & 0x0F));

		speak(packet[0] & 0x30, raster_1);
		speak(packet[0] & 0x30, raster_2);
	}
}

fclose(f);
fclose(w);
}

Picture-10.thumb.gif.35fcbc0e3e80db4234e38e50d0d84ab5.gif

Share this post


Link to post
Share on other sites

Excellent analysis and coding. Really appreciate the montage you just posted showing the evolution of the sound work.

 

Three Cheers!!

Share this post


Link to post
Share on other sites

All of the scratches are gone...woohoo! I agree--that montage is great!

 

So is this compression algorithm fairly typical of something you'd expect to see in the audio compression world back in those days? Does it lend itself to a particular type of sound?

Share this post


Link to post
Share on other sites

There are some guys here that probably know a lot about old hardware. I have an appreciation for it but I wasn't around for deep electronic stuff back then - I'm in my mid twenties.

 

I posted the code in color-coded form here, along with a little blurb/summary about the whole thing:

 

http://www.d.umn.edu/~bold0070/projects/easc_compression/

 

I try to post cool stuff on there for when people ask me what I do and what I've been up to.

Share this post


Link to post
Share on other sites

Here are the 3 wave types graphed in Excel - this is where I experimented to find the right values. All three are given the same first nibble, then the rest of the nibbles are all 0. So this is basically a unit step response for all 3 wave types.

 

Maybe the blue wave (type 1) starts 1 sample too soon?

5a1d03c779e61_Picture11.png.5d776a46b7c6d03886ad4f8c6300dc64.png

Share this post


Link to post
Share on other sites

In the type 3 mode step response, general shape and initial phase look great but:

 

- Amplitude is a tiny bit too high

- Frequency is notably too low

 

I'll see what I can do about tweaking that a little bit.

 

The small oscillations on the decoded version (black line) are from interpolation to get to 88.2 kHz in Sound Studio. They do not exist in the actual 22.05 kHz data. The large square step on the left side is from a maximum type 0 packet for normalization purposes.

Picture-1.gif.091a3bb3156017fd051d05a75ab2b341.gif

Share this post


Link to post
Share on other sites

In type 3, I was updating delta with this:

 

delta += (envelope / 3);

 

And the correct calculation is:

 

delta += (envelope * 3) / 8;

 

This produces a perfect result for type 3 as far as I can tell. I will apply this same method to types 1 and 2 now.

Share this post


Link to post
Share on other sites

I actually believe that it's perfect now. I can no longer see any differences that aren't caused by the analog connection. Both waves follow the exact same shape now. In this picture, the lighter line is an analog audio recording. Scrolling ahead and looking at lots of areas of the sound wave, I have found nothing that indicates a problem with the decompressor.

 

Latest decompressor code:

 

#include 
#include 
#include 
using namespace std;

// Global variable definitions
FILE *f = fopen("Q700RawSoundData2", "rb");
FILE *w = fopen("Q700DataOut", "wb");

int16_t envelope;
int16_t delta;

// Prototypes
void speak(int8_t type, int16_t raster);

int main(int argc, char *argv[])
{
int8_t packet[15];
int8_t nibble_1, nibble_2;
int16_t raster_1, raster_2;

// Init variables		
envelope = 0;
delta = 0;

while (fread(packet, 1, 15, f)) // Read in entire packets until EOF.
{	
	for(int i = 1; i < 15; i++)  // for each packet data byte
	{
		// Separate and sign extend each nibble.
		nibble_1 = packet[i] & 0x0F;  // Least significant nibble plays first.
		nibble_2 = (packet[i] & 0xF0) >> 4;
		if(nibble_1 & 0x08) nibble_1 |= 0xF0;  // Sign extend
		if(nibble_2 & 0x08) nibble_2 |= 0xF0;

		// Determine decompressed values based on data nibbles and header nibble
		raster_1 = (nibble_1 * 4096) / (1 << (packet[0] & 0x0F));
		raster_2 = (nibble_2 * 4096) / (1 << (packet[0] & 0x0F));

		speak(packet[0] & 0x30, raster_1);
		speak(packet[0] & 0x30, raster_2);
	}
}

fclose(f);
fclose(w);
}

void speak(int8_t type, int16_t raster)
{	
switch(type)
{
	case 0x00:  // Direct Sample
		envelope = raster;
		fwrite(&envelope, 2, 1, w);
		delta = 0;
		break;

	case 0x10:  // Relative Decay
		delta = -(raster);
		envelope -= delta;
		fwrite(&envelope, 2, 1, w);
		envelope -= envelope / 16;
		break;

	case 0x20:  // Relative Overshoot
		delta -= raster;
		envelope -= delta;
		delta -= delta / 8;
		fwrite(&envelope, 2, 1, w);
		envelope -= envelope / 8;
		break;

	case 0x30:  // Relative Oscillate
		delta -= raster;
		delta += (envelope * 3) / 8;
		envelope = (envelope - delta) - (envelope - delta) / 8;
		fwrite(&envelope, 2, 1, w);
		break;

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

Picture-6.gif.0635e0cabef9a1fddb37cf5a929b8119.gif

Share this post


Link to post
Share on other sites

Here are the 2 decoded startup sounds for everyone's collections:

 

https://sites.google.com/site/benboldt/files/Quadra%20700%20Normal.aif

https://sites.google.com/site/benboldt/files/Quadra%20700%20Easter%20Egg.aif

 

22.050 kHz, 16-bit signed mono

 

No better recordings of either of these sounds have ever been made! :D

 

A collection of startup sounds come with MacWorld Mac Secrets 5th Edition and MacTracker, so I don't think it's too bad to share them here.

 

The sound chip has a provision for peaking that this decompressor doesn't have - the chip will not exceed the peak but this decompressor does and it loops around from it creating a click like we were hearing before. There are no peaks generated in these startup sounds though, so it should be just fine.

 

edit

Also of note is that both of these compressed sounds exist in the LC III ROM.

 

Also the Macintosh Colour Classic II, LC 550, Performa 275, 550, 560, TV ROM (Checksum 0xEDE66CBD)

 

Also the Macintosh LC 475, 575, Performa 475, 476, 575, 577, 578, Quadra 605 rom (0xFF7439EE)

 

Also the Macintosh Centris 650, Quadra 650, 800 (0xF1ACAD13)

Share this post


Link to post
Share on other sites

Way to go Dennis Nedry! :-D This has been an awesome look into the EASC and Mac ROM history. Nice write-up on your website too!

 

Another interesting thing to do would be to determine which Macs have sound chips that support this mode. I have an LC 475, Centris 610, and Performa 630 that I could try (pretty sure the IIci wouldn't support that mode)...could just make a simple program similar to the control panel you have, but without the test to make sure it's running on a Quadra 700 (it does test for that, doesn't it?).

 

P.S. the code looks great! It really is amazing how simple the algorithm ended up being.

 

Edit: So are you comparing the generated waveform against an analog recording? If you really want to know for sure, you could play your decompressed raw 16-bit sound back through the Quadra 700 and compare an analog recording of it to an analog recording of the actual compressed sound...but based on that last waveform you posted, I agree -- it's probably perfect!

Share this post


Link to post
Share on other sites

AWESOME idea with playing the decoded sound back through the Quadra 700!

 

The BootBeep control panel can be easily modded to run on all Macs if you change the content of the 'mach' resource to 0xFFFF 0000. It might crash other Macs though.

 

I made this mod to BootBeep and now I'm using it to search for easter egg pictures in other ROMs.

Share this post


Link to post
Share on other sites

Here are a couple of easter egg images from other Macs. The Quadra 605, 630, LCIII, CCII all have the routine for this but apparently no image data stored.

 

I can't run the PowerBook 190 / 190cs ROM in Basilisk II because it's a 2MB ROM. I have this actual Mac but it's nowhere to be found. There may be an easter egg image on this PowerBook. If you have this PowerBook, you can interrupt and type the following to see if there's a secret image:

 

G 4081B854

5a1d03c7cc7c9_IIciIIfx.png.e09bb38159e34de441b702032ab7c027.png

5a1d03c7d09e1_Quadra650800.png.51360d904fbbce7bdb3948dc7ced82ce.png

Share this post


Link to post
Share on other sites

Apparently the Quadra 700 supports only 8-bit audio output. This is verified by garbage when I try to play a 16-bit system sound file on the Quadra. Admittedly the Quadra is running on 7.1. The sound header of the compressed startup sound clearly states that the sound is 16-bit though - and that's how I calculate it.

 

What are the chances that this is actually playing out of the Mac in 16-bit mode? I don't quite know how we could tell. It doesn't sound any worse coming from the real Quadra. I can't tell any difference at all. However when compared to the raw 8-bit sound, the sound quality is noticeably better than the 8-bit one. So that's a good sign.

 

I just don't want to jump to such a positive conclusion right away, but man that would be cool if we unlocked a feature like that.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×