Jump to content





wav2atari.pl

Posted by TROGDOR, 15 March 2009 · 1,373 views

Misc
I have a habit of checking out the 2600 programming forums every week or so, and two weeks ago I started commenting on the topic Advanced sound techniques: how do they work? My interest in this topic was mostly in sample playback. I've been messing with digital sound samples since 1990, and I was very impressed when I found out a couple years ago that the 2600 was capable of playing back decent quality sound samples. My only exposure to this was the Berzerk Voice-Enhanced, which is a thing of beauty (and a joy forever.)

While responding to that topic, I mentioned if I couldn't find a decent program to translate 8-bit PCM wav files into 4-bit Atari format, I'd try writing one myself. Well, tonight after I put the kids to bed, I had a couple beers and set to work. An hour later, I had wav2atari.pl:

use Getopt::Long;$result = GetOptions ("wav=s", \$wavfile, "outfile=s", \$outfile, "help", \&usage);sub usage {	print "\n$0 -wav <wav_file> -outfile <out_file> -helpThis program reads in the specified wav file.  The sound data is translatedinto a 4-bit Atari format that is dasm readible.  Two samples are encodedinto each byte of output.  A comment is placed every 256 bytes to denotea new page in Atari ROM.Note that the first of the two samples goes into the lower nibble.  Thisworks optimally in Atari code.  The first sample can be written directly toAUDV0, since the register will strip off the higher 4 bits automatically.Then the second sample can be written to AUDV0 after 4 LSRs.  This preventsthe need for temp variables and saves precious cycles during playback.The asm data is written to the specified -outfile.  RIFF PCM headerinformation is written to STDOUT.Currently only 8-bit PCM wav files are supported.\n";	exit;}#$wavfile = shift;if ($wavfile eq "") {	die "ERROR: Must provide input wave file.\n";}open (WAV, $wavfile)	or die "Could not open wavfile '$wavfile'.\n";# Read ChunkIDread (WAV, $data, 4);if ($data ne "RIFF") {	die "ERROR: This doesn't appear to be a RIFF PCM WAV file.\n";}print "      ChunkID = $data\n";# Read ChunkSizeread (WAV, $data, 4);$data = unpack ("V1", $data);print "    ChunkSize = $data\n";# Read Formatread (WAV, $data, 4);print "       Format = $data\n";if ($data ne "WAVE") {	die "ERROR: This is a RIFF file, but it doesn't appear to be a WAVE file.\n";}# Read Subchunk1IDread (WAV, $data, 4);print "  Subchunk1ID = $data\n";# Read Subchunk1Sizeread (WAV, $data, 4);$data = unpack ("V1", $data);print "Subchunk1Size = $data\n";# Read AudioFormatread (WAV, $data, 2);$data = unpack ("v1", $data);print "  AudioFormat = $data\n";# Read NumChannelsread (WAV, $data, 2);$data = unpack ("v1", $data);print "  NumChannels = $data\n";# Read SampleRateread (WAV, $data, 4);$data = unpack ("V1", $data);print "   SampleRate = $data\n";# Read ByteRateread (WAV, $data, 4);$data = unpack ("V1", $data);print "     ByteRate = $data\n";# Read BlockAlignread (WAV, $data, 2);$data = unpack ("v1", $data);print "   BlockAlign = $data\n";# Read BitsPerSampleread (WAV, $data, 2);$data = unpack ("v1", $data);print "BitsPerSample = $data\n";# Read Subchunk2IDread (WAV, $data, 4);print "  Subchunk2ID = $data\n";# Read Subchunk2Sizeread (WAV, $data, 4);$data = unpack ("V1", $data);print "Subchunk2Size = $data\n";# Read in all the data.$data_byte_count = 0;$page_count = 0;$output = "";while (read (WAV, $data, 2)) {	@data = unpack ("C2", $data);	if ($data_byte_count % 512 == 0) {		$output .= "; Page $page_count\n";		$page_count++;	}	# If there is an odd number of bytes in the sample, drop the last byte.	if ($data[1] eq "") {		print "Warning: last byte of sound data was ignored because this file\n";		print "contains an odd number of sound bytes.\n";		$data_byte_count++;		last;	}	# Note, the first of the two bytes goes into the lower nibble.  This works	# optimally in Atari code.  The first sample can be written directly to	# AUDV0, since the register will strip off the higher 4 bits automatically.	# Then the second sample can be written to AUDV0 after 4 LSRs.  This prevents	# the need for temp variables and saves precious cycles during playback.	#strip off the lower 4 bits.	$lownibble = $data[0] >> 4;	#shift 4 bits to the right, effectively stripping off the lower 4 bits.	$highnibble = $data[1] & 240;	$condensedbyte = $highnibble + $lownibble;	$output .= sprintf ("	.byte #%%%08b\n", $condensedbyte);	$data_byte_count += 2;}print "$data_byte_count bytes of sound data processed.\n";close WAV;open (OUTPUT, ">$outfile");print OUTPUT $output;close OUTPUT;
I had expected this program wouldn't be too difficult to write. The guts of the program is only about 10 lines. The rest is just spitting out the RIFF header info.

For those who aren't familiar with a .pl file, it's a perl script. You'll have to have a perl interpreter installed on your system to use the script. But it should be easy to translate this program into your scripting language of choice if you don't happen to use perl.


My quest to get my own samples working on the Atari started on Google. I quickly found a nice spec for the PCM wave format here. Next I grabbed a small random wav from Google to use as my test subject:

Attached File  hello.wav (4.59KB)
downloads: 223
I'm pretty sure this particular hello is Graham Chapman from Monty Python and the Holy Grail. All the better.

After I fed this wav file through wav2atari.pl, I made some modifications to my unsound wave generation demo, and then some more tweaking, and suddenly, it worked! Hearing this sample play back correctly in a 2600 emulator is one of the most satisfying moments I've ever had programming for the 2600. :)

The atari binary still needs work. I just threw this together, so the sample playback rates are not perfectly balanced (the delay between the low nibble sample and the high nibble sample should be exactly the same, but they're not), yet it still sounds pretty good.

If I get the time and energy, I'll enhance wav2atari to work on 16-bit samples, and add a downsampling option so you can specify the output sample rate. I also need to clean up the playback asm so the delays are balanced.

The zip file I'm including below contains the wav2atari.pl script, the HELLO.BIN Atari binary, the hello.asm dasm assembly file, and the original hello.wav file for comparison. Enjoy!

Attached File  HelloWorld.zip (11KB)
downloads: 228
Here's another demo that varies the pitch of the sample:

Attached File  HELLO2.BIN (4KB)
downloads: 202
Here's the modified source code:

Attached File  hello2.txt (45.82KB)
downloads: 197
The next thing to do is get a looping 256 byte guitar wave. :cool:




Nice Stuff! I ain't no programmer but i had fun playing around with your program and i even managed to make a working .bin myself.. ;)
I hope you keep working on it!
  • Report
Thanks Impaler. I've added a variant that demonstrates high resolution variations in pitch. This uses a NOP jump table to create delays with 2 cycle resolution. If necessary, it's also possible to get single cycle resolution using a C9 jump table. These subtle variations in delay change the playback rate of the sample, which alters the pitch of the sample.

I couldn't figure out how to attach it to this comment, so I put it at the end of the blog post above.
  • Report
Nice work - the quality is way better than I was expecting! It looks like there is plenty of room for optimisation in the code - it would be great to have a playfield display during playback, e.g. for a title screen?

Chris
  • Report
Thanks Chris. Playfield display with samples is possible, but you'd have to have a separate block of code to handle the kernel and the off-screen code to keep everything in sync. Note also that this would work for constant-rate samples, but would not be possible for variable-rate playback, unless you do something complex like wave tables.

I've been working on making a song with a guitar sample, but I'm finding a surprising amount of aliasing coming from the 16-bit to 8-bit downsampling. I expected aliasing from a reduction in sample rate, but I wasn't expecting it from a reduction in bit resolution. I'm going to consult some audiophile forums to see if any pre- or post-processing is possible to prevent this downsample aliasing. It adds too much distortion. (Then again, this wouldn't be a problem if your goal was a distorted guitar. ;) )
  • Report
Hey! Nice little bit of code you got here! I tried getting it working last night, but I think I'm getting a bit too greedy on my wave sizes. I'm also trying to incorporate it into Music Kit 2 for fun. Dunno if I'll be successful, but I'll give it a whirl. I know I'll have a lot of hacking to do regarding pretty much everything... We'll see.

Any tips, suggestions, insight, or motivational speech? ;P
  • Report

I've been working on making a song with a guitar sample, but I'm finding a surprising amount of aliasing coming from the 16-bit to 8-bit downsampling. I expected aliasing from a reduction in sample rate, but I wasn't expecting it from a reduction in bit resolution.

Quantization noise - the usual way to improve it is to add a noise signal (less than 1 bit of the output resolution) to the input before the requantization. Unfortunately, this only really works at high sample rates. Low sample rates & low resolution = low fidelity signal.
  • Report
Thanks B00daW. I'd suggest getting a small, simple sample working first, and then grow from there.

I've discovered that it's tough to do anything longer than 2 seconds without bank switching. I squeezed out another sample last month.

EricBall, thanks for the info. I'll do more research on it as time allows. The Hello sample seemed reasonably clear, even though it was down-sampled from 8 bits to 4 bits.
  • Report

I expected aliasing from a reduction in sample rate, but I wasn't expecting it from a reduction in bit resolution.


In my BTP2 music driver, every pitch is a power-of-two multiple of a submultiple of the output sample rate (15.75KHz, matching horizontal scan). This means that any unwanted frequencies resulting from quantizing noise will be "on pitch". For example, when outputting the top C (about 2096Hz) there will be some distortion noise at 1048Hz, but since 1048Hz is also a "C", it's not particularly objectionable.

I was somewhat surprised to discover, when I was simulating a more sophisticated synthesis engine, that there is surprisingly little room for improvement in my BTP2 driver. One could offer a somewhat better selection of timbres, but trying for more precise frequency outputs ends up making things sound worse because it disrupts the regular pattern of the quantizing noise.
  • Report