Saturday, March 15, 2025

A lightweight ADSR for AVRs

    I've been doing more projects that involve digital control of analog circuits, and I found myself needing to generate ADSR envelopes in software, on an Arduino. I found some approaches that implement a digital filter to simulate the RC curve you get from a capacitor charging. This usually involves slow floating-point calculations that the Arduino isn't well-suited to crunching.

RC curve ADSR - AudioMulch

Look up, it's a bird, it's a table

    A classic way to avoid slow math is to precompute it, and store the results in a table. Then you just look up the result for your given input. Storing the result for every possible input can result in an excessively large table though, so maybe you want to store fewer results and estimate values between them. This is called interpolation, and there are multiple ways of doing it. We could use the continuous curves of splines to connect our points, but this would be slower than the actual calculation we're trying to avoid. Instead we'll use fast linear interpolation, that just draws straight lines.

An interpolated attack curve

    Usually you would store a series of x and y coordinates, and then calculate the line between each pair. We really only care about the lines though, so we might as well precompute those also. Thinking back to algebra 1, we just need to store the slope and y-intercept to define them.

It's not a phase, period

    Now that we have a curve, we need to progress through it over time. A naive approach might be to march through every possible value, and simply do this as fast as needed. This is likely a waste of processing time, and we can instead strategically skip over some values to progress through the table more quickly. The Prophet 600 reportedly updates its envelopes at just 200Hz. It simply takes larger or smaller "steps" each update to change the length(period) of the stages.

Ain't no half‐steppin'

    Another beginner trap is to only think in terms of whole numbers, since we're avoiding floating points. Let's say we have 10 possible output values, and our smallest step is 1. It then takes 10 steps to get through all outputs. If our next smallest step is 2, it only takes 5, and that's twice as fast. It'd be nice to have 1.5 as an option. We can do this by using fixed point numbers. We can use steps of 15, and count up to 100 to get the same effect as steps of 1.5 counting up to 10. We just need to divide by 10 at the end to get the correct output. If we use powers of 2, instead of 10, the division becomes bit-shifting, and that's very fast.

Bit shifting to divide by 2 - Wikipedia

Non-canonical sections

    We just saw that a step size of 2 is twice as fast as 1. This hints at the funny relationship between the step size and the period. It's just period = max count / step, but this results in a very uneven response. When the step size is small, a slight change results in a very different period length. When the step is large, even significant changes have very little impact on the period length.

period = max count / step size

    Algebra 1 strikes again; This curve is a conic section called a hyperbola. It comes from us having a function in the form of 1/x. We can't really help that we're dividing a constant by a variable, but we can control the variable. If we replace x with 1/x (the reciprocal), we effectively multiply by x instead of dividing by it. 10/(1/x) = 10 * x. Now we have a straight line.

period = max count / (1 / step size)

    This looks better, but it doesn't feel right when mapped to a control. This is because the period is being adjusted with the same granularity on slow attacks vs fast attacks. That ends up being too coarse at one end, and too fine at the other. We need a curve after all.

The epitome of hyperbola

    The hyperbola we started with was too severe, and didn't pass through any particular points. What if we could fix those problems? Then a hyperbola might be a suitable curve.

    1 / (x + 1) / (10 - x) will approach points 0,10 and 10,0. That's helpful. We can change the severity of the curve by multiplying x by a coefficient in the numerator. Here's 1 / (0.25x + 1) / (10 - x)

period = max count / ((0.25 * step size + 1) / (10 - step size))

    By continuing to manipulate this formula, I landed on a new one that lets you adjust the maximum period without distorting the curve. I'm using 10 bits for the step size, so the number 1024 comes from the maximum step size. c sets the curve, and m sets the maximum period


    I'm using a sampling rate of 4kHz, and a maximum count of 2^20 = 1048576. This gives us the whole calculation to find the length of a period in seconds:

period length in seconds

    c, m and the constants are all known at compile time, so this can be simplified in our program prior to execution. A c of 4976 and an m of 3 simplify to roughly:

c = 4976, m = 3

    You can graph this function and adjust it in real time at this desmos link

Starting over

    There's another snag, restarting the envelope. It's tempting to begin counting from 0, but that's not how analog envelopes typically work. They start the attack stage from whatever their output level currently is. That would be simple, but we progress through our stages linearly, while the output is the result of our lookup table. So we have to convert from an output level, back to a position in the attack stage (that yields the same output). I chose to use a second lookup table to convert outputs back to attack stage positions. 

Release

    The C++ source code is on my github here. The focus is really the ADSR library, but the project is a functional example. It uses an Arduino nano, four potentiometers plus a trigger for input, and an MPC4728 for 12-bit analog output.

    Here's an incomplete prototype that I wrote in JavaScript. The gate is controlled in real time by mouse click. The red dots indicate the progress through each stage, and the black line is the actual output.




Thursday, January 23, 2025

Timex Sinclair 1000 New Case and Keyboard

    The Timex Sinclair 1000 (ZX81) is a charmingly limited computer from 1982. It has one of the worst keyboards of all time, and it begs for you to DIY something better. Many people have have already created improved keyboards and cases, but I wanted to try my hand. I figured there are enough chunky, old computers out there though; I want to make something thin and sleek.

Video

RF modulator

    The RF modulator is the tallest part of the board, and needed to go. It can be replaced with a simple transistor buffer to get composite video. Some ULAs require a more advanced mod to get usable video. One option is this Ginger Electronic board. It has the bonus feature of allowing you to invert the screen "colors".

Channel switch

    The channel switch on the bottom of the PCB also adds to the height, and needed to be removed.

Power

Heatsink

    The heatsink isn't thick, but it is quite large. It's meant to extend under the keyboard, but this would add to the thickness of our new keyboard. Out it went. The regulator can't run without it though, so that had to be replaced. I found these generic buck converters that are drop in replacements, and don't require heatsinks.

Keyboard

Redragon K603

    The original keyboard is as thin as it is unpleasant to use. To keep the new one thin, I looked for low-profile switches and especially short keycaps. I found the Redragon K603 keyboard to be a cheap source of both. The switches are even socketed, so they can be easily removed. The switches can be found separately though, and there are many options for keycaps.

Keycap stickers (image from 4keyboard.com)

    The next issue was the key legends. They're unique to this computer. Luckily I found these stickers from 4keyboard.com. They have all the information of the original keys, and they fit on a standard keycap.

Keyboard membrane connector

    It's easy enough to design a mounting plate and board for a custom keyboard, but it has to connect to the motherboard's strange connector. It's expecting the mylar sheets of the original button matrix. I could have bypassed or replaced the connector, but that felt like cheating. Instead, I designed a flat flex cable that fits into the original connector, and a standard flat flex connector on the new keyboard PCB.

New flex cable

Case

    I don't have any experience with 3D printing, and I'm not always impressed with the results I see. I stuck with 2D and designed a laser cut case. To keep things simple, I used all right angles, and dovetail joints. I settled for using super glue to join the majority of pieces (these nozzles helped immensely). The rest are held with screws and m2 nylon standoffs. Many of these are recessed into the bottom of the case as a way of achieving non-standard heights.
    A few standoffs go through the case lid also. This requires a screw with a head larger than the standoff to secure the lid. I found this pack that includes "large head" m2 screws.


M2 case screws

Jacks

    The power, video, ear & mic jacks need to be brought to the outside of the new case. One option is to butt the motherboard against the left side, and expose the original jacks. This offsets things strangely due to the width of the new keyboard. The expansion edge connector ends up in the middle of the case, and the mounting holes off to the left. So, I placed it at the far right, and used new panel mount connectors instead.


Motherboard placement

    I took the opportunity to remove the headphone-style connector that's misused for power, and replace it with a normal 2.1mm barrel jack. I also added the conspicuously missing power switch. Lastly, I included a small toggle switch that can be used to invert the video signal.

Lettering

    The case needed some lettering to differentiate the "Ear" jack from the "Mic" jack, and to add some character. These letters can be etched in, but you get very little contrast with acrylic. Paint is pretty much required. Luckily, laser etching usually gets you a free stencil because they etch through the protective film that comes on the acrylic. This makes it very easy to paint the parts, and then remove the film. I found that model spray paint works well for this. 

Lettering on the new case

Putting it all together

    After all the gluing, painting, soldering, and desoldering, I had a finished case.


    

    The design files are on my github.