Original Post

PCM, the common abbreviation of Pulse Code Modulation, is the simplest way to represent an audio stream digitally. It’s effectively a two-dimensional graph, where the vertical axis is value and the horizontal axis is time. By plotting data points along this graph, called samples, a wave form can be expressed as data and later used in computer audio systems.

PCM on Virtual Boy

First things first, Virtual Boy was not designed with PCM audio in mind. It was designed as a feature-rich “chiptune” generator, so all PCM techniques are dark wizardry and not intended use of the hardware. That’s not to say we can’t do it, though.

The most obvious solution is to rapidly update the Virtual Boy’s wave memory as a sort of small audio buffer. Unfortunately, this isn’t feasible for two reasons:

* Wave memory cannot be updated while sound is being generated, even by channels that are using different wave buffers. Yeah. Updating wave memory, therefore, absolutely necessitates stopping audio entirely, which produces a dead spot every 32 samples.

* Virtual Boy samples are unsigned, meaning the minimum value is 0. The aforementioned dead spots, then, drop the output level to 0. Why does this matter? It matters because PCM streams situate “silence” in the middle of their range, which for Virtual Boy would be 31 or 32. If samples less than “silence” can be considered negative, then 0 is the maximum negative value: it will produce a high-frequency, low duty cycle pulse every time wave memory is updated.

If only Virtual Boy allowed you to update wave memory while sound is being generated… You know, like Mednafen does! But alas, it does not. However, there is an alternative.

Software Modulation

So using wave memory as PCM buffers might be a wash, but that doesn’t mean we’re out of options. At the end of the day, what we want is to modify the output level of the audio being generated, and modify it at some predetermined interval. As long as all the ups and downs occur at the right moments, that’s what counts.

Here’s what we can do: take only one wave pattern, and initialize all of its samples to 63, the maximum positive value. Then fire up a sound channel using that wave, with any frequency, and let it fly. This will produce a constant high signal, which is inaudible, but it lets us do funky stuff with it.

The high-frequency hardware timer can be used to schedule changes to the volume level of that sound channel. The channel never stops emitting its constant high signal; we simply change on our own exactly how loud that signal is. If we do this fast enough, we can replicate the effects of PCM audio, and all on hardware that doesn’t natively support PCM.

The 16-Level Approach (4-bit)

I strongly suspect this is what Galactic Pinball does. But I haven’t actually seen it in action to verify one way or the other. But hey, it’s an option!

The volume level of a sound channel can be independently configured for both the left and right speakers. These levels are 4-bit, meaning there are 16 volume levels, with 0 being silence.

If a PCM stream is also 4 bits per sample, then it’s a simple matter of changing the volume of a sound channel each time. This works for both mono and stereo streams, and it couldn’t be easier!

The 46-Level Approach

4 bits might be fast and easy, but it’s also rather restrictive. Not that sounds are bad at 4 bits, but they certainly aren’t good either. Fortunately for us, we can step it up a bit.

A single sound channel can output levels from 0 to 15. Two sound channels mix together through addition. So using a second sound channel, we add levels 16 through 30. And using three channels, we add levels 31 through 45. Modifying three channels is much the same as modifying one; we just move onto the next channel if the value is above 15, or above 30.

Slight problem, though: Virtual Boy only has 5 wave channels, and we’d need 6 for 46-level stereo. We luck out again, because the noise channel can actually be used as that sixth channel. See, the way the noise algorithm works, the first 7 samples generated during a sequence will always be 63, the maximum value. And the sequence is reset whenever the tap location is configured. So if we just set the tap location every time we update the volume, we get a constant high signal out of the noise channel!

The 181-Level Approach

If you don’t need stereo, you can use all 6 sound channels with additive mixing for a single audio stream. By adjusting volume alone, you have 181 levels of output, and they can get quite loud.

The 256-Level Approach (8-bit)

Sound channels have more than just volume: they also have an envelope setting. The envelope value is also 4-bit, so 0 to 15 for that one as well. When a sample is generated, the volume and the envelope are multiplied with one another, producing the output sample. This can range from 0 * 0 = 0, to 15 * 15 = 225.

The full list of unique products is attached to this post as Produts.txt.

As it turns out, there’s only 90 of those. That’s wedged between 6 and 7 bits of data, and they’re not evenly-spaced. If you plot them out, the curve is roughly exponential. Not all that useful in its current form.

If you use two sound channels, on the other hand, it’s like taking two of these products and adding them together. And if you do that, you can make a whole slew of different values. Every level from 0 to 255 can be expressed as some sum of two sound channels.

The full list of sums, from 0 to 255, is attached to this post as Sums.txt.

Let’s take one as an example:

* Sample: 254
* Volume 1: 15
* Envelope 1: 14
* Volume 2: 11
* Envelope 2: 4

We know from earlier that each sound channel outputs a level that is the product of its volume and its envelope. That said, channel 1 here will output 15 * 14 = 210, and channel 2 will output 11 * 4 = 44. When mixed, the combined output will be 210 + 44 = 254.

By using 4 sound channels in this manner, you can have true 8-bit stereo coming out of the speakers.

Static!

While the high-pitched whine of the 0-based dead spots aren’t a problem with using software modulation, static certainly is. I haven’t put a great deal of research into why this happens, but I suspect it has more to do with the audio data I’ve been using and less to do with the techniques of software modulation. All I know is that when I did the 16- and 256-level methods, the static was prohibitively audible, but the 46-level method sounded okay. Go figure.

With some additional testing and experimentation, I’m hoping that we’ll find that the 256-level approach is fully adequate for general-purpose PCM output on Virtual Boy.

Demo

Attached to this post are the following files:

* AtlasParkSector.ogg – A sound clip from City of Heroes
* pcm_46.zip – Contains a Virual Boy ROM showcasing the 46-level PCM technique
* pcm_46.ogg – A recording of that program, run on the actual hardware.

Mednafen has different timing than the Virtual Boy hardware, so its output will not be identical. You can adjust the delay between samples by pressing Up and Down on the left D-Pad.

22 Replies

Smileys in text are used to indicate humor. They follow passages or messages that aren’t intended to be taken seriously.

DogP wrote:
Why did you modify the crt0, when you had such an amazing and intuitive control mechanism for it?

Configuring the ROM wait controller to use 1 wait makes sense: it speeds up cartridge reads. The register can be configured directly by the C program, but I don’t see any compelling reason to set it to 2 waits, much less have the default (via crt0) be 2 waits.

DogP wrote:
x++; is pretty archaic too, right? Is the libvueian way to do it x=addOneToThisValue(x); ? That’s too advanced for me… I’ll just stick with my stone and chisel.

Why use “HW_REGS[WCR]|=ROM1W;” when you can say “*(char *)0x02000024 = 1;”? In fact, why use a keyboard when you could just tap a couple of wires together to produce 1s and 0s? /oldjoke

DogP wrote:
On a serious note, is the last 0 saying that it’s the ROM bit, not the EXP bit? Maybe defining that would make it a bit more clear… everything else is actually descriptive, but a stray, undescriptive 0 looks arbitrary and out of place.

The macro is defined like this:

// Encodes wait controller fields into a bit-packed value.
#define vueWaitControl(rom, expansion) ( (rom) | ((expansion) << 1) )

The symbolic values to use for either are as follows:

// Wait Controller Constants
#define VUE_WAIT_1 1
#define VUE_WAIT_2 0

DogP wrote:
I still prefer specifically setting the bits, because I can read the dev manual, decide which bits I want to set, and set them, without digging through header files to figure out that what I want to do requires something called VUE_WAIT_CONTROL, vueWaitControl, and an argument of VUE_WAIT_1 (and apparently a 0 for some reason)...

Given the macro definition above, that zero was the parameter for configuring the expansion wait controller. Since I had no preference to how that wait controller was configured, I just set it to zero. As School House Rock always liked to say: knowledge is power!

Though it's worth bringing up that you're describing the same procedure requried to find out that there's an array called HW_REGS, which has a subscript called WCR, which can be configured with a constant called ROM1W. It's a case of the mallard calling the teal a quack. /newtwistonanoldjoke

DogP wrote:
my way (as in the way it's been done until the libvueians decended to Earth to spread their superior knowledge) has the names defined the same as the register names from the manual...

libvue does provide all of the dev manual register names as an undocumented courtesy (VUE_WCR in this case), but a design goal of libvue is to make distance from the dev manual. Partly because that manual is corporate proprietary content, partly because often times the register names aren't any easier to remember than their corresponding bus addresses.

The naming convention chosen for libvue is fairly verbose, and as a matter of aesthetics, no one's particularly fond of it. (-: However, it has been selected because at the end of the day, "VUE_CHANNELS[2].play_control" is easier to remember than "S3INT". It compiles to the same thing, either way. The existence of the vuePlayControl() macro also aids in configuration of hardware registers.

DogP wrote:
But I'm sure you're gonna say that libvue will have full documentation explaining every part of it... yay. My archaic way already does.

Scroll up a few paragraphs to the section about header files.

Not only is libvue being documented for each feature incorporated into it, but tutorials and sample programs are also in the works. It's a bit early to be knocking it based on overlooking a smiley face.

But this isn't PCM related, so look for more info when libvue and devkitV810 are closer to release. (-:

Guy Perfect wrote:
Smileys in text are used to indicate humor. They follow passages or messages that aren’t intended to be taken seriously.

Yes… sorry, I should have used more of them. 😉

Guy Perfect wrote:
Configuring the ROM wait controller to use 1 wait makes sense: it speeds up cartridge reads. The register can be configured directly by the C program, but I don’t see any compelling reason to set it to 2 waits, much less have the default (via crt0) be 2 waits.

The default at hardware reset is 2 waits, so I wouldn’t really say the crt0 default is 2 waits (I’m guessing it wasn’t previously setting it at all). Personally, I think setting the waits to 1 should be done in the program, not the crt0. By setting it to 1, you’re requiring the ROM to be <= 100ns. That's fine in some cases, where you're willing to give up compatibility for performance... but not in all cases. I personally frequently use 120ns flash (that's what's in my cart right now), and I'm not sure that all Flashboys have fast enough flash installed (I know at least one of the prototypes used 120ns). The problem is, that if I write a program using your 1 wait state crt0, I can't just set the wait states to 2 in my program for compatibility with my slow flash... where the opposite is possible (starting with 2 wait states, then changing to 1). I can't think of a realistic application where 2 wait states before execution of your init code would be problematic.

Guy Perfect wrote:
Given the macro definition above, that zero was the parameter for configuring the expansion wait controller. Since I had no preference to how that wait controller was configured, I just set it to zero. As School House Rock always liked to say: knowledge is power!

Ah, I see… though it seems that you should always use VUE_WAIT_1 or VUE_WAIT_2, since the value that you put there DOES do something (not a don’t care). Anything other than a valid value could produce unwanted results (yes, in this case, 0 is valid… but to promote just putting 0s in parameters that you don’t care about seems a bit dangerous). 😉

Guy Perfect wrote:
Though it’s worth bringing up that you’re describing the same procedure requried to find out that there’s an array called HW_REGS, which has a subscript called WCR, which can be configured with a constant called ROM1W. It’s a case of the mallard calling the teal a quack.

Not really… like I said, once I know that HW_REGS exists, by referencing the dev manual, I know that WCR, THR, CCR, etc. exist, and exactly what they do (yes, HW_REGS is a bit arbitrary, and if I was to make a change, I’d go with NVC_REGS, VIP_REGS, and VSU_REGS to be consistent). I’d argue that most of the names are pretty easy to remember, but that’s just a matter of opinion. But what is hard to remember is that VUE_CHANNELS[2].play_control is the same as S3INT (and whatever all the other names changed to)… so if I’m referencing the dev manual, there will be a lot of cross referencing to figure out how to make it work.

But, it sounds like we just disagree on a lot of stuff, so I’ll finish up with a great song:

And if you don’t like that song, then I’d say that we have nothing in common… which reminds me of another good song:
http://www.youtube.com/watch?v=1ClCpfeIELw (replace Breakfast at Tiffany’s with Virtual Boy).

DogP

 

Write a reply

You must be logged in to reply to this topic.