Software Developer (C++/Audio): Answers
My worked answers to the seven questions, with reasoning shown and code you can compile.
Arithmetic mean
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.
#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";
}
double to reduce rounding error, but that's overkill for this.
Parse a test method name into English
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.
What I do is pull out just the method name from the signature, drop the test prefix, then split into words at each capital letter. The first word keeps its capital and the rest go lowercase.
#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
}
()), so the name is the token before the (. If only the bare method name got passed in, the name-extraction step just returns the whole thing and the rest works the same.
Which depend on a #include vs a forward declaration
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?
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 of them actually need the include:
- #1 is the inheritance. fubar embeds widget as a base, so the compiler needs widget's full layout to build fubar.
- #11 is the by-value member. It lives inside every fubar, so fubar's size depends on widget's size, and for that you need the full type.
Everything else is fine with just 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 never needs to look inside widget.
- The by-value params and returns (#2 #5 #8) are fine too. You only need the complete type where the function is actually defined or called, not where it's declared.
virtualdoesn't change that.
The sneaky one is #14, the static by-value member. It looks identical to #11 but doesn't need the include. A static member is only declared inside the class. Its real definition lives in the .cpp, and that's the file that actually needs the include. So you forward declare in the header and include in the .cpp.
fubar could depend on widget in other ways too, like calling its methods, accessing its members, constructing or destroying it, deleteing a widget* (delete needs the full type to run the destructor), taking its sizeof, or using it as a template argument. All of those need the complete type at the point of use.
In practice you 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.
Code review: FileStar
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;
};
The comments I'd feed back:
- The filename is allocated with
strdupbut freed withdelete[]in the destructor, which is undefined behaviour.strdupallocates with malloc, so it has to be freed withfree(). Either callfree(filename), or better, store the name in astd::stringand stop managing raw memory at all. - The destructor throws. If
fclosefails it throwsFileStarErrorfrom inside~FileStar, and if that happens while another exception is already propagating you've got two exceptions in flight and the program callsstd::terminate. Destructors shouldn't throw, so log or swallow the close failure instead. - It breaks the Rule of Three. There's a destructor but no copy constructor or copy assignment, so if a
FileStargets copied thefilenamepointer and theFILE*are shallow-copied and two objects end up owning 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, implement them properly, or use RAII members so the defaults are correct. - The
read()check compares signed against unsigned.freadreturnssize_tbutsizeis anint, sofread(...) != sizeconverts the int to unsigned, and ifsizewere ever negative it'd wrap to a huge value and the check would misbehave. Makesizeasize_t.
A couple of smaller things too. what() doesn't modify the object, so the method itself should be const-qualified, const char *what() const. The const that's already there only applies to the returned pointer, not to the method, so right now you couldn't call what() on a const FileStarError. And storing the message as a raw const char* is fine for the string literals being thrown here but would dangle if anyone ever built one from a temporary buffer.
Overall I'd ask for changes before approving. The cleanest fix is to lean on RAII, with a 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 kills the delete[] bug, the double free and the throwing destructor all at once.
Reacting immediately when pre-calc takes 100s of ms
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 just carries on making sound with its current settings the whole time that runs.
Then you hand the finished result over to the audio thread lock-free. Taking a lock on the audio thread is itself blocking, an unbounded wait with the risk of priority inversion, so you avoid it. That hand-over can be an atomic pointer swap, or an SPSC FIFO if you're streaming smaller messages. The worker builds the whole new thing off to the side and publishes it in one atomic operation, so the audio thread only ever sees the old complete result or the new complete one.
While the calc runs you bridge the gap. Keep using the old processor until the new one is ready, then crossfade over to it when it arrives so there's no click at the switchover.
The point is the audio thread is never waiting on anything. It's always running either the old thing or the freshly published new one.
Measuring true (inter-sample) peak
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 catch is that the waveform leaving the DAC gets reconstructed between the samples, and that reconstructed curve can overshoot above the highest sample. Those overshoots are the inter-sample peaks. 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. You oversample the signal, typically by 4×, using interpolation or a low-pass filter to fill in the in-between values, then take the peak of that upsampled signal. That reveals the overshoots the raw samples were hiding.
This is basically what the ITU-R BS.1770 true peak measurement does. It oversamples by at least 4× and measures the max of the reconstructed signal. More oversampling is more accurate, but 4× is the standard practical amount.
So in short, sample peak measures the dots and true peak reconstructs the line between the dots and measures that, because the line is what actually hits the converter.
Architecture for device, mixer, plugins
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
I'd put an interface at each of the two points you want to swap out. One is the audio device, the other is the plugins. Concrete implementations then plug in without the engine or the mixer needing to know anything about them. It's the same thinking as dependency inversion, where the high-level parts depend on abstractions instead of on specific devices or plugins.
+------------------+
| AudioEngine | owns + drives everything
+------------------+
|
| talks to an interface, not a specific device
v
+------------------+
| AudioDevice | 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
+------------------+
| Plugin | interface: process(buffer) / setParam / prepare
+------------------+
^ ^
| | concrete plugins implement it
+----------------+ +----------------+
| ReverbPlugin | | EqPlugin |
+----------------+ +----------------+
The AudioEngine grabs one of the installed devices through the AudioDevice interface, so it doesn't care which one it actually is. The device fires its callback on its own high-priority thread, the audio passes through the Mixer, and the Mixer runs its plugin chain by calling process() on each plugin through the common Plugin interface.
Having an interface in both spots means you can add a new device backend or a new plugin just by implementing it, without touching the engine or the mixer. It also lets you drop in a mock device or mock plugin when you want to test.
process()) has to be real-time safe though, so 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.