• 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.

Dennis Nedry

Well-known member
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

Code:
#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);
}
 

dougg3

Well-known member
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!

 

Dennis Nedry

Well-known member
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.

 

Dennis Nedry

Well-known member
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.

Code:
#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);
}
 

Dennis Nedry

Well-known member
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

 

Dennis Nedry

Well-known member
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.

Code:
#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.gif

 

jongleur

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

Three Cheers!!

 

dougg3

Well-known member
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?

 

Dennis Nedry

Well-known member
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.

 

Dennis Nedry

Well-known member
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?

Picture 11.png

 

Dennis Nedry

Well-known member
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

 

Dennis Nedry

Well-known member
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.

 

Dennis Nedry

Well-known member
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:

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

 

Dennis Nedry

Well-known member
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)

 

Dennis Nedry

Well-known member
Also Macintosh LC 630, Performa 630, Quadra 630 (0x06684214)

I don't have many PowerBooks so there may be some of those with this too, you never know.

 

dougg3

Well-known member
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!

 

Dennis Nedry

Well-known member
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.

 

Dennis Nedry

Well-known member
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

IIci, IIfx.png

Quadra 650, 800.png

 

Dennis Nedry

Well-known member
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.

 
Top