inMusic · Technical Test

Software Developer (C++/Audio) — Answers

My worked answers to the seven questions, with reasoning shown and code you can compile.

A note up front: where a question is vague I've stated my assumptions inline, as requested.
Question One

Arithmetic mean

The question

Write a function that, given some numbers, returns the arithmetic mean of them.

Demonstrate it working.

I'm returning a small struct with the mean plus a validity flag, so the empty case has a clear answer rather than dividing by zero or returning a misleading 0.

mean.cpp
#include <vector>
#include <iostream>

struct MeanReturn
{
    float mean;
    bool isValid;
};

MeanReturn meanFinder(const std::vector<float>& numbers)
{
    MeanReturn result;

    if (numbers.empty())
    {
        result.isValid = false;
        result.mean = 0.0f;
        return result;
    }

    float runningTotal = 0.0f;
    for (float number : numbers)
        runningTotal += number;

    result.mean = runningTotal / numbers.size();
    result.isValid = true;
    return result;
}

int main()
{
    MeanReturn r = meanFinder({2.0f, 4.0f, 6.0f});
    if (r.isValid) std::cout << "Mean: " << r.mean << "\n";   // Mean: 4

    MeanReturn empty = meanFinder({});
    if (!empty.isValid) std::cout << "Empty collection - no mean\n";
}
Assumption Floats are fine for this. For a very large collection you'd accumulate into a double to reduce rounding error, but that's overkill here.
Question Two

Parse a test method name into English

The question

You are writing a JUnit-style test framework. The test method names take the form:

public void testCowsCanBeMilked()
public void testSheepAreNotTheOnlyFruit()

When we report a test running, the name is displayed in English, thus:

"Cows can be milked"
"Sheep are not the only fruit"

In C++, write a function that parses the method signature and returns the equivalent display version. Demonstrate it working. Well.

Approach: pull out just the method name from the signature, drop the test prefix, split into words wherever there's a capital letter, then keep the first word's capital and lowercase the rest.

parse.cpp
#include <string>
#include <vector>
#include <cctype>
#include <iostream>

std::string parseFunction(const std::string& signature)
{
    // grab just the method name - it sits between the last space and the '('
    std::size_t start = signature.find_last_of(' ');
    std::size_t end   = signature.find('(');
    std::string name  = signature.substr(start + 1, end - start - 1);

    // drop the "test" prefix
    const std::string prefix = "test";
    if (name.rfind(prefix, 0) == 0)
        name = name.substr(prefix.size());

    // split into words at each capital letter
    std::vector<std::string> words;
    std::string currentWord;
    for (char c : name)
    {
        if (std::isupper(c) && !currentWord.empty())
        {
            words.push_back(currentWord);
            currentWord.clear();
        }
        currentWord += c;
    }
    if (!currentWord.empty())
        words.push_back(currentWord);

    // first word keeps its capital, the rest go lowercase, joined by single spaces
    std::string result;
    for (int i = 0; i < words.size(); ++i)
    {
        std::string word = words[i];
        if (i > 0)
        {
            for (char& ch : word)
                ch = std::tolower(ch);
            result += ' ';
        }
        result += word;
    }
    return result;
}

int main()
{
    std::cout << parseFunction("public void testCowsCanBeMilked()") << "\n";
    std::cout << parseFunction("public void testSheepAreNotTheOnlyFruit()") << "\n";
    // Cows can be milked
    // Sheep are not the only fruit
}
Assumption The input is the full signature in the form shown (a return type, then the method name, then ()), so the name is the token before the (. If only the bare method name was passed, the name-extraction step just returns the whole thing and the rest works the same.
Question Three

Which depend on a #include vs a forward declaration

The question

Suppose we have a C++ class called widget defined in widget.h:

class widget
{
    ...
};

The definition of the class fubar may depend on widget in many ways. Here are sixteen:

class fubar : public widget          // 1
{
    void value_parameter(widget );          // 2
    void ref_parameter(widget &);           // 3
    void ptr_parameter(widget *);           // 4

    virtual void value_parameter(widget );  // 5
    virtual void ref_parameter(widget &);   // 6
    virtual void ptr_parameter(widget *);   // 7

    widget value_return();                  // 8
    widget & ref_return();                  // 9
    widget * ptr_return();                  // 10

    widget instance_value_member;           // 11
    widget & instance_ref_member;           // 12
    widget * instance_ptr_member;           // 13

    static widget static_value_member;      // 14
    static widget & static_ref_member;      // 15
    static widget * static_ptr_member;      // 16
};

Which of these require a #include "widget.hpp" as opposed to a forward declaration class widget;? State any interesting reasons why.

What other ways might fubar depend on widget, and how does this relate to header dependency?

The rule throughout: you only need the full #include when the compiler has to know widget's actual size or layout. If it only needs to know the name exists, a forward declaration (class widget;) is enough.

Only two need the include:

  • #1, the inheritance — fubar embeds widget as a base, so the compiler needs widget's full layout to build fubar.
  • #11, the by-value member — that member lives inside every fubar, so fubar's size depends on widget's size. Needs the full type.

Everything else is fine with a forward declaration:

  • Anything that's a pointer or reference (params #3 #4 #6 #7, returns #9 #10, members #12 #13 #15 #16) is just an address, so the compiler doesn't need to look inside widget.
  • The by-value params and returns (#2 #5 #8) are also fine with a forward declaration. You only need the complete type where the function is actually defined or called, not where it's declared. virtual doesn't change that.

The sneaky one is #14, the static by-value member. It looks identical to #11 but does NOT need the include, because a static member is only declared inside the class. Its real definition lives in the .cpp, and that's the file that needs the include. So forward declaration in the header, include in the .cpp.

Other ways fubar could depend on widget: calling widget's methods, accessing its members, constructing or destroying it, deleteing a widget* (delete needs the full type to run the destructor), sizeof(widget), or using it as a template argument. All of those need the complete type at the point of use.

The practical point Forward declare in headers wherever you can and push the real #include down into the .cpp. That stops a change to widget.h forcing half the codebase to recompile.
Question Four

Code review — FileStar

The question

A colleague has presented the following code to you for review. What comments would you feed back to the author, and what would be the outcome of your review?

class FileStarError
{
public:
    FileStarError(const char *e)
    {
        message=e;
    }
    const char *what() { return message; }

    const char *message;
};

class FileStar
{
public:
    FileStar(const char *fn, const char *m="r")
    {
        filename=strdup(fn);
        f=fopen(fn,m);

        if (f==NULL)
        {
            throw FileStarError("Error opening file");
        }
    }

    ~FileStar()
    {
        delete [] filename;
        if (fclose(f)<0)
        {
            throw FileStarError("Error closing file");
        }
    }

    void read(char *buf, int size)
    {
        if (fread(buf, 1, size, f)!=size)
        {
            throw FileStarError("Error reading from file");
        }
    }

    const char *filename;
    FILE *f;
};

Comments I'd feed back:

1. strdup is freed with delete[], which is undefined behaviour.

strdup allocates with malloc, so it has to be freed with free(). Mixing malloc with delete[] is undefined. Either use free(filename), or better, store the name in a std::string and stop managing raw memory at all.

2. The destructor throws.

fclose failing throws FileStarError from inside ~FileStar. If the destructor runs while another exception is already propagating, you've got two exceptions in flight and the program calls std::terminate. Destructors should not throw — log or swallow the close failure instead.

3. Rule of Three violation.

There's a destructor but no copy constructor or copy assignment. If a FileStar is copied, the filename pointer and the FILE* get shallow-copied, so two objects own the same memory and the same file handle. When both are destroyed you get a double free and a double fclose. Either delete the copy operations, or implement them properly, or use RAII members so the defaults are correct.

4. The read() comparison is signed vs unsigned.

fread returns size_t, but size is an int, so fread(...) != size is a signed/unsigned comparison. The int gets converted to unsigned, and if size were ever negative it'd wrap to a huge value and the check would misbehave. Make size a size_t.

Minor

what() should be const, and FileStarError storing a raw const char* is fine for the string literals being thrown here, but it'd dangle if anyone ever constructed it from a temporary buffer.

Outcome of the review I'd ask for changes before approving. The cleanest fix is to lean on RAII — std::string for the filename and a self-closing wrapper (or a unique_ptr<FILE, ...> with a custom deleter) for the file. That makes the class Rule of Zero and removes the delete[] bug, the double-free, and the throwing destructor in one go.
Question Five

Reacting immediately when pre-calc takes 100s of ms

The question

In a real-time audio application, the software needs to react to control changes (e.g. change the sound) immediately.

Some operations, for example time-stretching, need a lot of pre-calculations, that can typically take 100s of milliseconds.

What general concepts come to your mind to deal with this situation?

The pre-calc can never run on the audio thread — it would blow the buffer deadline and you'd get a glitch. So the concepts are:

Do the heavy pre-calculation on a background/worker thread, off the audio thread entirely. The audio thread carries on producing sound using its current settings the whole time it's running.

Hand the finished result over to the audio thread lock-free, because taking a lock on the audio thread is itself blocking (unbounded wait, plus priority-inversion risk). That's an atomic pointer swap, or an SPSC FIFO for streaming smaller messages. The worker builds the whole new thing off to the side and publishes it with a single atomic operation, so the audio thread only ever sees the old complete result or the new complete one.

Bridge the gap while the calc runs: keep using the old processor until the new one is ready, and crossfade over to it when it arrives so there's no click at the switchover.

The mental model is that the audio thread is never waiting on anything — it's always running either the old thing or the freshly published new thing.

Assumption A short latency before the new processing becomes audible is acceptable, as long as the audio itself never drops out.
Question Six

Measuring true (inter-sample) peak

The question

How would you measure the true (inter-sample) peak level of a digital signal?

Sample peak just looks at the actual sample values and takes the highest one. The problem is that the waveform leaving the DAC is reconstructed between the samples, and that reconstructed curve can overshoot above the highest sample. Those are inter-sample peaks, and it means a signal that reads exactly 0 dBFS on its samples can still clip the converter.

To catch them you reconstruct what happens between the samples: oversample the signal (typically 4×) using interpolation / a low-pass filter, which fills in the in-between values, then take the peak of that upsampled signal. That reveals the overshoots the raw samples hid.

This is basically what the ITU-R BS.1770 true peak measurement specifies — oversample by at least 4× and measure the max of the reconstructed signal. More oversampling is more accurate, but 4× is the standard practical amount.

Short version Sample peak measures the dots, true peak reconstructs the line between the dots and measures that, because the line is what actually hits the converter.
Question Seven

Architecture for device, mixer, plugins

The question

Design (basic UML-like shapes) an architecture for a program that can:

  • use one of the installed audio devices on a system
  • pass audio through a mixer component
  • load plugins into the mixer

The idea is to use interfaces at the two swappable points — the audio device and the plugins — so concrete implementations plug in without the engine or mixer needing to know about them. Same thinking as dependency inversion: the high-level parts depend on abstractions, not on specific devices or plugins.

        +------------------+
        |   AudioEngine    |   owns + drives everything
        +------------------+
                 |
                 | talks to an interface, not a specific device
                 v
        +------------------+
        |  IAudioDevice    |   interface: open / close / start / callback
        +------------------+
            ^          ^
            |          |        concrete backends implement it
   +----------------+ +----------------+
   | CoreAudioDevice| |   AsioDevice   |
   +----------------+ +----------------+

   audio from the device callback passes through:

        +------------------+
        |     Mixer        |   sums / routes channels
        +------------------+
                 | hosts a list of...
                 v
        +------------------+
        |    IPlugin       |   interface: process(buffer) / setParam / prepare
        +------------------+
            ^          ^
            |          |        concrete plugins implement it
   +----------------+ +----------------+
   |  ReverbPlugin  | |    EqPlugin    |
   +----------------+ +----------------+

Flow: the AudioEngine grabs one of the installed devices through the IAudioDevice interface, so it doesn't care which one it is. The device fires its callback (on its own high-priority thread), audio passes through the Mixer, and the Mixer runs its plugin chain by calling process() on each plugin through the common IPlugin interface.

Why interfaces in both spots: you can add a new device backend or a new plugin just by implementing the interface, without touching the engine or the mixer. It also lets you drop in a mock device or mock plugin for testing.

Assumption / important caveat Everything inside the device callback (the mixer sums and every plugin's process()) has to be real-time safe — no allocating, no locking, no I/O. Plugin loading and any heavy setup happens off the audio thread and gets handed in, which ties back to Question Five.