Software Design V1

Arduino Program Dependencies

NameVersionPurposeLinkConfiguration
platformIO>= 6.1.12compiler toolchainhttps://platformio.orgplatform = 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

NameVersionPurposeLink
cmake/makecmake ≥3.9compiler toolchainhttps://cmake.org/
g++any supporting std=gnu++17compiler toolchainhttps://gcc.gnu.org/
portaudio≥ v19.7.0audio driver https://github.com/PortAudio/portaudio
libserial≥ v1.0.0serial processinghttps://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.

  1. 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.
  1. 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.

  1. To verify multithreading safety, we wrap primitives inside of an std::atomic<T> data structure.
  1. 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 freqtracefreq_{trace}. The DAC from our actual device has a physical limitation on the frequency at which audio can be outputted. Let’s denote this freqDACfreq_{DAC}.

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:

  1. current_buffered_trace_time which represents the current time based on the number of audio samples that we have buffered to the DAC.
  1. 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 1freqDAC\frac{1}{freq_{DAC}} 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 1n1freqtrace\frac{1}{n} * \frac{1}{freq_{trace}} seconds. This is because we want to trace the entire Etch-a-Sketch picture freqtracefreq_{trace} 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_value

Note 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, freqtracefreq_{trace} 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 freqtracefreq_{trace} being a relatively large value.