Dynamic Music System

Category
Plugin
Status
In Development
Duration
12+ weeks
Team Size
Solo Developer
Role
Audio Programmer
Engine
Engine Agnostic
Languages
C/C++, C#
Developed a custom dynamic music system plugin featuring adaptive audio
techniques commonly used in video games, including real-time volume
mixing and instrument layering. Currently implemented as a libopenmpt
wrapper and actively being expanded to support additional audio formats.
using UnityEngine; using System; using System.IO; using System.Runtime.InteropServices; public sealed class ModulePlayer : IDisposable { private IntPtr handle = IntPtr.Zero; private GCHandle dataHandle; private byte[] pinnedData; private bool dataPinned; public bool IsLoaded => handle != IntPtr.Zero; // Expose handle safely (no reflection needed) public IntPtr Handle => handle; public void Load(byte[] moduleData, int maxFrames = 2048) { Unload(); if (moduleData == null || moduleData.Length == 0) throw new ArgumentException("Invalid module data"); pinnedData = moduleData; dataHandle = GCHandle.Alloc(pinnedData, GCHandleType.Pinned); dataPinned = true; IntPtr dataPtr = dataHandle.AddrOfPinnedObject(); handle = ModuleEngineNative.CreateModule( dataPtr, moduleData.Length, IntPtr.Zero, 0, maxFrames ); if (handle == IntPtr.Zero) { CleanupPinnedData(); throw new Exception("CreateModule failed"); } } public void LoadFromFile(string file, int maxFrames = 2048) { string path = Path.Combine( Application.streamingAssetsPath, "Audio", "Music", "Modules", file ); byte[] moduleData = File.ReadAllBytes(path); Load(moduleData, maxFrames); } public void Unload() { if (handle != IntPtr.Zero) { ModuleEngineNative.DestroyModule(handle); handle = IntPtr.Zero; } CleanupPinnedData(); } private void CleanupPinnedData() { if (dataPinned) { dataHandle.Free(); dataPinned = false; } pinnedData = null; } public byte SetLooping(byte loopMode) { if (!IsLoaded) return byte.MaxValue; return ModuleEngineNative.ModuleSetLooping(handle, loopMode); } public uint AddLayerWithMask(ulong mask) { if (!IsLoaded) return uint.MaxValue; return ModuleEngineNative.ModuleAddLayerWithMask(handle, mask); } public uint AddLayerWithChannels(int[] channels) { if (!IsLoaded || channels == null || channels.Length == 0) return uint.MaxValue; return ModuleEngineNative.ModuleAddLayerWithChannels(handle, channels, channels.Length); } public bool RemoveLayer(uint id) { if (!IsLoaded) return false; return ModuleEngineNative.ModuleRemoveLayer(handle, id) != 0; } public void SetLayerVolume(uint layerId, double volume, float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModuleSetLayerVolume(handle, layerId, volume, fadeSpeed); } public void SetMasterVolume(double volume, float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModuleSetMasterVolume(handle, volume, fadeSpeed); } public void MuteLayer(uint layerId, float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModuleMuteLayer(handle, layerId, fadeSpeed); } public void MuteAllLayers(float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModuleMuteAllLayers(handle, fadeSpeed); } public void Play(float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModulePlay(handle, fadeSpeed); } public void PlayAt(double seconds, float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModulePlayAt(handle, seconds, fadeSpeed); } public void Pause(float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModulePause(handle, fadeSpeed); } public void Stop(float fadeSpeed = 0.0f) { if (!IsLoaded) return; ModuleEngineNative.ModuleStop(handle, fadeSpeed); } public void Seek(double seconds) { if (!IsLoaded) return; ModuleEngineNative.ModuleSeek(handle, seconds); } public void FlushAudioLogs() { ModuleEngineNative.FlushAudioLogs(); } public void Dispose() { Unload
Module Player: Unity Integration
& Native Bindings
The Unity C# interface of the native
dynamic music system, responsible
for module loading, memory pinning,
and safe interop with unmanaged
audio code. ModuleEngineNative
exposes the underlying DLL
functions, enabling runtime control
of playback, adaptive layer mixing,
looping, and real-time audio
parameters such as volume and
fading.
Module Player: Audio
Callback, Track Progression
Implementation of the
UpdateBuffer function, serving
as the core audio-thread pipeline
of the dynamic music system. It
handles playback state, smooth
layer transitions (volume fading),
OpenMPT channel updates, loop
behaviour, and low-latency audio
rendering in real-time. Written in
C++.
/** * Audio Thread (Main audio callback function, main update) * Called during every audio callback from Unity. * * Does the following: * - Apply queued commands * - Update layer fades (fade-in/fade-out) * - Push specific channel updates (volume, mute etc) to OpenMPT * - Render audio into output buffer (final output mix) */ void LayeredModule::UpdateBuffer(float* buffer, int frames, int numChannels, int sampleRate) { if (!module) { audio::log::AudioLogModuleNull("UpdateBuffer"); return; } if (!buffer) { audio::log::AudioLog("UPDATE BUFFER | invalid buffer"); return; } // Ensure FP mode on audio thread static thread_local bool fpInit = false; if (!fpInit) { EnableFastFP(); fpInit = true; } AudioFrameContext audioFrameCtx(frames, sampleRate, numChannels, renderer.GetMaxFrames()); ProcessCommands(); // Transport Update bool shouldPause = false; bool shouldStop = false; TransportState prevState = transport.GetCurrentState(); float tCurrent = transport.Update(audioFrameCtx.deltaTime, currentPosition, moduleLength, shouldPause, shouldStop); if (transport.IsIdle()) { //std::memset(buffer, 0, sizeof(float) * frames * numChannels); renderer.Clear(buffer, audioFrameCtx); return; } float master = masterVolume.load(std::memory_order_relaxed); /** * globalGain must be computed before layers */ float globalGain = master * tCurrent; // Pause completion if (shouldPause || transport.IsAdvancing()) { currentPosition = module->get_position_seconds(); if (loopSettings.enabled) { double loopEnd = (loopSettings.end > 0.0) ? loopSettings.end : moduleLength; // Normal loop // right now, the only available mode! if (loopSettings.mode >= 1) { if (currentPosition >= loopEnd) { module->set_position_seconds(loopSettings.start); currentPosition = loopSettings.start; forceVolumeSyncFrames = audio::MAX_FORCED_VOLUME_SYNC_FRAMES; } } } } // Stop completion if (shouldStop) { currentPosition = 0.0; module->set_position_seconds(currentPosition); } /** * Update all layers and apply the changes to OpenMPT, if any. * Smoothly moves currentVolume towards targetVolume by using * smoothstep based method. * Note: deltaTime must equal (frames / sampleRate) for consistent timing. */ bool forceVolumeSync = (forceVolumeSyncFrames > 0); if (layeringEnabled) { bool once = true; for (auto& layer : layers) { int layerIndex = &layer - layers.data(); //Only used for logging if (once) { int totalChannels = module->get_num_channels(); once = false; } // Skip unused layers if (layer.channelMask == 0) { continue; } // Fade progression (Time-Based) layer.fadeTimer += audioFrameCtx.deltaTime; float alpha = (layer.fadeDuration > 0.0f) ? layer.fadeTimer * layer.fadeInverseDuration : 1.0f; alpha = std::clamp(alpha, 0.0f, 1.0f); alpha = alpha * alpha * (3.0f - 2.0f * alpha); // smooth interpolation (smoothstep) float prevVolume = layer.currentVolume; layer.currentVolume = layer.startVolume + (layer.targetVolume - layer.startVolume) * alpha; // Snap to target to avoid floating-point drift if (std::abs(layer.currentVolume - layer.targetVolume) < audio::FADE_EPSILON) { layer.currentVolume = layer.targetVolume; } // Denormal protection if (std::abs(layer.currentVolume) < audio::DENORMAL_THRESHOLD) { layer.currentVolume = 0.0f; } // Sample level edge stability (not a fade) float edgeSmooth = 1.0f; bool justFadedOut = (prevVolume > 0.0f && layer.currentVolume == 0.0f); bool justFadedIn = (prevVolume == 0.0f && layer.currentVolume > 0.0f); if (justFadedOut || justFadedIn) { edgeSmooth = std::clamp(layer.fadeTimer / 0.002f, 0.0f, 1.0f); } /** * Apply changes (channel control) to OpenMPT only when necessary. */ float finalVolume = std::clamp(layer.currentVolume * edgeSmooth, 0.0f, 1.0f); if (layer.channelMask != 0) { bool logicalSilent = (layer.currentVolume <= 0.0001f); bool wasLoudBefore = (layer.lastAppliedVolume > 0.8f); if (logicalSilent && wasLoudBefore) { audio::log::AudioLogMismatch(layerIndex, layer.lastAppliedVolume); } } // Detect changes bool volumeChanged = forceVolumeSync || std::abs(finalVolume - layer.lastAppliedVolume) > audio::VOLUME_CHANGE_EPSILON; if (!volumeChanged) { audio::log::AudioLogSkipUpdate(layerIndex, finalVolume, layer.lastAppliedVolume); } // OpenMPT update if (interactive && (volumeChanged || forceVolumeSync)) { uint64_t mask = layer.channelMask; while (mask) { int ch = GetBitIndex(mask); if (ch < 0 || ch >= numOpenMPTChannels) { audio::log::AudioLogInvalidChannel("UpdateBuffer: bitmask", ch); break; } audio::log::AudioLogSend(layerIndex, ch, finalVolume, globalGain); interactive->set_channel_volume(ch, static_cast<double>(finalVolume)); mask &= (mask - 1); } layer.lastAppliedVolume = finalVolume; } } if (forceVolumeSyncFrames > 0) { forceVolumeSyncFrames--; } } else { for (auto& layer : layers) { if (layer.channelMask == 0) { continue; } char msg[256]; int layerIndex = &layer - layers.data(); // time-based fade layer.fadeTimer += audioFrameCtx.deltaTime; float fadeAlpha = (layer.fadeDuration > 0.0f) ? (layer.fadeTimer / layer.fadeDuration) : 1.0f; layer.fadeTimer = std::min(layer.fadeTimer, layer.fadeDuration); layer.currentVolume = layer.startVolume + (layer.targetVolume - layer.startVolume) * fadeAlpha; if (std::abs(layer.currentVolume) < audio::DENORMAL_THRESHOLD) { layer.currentVolume = 0.0f; } float finalVolume = std::clamp(layer.currentVolume, 0.0f, 1.0f); if (interactive) { uint64_t mask = layer.channelMask; while (mask) { int ch = GetBitIndex(mask); if (ch < 0 || ch >= numOpenMPTChannels) { break; } audio::log::AudioLogSend(layerIndex, ch, finalVolume, globalGain); interactive->set_channel_volume(ch, static_cast<double>(finalVolume)); mask &= (mask - 1); } } } } // If paused, no advancing the music track if (transport.IsPaused()) { //std::memset(buffer, 0, sizeof(float) * frames * numChannels); renderer.Clear(buffer, audioFrameCtx); return; } // Render audio from OpenMPT. Returns the number of frames actually read int framesRead = renderer.ReadFrames(*module, audioFrameCtx); // Fade-out discard case (silent, but still advancing the music track) if (transport.IsFadingOut() && globalGain <= audio::DENORMAL_THRESHOLD) { renderer.Clear(buffer, audioFrameCtx); return; } renderer.RenderAudio(framesRead, buffer, audioFrameCtx, globalGain
© 2026 Sebastian Valck. All rights reserved.