Software Design V1
Arduino Program Dependencies
| Name | Version | Purpose | Link | Configuration |
| platformIO | >= 6.1.12 | compiler toolchain | https://platformio.org | platform = atmelavr board = uno framework = Arduino |
The Arduino program can be built from the following project folder: https://github.com/daniel-sudz/pie/blob/main/arduino/src/main.cpp.
Visualizer Program Dependencies
| Name | Version | Purpose | Link |
| cmake/make | cmake ≥3.9 | compiler toolchain | https://cmake.org/ |
| g++ | any supporting std=gnu++17 | compiler toolchain | https://gcc.gnu.org/ |
| portaudio | ≥ v19.7.0 | audio driver | https://github.com/PortAudio/portaudio |
| libserial | ≥ v1.0.0 | serial processing | https://github.com/crayzeewulf/libserial |
The visualizer program can be built from the following project folder: https://github.com/daniel-sudz/pie/blob/main/visualizer/src/bin/etch_a_sketch.cpp.
Visualizer Architecture: Data Structure

The portaudio library allows the user to output audio in a cross-platform and performant way using a callback design pattern. When the operating system requests more audio to play, the callback will be triggered, and the user is required to fill an audio buffer with sound that is about to be played.
Because of the way audio is implemented in most operating systems, the portaudio callback is handled on a separate thread compared to the main program thread. This raises the typical concerns of shared memory access that multithreaded programs bring. This issue is exacerbated by the fact that portaudio recommends not using multithreaded primitives such as locks or other unbounded system calls such as memory allocation due to concerns about performance and undefined behaviour (https://www.portaudio.com/docs/v19-doxydocs/writing_a_callback.html).
As a solution to these limitations, we have decided to store the Etch-a-Sketch potentiometer values received from the serial in a linked list data structure. The benefits of doing so are numerous compared to the typical approach of a dynamic arrays.
- Dynamic arrays (ex. std::vector<T>) re-allocate memory using an exponential pattern every time they fill up. This re-allocation is blocking and time-consuming operation during which time we are unable to render audio leading to dropped audio samples and poor user experience. For more information about dynamic arrays, consult a data structures resource such as https://en.wikipedia.org/wiki/Dynamic_array.
- On the contrary, the link-list approach always allocates O(1) (constant amount) of memory when a new sample is added. Furthermore, the modification of the link-list to append a new value after it has been allocated is non-blocking on most operating systems. In general, a reasonable assumption is that primitives aligned with the machine-size are atomic (ie. can be safely written and read to by multiple threads without concern about memory corruption). To read more about atomicity in machine instructions, consult a software systems resource such as https://en.wikipedia.org/wiki/Atomic_semantics.
In general, the visualizer program is design in such as way that no multi-threading primitives such as locks are needed. This is despite the fact that some memory is modified by both the audio callback thread and main thread at the same time. We are able to get away with this because the only shared-memory are 64-bit primitives that are atomic on our platform.
- To verify multithreading safety, we wrap primitives inside of an std::atomic<T> data structure.
- To verify non-blocking behaviour we use static_assert(std::atomic<T>::is_always_lock_free, "T should be lock-free to avoid undefined behaviour in portaudio").
Visualizer Architecture: Etch-a-Sketch Audio Playback
As mentioned in the previous section, we have a link-list data-structure that stores all the potentiometer values for the Etch-a-Sketch. The actual behaviour to render the audio is described here.
The keyboard in our project controls the frequency at which we want to trace the Etch-a-Sketch picture. Let’s denote this as . The DAC from our actual device has a physical limitation on the frequency at which audio can be outputted. Let’s denote this .
The challenge here is that we need to sync up the audio samples that we are outputting to the DAC with the actual desired frequency of the sketch. To accomplish this, we keep track of two state variables:
- current_buffered_trace_time which represents the current time based on the number of audio samples that we have buffered to the DAC.
- current_trace_time which represents the current time based on the number of times that we have traced the Etch-a-Sketch potentiometer values.
For every audio samples that is buffered to the DAC, we increment current_buffered_trace_time by seconds as expected.
Let’s assume the number of (x,y) potentiometer values currently stored in our data-structure is n. Then every time we “move” from one potentiometer value to another, we increment current_trace_time by seconds. This is because we want to trace the entire Etch-a-Sketch picture times a second.
The logic for rending a given audio buffer when requested is then as follows:
// sync current_trace_time with current_buffered_trace_time
while(current_trace_time < current_buffered_trace_time)
seek(next_potentiometer_value)
current_trace_time += (1 / num_pot_values) * (1 / freq_trace)
// sync current_buffered_trace_time with current_trace_time
current_buffered_trace_time += (1 / freq_DAC)
// value to be outputted to the DAC
return current_potentiometer_valueNote that we traverse the potentiometer values in a circular notion. So if the seek(next_potentiometer_value) reaches the end, the we loop back to the first value. Also, and the number of potentiometer samples (n) can change as data is processed from serial. This is not a big deal though because current_trace_time and current_buffered_trace_time both operate at the same time-scale. It just means that the frequency might be off for a single trace which is imperceptible to the user based on being a relatively large value.