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.