The Spatial part renders mono sources in space: for headphones through a head-related transfer function (HRTF) set, or for a speaker layout. It is a set of effects the program chains on its own audio thread, each processing a block in place or into caller buffers. An effect or decoder that refuses a call writes silence where it would have written, so a block played after an unchecked error is quiet; a buffer a call adds into (a bed, a panner's channels) keeps what it held. None of them needs a context, a device or a thread; they work as well in an offline renderer as in a game's mixer.
A source usually goes through two stages: the direct effect applies what the path does to it (distance, walls, air), then the binaural effect, the ambisonic bed or a speaker panner places it. A reverb adds the room. The spatializer can compute every effect's parameters from a scene.
Positions are in metres in the listener's frame: +x to the right, +y up, -z straight ahead, the convention of OpenAL, Steam Audio and Resonance Audio. A program with another convention converts at the call. Rotations are quaternions, normalized where they are used.
Headphone rendering needs an HRTF set in the library's own format (see File formats). data/hrtf/sadie2-ku100-48k.maudhrtf is the default set, a KU100 dummy head from the SADIE II database, under the Apache License 2.0. tools/sofa_to_maudhrtf.py makes one from any SOFA file.
The library opens no files. The program reads the file and hands the bytes over; the loader treats them as hostile, checks every count and the checksum before it allocates, and copies what it keeps:
maudHrtfDef def = maudDefaultHrtfDef();
def.bytes = bytes; // the file's bytes, the host's to keep or free
def.byteCount = byteCount;
maudHrtf* hrtf = NULL;
maudResult result = maudLoadHrtf(&def, &hrtf);
// maud_errorFormat: a damaged or foreign file; maud_errorVersion: another version.
Set def.sampleRate to the rate the effects run at, and the loader resamples the set to it. One set serves every effect that uses it and must outlive them. maudHrtf_GetInfo gives its name and license text, which a program that ships the set shows with its credits.
The binaural effect renders one mono source for both ears and moves smoothly between calls:
maudBinauralDef def = maudDefaultBinauralDef(); // the near field on
def.hrtf = hrtf;
maudBinaural* effect = NULL;
maudResult result = maudCreateBinaural(&def, &effect);
maudBinauralParams params = {{2.0f, 0.0f, -1.0f}, 0.5f}; // ahead and right; its gain
float* ears[2] = {left, right};
if (result == maud_success)
{
result = maudBinaural_Process(effect, ¶ms, mono, ears, 480);
}
headRadius sets the head's size. A source exactly at an ear leaves that ear no direction to see it from (the effect then takes the direction from the head's centre), and near an ear the direction turns fast as the source moves, so keep sources outside headRadius of the head's centre.maudBinaural_Reset forgets the previous call, for a source that jumps (a cut, a respawn).A binaural effect per moving source costs about 12 microseconds per 10 ms block of 48 kHz audio on a desktop core. Distant sources and ambience cost less through a bed: each source is encoded into a sound field, the field turns with the listener's head, and one decoder renders it for the ears.
uint32_t order = 3;
maudPanSource was = {{0.0f, 0.0f, -1.0f}, 1.0f}; // where it was at the last call
maudPanSource now = {{1.0f, 0.0f, -1.0f}, 1.0f}; // and is at this one's end
maudResult result = maudEncodeAmbisonic(order, &was, &now, mono, bed, 480);
maudQuaternion head = {0.0f, 0.0f, 0.0f, 1.0f}; // the listener's turn, from its pose
if (result == maud_success)
{
result = maudRotateAmbisonic(bed, 480, order, &head, &head);
}
maudBinauralDecoderDef def = maudDefaultBinauralDecoderDef();
def.hrtf = hrtf;
def.order = order;
maudBinauralDecoder* decoder = NULL;
if (result == maud_success)
{
result = maudCreateBinauralDecoder(&def, &decoder);
}
float* ears[2] = {left, right};
if (result == maud_success)
{
result = maudBinauralDecoder_Decode(decoder, (const float* const*)bed, ears, 480);
}
maudDestroyBinauralDecoder(decoder);
A bed of order 1 to 3 has maudGetAmbisonicChannelCount(order) planar channels (16 at the third), in AmbiX's ACN order with SN3D weights, so other ambisonic tools read it. Higher orders place sources more sharply and cost more. Encoding adds into the bed, so clear it before each block. The bed's channels are the program's buffers; the encoder and rotation keep no state.
For a speaker layout there are two ways:
maudCreateSpeakerPanner) pans each source straight into the layout's channels by vector base amplitude panning. Once made it is read only: any number of sources share it, from any thread.maudCreateSpeakerDecoder, maudSpeakerDecoder_Decode) renders an ambisonic bed to the layout, so the same bed serves headphones and speakers.The layout's speaker positions are the nominal ones of ITU-R BS.2051.
The direct effect filters a source by what lies between it and the listener, in three bands (up to 800 Hz, 800 Hz to 8 kHz, above 8 kHz): air absorption over the distance, occlusion and transmission through obstacles, and the source's directivity.
maudDirectEffectDef def = maudDefaultDirectEffectDef();
maudDirectEffect* effect = NULL;
maudResult result = maudCreateDirectEffect(&def, &effect);
maudDirectParams params = maudDefaultDirectParams(); // a clear path
params.distance = 30.0f; // air absorbs the highs over 30 m
params.occlusion = 1.0f; // behind a wall...
params.transmission[0] = 0.3f; // ...that lets some lows through
params.transmission[1] = 0.1f;
params.transmission[2] = 0.02f;
if (result == maud_success)
{
result = maudDirectEffect_Process(effect, ¶ms, mono, filtered, 480);
}
maudDestroyDirectEffect(effect);
maudGetAirAbsorption computes the def's absorption per band from ISO 9613-1 for a temperature and humidity; maudGetDirectivity turns a directivity pattern and the angle to the listener into the per-band values. The effect sits before the binaural effect or panner, mono in and mono out.
The reverb takes a mono send and adds the room's late sound into a first-order bed, decaying in three bands as its parameters say. One per listener is usual; decode its bed with the binaural or speaker decoder.
maudReverbDef def = maudDefaultReverbDef(); // 48 kHz
def.maxDelay = 0.1f; // the longest send delay it will take, s
maudReverb* reverb = NULL;
maudResult result = maudCreateReverb(&def, &reverb);
maudReverbParams params = {
.reverbTime = {1.8f, 1.2f, 0.6f}, // seconds to fall 60 dB: lows, mids, highs
.level = {-12.0f, -12.0f, -18.0f}, // the send's level per band, dB
.delay = 0.02f, // the send's delay, s
};
if (result == maud_success)
{
result = maudReverb_Process(reverb, ¶ms, mono, bed, 480); // four channels
}
maudDestroyReverb(reverb);
Coupled spaces (a hall behind an open door) decay in two slopes. A reverb made with enableTail set also renders the slower one from tailTime and tailLevel, at twice the memory.
_Reset; the binaural decoder's output, for its filters' length or until its reset; the spatializer's reflections, for about reflectionDuration. The encoder, the rotation, the panner and the speaker decoder keep nothing, so only that sample's frame goes wrong.