LithoLantern: Let your friends be your light!

July 18th, 2021

I love practical effects. I also love my friends. Therefore, I’m excited to show you a new side project I’ve been working on that involves building a practical effect featuring my friends!

The LithoLantern!

In short, this is a 3D printed lantern I designed and built that features interchangeable plastic photo panels, known as “lithophanes,” and a custom RGB LED lighting board that allows you to change the color scheme backlighting the photos to what ever you want.

Backstory

So, during the pandemic, the world kinda’… well, stopped. Access to friends was limited, so admittedly, it got a bit lonely. However, I had picked up a virtual reality headset earlier in the year and I decided I was going to put it to use playing a social game called “VRChat.” VRChat is basically just a sandbox virtual reality game where you can chose to play as any avatar you want in just about any virtual setting you can image. It’s a fantastic, sureal way to meet new people.

Long story short, I eventually established a robust friend group who I really love being with. Frequently, we visit our friend’s virtual karaoke world to sing songs together (much to the dismay of our neighbors at 3AM). This karaoke world is Japanese themed, and I noticed the little traditional lanterns decorating the level.

The traditional lantern design found in our virtual karaoke world that inspired me.

In a passing comment, I mentioned that it would be neat to explore lithophanes using a lantern design like this. It didn’t take long after that comment that I started theorizing designs and prototyping to bring my idea to life. I thought to myself, “What if I could create one of these lanterns with interchangeable panels featuring images of my virtual friends? Wouldn’t it be amazing if you could change the color of the lantern to what ever you wanted?” Uh, yeah!

Designing the Lantern Body

I’m not a particularly skilled 3D artist, but I figured that the boxy shape of these lanterns wouldn’t be too difficult to model out. Spoiler: There were a lot of different things to consider about the design other than just the shape.

My tool of choice? Fusion360. As much as I despise Autodesk, there’s simply no contest when it comes to intuitive parametric 3D modeling.

I got to work designing the body and lid. There were a few main considerations for me:

  1. What is the aspect ratio of the photos I’m going to use for the panels? Answer: 9:16. Therefore, the “frame” of each side of the lantern should be sized appropriately to accommodate a portrait photo.
  2. How thick will the lithophane panel be? Answer: 3mm. I needed to design a way for the panel to slot-in and friction-fit itself so that it was centered and held-in-place.
  3. What is the print volume of your 3D printer? Answer: 250 x 210 x 210mm. I needed to keep this in mind so that my 3D printer could actually print the entire body of the lantern and all of its pieces.
  4. Tolerances. 3D printers are not very accurate when it comes to super close fits, so keeping “wiggle-room” in mind for all of my interference fits was pivotal in making sure the parts fit together properly.
  5. Aesthetics. These types of lanterns are very pretty when it comes to their proportions and attention to detail. I was fond of the lantern designs that emphasized an even frame size around the panels, so I chose to design my lantern in a similar way.
  6. How will you light the panels? Answer: Custom lighting board fit to the bottom of the lantern body’s interior.
  7. How will you power and adjust the lighting board? Answer: Via USB as it’s common and can supply 1-2 Amps of power at 5V, which is what I need. Careful consideration to where buttons and USB ports were going to be laid out was important as type-B USB cables tend to have pretty chunky connectors. Clearances for those parts were definitely thought about in great detail, both for practical and aesthetic purposes. I decided that a USB type-B connector mounted upside down on the bottom of the board facing towards the back of the lantern was best. A notch was designed into the back of the lantern body to accommodate the size of the USB connector. As for color adjustments, I figured a single “setting” button on the bottom of the board would suffice.
  8. How will light bounce around your enclosure? Answer: That was the most difficult problem to think of theoretically. Bottom-lighting these panels might not work and I may need supplemental lighting or a light-guide to evenly light the panels. I figured a strip near the bottom of each panel paired with a central ring of LEDs would provide enough light to saturate the panels if diffused correctly.
  9. How will you make the panels interchangeable? Answer: A lid that you can open that features slats that allow the panels to fit snugly inside. The lid will be designed in a way that (mostly) sits flush with the rest of the lantern, preserving the clean, even and proportional aesthetic of the lamp.

I decided to reference lanterns with this type of body as the thin top features of the virtual lantern would be fairly difficult to print due to the large overhangs that would leave lots of visible support material scaring…

Here’s the model I came up with

Notice the slats along the sides to fit the panels into, the notch in the bottom back side to accommodate the USB socket, the mounting holes for the future PCB, the flush-fitting lid with slats on the inner walls (not seen here), the upside-down pyramid light guide, even the little ledge on the bottom of the base to hold up the lithophane panels. There was a whole lot to consider, and given than this was my first attempt, I’m pretty proud of the result!

Printing the Lantern Body

The design of this lantern makes the printing process relatively straight forward, but very long. The lantern body itself took about 19 hours to print, the lid took another 4 hours, the light-guide another 4 hours still, and each litho panel took about 12-16 hours to print.

Since the lantern body features large overhangs on the bottom section, support material is required, though, due to the boxy, high-surface area shape, supports are really not too much of an issue and are fairly easy to remove. Just be sure to be careful of support material ending up in the screw holes or along the lithophane guide-slats.

Away we go!

19 hours later… beautiful!

Starting the lid print. Great first layer!

Looking alright, but it appears that the interior support bars in the lithophane slats are hard to fit flush within the lid’s square corner-mounts. This required a couple tries to get right.

The light-guide that will be affixed to the bottom of the lid with glue! Looks like something out of a sci-fi movie.

Designing the Lighting Controller

Here’s the part I was most looking forward to: the electronics!

There are indeed pre-made products a’plenty out there that would probably get the job done just fine, but where’s the fun in that? Here, we go full custom.

Therefore, I needed to consider my goals once again before I went to work designing this circuit board.

  1. Dimensions: This was actually premeditated during the design phase of the lantern body. I wanted a circuit board that was under 100 x 100mm, which is around the cut-off point for very cheap PCBs. I also had to consider that the board’s footprint would need to be shaped to the lantern to fit properly.
  2. LEDs: I wanted addressable RGB LEDs, that way, I could assign a color to every single LED individually. WS2812B’s (AKA, “Neopixels”) worked just fine for this.
  3. Power: Since I wanted this to run off of USB power, namely, a cellphone charger, I set 5V 1A, or 5 Watts as my power target.
  4. Heat: 5 Watts isn’t too much heat to deal with, but if it isn’t dissipated correctly, things might get toasty.. melty… burny… I chose to go with a 4-layer PCB to sink away that heat with the extra thermal mass of the inner copper layers. It also made routing easier.
  5. LED Positioning: Like I mentioned before, I wanted a strip of LEDs on each side plus a ring in the center. Finding an appropriate number of LEDs and spacing them out correctly was another challenge that I needed to consider.
  6. Microcontroller: So, for something like this, a modern ARM microcontroller paired with a level translator would probably do fine… but uh, as of mid 2021, there is an insane electronics part shortage, so they’re impossible to find. So, I decided that I wanted a 5V microcontroller that was produced in serious volume and is very common. It also had to have enough RAM to handle all of the LEDs I wanted to drive. Hey, the ATMega328p is the same one used in Arduino Unos and is very common! Look, Digikey still has the DIP versions in stock! RAM is a little tight (2K), but we can make it work!
  7. Programming: I wanted a USB-Serial converter chip so I could program this lighting board directly over USB just like an Arduino. I chose the CH340G, which promptly went out of stock after I designed the board. Oh well, at least I had some spares on hand.
  8. Light-sensing: This was kind of a last-minute thought, but I figured it might be handy for this lantern to be able to sense ambient light levels so it could turn on when it detects that other lights are on, or vice versa. I added a little photoresistor circuit to the bottom of the board just in case I wanted that feature later.
  9. Test points for future expansion: Never know when you’re gonna need them! I added test points to allow me to break out the SPI bus and an extra set of test points at the start and end of the LED’s data chain, just in case I needed to add more lights. This is also handy for direct in-circuit debugging if the need arises.

Boy, all those LEDs were a pain to hook up. Don’t forget your bypass capacitors on each one!

Drawing out the board, making sure the dimensions were correct, positioning the screw-holes, and determining how many LEDs to place was a huge challenge, but I nailed it! I actually did this before designing the schematic. Notice that here too I accounted for tolerances. The yellow perimeter is the actual board side, the white perimeter is the interior outline of the lantern’s seating plane.

Getting closer!

Looking good! The PCB was originally going to be white to reflect light better, but this made my PCB costs explode by a factor of 4, so I eventually changed it to the far-cheaper black color solder mask.

About a week later…

Beautiful! By the way, “Fervid” is my online pseudonym, hence “Fervid’s Lab.” I also play as a virtual tiger, so big cats on the silk-screen!

Let’s get it all put together, programmed up and…

Forgive the camera faceplant. This was just a quick test video and I ran out of hands. But, as you can see, we’ve got a bunch of really pretty, individually programmable lights! Tada!

The order of the LEDs starts at the top right corner (right of the USB socket), and goes clockwise. After the last LED along the sides, the inner ring is addressed starting at the top center going clockwise again.

Creating the Lithophanes

Can you believe that I did all this without testing if I could even make a viable lithophane first? Perhaps that was a poor idea, given how much time and money I’ve put into the project already, but hey, this stuff is exciting.

I’ve never actually attempted to print a lithophane before, so this was going to be an interesting experience.

Simply put, lithopanes are just sheets of 3D printed plastic with photos embedded in them. Dark areas of the photos have thicker plastic, blocking more light, while brighter areas of the photos are thinner, allowing more light to pass through. The result is a remarkably detailed light illusion embedded in a sheet of plastic.

It’s actually not all too difficult to create a lithophane as there are several free websites and programs dedicated to converting your photographs into printable 3D models. My personal favorite is this site: https://lithophanemaker.com/Framed%20Lithophane.html

I feel like these settings worked best for my project. The frames come out perfectly and fit nicely in the lantern. Notice how I’ve rotated my image beforehand. I think this preserves a lot of the detail.

Here was the result:

A panel of the tiger character I play as! I’m always blown away at the level of detail in these lithos. It’s like magic.

After a bit of research, I’ve found that lithophanes print best when they’re printed on their sides, rather than flat on the print bed, which is counterintuitive at first, but makes sense when you consider that the print head moving across your photo would leave unsightly marks if it was flat on your print bed. Therefore, a brim is practically required for your print to stay affixed to the bed while printing. You will also want to make sure that the lithophane’s longer dimension is moving with the print bed (Y-axis) rather than with the print head (X-axis), as you want to reduce the leverage and inertia acting upon the fairly precarious vertical print. PLA is my personal plastic of choice since stringing and warping is minimal.

A layer hight of 0.1mm produces great results. You’ll also want to make sure that your infill is set to 100%.

Here is an example of my Cura slicer settings:

You may want to experiment with your temperatures a bit as I tend to run my PLA a little on the hot side.

A panel sliced and ready for printing. Notice how the panel is on it’s side with a brim and is aligned with the Y-Axis. Send it to the printer and get ready to wait for about 12-16 hours!

After your print is finished, you can remove the brim with a set of needle-nose pliers, a little bit of elbow grease, and a knife to clean up the sides. you want to make sure the brimmed edge is flush to ensure that it fits in the tight-tolerance lantern body slats.

The End Result

Once everything was finally printed out and put together, this was the final result…

…Featuring portraits of lots of my friends in their virtual characters! And, uh oh, looks like one of my LEDs in the center ring died, preventing the others in the chain from lighting up. WS2812’s are sensitive to ESD and heat, so I wonder if I shocked it on accident or if the reflow oven busted it. Oh well, the lantern still looks great and I’ll get that fixed up!

I’m really impressed at how well the lithophanes came out. For example, here’s an original image and the resulting litho…


The ability to change the colors on each panel is super awesome. This was a really fun project with an excellent result for just winging it on a whim. My camera seems to struggle capturing the colors of the lantern as it’s seen in real life, so the colors are much more subtle when viewed in reality.

Still looks amazing though.

…And, the ability to swap lithophane panels is also really cool! Let’s say you’re playing a game of Murder and your friend decides to shoot you in the face for no reason. Well guess what? You can just swap their panel out for a new friend!

Just kidding, Artie. Kind of.

What next?

I’m thinking about cleaning up this project and making it open. In its current state, it works pretty well, but isn’t really polished enough for any person to just go out and build one. There’s quite a bit of first-hand knowledge required to get it to run properly, so I’d like to make it a lot easier for others to assemble and program. For now though, I’ll just keep it as a personal project that I thought would be really cool to make and share. Hope you enjoyed learning about the process!

Audio Mixing, Big Class D Amps, and Light Shows

July 21, 2020

Strap in, big post.

(If you just want to quickly see the project working, click here)

I’m fascinated by audio electronics. Arthur C. Clarke’s quote, “Any sufficiently advanced technology is indistinguishable from magic,” really does become relevant when developing analog circuits. While digital circuits can also be challenging at times, their trouble-shooting is generally constrained to “working, or not working.” Analog circuits, especially audio circuits, are crafted out of components that exploit the fundamental physical properties of various materials and are entirely dictated by often complicated mathematics and theories. You are at the mercy of seemingly arcane and arbitrary phenomena that permeate every facet of our existence. I often stop and marvel at the madness happening on my workbench when working with analog circuits. Magnets, coils, resistors, capacitors, amplifiers – a big jumble of crazy things all working together, doing their little parts applying their unique properties to condition signals suitable for other components or the end user. It’s all endlessly fascinating to me, and I thought I’d share my appreciation for those who also feel the same way.

I write this article today to share with you a new audio project I’ve been working on, as well as give some insight into the things I learned along the way. I’ve never received a formal education in electronics engineering, so I feel as though my naive perspective may in-fact be of use to those who are also at my level of understanding. In my opinion, the most of the difficulty in designing anything is going into the project without knowing of the names and techniques used by your not only your contemporaries, but the great minds from a century ago. Once you have a searchable term that you can plop into Google, the mysteries quickly solve themselves and the fog begins to clear. Also, as I’ve found, fundamental concepts that are simple in theory may take a little bit of time to wrap your head around as prerequisite knowledge is often assumed, even though the reader may not have those building blocks under their belt yet. Hopefully, I can convey what I learned in a way that makes sense to novices, even though what I will be talking about is fairly complicated.

Anyways, enough rambling, let’s get to the project…

Boombox Mascot

This project began when I was contacted by a person I worked with previously. They are starting up their own children’s TV/web show and wanted to create a custom “boombox character” with lights, speakers (duh), and a screen acting as their “face.” Admittedly, this did sound a bit silly to me at first, but truthfully, I’ve grown very fond of this project. At this stage in the game, I’ve only developed the audio and light-show portion of the character, but I feel that it is by far the most fascinating aspect of the project. You won’t see much, or any of the character itself in this post, simply the audio and lighting electronics, but I digress.

I didn’t have much to go on specification-wise. All I knew is that I’d require a mixing circuit to handle multiple audio inputs, a power amplifier to drive some 5″ speakers, and a way to sample the audio stream to create a reactive light show. Board and component size was not really an issue as the custom enclosure will be fairly large.

That ‘specification’ above was all I really had to work with, so I decided that my first task was to select some of the main components, like the speakers, so I could have some solid numbers to work with.

Here are the things I thought about before I began:

  • The boombox was going to be in stereo.
  • There will be four speakers in total to drive. Two tweeters handle the high frequencies, two mid-range speakers to handle just about everything else. All speakers will be 8 ohms to keep things simple.
  • No subwoofer on this design, but keep one in mind while designing just in case you’d like to add one later.
  • The amplifier had to be powerful enough to drive my speakers with a reasonable volume level, but since the speaker specification was fairly nebulous at the time, I needed to choose an amp that had the power head-room to support more, or bigger speakers, just in case the project changed over time.
  • I wanted the amp to be fairly efficient, as dealing with a ton of heat would just be another engineering hurdle.
  • This is not a “critical listening” device, meaning audio quality does not need to be absolutely pristine. Subjectively sounding good is the goal. Objectively sounding good is something that I’ll leave to the audio engineering professionals.
  • I needed a way to control addressable LEDs. I think a Teensy board should be more than enough to handle that while simultaneously sampling the audio stream to perform some neat tricks, like fast-Fourier transformations or VU metering.

With those ideas in mind, I began breadboarding.

Getting an Early Feel for Things

I’ve worked with audio stuff before, of course, so I had a rudimentary idea of what I was doing, but I have never worked on audio circuits with this much power behind it before. 5″ speakers are still fairly big components and they can take a ton of power to drive correctly. Before I committed to purchasing any big speakers, though, I decided to make a few lower-powered prototypes with some smaller, but still fairly beefy speakers. I picked up a couple of 2 inch, 5 watt, 4 ohm speakers just to test with and began developing some breadboard circuits.

Picking a Power Amplifier

Picking an amplifier class was also on my mind at this time. For those who don’t know, there are several audio amplifier classes, each with their own unique properties. Some of the most common are Class A, Class B, Class AB, and Class D. There are others, but I’ll quickly go over these ones.

Class A amps are usually found in systems where quality is more important than efficiency. Therefore, they have a tendency to consume quite a bit of power and get really hot. Class B amplifiers were designed to improve efficiency at the expense of some potential distortion issues at the signal’s “zero-crossing” (the point where the audio signal nears 0V).

As a means to combine the improved efficiency of Class B amps and the minimal distortion of the Class A amp, the class AB amplifier was invented. Class AB amplifiers are extremely common and provide a good middle-ground between A and B class amps. Unfortunately, at higher powers, Class AB amplifiers still have a tendency to get really hot and require beefy heat sinking.

Finally, there’s Class D amplifiers. Class D amplifiers are extremely efficient. Like… extremely extremely efficient, to the point where it kind of feels like cheating the laws of physics a bit. Class D amplifiers work on the principle of filtered high-frequency pulse-width modulation driven transistors, similar to a how a switch-mode power supply boosts up voltages, but these devices are for audio signals. I’ll explain more in detail in just a bit since there’s a bit of technical jargon there. The switching frequencies at which these amps operate at are well above the range of human hearing and are therefore inaudible should the circuit be designed correctly. The upshot of this design is a versatile amplifier capable of extremely high power levels with unbelievably low waste heat. Class D amps can theoretically reach 100% efficiency, though, in the real world, you’ll usually see them near the 85-95% range. Their downside? Due to their switch-mode design, Class D amps have caught a bit of a bad wrap from the high-fidelity crowd. With a Class D amp, you are essentially filtering and ‘averaging*’ the voltage of a bunch of sharp square waveforms, so the output waveform will never be absolutely pristine. But… to be totally honest, the output quality is good enough to the point where you’d be really hard pressed to hear the difference if your circuit is designed correctly. Class D amps, as we’ll discover, are also fairly picky and can be difficult to keep stable. Effective Class D designs truly are fantastic though, and their versatility and efficiency is why they’re becoming the de facto amplifier component for many new products.

*Not exactly averaging per se, but you get the idea.

I mentioned pulse-width modulation in the paragraph above. I’d like to clarify a bit on the subject just in case the reader has never heard of it before.

Basically, PWM is a technique where you approximate voltages with a digital circuit using on-time, and off-time. Remember, a digital device can only produce two discrete voltage levels, HIGH, and LOW. There is no “in-between.” Those “in-between” voltages are important for analog signals, like audio, as those distinct voltages are where the information is. Analog in this context simply means “no discrete steps. Infinite resolution between points.” The higher the voltage, the more the electromagnet in the speaker cone repels the permanent magnet in the speaker base, moving some air, which we perceive as sound, and vice versa for negative voltages. With PWM, we can allow digital devices to approximate those vitally important “in-between” voltages and create an analog signal.

PWM creates a series of square waves, meaning the voltage level of the wave goes to it’s maximum point and minimum point (HIGH and LOW), with no voltage levels in between. By changing the ratio of HIGH time to LOW time, we adjust what is known as the “duty cycle.” The more time the PWM signal spends at the HIGH voltage, the higher the average voltage will be. The approximate DC voltage can be calculated simply by using D*V (D = Duty Cycle, V = peak voltage, AKA what ever our HIGH level is).

If our peak voltage is 12 volts, and our duty cycle is 50%, we would calculate the average voltage to be: 12*0.5 = 6V.

If our peak voltage is 12 volts, and our duty cycle is 14%, we would calculate the average voltage to be: 12*0.14 = 1.68V.

If our peak voltage is 12 volts, and our duty cycle is 80%, we would calculate the average voltage to be: 12*0.8 = 9.6V.

You see, by adjusting the duty cycle, we can achieve a synthetic analog voltage using only digital signals.

Once we achieve the voltage we’re looking for, we can filter the square-wave to remove any harsh high-frequency signal edges that may make it into our synthetic analog stream. Think of the filter circuitry as some sandpaper to smooth out the rough edges of our signal. PWM can be found in just about everything “adjustable” nowadays, from your light dimmer, to an electric car’s speed controller, to synthesizers. If you ever hear a weird high frequency buzzing coming lights or motors, that is probably the PWM duty cycle you’re hearing!

An example of PWM (labeled PDM). Notice how the longer periods of HIGH voltages on the blue square wave correlates to a higher voltage on the green analog wave while longer periods of LOW voltages drop the green wave down. Image source.

Okay, we get it. Class D is Pretty Neat-o

As you’ve probably guessed by now, I’m going with a Class D amp. They’re efficient, they’re powerful, and they’re versatile. How in the hell do you get one to work properly though?

Sadly, getting Class D amps to work on a breadboard is fairly tough. Remember, we’re dealing with high-frequency analog magic here, so stray capacitance, spaghetti wiring, and components mashed together in every which direction makes a dramatic impact on performance. The Class D amps I work with require fairly pristine electrical conditions to work in or else they become unstable or wont operate at all. I decided to give it a go anyways just for a proof of concept.

For my low-power proof-of-concept circuit, I went with a 15W/ch TPA3122D2 Class D amp. It’s the only DIP, stereo Class D I know of, and I’ve used it before, albeit, with questionable design decisions in retrospect. It should be good enough just for testing. I’ll chose a much more powerful 140W Class D for the final circuit, but we’ll get to that later.

The first circuit was fairly simple in concept.

Audio input -> pre-amplifier -> power amplifier -> speakers AND have the Teensy microcontroller sample the audio to create a little light show.

Pre-Amplification

For the pre-amplifier, I was simply going to use a biased unity-gain op-amp.

If you already understand the concept of “biasing” and “DC offsets,” feel free to skip this section by clicking here, otherwise, read on!

More technical jargon, allow me to explain, as this concept took me a little bit to wrap my head around too, even though it’s fairly simple. A unity-gain op-amp’s job is simply to “buffer” the incoming signal. It will one-for-one match the input voltage level and provide no gain, therefore, no amplification. What’s the point of a unity-gain op-amp if it doesn’t amplify anything? For one, it gives the audio source one degree of separation from the rest of the audio circuit. More importantly, the incoming signal source doesn’t need strain itself at all in order to drive the main amplification circuit. The op-amp doesn’t sink any current* (ideally) from its inputs and only samples the signal voltage, essentially producing an isolated equivalent output on the other end. This way, it doesn’t matter if your signal source can provide 1000mA of current, or 10 mA, the voltage is all that matters and the signal source remains unloaded.

As for biasing, this is also a fairly simple concept that gets taken for granted and is therefore often skipped over when explaining a circuit. Basically, an op-amp can only produce output voltages between it’s positive power supply voltage pin and negative voltage supply pin. Op-amps can, in fact, happily handle fairly large input voltages and negative voltages on their inputs, which is somewhat uncommon to see when you’re used to digital circuits that tend to hang around the 1.8-5V range.

TL;DR: A bias is simply (your signal + a voltage). It allows you to shift a signal up. It is a “DC Offset.”

If you had the positive power supply pin of an **ideal** op-amp hooked up to a +15V supply and the negative power supply pin hooked up to -15V, you can produce any voltage on the output between -15 and +15 volts. “Ideal” is heavily emphasized, as most op-amps are not rail-to-rail (able to hit exact supply voltages), let alone ideal. Therefore, you’re looking at around +/-14V worth of room to work with depending on the op-amp, which is still more than enough for what we’re doing.

Normal audio signals will almost always center around 0V, meaning they will have positive voltages and negative voltages. “Line level” is generally 3 volts peak-to-peak, though particularly hot audio sources can sometimes go all the way up to 6 volts peak-to-peak or more. 3 volts peak-to-peak meaning the signal’s highest point will be around +1.5V and the lowest point will be around -1.5V, the distance between those two peaks would be 3V. With an op-amp that has access to +/-14V, we have plenty of room to recreate our signal.

A unity-gain op-amp with a +/-15V power supply (power supply pins not shown). Input signal is 5 Volts, peak-to-peak. Notice how the input signal is perfectly recreated on the output at the bottom of the image. 1K resistor is just to load the circuit.

Here’s the problem though, bipolar power supplies that can supply negative voltages are actually surprisingly uncommon and tricky to find unless you’re willing to create your own DC/DC circuit or transformer-based power supply, therefore, we usually only have positive voltages to work with.

Let’s assume our op-amp is now supplied with +12V on the positive power supply pin and 0V on the negative supply pin. If our audio signal produces any voltage between 0V and 11V, we’d be totally fine. The problem is, as mentioned before, normal audio signals will produce negative voltages. We cannot produce any voltage lower than 0V on our op-amp, therefore, the signal is “clipped,” and the result is heavy distortion. Half of our waveform is now gone, which is obviously no good. How can we use a unity-gain buffer with a single-voltage power supply? Biasing.

12V and 0V power-in, same 5Vpp input. Notice how the top of the waveform translates over fine, but the bottom half is cut off at the 0V line. Since we don’t have a negative supply, we can’t produce any voltage under 0V!

What if we could make the input signal ‘zero-cross’ at 6V instead of 0V?  That way, we have 6V on each side of the waveform to work with instead of +12V in one direction. That is what biasing does.

Basically, all we’re doing is shifting the input wave up a little to make sure it stays within our valid voltage range. We can create a rudimentary bias voltage by simply using a voltage divider to split +12V in half. Two 10K resistors in series to ground will produce +6V in the middle when connected to +12V. Then, we simply connect that +6V to our audio signal source through a high-value resistor (1M does well), and boom, biased op-amp. Adding a couple support components like capacitors will help with stability and blocking DC.

12V & 0V supply, but this time with a 6V bias on the input signal. Notice how the output waveform is no longer clipped at the thick grey 0V line and has been shifted up. (Ignore the misleading graph scaling, the AC voltage is basically untouched. Notice how the voltage-per-division boxes are stretched differently.)

Blocking DC? Yep, we’re not done yet. DC voltages are extremely undesirable for audio circuits. Not only do DC voltages have the ability to accidentally bias components that aren’t expecting it, but they can burn out your speakers! DC voltages should be isolated and avoided at all costs. But wait, we just introduced DC voltage directly into our circuit via our bias circuit. How do we get rid of it and return to an unbiased, 0V zero-crossing AC signal? DC blocking capacitors. Capacitors will block DC and will only allow AC to pass through them. Therefore, in this example, adding a 10uF capacitor to the output of the op-amp will remove our DC bias and leave us with a perfectly buffered signal with no distortion.

Notice how the 10uF cap on the end shifts the AC voltage back down again. (Again, please ignore the misleading graph scaling. An RC filter was formed in this simulation, but is unrelated to the point, ignore the slight voltage drop.)

ALL OF THIS TO SAY…

Again, a bias is simply (your signal + a voltage). It will shift up your signal by the voltage you apply. Then, you can remove that shift up with a DC blocking cap. Phewph.

By the way, those simulator schematics above are kind of janky and were only required to make it work. Here is the actual circuit I used:

The way I would bias an unity-gain op-amp with a +12V power supply. R1-R3 divide 12V/2 to form 6V. R2 prevents R3 from sinking nearly any input signal power. C1 stabilizes the 6V from the voltage divider. C2 is a decoupling cap. C3 removes the DC offset (bias voltage).

If you are using a stereo audio stream, like I was, you will require two of those circuits. Fortunately, most op-amps come in dual or quad varieties. I used the LM833, but there are lots and lots of them out there. SA5532, TL072, LM358, just to name a few.

Breadboard Power Amp

Okay, finally, on to the rest of the prototype. For the most part, assembly was fairly straight forward. I just followed the datasheet provided by TI. They almost always include an example diagram that should get you up and running in no time.

You can find the datasheet I used for the test amp here

To start, I always set the gain control to the lowest value, so I would ground pins 14 & 15. Pin 3, the MUTE pin, would also be grounded. Pin 2, the SHUTDOWN pin, is connected to 12V along with the AVCC and PVCCL/R pins.

In the datasheet on page 12, you will also find a section on how to choose filter cap and inductor values. I am using a 4 ohm, single-ended configuration. A single-ended configuration means that the speaker is tied to one audio output line and ground. BTL, or “Bridge Tied Load,” means the speaker is connected between two output lines, usually for more power. For now, I’ll be using a single-ended configuration, but later, we’ll move to BTL. Anyways, according to the charts, I need a 22uH inductor and a 680nF cap for my filter.

 

Once the amp was set up, I hooked up my cheap little speakers and used their box as a temporary enclosure. Additionally, I set up a little Teensy board to sample the right channel of the audio input line. I create a circuit to do this correctly a bit later, but for now, I’m able to run a cool little FFT neopixel program on it to give me a mini light show to the music. I used a SN74LVC245 to translate the 3.3V signals from the Teensy to the 5V neopixels (WS2812), which were running off a 5V buck converter module. The Teensy was also powered by this buck converter.

Here’s a video of the first signs of life!

Some early pitfalls:

Class D amps, and well, anything analog really, hate breadboards. I don’t blame them, to be honest. Loose connections, stray capacitance everywhere, long wires, high-frequency components all smushed together. If you build an amp on a breadboard, be prepared for miserable performance. That being said, I love to always test my projects on breadboards first because it generally makes understanding the fundamentals a lot easier.

One issue I ran into was the Class D amp I used refusing to operate, instead, it gave off an odd-clicking noise. This is likely either the thermal protection or the short-circuit protection oscillating, turning the amp on and off since it couldn’t stabilize. To get around this issue, I would keep power-cycling the amp by unplugging and re-plugging the main 12V power wire. If I could catch the power cycle just right, the amp would fire up. Obviously, this is probably terrible for everything involved, but, you know… prototype.

I also discovered that +24V is absolutely the way to go when it comes to powering these amps. The quality is substantially better and you get much less distortion or dropping out when loud bass notes hit. I rewired the circuit a bit to run on +24V instead of +12V. I added in an adjustable linear regulator to generate +12V for the op-amp and it’s bias supply as +24V is far too high for op-amps. That linear regulator is hooked up to the potentiometer you see in the next video below.

Yeah, don’t be jealous of my sick reflex enclosure. Funny thing about that cardboard box though. In the video above, you’ll see me press the driver down into the box, snubbing some of the vibrations it generated. In person, it makes a pretty dramatic difference to the audio quality. Enclosures, vibration control, and predicting how the air will interact with itself are all parts of the amazing audio engineering puzzle that we sadly won’t be touching upon here, but are fascinating none the less.

Mixing

Alright, so that simple pre-amplifier “unity-gain op-amp” circuit that we spent so much time learning about has done it’s job admirably, but it’s time for an upgrade. For the final circuit, I’d like to have two audio inputs on the outside of the unit and one audio input on the inside of the unit. I’d like to be able to mix between them on the fly. The internal audio input will be useful if my client wants to add bluetooth audio support or have something inside of the enclosure to send audio to the amp. I’d also like a couple outputs – one external “mix out” line and one buffered audio line that the microcontroller can sample for the lights. The final mix will be sent to both the power amplifier and the two line level output lines.

This mixing circuit prototype actually went really, really well. I followed this guide and I really love the modular mindset. The performance is fantastic as well.

For the most part, I simply copied this schematic set verbatim and it worked out fine. I only changed a couple things:

  1. I got rid of the gain potentiometers, but kept the volume pots. I removed gain pots (R2A & R2B) and R3 & R6 and replaced that part of the circuit with a single 47K resistor. This gave the input amps unity gain, similar to our previous amp. I didn’t need those gain knobs as I felt 1-to-1 gain was fine and I’d prefer the PCB not be mega huge because of the interface panel.
  2. I ditched the biasing trick we learned in the previous section. Biasing all of those inputs correctly would have been a real pain, so I decided to move over to bi-polar power supplies at that point. Also, the op-amp topology was fairly different, as this approach relied on an inverting schema rather than non-inverting. Don’t have a bi-polar supply? I’ll show you how to make one for cheap!

Here’s how to make a +/-9V power supply using two 9V batteries and a bit of wire.

Pinch a bit of stranded wire in-between the positive and negative terminals of two 9V batteries. This is your “0V” point. The exposed + terminal is your +9V, the exposed – terminal is your -9V. Measuring between the two exposed terminals will give you about 18-19V.


Make sure to connect the 0V middle wire to the GND of your mixer circuit!

You can also attempt to create a negative voltage generator using a 555, which does work, but they can’t really supply a whole lot of current and they tend to operate smack dab in the middle of the audible frequencies, so your signal is tainted with an annoying tone. I don’t recommend it, but you’re free to give it a try! 

Oh, another annoying thing, you’re going to need some dual gang potentiometers. The most common types do not fit in a breadboard, so I had to assemble a little breakout board.

Damn you, dual-gang pots. By the way, if you’re searching for audio potentiometers, search for “Dual Gang Logarithmic” ones. They will be marked with an ‘A.’ Linear potentiometers, marked with a ‘B,’ will work in a pinch, but their tapers are not designed for audio. Remember, audio volume is measured in decibels. The decibel scale is logarithmic, not linear. Check here for a bit more information on linear v. log pots.

After it was all hooked up, I had this!

Two mixing channels and a master volume controller on the final summing amp for this prototype. This circuit worked stunningly well, even on a breadboard. I was super impressed. The modular design of it makes it super easy to add as many channels as you want, too.

I used all LM833 op-amps soldered to little DIP adapter boards for this (though I was also testing an LM358 in the video).

Flux, good solder, and a chisel tip is all you need for sparkly clean SMD joints!

Eventually, I just ordered up a little +/-15V switch mode power supply board.

Bastards lasered off the part number on the main IC, making it “impossible” to look up the chip they used to design the circuit.

By “impossible,” I mean “minor annoyance.” I placed a little piece of kapton tape over the lasered part and firmly pressed it in place. Angle the light just right and hey, what do you know, it’s the XLSemi XL6019 

This board works a treat and injects only a tiny amount of noise into the circuit, only really audible using headphones. I can’t really hear anything on full-sized speakers.

Makin’ the BIG BOARD

Cool. By now, I’ve become pretty confident with my ideas so far, so I figured I’d go big or go home. Yeah, enough of this 15W nonsense. Let’s bump it up by an order of magnitude. Let’s go with the raw, ear shredding fury of the

Hell. Yes. Check this bad boy out https://www.ti.com/store/ti/en/p/product/?p=TPA3156D2DAD

70W and channel, 140W total. Damn, I bet this amp is gonna be…

..ww…wha.. It’s so.. tiny… How the heck do you push 70W through those teeny-tiny legs? That’s more power than an old-school incandescent light bulb consumes.

Do you see why I think using Class D amps feels like you’re cheating the laws of physics? I understand the relationship between conductor length, resistance, and heat dissipation but STILL.

Alright, I’ll trust the TI engineers. In fact, I’m going to try and follow their reference verbatim. Honestly, with circuits that sink this much energy and rely on precision high-frequency switching, you really don’t want to free-hand this IC. Instead, look around to see if the IC manufacturer has created a demonstration board for their product. More often than not, their demo board web page will contain a whole bunch of great resources and in-depth examples that will help you create a high-performance design. You don’t have to buy the demo board of course, just reference it’s resources.

The TPA3156D2DAD does indeed have one of these demo boards. https://www.ti.com/tool/TPA3156D2EVM

More importantly, the demo board has it’s own datasheet with a TON of reference material and design examples. It even tells you the exact components they selected in the bill of materials!

Example schematics can also be found in the main datasheet, as well as a ton of helpful formulas and component value recommendations.

For the most part, the power-amp section of my design tries to follow the same design patterns as the development board. It may sound fun to cowboy the layout, but trust me, leave it to the brilliant engineers that designed this IC. They are going to know what’s best, so it’s really smart to use the resources they provide to create a design that mimics theirs. It’s not art theft, it’s a manufacturer recommendation… at least that’s what I told my Deviant Art followers.

My Design Considerations

  • This power amp will be a “Master” device with the gain programmed to be 26 dB. Page 13 of the main datasheet tells you which resistor values to use for that. Breakout the SYNC pin in-case you want to add another slave amp in the future.
  • My speakers will all be 8 ohms, therefore, the recommended output filter shown in the demo board datasheet example schematics (page 11) will work just fine.
  • The example mute circuit shown in the demo board schematic is designed for a push-button. Modify it to work with a slide switch.
  • Break-out the AM avoidance lines with with jumpers. You never know when you might have to change one of those settings in the future.
  • I’ll use fork (spade) connectors to inter-connect everything since they’re fairly robust and easy to connect and disconnect.
  • I’ll want some reverse current protection. A simple diode-fuse configuration should work just fine.
  • My main power supply will be 24V. I’ll use a external switch mode module to generate +/-15V for the op-amps.
  • I want three input mixing channels, all unity gain with volume control. There will be one unbuffered mixed output and a buffered mixed output for the MCU.
  • For everything else, I will try to recreate the TI example circuit as best I can to optimize performance. Keep those traces thick!

Here’s what I came up with for my amp and mixer schematic, click for a full view…

And the PCB without the ground pours. Notice how my topography tries to copy the TI demo board. I kept my power traces and output traces like Aku likes his Pizza. Plop down vias like they’re going out of style. This design worked almost perfectly. There is a sneaky little error with the C17 and C18 biasing the MCU op-amp, but I’ll go over how I fixed that soon.

 

Here’s the microcontroller board in charge of sampling the audio mix…

I made sure this board was fused as well. I also found a great little DC-DC buck converter module capable of driving a 4 amp load in a 5x5mm footprint, which is incredible. The unit itself is INSANELY tiny. The box they shipped in seems comically over sized, three of them are in the top right of this image. Again, I simply followed the datasheet recommendations for the buck-converter components and board layout since it is a high-current, high-frequency device. She works a treat!

 

For everything else on the breakout board, I simply broke out a few pins on the Teensy for possible future use, as well as a few extra level-translated outputs, again, just in case. In retrospect, I probably should have gone with one of the smaller Teensys, but they are mostly pin-compatible with one another, so it’s not too big of a deal. It’s nice to have an upgrade-downgrade path if needed. I did have a minor oversight on this board as well, but I’ll go over how I fixed it in my troubleshooting section.

After sending my gerber files over to the fab, a couple weeks later, I had my PCBs and components! Let’s get building!

I have an SMD workflow. I greatly prefer it over through-hole. If you have a steady hand and good eyesight, it’s substantially faster than THT.

My “pick and place” machine

Board is populated and fresh out of the reflow oven!

All done! How pretty… No shorts whatsoever, first reflow worked out great!

I managed to find some aluminum heatsinks on Amazon that fit the TI specification of 25×50 fairly well. I also picked up some cheap tubes of thermal glue – not thermal paste, mind you. I’d like the heatsink to remain in-place, so thermal glue fits the build better. I cleaned the heatsink off with a little acetone first, then applied a tiny amount of thermal glue to it and the power amp IC. It only took about a day to cure and it’s on there pretty good!

I also had the chance to try out a new solder paste that I just love. It worked SO well, it’s crazy. It smells like pine-sol and death, so it must be good.

SMD Reflow Tips

Time for Aidan’s QUICK SMD REFLOW AT HOME TIPS:

  1. Get a stencil. No seriously, get one. Trust me, it’s worth it.
  2. Use leaded solder if you can get it. Tin solder just SUCKS when you try to reflow it as it takes way higher temps.
  3. Use tubed solder-paste, rather than paste from a tub container. You usually get less paste in a tube for the money, but since it never dries out, you get substantially better performance.
  4. Use other PCBs around your main PCB when using the stencil to keep it in place and to have a flat surface to squeegee your paste on.
  5. Make sure your stencil is lined up as perfectly as possible. Try your best to get an even spread on the first pass, double checking that every component got covered. Then, quickly remove the stencil in one quick movement.
  6. A smudgy paste-job will make shorts much more frequent. If you can get the paste perfectly applied on each individual pad without any overlap, your board might come out with zero shorts on the first try.
  7. A steady hand and good eyesight really makes all the difference. If you’re dealing with a tricky part, hold your breath while placing it to stabilize yourself. If you can place the part with minimal smudging and good accuracy, you will get less or no shorts.
  8. Parts will move in-place by themselves when reflowing due to surface tension. You don’t have to place parts absolutely dead center, but good accuracy really reduces shorted or “tombstoned” parts. Just make sure the pads are all lined up.
  9. You don’t need a dedicated reflow oven, a toaster oven works just fine. With leaded solder, place your board in the oven and let it heat up to around 350°F (~175°C). You’ll see the components seat themselves. Wait a bit, turn off the oven, open the door. Let it cool naturally. I advise not using a toaster oven that you eat food out of.
  10. Examine your boards carefully for shorts using a microscope or magnifier. You can also hold your board up to a light source to see through it and spot shorts. Touch up shorts with an iron and lots of flux.
  11. A hot-air gun is extremely useful for parts that have phantom shorts or are not seated correctly. Highly recommended.

Audio Crossovers

Here’s something new that I wasn’t really aware of before this project: Audio Crossovers. What is a crossover? It’s a set of frequency filters that assign frequency bands to specific speakers. In a two-way crossover, the higher frequencies are sent to the tweeter while the lower frequencies are sent to the mid-range speaker. These protect your speakers from frequencies outside of their “frequency response,” which may distort or damage them if played. The frequency response of a speaker is simply the range of audio frequencies that the speaker can produce.

Basically, your amp is capable of producing the full audible spectrum of sound. The crossover is in charge of splitting that full spectrum up and delegating portions of it to each speaker. They’re simply arrangements of low-pass, high-pass, and band-pass filters, if you’re looking for google-able terms.

Ideally, you’d generally want to look for a set of speakers and a crossover circuit that would give you a “flat” frequency response. This generally involves quite a bit of engineering, equipment, and expertise to achieve. Since these speakers are not going to be for critical listening, some generic commercial crossovers will likely do just fine as you can find them for fairly cheap online.

How do you choose a crossover?

  1. How many types of speakers are you driving per channel? A tweeter and a midrange? You’ll need a “2-way” crossover. If you have a subwoofer, you will need a “3-way” crossover.
  2. What is the frequency response of your speakers? You can usually find this on their store page, box, or label. In my case, I have a tweeter and a midrange on each channel. The midrange’s FR is 600-10,000Hz and the tweeter’s FR is 3,200-20,000 Hz. I need a 2-way crossover that begins operating around 3,200 Hz, as that is where my tweeter starts responding. My crossover should split up the 0-20,000+Hz full spectrum from my amp into two sections, <3200’ish Hz for the midrange, >=3200 Hz for the tweeters.
  3. Make sure the crossover you select can handle your power requirements, otherwise, you might burn something out!
  4. You will require a crossover for each channel. Therefore, if you have a stereo setup, you will need two crossovers.
  5. Proper custom crossover design requires extensive testing and expertise. If you’re designing for critical listening, avoid pre-made crossovers. Custom crossovers will almost always sound better, but for all intents and purposes, commercial crossovers will work just fine.

Here are the ones I bought.

Power Supplies

I went with a 24V industrial power supply since they are super easy to connect multiple devices to. The particular one I went with is a 350W Meanwell, which is absolutely overkill in terms of power required, but Meanwell makes really good power supplies, so I went with them rather than a cheaper, sketchier PSU vendor. You could also look for 12-24V DC power bricks too, but those are generally a bit harder to connect multiple devices to. Be careful when working with industrial power supplies though, as they really mean business with their output capabilities and require you to work with mains power. If you’re not comfortable working with mains, stick with a power adapter. If you work with an industrial power supply, you’re also probably going to want a power switch too!

First Tunes!

Everything is all hooked up for the first test! Make sure to test before applying your heatsink glue ;)

Finally, after a month or so of R&D, the moment has come. After testing the board with my current-limited bench power supply to make sure there weren’t any obvious shorts, I migrated over to my floor and began connecting all of the components together. the two red boards you see are my crossovers. The industrial power supply is just out of frame and is powering the amp board and the little blue +/-15V voltage generator. State of the art ESD protection was implemented by me trying not to rub my feet on the carpet too much and touching the chassis of the power supply before I poked anything else.

Yeah! Nothing feels better than a PCB that works on the first try! There was a tiny issue that I discovered a little bit later into testing, but it wasn’t anything I couldn’t fix with a couple resistors. Again, I’ll explain in the final troubleshooting section later.

Light Show!

The sounds work great, what about the sights? Time to put the light-show board to work. After a little bit of hooking-up, the light-show board was ready to go and was sampling audio from the mixer. This time, I hooked up about 90 addressable LEDs.

…but.. hm.. It “works,” but just.. not that well. Huh. The performance was inconsistent and the light circuit seemed to be picking up a ton of noise. After a bit of deliberation, I decided that the way I was sampling audio was really not ideal. It was at this point where I made a really dumb mistake – a particularly catchy tune was playing on the amp board, but I wanted to work on the light-show board. Okay, not an issue, just disconnect the power cables from the board and..

What ever happened here was fairly violent. Caps moved around from the hot air gun btw.

OH GOSH. Wow, sorry little buck converter module. What happened? Truthfully, I’m not too sure. I disconnected the positive 24V in lead first, so I didn’t have a “ungrounded” fault. I suspect that I may have caused some sort of fly-back malfunction while loosening the 24V cable. The module was running well within it’s spec, with the lights only drawing about 1.5A at max. Either way, that scared the hell out of me and necessitated me building another board since the traces were so damaged. Ughk, fine, whatever.

Troubleshooting

Other than the magical exploding DC/DC module, there were only a few small fixes I needed to make.

First up, I wanted to fix the sampling circuitry for the Teensy’s ADC audio input. The problem was that I was relying too much on resistors and relatively weak clamping diodes to protect me from audio sources that are a bit too hot. Also, the Teensy is unable to measure negative voltages, which is a real disadvantage for audio sampling. Not only would I not get the negative end of the audio waveform, but I’d also risk damaging the Teensy if an operator attached an extremely hot audio source. PJRC actually made a brief post on how to translate +/-5V “control-voltage” signals down to the 0-1.2V range, which would be perfect, since I could then use the Teensy’s high-accuracy internal voltage reference instead of the flakier 3.3V external reference. I changed a couple component values to adjust the input voltage range to around 3.5Vpp, but the result is fairly similar. The 27K resistor should also help protect against extremely hot inputs.

I doubled up this circuit, one for each channel. The two 1K resistors on the PCB will mix them down to a single channel. There is a LD1117 linear regulator on my new breakout to supply a stable 3.3V.

According to my simulator, this little circuit should have done the job nicely. But huh… I’m still getting really inconsistent performance with my LEDs. What gives? It’s like there is a ton of noise on this line or something. Wouldn’t it be funny if there was like a DC bias or something on the buffered mixer? Heh I’ll just go ahead and check with my multimeter just in ca…

OH MY GOD THERE IS +15V DC GOING INTO THE TEENSY WTF?

Thankfully, those 1K mixing resistors combined with the clamping diodes inside of the Teensy likely saved me, but I was still 50% over the current limit of 10mA. I think I got really lucky, my Teensy should have been toast. What the hell is supplying 15V to the MCU buffer op-amp?

As it turns out, it was C17 and C18. They shouldn’t have ever been at that voltage, but it seems as though the initial power-up, or perhaps a stray voltage source, tosses a 15+V pulse into the system, which charged those caps up. Since those caps aren’t being sampled by anything that sinks any significant direct current, they never discharge, and the MCU buffer op-amp is registering them as a DC offset and biasing itself to compensate. Super strange issue that I would have never caught beforehand. Thanks for the tip, Natalie! The result is a permanent +15V on the output of the MCU buffer op-amp, instead of a purely AC audio signal. A couple bodged 100K resistors to ground quickly fixed that issue. I updated my schematic to reflect those resistors.

No shame in a little bodge. It’s better than damaged components.

After those two fixes, BOOM! Lights worked GREAT! Here’s a demonstration of them running in “VU Meter” mode.

Amazing! It looks so cool! That little DC/DC module is seriously impressive. It’s about the size of my pinky finger nail and is supplying about 1.5-2A of power without a sweat.

The speakers themselves sound amazing in person. They come across as tinny on my phone’s microphone, but in real life, they’re fairly full and impressive sounding, even without a subwoofer or enclosure. Totally loud enough to shake my fillings. Wait, I don’t have any fillings. You get the idea.

What’s Next?

Well, this only happens to be one half of the entire project. The enclosure is being handled by the person that commissioned this system, so I’ll leave that part to them. At this point, I’m interested in creating some more amazing amplifier projects. This stuff is endlessly fascinating. Who knew so much went into “make small noise more louder.”

If I had to re-design this board, I would make the following changes:

  1. Find a way to make the mixer-amp board a bit smaller. Maybe even separate the mixer and amp board to make it easier to expand the mixer input panel a bit and not worry about the size of the amp board.
  2. Find a smaller sized 1000uF 24V+ cap. The ones I got work fine but are a little big.
  3. Integrate my new ADC breakout circuit on the light-show board.
  4. Create my own custom power supply solution for the +/-15V generator.

Helpful Resources

Here are some of the places I searched around for helpful info and tips:

https://www.circuitlib.com/index.php/tutorials/product/39-how-to-build-an-audio-mixer

https://electronics.stackexchange.com/questions/153911/single-supply-op-amp-audio-amplifier

https://dorkbotpdx.org/blog/paul/control_voltage_cv_to_analog_input_pin/

https://sound-au.com/p-list.htm

And yet again, my good friend Natalie who was always happy to give me extremely helpful answers no matter how trivial they seemed. You rock.

Mega MIDI: A Playable Version of my Hardware Sega Genesis Synth

March 13, 2019

Now Available for Sale Here!

Familiar and Foreign

Those of you that are familiar with my previous works, namely the MegaBlaster, know that I’m very much into old-school FM synthesis chips that were mainly found in 80’s/90’s arcade systems and home video game consoles. I’m pretty well-versed when it comes to playing VGM files on these old chips. The VGM format, while confusing in its own right, mostly handles all of the complicated register-specific aspects of these sound chips. Essentially, you just read-in the commands and push them to the chips without caring about what that data means. The chip knows what to do with that data so I don’t have to.

Almost immediately after I published my work on the MegaBlaster, I was asked about possibly adding MIDI functionality to it in order to use it as an instrument. At first, I was hesitant to do so – While I’m sure the MegaBlaster itself is more than capable of functioning as a MIDI device, it wasn’t really designed for that. Further still, controlling the synth chips with MIDI would require me understanding the complete ins-and-outs of each chip. It was possible, but I didn’t really think it was viable with the knowledge that I had at the time.

….Until one day where I just sucked it up and did it.

A Learning Experience

Buckle up, because this is gonna’ be a long one.

If I was going to attempt driving these ancient chips through MIDI, I was going to design a dedicated device for it, rather than shoe-horning that functionality into the MegaBlaster. This was going to be a long, multi-month learning process filled with many hours of trial and error.

First thing’s first: Which microcontroller should I use? While I love the STM32 series of BluePill boards, I figured that would be a little over-kill since I’m not dealing with the same demanding overhead requirements as playing VGM files. I really didn’t need the horsepower of 32-bit ARM and figured 8-bit AVR would be a lot easier to work with. What I really desired was native USB host support. This would make designing a MIDI device a hell of a lot easier with significantly less software pain in the long run. I settled on using the AT90USB1286 since it was the same MCU used in PJRC’s Teensy++ 2.0 boards. 

While I was in the bread-boarding stage, I just used a Teensy++ 2.0. It made prototyping SO much easier since it featured just about everything I needed with a lightning-fast programming toolchain. Super handy. This would later result in a bit of pain when it came to handling proprietary bootloaders, but I’ll explain that a little later.

The First Prototypes

The first breadboard prototype

Creating the first breadboard prototype wasn’t actually all that bad. I essentially just duplicated the MegaBlaster’s analog stage and hooked up power, ground, clocks, and data lines. I was only working with the YM2612 for now as I was unsure if I even wanted to include the PSG in this design. One advantage of using AVR is the ability to directly write to 8-bit ports. Other microcontrollers can do this as well of course, but Atmel makes it super easy to do so. You may also notice a little bar of LEDs on a PCB off to the side – I designed this PCB a couple years ago. It’s just a single row of 8-LEDs and a common ground. You can hook this LED board up to 8-bit data busses and have a really quick and easy visualization of the data going across. This little board was invaluable for debugging. You could use a logic-analyzer of course, but when it comes to functional low-tech solutions, it’s hard to beat the ease-of-use and simplicity of a few blinky lights.

With the breadboard all set for testing, I needed a quick way to verify that I hooked the YM2612 up correctly. I referenced this super handy test code which replicates the “Piano Test Voice” found in the Sega Genesis’ technical manual. Both this test code and technical manual excerpt would be a godsend down the line since the official YM2612 application manual has been lost to time. After a bit of tweaking, a single channel of my YM2612 began roughly belching out a single piano tone. Good enough, let’s draw the rest of the owl.

Register Maps

Here’s where it get’s tough. First, let’s define what registers do when referring to these old sound chips. They are simply memory addresses on the chips that represent synthesizer settings. If you’ve ever seen a proper professional synthesizer keyboard, you’ve surely noticed all of the crazy knobs, switches, buttons, settings, etc. All of those sources of input are modifying the circuitry inside of the synthesizer (or values to a specific algorithm) in order to produce your custom waveform. The YM2612 et. all have lots and lots of settings to change in order to modify the final waveform and we can access those settings through the chip’s on-board registers. You can just imagine the registers as a table of values that we can change in order to make the chip do something. Indeed, this is not far off to what is really happening. Not only can you control voice (the overall sound) values, but you can also tell the chip to turn off and on specific channels or to add effects to them.

Usually, register maps are fairly straight forward. Change a value at this register address and boom, something happens. For the most part, that is true, but YM2612 is a complicated chip, so fully grasping the register system and how to write to it properly took ages. The Genesis Technical Manual I mentioned previously provided a nice starting point, but as you’ll soon see, it becomes fairly confusing and difficult to decipher without proper documentation. Before messing around with all these complicated voice register settings, the first thing I needed to do was to handle keyboard MIDI commands, and the easiest one I could think of was the “Key-On” and “Key-Off” commands.

MIDI, HID, and some Other Annoying Protocols

In order to interpret MIDI commands coming in to my prototype board, I needed a library. You could spend the time and interpret the MIDI commands manually since the protocol isn’t all that complicated, but I figured since I’d like USB MIDI support and serial MIDI support, I’d best start off not reinventing the wheel and go with something proven. Also, I didn’t want to deal with the nightmare that is HID packet interpretation. Thankfully, PJRC and the Arduino framework already provide great MIDI libraries to use free of charge. Awesome! I’ll just create my USB MIDI instance and I’ll be off!

…Or so I thought. Since the AT90USB1286 microcontroller on the Teensy was acting as an HID host, telling it to accept MIDI commands means I lost access to my invaluable serial console that I used to debug with. Not to worry, however, as you can make a few changes to the preprocessor directives of the MIDI library to get the best of both worlds. By doing this, you can have access to both USB MIDI commands and have a pseudo serial connection. Why pseudo? Well, the Teensy is actually sending your serial data through HID packets. These packets can be interpreted by the teensy_gateway.exe program included with the Teensy Framework and are broadcasted locally over Telnet to be received by any terminal client you connect to it. After adding that modification script, I wrote a little batch file that invokes the teensy_gateway program and an instance of putty to connect to it over Telnet. Boom, instant “serial” connection and USB MIDI access. You can also use the Arduino IDE’s serial console as well, but you will need to install the Teensy toolchain.

Also, if you’re using PlatformIO, be sure to tell it which build flags you’d like to use for this dual MIDI/Serial functionality.

First Notes

Okay, finally, time to make this chip sing to keyboard commands. Remember how I mentioned the “Key-On” and “Key-Off” MIDI commands? Now it was time to interpret those into commands that the YM2612 could understand. I’ll spare you the details on setting up the boilerplate MIDI side of things since it’s pretty simple and well documented (Got a MIDI command? Call X function).

Recall that the YM2612 is essentially a synthesizer hooked up to a big table of registers that control everything from voices, to effects, and most importantly, whether a key is on or off. In order to turn on a channel, we first need to provide it with a voice, then activate the Key-On register. I’ll stick with the piano test code and a single channel for now. At this stage in development, I am not concerned with pitch or how the voice sounds, I just want to be able to turn a single channel on and off through MIDI commands.

If we take a look at the register map of the YM2612, we’ll notice that register 0x28 is the Key On/Off register. This register isn’t as straight-forward as “1 in this register turns on a voice.” It’s a bit more complicated than that, but since we’re only here to test a single voice, we’ll just pass in the value 0xF0. Once we have the register/value we want to modify in mind, it’s time to tell the YM2612 what to do. Here’s where a little hardware know-how comes into play.

In order to write to the YM2612, we need to follow a strict order of operations. The YM2612 actually has two banks of nearly identical registers. You can select the upper bank by pulling the A1 pin HIGH, and the lower bank by pulling the same pin LOW. Keep this in mind for later. For now, though, the write order of operations is the following:

  1. Set the A1 pin for the register bank you want to access
  2. Set A0 to LOW
  3. Set Chip Select (CS) to LOW
  4. Write the address of the register you’d like to access to the 8-bit bus
  5. Set WR to LOW
  6. Wait a microsecond
  7. Set WR, CS, and A0 back to HIGH to finish the address write
  8. Pull CS down to LOW again since we need to write the data now
  9. Write your new register value to the 8-bit bus
  10. Set WR to LOW yet again and wait a microsecond like last time
  11. Set WR and CS back to HIGH to finish the data write
  12. Set A1 and A0 back to LOW to prepare for next time

Sound like a pain in the ass? That’s because it is! You can see an example of my YM2612 “send” function where I’ve implemented the above procedure here:

void YM2612::send(unsigned char addr, unsigned char data, bool setA1)
{
    digitalWriteFast(_A1, setA1);
    digitalWriteFast(_A0, LOW);
    digitalWriteFast(_CS, LOW);
    PORTC = addr;
    digitalWriteFast(_WR, LOW);
    delayMicroseconds(1);
    digitalWriteFast(_WR, HIGH);
    digitalWriteFast(_CS, HIGH);
    digitalWriteFast(_A0, HIGH);
    digitalWriteFast(_CS, LOW);
    PORTC = data;
    digitalWriteFast(_WR, LOW);
    delayMicroseconds(1);
    digitalWriteFast(_WR, HIGH);
    digitalWriteFast(_CS, HIGH);
    digitalWriteFast(_A1, LOW);
    digitalWriteFast(_A0, LOW);
}

With ALL of this in mind, I can finally toggle a single channel through MIDI. Using the MIDI library, any time I receive a Key On command, I’ll simply call my YM2612 ‘send()’ function and write 0xF0 to register 0x28. Once I receive a Key Off command, I’ll send over the value 0x00 to clear register 0x28 which will tell the synth to being the “decay” of the voice.

ym2612.send(0x28, 0xF0, 0); //Reg 0x28, Value 0xF0, A1 LOW. Key On
ym2612.send(0x28, 0x00, 0); //Reg 0x28, Value 0xF0, A1 LOW. Key Off

Writing to the registers like this will be the basis of everything we do moving forward. It only becomes more complicated from here on out!

Understanding the Language of Music …And Japanese

So I’ve got a box that goes ‘bing’ and that’s about it. No chords, no other voices, not even any other notes. How can we take in MIDI Key-On commands and translate them into properly tuned notes that represent the keys you’re pressing? By doing a shit ton of annoying math. You see, instead of just pushing over your desired frequency to some arbitrary register and just letting the chip figure it out, you have to compute something known as the “F-Number.” This F-Number is what’s sent to the F-Number register and is what determines the output frequency of your note. Okay, where can we find the formula for this F-Number? In the YM2612 Application Manual!  ….that doesn’t exist. Aw.

But, fortunately for me, the YM2612 has a twin CMOS sister, the YM3438, AKA the OPN2c. These two chips are functionally identical and share the same register map, the only real difference being the output stage. A manual does in fact exist for the YM3438, and you can find it here. Unfortunately for us, it’s entirely in Japanese.

Well as it turns out I can read Japanese just fine so we’re good.

Anyways, let’s get to what we need. The F-Number. On page 23 and 24, you can see information related to the F-number, as well as a little register map showing where that value should eventually end up. The formula for calculating the F-number is:

(144 × fnote × 220 ÷ fM) ÷ 2(B-1)
fnote = Your desired note frequency
fM = The frequency of your master clock
B = Block number

Okay, not the worst formula ever, but for little microcontrollers with no floating point units, this is kind of a nightmare. Let’s try and compute an F-number for note A4, just like the application manual does.

The frequency of an A4 note is 440 Hz. Our master clock frequency is 8MHz (8,000,000 Hz). The block number is… wait, damn it, block number?

Yep, yet another gotcha. As you move up the musical scale, your block number will change. The block number is a 3-bit value that basically represents which octave you’re on. If you compute a table for a single octave, you can use the block number to shift that table up and down for each octave instead of computing the F-numbers each time. For the sake of this example, let’s assign it to 4 (Get it? A4 is in block 4!).

(144 × fnote × 220 ÷ fM) ÷ 2(B-1) 
fnote = 440 
fM = 8000000 
B = 4
FNum = (144 × 400 × 220 ÷ 8000000) ÷ 2(4-1) 
FNum = 1038.09024

And if we look at the example table in the YM3438 manual…

Perfect! Exactly what we were looking for.

Now, every time we receive a MIDI command, we can grab the key number, convert it to its frequency using some more math, then convert that frequency into our F-Number using the formula above. Don’t forget the block number calculation too!

You’re not done yet. Now you have to pack all of that information into a byte and send it out to each channel’s frequency register! I’ll spare you the details here and have you look the source code for that one. Since this was such a computational nightmare, I scoured the web for more elegant solutions that used LUTs (Look-up tables) instead and discovered a master-class solution by

I incorporated a similar design into my final project since it supported pitch bending better, but before I discovered, I did all of the math on the fly.

No polyphony yet, just one dinky FM piano voice and a single key at a time, but it was progress.

Welcome to Register Hell

AHHHHHHHHHH

Monophony was pretty tricky, but polyphony was going to be an even bigger nightmare. The image above is the complete memory map of the OPN (YM2612/YM3438). Understanding this map requires a keen eye in pattern recognition and a mountain of patience for cryptic and unhelpful application notes (in a foreign language!). Oh and did I mention: pulling A1 HIGH will swap you over to another identical bank of registers for the upper 3 channels that you must also set. Goodie.

Quickly take a look at that register map and see if you can spot any obvious patterns. To me, the first one that stands out is that each address along the Y axis is spaced out by 0x10. This is handy to keep in mind as we will use it to quickly iterate through each address. Also remember that since we can jump up to the upper register bank that controls channels 4, 5 and 6 using pin A1, the register addresses are identical but the values are not! Also, notice along the X-axis, each address seems to be spaced out by 0x04. Why? There are four operators per channel! (The manual refers to channels as “slots,” by the way.)

Let’s hop waaaaaay back into my code when I first implemented polyphony.

Take a look at the setup() function. You can see that I first generate a LUT for F-numbers using GenerateNoteSet(), then perform a little housekeeping to set the clocks and such.

But then you will notice this block:

  ym2612.send(0x22, 0x00); // LFO off
  ym2612.send(0x27, 0x00); // CH3 Normal
  ym2612.send(0x28, 0x00); // Note off (channel 0)
  ym2612.send(0x28, 0x01); // Note off (channel 1)
  ym2612.send(0x28, 0x02); // Note off (channel 2)
  ym2612.send(0x28, 0x04); // Note off (channel 3)
  ym2612.send(0x28, 0x05); // Note off (channel 4)
  ym2612.send(0x28, 0x06); // Note off (channel 5)
  ym2612.send(0x2B, 0x00); // DAC off

Right here, I am simply turning off the Low-Frequency Oscillator (LFO), setting Channel 3 to it’s “normal” mode, sending a Key-Off command to each channel, and finally, disabling the Digital to Analog Converter (DAC). This is just a bit of setup code that prepares the YM2612 without having it digitally scream at us.

The main part of this function is where I actually set the piano test voice for every channel:

  for(int a1 = 0; a1<=1; a1++)
  {
    for(int i=0; i<3; i++)
    {
          //Operator 1
          ym2612.send(0x30 + i, 0x71, a1); //DT1/Mul
          ym2612.send(0x40 + i, 0x23, a1); //Total Level
          ym2612.send(0x50 + i, 0x5F, a1); //RS/AR
          ym2612.send(0x60 + i, 0x05, a1); //AM/D1R
          ym2612.send(0x70 + i, 0x02, a1); //D2R
          ym2612.send(0x80 + i, 0x11, a1); //D1L/RR
          ym2612.send(0x90 + i, 0x00, a1); //SSG EG
           
          //Operator 2
          ym2612.send(0x34 + i, 0x0D, a1); //DT1/Mul
          ym2612.send(0x44 + i, 0x2D, a1); //Total Level
          ym2612.send(0x54 + i, 0x99, a1); //RS/AR
          ym2612.send(0x64 + i, 0x05, a1); //AM/D1R
          ym2612.send(0x74 + i, 0x02, a1); //D2R
          ym2612.send(0x84 + i, 0x11, a1); //D1L/RR
          ym2612.send(0x94 + i, 0x00, a1); //SSG EG
           
         //Operator 3
          ym2612.send(0x38 + i, 0x33, a1); //DT1/Mul
          ym2612.send(0x48 + i, 0x26, a1); //Total Level
          ym2612.send(0x58 + i, 0x5F, a1); //RS/AR
          ym2612.send(0x68 + i, 0x05, a1); //AM/D1R
          ym2612.send(0x78 + i, 0x02, a1); //D2R
          ym2612.send(0x88 + i, 0x11, a1); //D1L/RR
          ym2612.send(0x98 + i, 0x00, a1); //SSG EG
                   
         //Operator 4
          ym2612.send(0x3C + i, 0x01, a1); //DT1/Mul
          ym2612.send(0x4C + i, 0x00, a1); //Total Level
          ym2612.send(0x5C + i, 0x94, a1); //RS/AR
          ym2612.send(0x6C + i, 0x07, a1); //AM/D1R
          ym2612.send(0x7C + i, 0x02, a1); //D2R
          ym2612.send(0x8C + i, 0xA6, a1); //D1L/RR
          ym2612.send(0x9C + i, 0x00, a1); //SSG EG
          
          ym2612.send(0xB0 + i, 0x32); // Ch FB/Algo
          ym2612.send(0xB4 + i, 0xC0); // Both Spks on
          ym2612.send(0xA4 + i, 0x22); // Set Freq MSB
          ym2612.send(0xA0 + i, 0x69); // Freq LSB
    }
  }
  ym2612.send(0xB4, 0xC0); // Both speakers on
  ym2612.send(0x28, 0x00); // Key off

There are two loops. A loop with an iterator that represents A1 and i that represents a single channels specific register index. As I loop the first time, a1 is set to 0, meaning I keep the A1 pin set to LOW. We’ve selected the lower bank of registers. We then iterate through all of the voice setting registers, one by one, and place our test piano voice values inside of them. We do this three times, as there are three channels on the lower bank. Once we loop three times, we increment a1 by 1. Pin A1 is now pulled HIGH, so we are now on the upper register bank. Recall that the addresses in each register bank are identical, so we just iterate over all of the same addresses with the same data yet again, this time for channels 4, 5, and 6. Finally, I activate both the left and right speaker outputs by writing 0xC0 to address 0xB4 and made sure that the “Key-down” register was reset to 0x00.

Are we ready for polyphony yet? Haha, no.

We’ve only just set the register’s voice values. Meaning, were this a real standalone synthesizer keyboard, we just set all the knobs and dials to their correct settings. In order to produce polyphony, we still need to tell the YM2612 which channels are on, which channels are off, and at what frequency each channel is supposed to be set to.

In order to spare you from another math onslaught, I figured it’s probably best for you to see the old project code itself.

In a nutshell, I kept track of how many keys were being pressed on. The YM2612 only has 6 channels, so a maximum of six keys could be pressed at a time. Every time a key was pressed, I would check to see how many other keys were pressed and assign the note a channel after performing the F-number, Block number, etc. calculations. If the channel assigned was 4, 5, or 6, I had to set pin A1 to HIGH so I could select the upper register. To handle a key-off event, I simply referenced which channels were assigned which MIDI note number when they were turned on and would simply reverse the process (without having to calculate for F-Number, of course).

void KeyOn(byte channel, byte key, byte velocity)
{
  uint8_t offset, block, msb, lsb;
  uint8_t openChannel = ym2612.SetChannelOn(key); 
  offset = openChannel % 3;
  block = key / 12;
  key = key % 12;
  lsb = fNumberNotes[key] % 256;
  msb = fNumberNotes[key] >> 8;

  bool setA1 = openChannel > 2;

  if(openChannel == 0xFF)
    return;

  Serial1.print("OFFSET: "); Serial1.println(offset);
  Serial1.print("CHANNEL: "); Serial1.println(openChannel);
  Serial1.print("A1: "); Serial1.println(setA1);
  ym2612.send(0xA4 + offset, (block << 3) + msb, setA1);
  ym2612.send(0xA0 + offset, lsb, setA1);
  ym2612.send(0x28, 0xF0 + offset + (setA1 << 2));
}

void KeyOff(byte channel, byte key, byte velocity)
{
  uint8_t closedChannel = ym2612.SetChannelOff(key);
  bool setA1 = closedChannel > 2;
  ym2612.send(0x28, 0x00 + closedChannel%3 + (setA1 << 2));
}

I kept track of all the note channel assignments in the YM2612 driver. Prototype code is messy!

But hey, look! Full six-channel polyphony! Yay, we’ve made a budget-conscious, early-90’s toy keyboard!

New Voices and The OPM File Format

So we’ve got basic polyphony, but we’re still using the same lame piano test voice. The ultimate goal of this project is to be able to play any instrument from any Sega Genesis soundtrack. Now we already know that there are a whole host of complicated registers that we can set to control all of the intricate details that go into crafting each FM voice. If we wanted to, we could go in manually using a Sega emulator, scope out all of the individual memory addresses that make up each voice, and push those values one-by-one into the YM2612’s voice registers. If you really don’t value your time and want to do something mind numbing, this is the route for you.

Fortunately for us, there is indeed an easier solution. The OPM file format.

While the OPM file format was originally designed for… well.. the OPM FM synthesizer chip (AKA, YM2151), we can use it to also program our YM2612’s. Since the YM2612 and the YM2151 are so similar, the voice registers are practically identical. Originally, this file format was designed to be used with VOPM, the YM2151 VST that can also be used to craft custom patches for our final synth project. We can also convert .VGM files to OPM files with ease using a tool that I’ll talk about a little later. OPM files perfectly map out all of the register settings to give us the exact voices we need to replicate those found in Sega Genesis games. The downsides? The format itself is really poorly documented, and since it’s text-based, it’s kind of a pain to parse.

How do we get YM2612 OPM files? Glad you asked, because I have a package of practically every YM2612 OPM voice file available. You’d be hard-pressed to not find a game track in this pack. You can download the YM2612 OPM files here.

Can’t find your game’s patch listed in that archive but you have the VGM file? Don’t worry, I’ve got a tool for you here! Simply follow the directions in the readme file and you should be all set. This should make converting VGM files to OPM super easy.

Now that we’ve got our OPM patch files, let’s explore the format a little bit and see how I applied them to my YM2612. Since OPM files are text-based, you can open them up in a text editor and see all of the individual voice settings. (You can also manually tweak these voice settings in a text editor too, if you know what you’re doing)

Once we open up an OPM file, you will see all of the individual instrument patches. They look like this:

@:0 Instrument 0
LFO: 0 0 0 0 0
CH: 64 6 6 0 0 120 0
M1: 31 18 0 15 15 24 0 15 3 0 0
C1: 31 17 10 15 0 18 0 1 3 0 0
M2: 31 14 7 15 1 18 0 1 3 0 0
C2: 31 0 9 15 0 18 0 1 3 0 0

If we take a look at the anemic specification and compare it to the YM2612 register documentation, we can slowly work out what all of these settings mean. Here’s what the specification says:

@:voice_num voice_title
LFO: LFRQ AMD PMD WF NFRQ
CH: PAN FL CON AMS PMS SLOT NE
M1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AME
C1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AME
M2: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AME
C2: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AME

At the top, you will see an @ symbol, a colon :, a voice number, and the instrument number. These headers are standardized, so we can use them to know which voice we’re looking at. Next, let’s look at the LFO: CH: M1:, C1:, M2:, C2: parameters.

LFO stands for Low Frequency Oscillator. When dealing with standard YM2612 OPMs, these values are always zero. CH is used for global channel algorithm settings that affect every operator at once. M1, C1, M2, and C2 are the names of the 4 operators within every synth channel. If we’re creating a polyphonic, mono-timbral synth, we need to set every channel’s 4 operators with the exact same data. Don’t confuse operators with channels. There are six individual channels, each with their own set of four operators, meaning there are 24 operators in total to set.

Let’s define what all of the OPM settings stand for.  Every value is in decimal, not hex.

LFO: Low Frequency Oscillator Settings (Usually all 0)
LFRQ: LFO Frequency
AMD: Amplitude Modulation Enable
PMD: Phase Modulation
WF: Waveform
NFRQ: eNable Low Frequency Oscillator
CH: Channel Algorithm Settings
PAN: Panning? This setting is unknown and is unused.
FL: Feedback Loop
CON: Connection, AKA "Algorithm"
AMS: Amplitude Modulation Sensitivity 
PMS: Phase Modulation Sensitivity (AKA Frequency Modulation Sensitivity)
SLOT: Slot Mask
NE: Noise Enable. Unused in our case.
Operators: M1, C1, M2, C2
AR: Attack Rate
D1R: Decay Rate 1
D2R: Decay Rate 2
RR: Release Rate
D1L: Decay Level 1
TL: Total Level
KS: Key Scaling 
MUL: Multiplier
DT1: Fine Detuning 
DT2: Coarse Detuning 
AME: Amplitude Modulation Enable

If you take a look at the register map for the YM2612, you’ll find lots of these settings in there. Essentially, all we have to do is read-in these OPM file settings and transfer them over to the appropriate YM2612 registers. Making a plain-text file parser is always a bit of a nightmare, and I’m completely positive there is a better way to do this, but I’ve developed a function that seems to parse OPM files perfectly fine.

First, I create a struct to store my voice data, as well as an array of voices since each OPM file will contain multiple different voices. Each instrument you hear in a Sega Genesis music track (with the exception of PCM samples), will have it’s own voice patch.

Once I have my “voice” struct array, I then populate it with the following function:

void ReadVoiceData()
{
  size_t n;
  uint8_t voiceCount = 0;
  char * pEnd;
  uint8_t vDataRaw[6][11];
  const size_t LINE_DIM = 60;
  char line[LINE_DIM];
  while ((n = file.fgets(line, sizeof(line))) > 0) 
  {
      String l = line;
      //Ignore comments
      if(l.startsWith("//"))
        continue;
      if(l.startsWith("@:"+String(voiceCount)+" no Name"))
      {
        maxValidVoices = voiceCount;
        break;
      }
      else if(l.startsWith("@:"+String(voiceCount)))
      {
        for(int i=0; i<6; i++)
        {
          file.fgets(line, sizeof(line));
          l = line;
          l.replace("LFO: ", "");
          l.replace("CH: ", "");
          l.replace("M1: ", "");
          l.replace("C1: ", "");
          l.replace("M2: ", "");
          l.replace("C2: ", "");
          l.toCharArray(line, sizeof(line), 0);

          vDataRaw[i][0] = strtoul(line, &pEnd, 10); 
          for(int j = 1; j<11; j++)
          {
            vDataRaw[i][j] = strtoul(pEnd, &pEnd, 10);
          }
        }

        for(int i=0; i<5; i++) //LFO
          voices[voiceCount].LFO[i] = vDataRaw[0][i];
        for(int i=0; i<7; i++) //CH
          voices[voiceCount].CH[i] = vDataRaw[1][i];
        for(int i=0; i<11; i++) //M1
          voices[voiceCount].M1[i] = vDataRaw[2][i];
        for(int i=0; i<11; i++) //C1
          voices[voiceCount].C1[i] = vDataRaw[3][i];
        for(int i=0; i<11; i++) //M2
          voices[voiceCount].M2[i] = vDataRaw[4][i];
        for(int i=0; i<11; i++) //C2
          voices[voiceCount].C2[i] = vDataRaw[5][i];
        voiceCount++;
      }
      if(voiceCount == MAX_VOICES-1)
        break;
  }
  Serial.println("Done Reading Voice Data");
}

Those who are more gifted in C-string manipulation than I am are probably wincing a little right now, but this function works perfectly, so I think I’ll keep it. This function will also automatically detect when there are no more valid voices to read in since the OPM format likes to generate way more voice blocks than are actually available. Brilliant format, right? But hey, it’s what we’ve got to work with.

Now here’s the tricky part. If we take a peek through the YM2612’s register map, you’ll notice that several register settings are actually packed together into a single byte. This is the late 80’s/early 90’s after all, and storage space is at a premium! No worries, though. A bit of careful bitwise scrunching of our otherwise organized voice data is all we need. Once we’ve packed everything into their appropriate bytes, we can send our new fancy voice register data over to the YM2612 just like we did with the old piano test voice.

void YM2612::SetVoice(Voice v)
{
  currentVoice = v;
  bool resetLFO = lfoOn;
  if(lfoOn)
    ToggleLFO();
  send(0x22, 0x00); // LFO off
  send(0x27, 0x00); // CH3 Normal
  for(int i = 0; i<7; i++) //Turn off all channels
  {
    send(0x28, i);
  }
  send(0x2B, 0x00); // DAC off

  for(int a1 = 0; a1<=1; a1++)
  {
    for(int i=0; i<3; i++)
    {
          uint8_t DT1MUL, TL, RSAR, AMD1R, D2R, D1LRR = 0;

          //Operator 1
          DT1MUL = (v.M1[8] << 4) | v.M1[7];
          TL = v.M1[5];
          RSAR = (v.M1[6] << 6) | v.M1[0];
          AMD1R = (v.M1[10] << 7) | v.M1[1];
          D2R = v.M1[2];
          D1LRR = (v.M1[4] << 4) | v.M1[3];

          send(0x30 + i, DT1MUL, a1); //DT1/Mul
          send(0x40 + i, TL, a1); //Total Level
          send(0x50 + i, RSAR, a1); //RS/AR
          send(0x60 + i, AMD1R, a1); //AM/D1R
          send(0x70 + i, D2R, a1); //D2R
          send(0x80 + i, D1LRR, a1); //D1L/RR
          send(0x90 + i, 0x00, a1); //SSG EG
          
          //Operator 2
          DT1MUL = (v.C1[8] << 4) | v.C1[7];
          TL = v.C1[5];
          RSAR = (v.C1[6] << 6) | v.C1[0];
          AMD1R = (v.C1[10] << 7) | v.C1[1];
          D2R = v.C1[2];
          D1LRR = (v.C1[4] << 4) | v.C1[3];
          send(0x34 + i, DT1MUL, a1); //DT1/Mul
          send(0x44 + i, TL, a1); //Total Level
          send(0x54 + i, RSAR, a1); //RS/AR
          send(0x64 + i, AMD1R, a1); //AM/D1R
          send(0x74 + i, D2R, a1); //D2R
          send(0x84 + i, D1LRR, a1); //D1L/RR
          send(0x94 + i, 0x00, a1); //SSG EG
           
          //Operator 3
          DT1MUL = (v.M2[8] << 4) | v.M2[7];
          TL = v.M2[5];
          RSAR = (v.M2[6] << 6) | v.M2[0];
          AMD1R = (v.M2[10] << 7) | v.M2[1];
          D2R = v.M2[2];
          D1LRR = (v.M2[4] << 4) | v.M2[3];
          send(0x38 + i, DT1MUL, a1); //DT1/Mul
          send(0x48 + i, TL, a1); //Total Level
          send(0x58 + i, RSAR, a1); //RS/AR
          send(0x68 + i, AMD1R, a1); //AM/D1R
          send(0x78 + i, D2R, a1); //D2R
          send(0x88 + i, D1LRR, a1); //D1L/RR
          send(0x98 + i, 0x00, a1); //SSG EG
                   
          //Operator 4
          DT1MUL = (v.C2[8] << 4) | v.C2[7];
          TL = v.C2[5];
          RSAR = (v.C2[6] << 6) | v.C2[0];
          AMD1R = (v.C2[10] << 7) | v.C2[1];
          D2R = v.C2[2];
          D1LRR = (v.C2[4] << 4) | v.C2[3];
          send(0x3C + i, DT1MUL, a1); //DT1/Mul
          send(0x4C + i, TL, a1); //Total Level
          send(0x5C + i, RSAR, a1); //RS/AR
          send(0x6C + i, AMD1R, a1); //AM/D1R
          send(0x7C + i, D2R, a1); //D2R
          send(0x8C + i, D1LRR, a1); //D1L/RR
          send(0x9C + i, 0x00, a1); //SSG EG

          uint8_t FBALGO = (v.CH[1] << 3) | v.CH[2];
          send(0xB0 + i, FBALGO, a1); // Ch FB/Algo
          send(0xB4 + i, 0xC0, a1); // Both Spks on

          send(0x28, 0x00 + i + (a1 << 2)); //Keys off
    }
  }
  if(resetLFO)
    ToggleLFO();
}

And the result: A little rough around the edges, but hey! Listen to that! Actual voice patches from Genesis games!

Adding the PSG

Once I was happy with how my YM2612 was turning out, I made the decision to also include the PSG. While PSG synths have been done to death, I figured my project wouldn’t be a “complete” Genesis synth without including it. I assumed since the PSG was just a simple 3-channel square wave synth, it should be much easier to implement than the YM2612.

Weeeeeeellllll….

The SN76489 Programmable Sound Generator is an ancient chip. This IC was first minted in the late 1970’s, so writing to the chip is kind of a pain. The convoluted method involved in writing to this chip makes pitch-bending fairly difficult as well.

Big-Endian. Yuck.

First, let’s focus on how we can transfer data over to the PSG. This part is actually fairly straight forward, especially compared to the YM2612.

  1. Make sure the WE pin is set HIGH
  2. Send your byte over the 8-bit data bus
  3. Set WE to LOW
  4. Wait 14 microseconds to let the PSG process that data
  5. Pull WE HIGH again to complete the write

Technically, step 4 is kind of faux pas, since there is a dedicated READY pin on the IC that will go HIGH once the IC has completed it’s data transaction, but since the IC consistently completes writes within the same amount of clock cycles, it’s not entirely necessary. If you do decide to go with the READY pin instead of waiting 14 microseconds, the READY pin is open-drain and needs to be pulled-up with an external 2K resistor to 5V. The internal 10K pull-ups that are normally found on microcontrollers will not work as they are too weak for the PSG.

void SN76489::send(uint8_t data)
{
    //Byte 1
    // 1   REG ADDR        DATA
    //|1| |R0|R1|R2| |F6||F7|F8|F9|

    //Byte 2
    //  0           DATA
    //|0|0| |F0|F1|F2|F3|F4|F5|

    digitalWriteFast(_WE, HIGH);
    PORTC = data;
    digitalWriteFast(_WE, LOW);
    delayMicroseconds(14);
    digitalWriteFast(_WE, HIGH);
}

Much like the YM2612, there is also a formula required to calculate the data found in the PSG’s frequency register. Thankfully though, this equation is much simpler:

F = N ÷ (32 × n)
F = Desired PSG Frequency
N = Master Clock Frequency
n = Desired Note Frequency (10-bit number)

Let’s calculate for note A4 again using the above equation:

F = 4000000Hz ÷ (32 × 440Hz)
F = 284.1

Again, this part is pretty simple.

Once you have that value, you can send it to the PSG using the following method:

void SN76489::SetSquareFrequency(uint8_t voice, int frequencyData)
{
    if (voice < 0 || voice > 2)
        return;
    send(0x80 | frequencyRegister[voice] | (frequencyData & 0x0f));
    send(frequencyData >> 4);
}

Where it gets super annoying though, is when you have to account for pitch bending. That required a lot more math. You can view the entire PSG driver script here if you’re interested in seeing how it was done.

This video also shows the project after I had implemented hardware serial MIDI.

The Finishing Touches

Don’t judge. It worked perfectly.

All that was really left was a bit of trial-and-error tuning, adding a MIDI circuit, and then finally producing a printed circuit board. I also ditched the buck-converter I was using to knock 12V down to 5V in favor of a linear regulator since the buck converted produced quite a bit of audible noise. In order to test the PCB heat-sinking performance of said regulator, I decided to sacrifice one of my (silly) TQFP-64 to DIP boards and solder the regulator to the ground plane that I exposed by sanding off the solder mask.

Another thing I wanted to test is to see if I could run my code on a bare AT90USB1286 microcontroller without the use of PJRC’s proprietary HalfKay bootloader. With a bit of fiddling on a demo board, yes indeed, I can run my code without the bootloader. I just have to upload the code every time using an ATMEL ICE, or any other ISP (In-circuit Serial Programmer). I also switched from using LTC6904s for my clock generators to discrete crystal pierce oscillators. They’re far more accurate, cheaper, and easier to use.

My AT90USB1286 Test Board

I also added a rotary encoder and a 20×04 character LCD to act as the user interface. All that was left was to plot the schematic and design the PCB.

The schematic

The final breadboard prototype looked like this monster:

Finally, it was time to jump into the KiCAD PCB Editor and design a board. I wanted a relatively compact board, around 100x100mm that had a rotary encoder, several “favorite patch” buttons, and a way to mount my LCD above the assembly. This took a lot of research, measuring, and double-checking. Once I got the boards made, the quality was good and the screen mounted perfectly with 15mm nylon standoffs and 21mm header pins. …Only problem is that I pulled the LCD’s data R/W pin to VCC when it should have been GND. Darn, so close to perfect. Oh well, I just cut the lead and stretched it over to my GND test point. The Op-amp also had an issue – KiCAD didn’t recognize that pin 4 and pin 8 were part of the netlist, so the DRC overlooked them and didn’t tell me they weren’t connected. Damn. A couple of bodges later though, and we were all patched up.

Here’s the board without the LCD or chips installed yet.

I also needed to turn off the JTAG fuse on the new AT90USB1286. This gave me a mini heart attack since it was preventing the main data bus from operating correctly. I thought I was screwed, but all I had to do was change the fuses to:

EXTENDED: 0xF3
HIGH: 0xDF
LOW: 0x5E

Phew.

Once it was all said and done, I polished the unit up and recorded my first video on it! (Top of the page)

All that’s left is to modify the board a little bit to remove the bodged connections. I also want to transition to as many surface-mounted parts as possible as it will likely speed up production times.

GUH! That was a lot and it was only a very high-level overview. In reality, this entire process took many long months. If you actually did read all of this, thank you very, very much! I appreciate your interest in the project! Feel free to look through the source material. Once version 2 is complete, I will release the KiCAD PCB files.

You can view the Github repo here!

Here is a link to the May 2019 Instruction Manual for the Mega MIDI