The Audio library for Arduino (both the Teensy version and the Adafruit port to the M4F processor) is written in C++. I don't really care for C++, so I would like to create my audio effect chains in a different way and then automatically generate the C++ code. The Teensy project already has a similar tool, the Audio System Design Tool. This is a graphical, graph-based tool that will generate C++ code for you. This is a good tool to use if you are getting started with the Audio library. It generates code which follows some hidden constraints (for instance, inputs must be declared before outputs) that hand-written code might not follow and therefore could crash. I don't love this graph-based programming paradigm, though, so I want to write my own tool to accomplish the same purpose, but also be more extensible to my own needs.

Before I can start doing the fun stuff, I need to basically reproduce all of the functionality of the existing tool. For instance, the tool has a list of all of the various Audio library classes you can wire together, such as inputs, outputs, and effects. How do I get this list? That is the problem to be covered in this post. It may seem like a simple problem, but real-world problems like this contain subtleties that allow me to reveal my pragmatic philosophy of programming style.

The first thing to consider in solving this problem is how Arduino library code is structured. You can usually find the Arduino libraries on macOS in ~/Documents/Arduino/libraries, although this is both platform-specific and user-configurable, so I let the user specify the path to the library code. The Audio library would usually be found in the Audio/ subdirectory and it is common for Arduino libraries to then have a file called, for instance, Audio.h that includes all of the project's header files (.h files). This is a good place to look for a list of files containing the major Audio classes. Looking here is better than looking in individual .h files because there will often be some additional support header files that will not contain the classes we are looking for, but may contain other classes that we do not want.

Here is the contents of the Audio.h file installed on my system (yours may vary):

/* Audio Library for Teensy 3.X
 * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#ifndef Audio_h_
#define Audio_h_

#if TEENSYDUINO < 120
#error "Teensyduino version 1.20 or later is required to compile the Audio library."
#endif
#ifdef __AVR__
#error "The Audio Library only works with Teensy 3.X.  Teensy 2.0 is unsupported."
#endif

#include "DMAChannel.h"
#if !defined(DMACHANNEL_HAS_BEGIN) || !defined(DMACHANNEL_HAS_BOOLEAN_CTOR)
#error "You need to update DMAChannel.h & DMAChannel.cpp"
#error "https://github.com/PaulStoffregen/cores/blob/master/teensy3/DMAChannel.h"
#error "https://github.com/PaulStoffregen/cores/blob/master/teensy3/DMAChannel.cpp"
#endif

// When changing multiple audio object settings that must update at
// the same time, these functions allow the audio library interrupt
// to be disabled.  For example, you may wish to begin playing a note
// in response to reading an analog sensor.  If you have "velocity"
// information, you might start the sample playing and also adjust
// the gain of a mixer channel.  Use AudioNoInterrupts() first, then
// make both changes to the 2 separate objects.  Then allow the audio
// library to update with AudioInterrupts().  Both changes will happen
// at the same time, because AudioNoInterrupts() prevents any updates
// while you make changes.
//
#define AudioNoInterrupts() (NVIC_DISABLE_IRQ(IRQ_SOFTWARE))
#define AudioInterrupts()   (NVIC_ENABLE_IRQ(IRQ_SOFTWARE))

// include all the library headers, so a sketch can use a single
// #include <Audio.h> to get the whole library
//
#include "analyze_fft256.h"
#include "analyze_fft1024.h"
#include "analyze_print.h"
#include "analyze_tonedetect.h"
#include "analyze_notefreq.h"
#include "analyze_peak.h"
#include "analyze_rms.h"
#include "async_input_spdif3.h"
#include "control_sgtl5000.h"
#include "control_wm8731.h"
#include "control_ak4558.h"
#include "control_cs4272.h"
#include "control_cs42448.h"
#include "control_tlv320aic3206.h"
#include "effect_bitcrusher.h"
#include "effect_chorus.h"
#include "effect_fade.h"
#include "effect_flange.h"
#include "effect_envelope.h"
#include "effect_multiply.h"
#include "effect_delay.h"
#include "effect_delay_ext.h"
#include "effect_midside.h"
#include "effect_reverb.h"
#include "effect_freeverb.h"
#include "effect_waveshaper.h"
#include "effect_granular.h"
#include "effect_combine.h"
#include "effect_rectifier.h"
#include "effect_wavefolder.h"
#include "filter_biquad.h"
#include "filter_fir.h"
#include "filter_variable.h"
#include "filter_ladder.h"
#include "input_adc.h"
#include "input_adcs.h"
#include "input_i2s.h"
#include "input_i2s2.h"
#include "input_i2s_quad.h"
#include "input_i2s_hex.h"
#include "input_i2s_oct.h"
#include "input_tdm.h"
#include "input_tdm2.h"
#include "input_pdm.h"
#include "input_pdm_i2s2.h"
#include "input_spdif3.h"
#include "mixer.h"
#include "output_dac.h"
#include "output_dacs.h"
#include "output_i2s.h"
#include "output_i2s2.h"
#include "output_i2s_quad.h"
#include "output_i2s_hex.h"
#include "output_i2s_oct.h"
#include "output_mqs.h"
#include "output_pwm.h"
#include "output_spdif.h"
#include "output_spdif2.h"
#include "output_spdif3.h"
#include "output_pt8211.h"
#include "output_pt8211_2.h"
#include "output_tdm.h"
#include "output_tdm2.h"
#include "output_adat.h"
#include "play_memory.h"
#include "play_queue.h"
#include "play_sd_raw.h"
#include "play_sd_wav.h"
#include "play_serialflash_raw.h"
#include "record_queue.h"
#include "synth_tonesweep.h"
#include "synth_sine.h"
#include "synth_waveform.h"
#include "synth_dc.h"
#include "synth_whitenoise.h"
#include "synth_pinknoise.h"
#include "synth_karplusstrong.h"
#include "synth_simple_drum.h"
#include "synth_pwm.h"
#include "synth_wavetable.h"

#endif

As you can see, there are indeed a bunch of #include statements for all of the Audio library classes, but also a bunch of other stuff. In particular, there is an include for "DMAChannel.h" which is not an Audio library classes. This file is instead part of the Teensy fork of the Arduino IDE. So just searching for #include lines will not be sufficient. I've written some Swift code to parse out the relevant lines:

    public func makeList(directory: URL) throws -> [URL]
    {
        let libraryName = directory.lastPathComponent
        let filenamePath = directory.appendingPathComponent("\(libraryName).h")
        let contents = try String(contentsOf: filenamePath)
        let text = Text(fromUTF8String: contents)

        let parts = text.split("\n")
        let lines = parts.filter
        {
            line in

            return line.contains("#include") && line.contains("\"")
        }

        let includes: [URL] = lines.compactMap
        {
            line in

            let rightOfOpenQuote = line.split("\"")[1]
            let leftOfCloseQuote = rightOfOpenQuote.split("\"")[0]
            let filename = leftOfCloseQuote

            let includedFilePath = directory.appending(component: filename, isDirectory: false)
            guard File.exists(includedFilePath.path) else
            {
                return nil
            }

            return includedFilePath
        }

        return includes
    }

Let's take this code in chunks. We start with an URL to the directory containing the files, which by convention will end with the name of the library. First, we find the main .h files, which we can derive from the name of the directory:

let libraryName = directory.lastPathComponent
let filenamePath = directory.appendingPathComponent("\(libraryName).h")

Next we load the contents of this file and put it in a Text object. Text is my own library for string manipulation, because I find Swift's built-in String class to be difficult to use. The API for Text will be more familiar to users of languages used extensively for text processing, such as Python.

let contents = try String(contentsOf: filenamePath)
let text = Text(fromUTF8String: contents)

Next, we do a little functional-style programming to break this big chunk of text that we loaded from the file into individual lines and find only the lines that feature an #include statement. Specifically, we only want the form of include for local files, #include "filename" and not the form #include <filename> for system files.

let parts = text.split("\n")
let lines = parts.filter
{
    line in
    
    return line.contains("#include") && line.contains("\"")
}

This gives us all the includes, but unfortunately it also gets some includes we don't want, such as DMAChannel.h. We also need to extract just the filename, throwing out the "#include" part and the enclosing quotation marks. We could do this in multiple steps, but I combine the filtering and extraction into one step using compactMap. The compactMap function is a map function where the return values of the mapped function are optionals, combined with a filter in which all of the nil optionals are removed.

let includes: [URL] = lines.compactMap
{
    line in

    let rightOfOpenQuote = line.split("\"")[1]
    let leftOfCloseQuote = rightOfOpenQuote.split("\"")[0]
    let filename = leftOfCloseQuote

    let includedFilePath = directory.appending(component: filename, isDirectory: false)
    
    guard File.exists(includedFilePath.path) else
    {
        return nil
    }

    return includedFilePath
}

return includes

The code above splits the line based on the leftmost enclosing quote and then takes that result and splits it on the rightmost enclosing quote. The result is the item in between the quotes, which is the filename that we want. It then adds this to the original directory path to get the path to the included file. It then checks to see if that file actually exists in that location. If so, then it is likely a file containing an Audio library class. If not, then it is likely a system file (even though it was included with #include "filename") and so we don't want it. In the latter case, we return nil and the compactMap filters it out of the results list.

This concludes our simple code to find the list of files containing Audio library classes. There are many other ways that you could have written this code and the chosen method has advantages and disadvantages when compared to other approaches. The primary advantage is that it is short, about one page of code, and easy to understand how it works and its limitations. The disadvantage is that it is requires consistency in the way that code is Arduino library is constructed. If the naming conventions are not followed, or there is no main .h file, or the main .h file has a lot of #ifdefs and such, then this code will not produced the desired results.

However, if you want to support all possible configurations then your options are to either use a system in which the user of the software has to manually configure which files contain classes of interest or you will have to write a full C preprocessor language parser. In the former case, you lose all the value of this automated function. In the latter case, well now you're writing a C preprocessor language parser instead of doing the original project. In my use case, I just want to automatically find all of the Audio library classes that someone might want to configure in a tool like the Audio System Design Tool. I believe that this code achieves that goal, and if not then it's pretty close and just needs a little tweaking.

I hope that you enjoyed this exposition of a relatively simple piece of code. My pragmatic programming philosophy is that the primary principle is correctness of the code for the stated use case and the second is clarity of the code so that readers can perceive its correctness for the stated use case. Attempting premature optimization for hypothetical future performance problems or premature generalization for imagined possible future use cases is done at the cost either correctness or clarity. This is just my opinion, and you are welcome to write your code however you like. In our next post, we'll take a high-level look at some of the components of the Audio library. My best wishes to you on your projects, programming friends!

Code Generation for Arduino Audio - Finding Files Containing Audio Library Classes