├── .gitignore ├── LICENSE ├── README.md ├── dub.json ├── examples ├── binaural-beat │ ├── dub.json │ └── source │ │ └── main.d ├── drum-machine │ ├── bass.wav │ ├── clap.wav │ ├── dub.json │ ├── hihat.wav │ ├── kick.wav │ ├── openhat.wav │ ├── snare.wav │ ├── source │ │ └── main.d │ └── wood.wav ├── loopback │ ├── dub.json │ └── source │ │ └── main.d └── music-playback │ ├── dub.json │ ├── first_last.mod │ ├── lits.xm │ └── source │ └── main.d ├── game-mixer.jpg └── source └── gamemixer ├── bufferedstream.d ├── chunkedvec.d ├── delayline.d ├── effects.d ├── mixer.d ├── package.d ├── resampler.d └── source.d /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.o 3 | *.obj 4 | 5 | # Compiled Dynamic libraries 6 | *.so 7 | *.dylib 8 | *.dll 9 | 10 | # Compiled Static libraries 11 | *.a 12 | *.lib 13 | 14 | # Executables 15 | *.exe 16 | 17 | # DUB 18 | .dub 19 | docs.json 20 | __dummy.html 21 | docs/ 22 | 23 | # Code coverage 24 | *.lst 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # game-mixer 2 | 3 | A simple-to-use library for emitting sounds in your game. 4 | It was thought of as a replacement of SDL2_mixer. 5 | 6 | ## Features 7 | 8 | - ✅ MP3 / OGG / WAV / FLAC / XM / MOD playback 9 | - ✅ Threaded decoding with progressive buffering. Unlimited channels, decoded streams are reused 10 | - ✅ Looping, fade-in/fade-out, delayed triggering, synchronized triggering 11 | - ✅ Playback volume, channel volume, master volume 12 | - ✅ Integrated resampling 13 | - ✅ Loopback: you can get mixer output in pull-mode instead of using audio I/O 14 | - ✅ `nothrow @nogc` 15 | - ✅ Based upon `libsoundio-d`: https://code.dlang.org/packages/libsoundio-d 16 | 17 | 18 | 19 | ## Changelog 20 | 21 | ### 🔔 `game-mixer` v1 22 | - Initial release. 23 | 24 | 25 | ### How to use it? 26 | 27 | - Add `game-mixer` as dependency to your `dub.json` or `dub.sdl`. 28 | - See the [drum machine example](https://github.com/AuburnSounds/game-mixer/tree/main/examples/drum-machine) for usage. 29 | 30 | 31 | --- 32 | 33 | ## Usage tutorial 34 | 35 | ### The Mixer object 36 | All `game-mixer` ressources and features are accessible through `IMixer`. 37 | 38 | ```d 39 | interface IMixer 40 | { 41 | // Create audio sources. 42 | IAudioSource createSourceFromMemory(const(ubyte[]) inputData); 43 | IAudioSource createSourceFromFile(const(char[]) path); 44 | 45 | // Play audio. 46 | void play(IAudioSource source, PlayOptions options); 47 | void play(IAudioSource source, float volume = 1.0f); 48 | void playSimultaneously(IAudioSource[] sources, PlayOptions[] options); 49 | 50 | /// Stop sounds. 51 | void stopChannel(int channel, float fadeOutSecs = 0.040f); 52 | void stopAllChannels(float fadeOutSecs = 0.040f); 53 | 54 | /// Set channel and master volume. 55 | void setChannelVolume(int channel, float volume); 56 | void setMasterVolume(float volume); 57 | 58 | /// Adds an effect on the master bus. 59 | void addMasterEffect(IAudioEffect effect); 60 | 61 | /// Create a custom effect. 62 | IAudioEffect createEffectCustom(EffectCallbackFunction callback, void* userData = null); 63 | 64 | // Mixer status. 65 | double playbackTimeInSeconds(); 66 | float getSampleRate(); 67 | bool isErrored(); 68 | const(char)[] lastErrorString(); 69 | 70 | /// Manual output ("loopback") 71 | void loopbackGenerate(float*[2] outBuffers, int frames); 72 | void loopbackMix(float*[2] inoutBuffers, int frames); ///ditto 73 | } 74 | 75 | 76 | ``` 77 | 78 | 79 | ### Create and Destroy a Mixer object 80 | 81 | - To have an `IMixer`, create it with `mixerCreate` 82 | 83 | ```d 84 | MixerOptions options; 85 | IMixer mixer = mixerCreate(options); 86 | ``` 87 | - The `MixerOptions` can be customized: 88 | ```d 89 | struct MixerOptions 90 | { 91 | /// Desired output sample rate. 92 | float sampleRate = 48000.0f; 93 | 94 | /// Number of possible sounds to play simultaneously. 95 | int numChannels = 16; 96 | 97 | /// The fade time it takes for one playing channel to change 98 | /// its volume with `setChannelVolume`. 99 | float channelVolumeSecs = 0.040f; 100 | } 101 | ``` 102 | Mixers always have a stereo output and mixing engine stereo. 103 | 104 | 105 | - Destroy it with `mixerDestroy`: 106 | 107 | ```d 108 | mixerDestroy(mixer); 109 | ``` 110 | This terminates the audio threaded playback and clean-up resources from this library. 111 | 112 | 113 | ### Load and play audio streams 114 | 115 | - Create audio sources with `IMixer.createSourceFromMemory` and `IMixer.createSourceFromFile`. 116 | 117 | ```d 118 | IAudioSource music = mixer.createSourceFromFile("8b-music.mod"); 119 | mixer.play(music); 120 | ``` 121 | 122 | - You can play an `IAudioSource` with custom `PlayOptions`: 123 | 124 | ```d 125 | IAudioSource music = mixer.createSourceFromFile("first_last.mod"); 126 | PlayOptions options; 127 | options.pan = 0.2f; 128 | mixer.play(music, options); 129 | ```` 130 | 131 | The following options exist: 132 | ```d 133 | struct PlayOptions 134 | { 135 | /// Force a specific playback channel 136 | int channel = anyMixerChannel; 137 | 138 | /// The volume to play the source with 139 | float volume = 1.0f; 140 | 141 | /// Stereo pan 142 | float pan = 0.0f; 143 | 144 | /// Play in x seconds (not compatible with startTimeSecs) 145 | float delayBeforePlay = 0.0f; 146 | 147 | /// Skip x seconds of source (not compatible with delayBeforePlay) 148 | float startTimeSecs = 0.0f; 149 | 150 | /// Looped source plays. 151 | uint loopCount = 1; 152 | 153 | /// Transition time on same channel, new sound 154 | float crossFadeInSecs = 0.000f; 155 | 156 | /// Transition time on same channel, old sound 157 | float crossFadeOutSecs = 0.040f; 158 | 159 | /// Transition time when channel was empty 160 | float fadeInSecs = 0.0f; 161 | } 162 | ``` 163 | 164 | 165 | ### Loopback interface 166 | 167 | You can reuse `game-mixer` in your own audio callback, for example in an audio plug-in situation. 168 | 169 | 170 | Create an `Imixer` with `isLoopback` option. 171 | ```d 172 | MixerOptions options; 173 | options.isLoopback = true; 174 | IMixer mixer = mixerCreate(options); 175 | ``` 176 | 177 | Then generate mixer output in your own stereo buffers: 178 | ```d 179 | float*[2] outBuffers = [ left.ptr, right.ptr ]; 180 | mixer.loopbackGenerate(outBuffers, N); // can only be called if isLoopback was passed 181 | ``` 182 | 183 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-mixer", 3 | 4 | "description": "Sound API for your game", 5 | 6 | "license": "BSL-1.0", 7 | 8 | "dependencies": 9 | { 10 | "dplug:core": ">=12.1.2 <15.0.0", 11 | "dplug:audio": ">=12.1.2 <15.0.0", 12 | "audio-formats": ">=3.0.1 <4.0.0" 13 | }, 14 | 15 | "configurations": [ 16 | { 17 | "name": "regular", 18 | "dependencies": 19 | { 20 | "libsoundio-d": "~>1.0", 21 | } 22 | }, 23 | { 24 | "name": "only-loopback", 25 | "versions": ["onlyLoopback"] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/binaural-beat/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binaural-beat", 3 | 4 | "dependencies": 5 | { 6 | "game-mixer": { "path": "../.."} 7 | } 8 | } -------------------------------------------------------------------------------- /examples/binaural-beat/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.math; 3 | 4 | import gamemixer; 5 | 6 | void main() 7 | { 8 | // The whole API is available through `IMixer` and the interfaces it may return. 9 | IMixer mixer = mixerCreate(); // Create with defaults (48000Hz, 512 or 1024 samples of software latency). 10 | 11 | mixer.addMasterEffect( mixer.createEffectCustom(&addSinusoid) ); 12 | 13 | writeln("Press ENTER to end the playback..."); 14 | readln(); 15 | mixerDestroy(mixer); // this cleans up everything created through `mixer`. 16 | } 17 | 18 | nothrow: 19 | @nogc: 20 | 21 | void addSinusoid(ref AudioBuffer!float inoutBuffer, EffectCallbackInfo info) 22 | { 23 | double invSR = 1.0f / info.sampleRate; 24 | int frames = inoutBuffer.frames(); 25 | for (int chan = 0; chan < inoutBuffer.channels(); ++chan) 26 | { 27 | float* buf = inoutBuffer[chan].ptr; 28 | for (int n = 0; n < frames; ++n) 29 | { 30 | float FREQ = (chan % 2) ? 52.0f : 62.0f; 31 | double phase = ((info.timeInFramesSincePlaybackStarted + n) * invSR) * 2 * PI * FREQ; 32 | buf[n] += 0.25f * sin(phase); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /examples/drum-machine/bass.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/bass.wav -------------------------------------------------------------------------------- /examples/drum-machine/clap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/clap.wav -------------------------------------------------------------------------------- /examples/drum-machine/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drum-machine", 3 | 4 | "dependencies": 5 | { 6 | "game-mixer": { "path": "../.."}, 7 | "turtle": ">=0.0.9 <1.0.0" 8 | }, 9 | 10 | "versions": [ "SDL_2010" ] 11 | } -------------------------------------------------------------------------------- /examples/drum-machine/hihat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/hihat.wav -------------------------------------------------------------------------------- /examples/drum-machine/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/kick.wav -------------------------------------------------------------------------------- /examples/drum-machine/openhat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/openhat.wav -------------------------------------------------------------------------------- /examples/drum-machine/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/snare.wav -------------------------------------------------------------------------------- /examples/drum-machine/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.math; 3 | import turtle; 4 | import gamemixer; 5 | 6 | int main(string[] args) 7 | { 8 | runGame(new DrumMachineExample); 9 | return 0; 10 | } 11 | 12 | enum int numTracks = 7; 13 | enum int numStepsInLoop = 16; 14 | enum double BPM = 123.0; 15 | 16 | enum Sounds 17 | { 18 | kick, 19 | hiHat, 20 | openHat, 21 | snare, 22 | cowbell, 23 | wood, 24 | delay 25 | } 26 | 27 | static immutable string[numTracks] paths = 28 | ["kick.wav", "bass.wav", "hihat.wav", "openhat.wav", "snare.wav", "wood.wav", "clap.wav"]; 29 | 30 | static immutable float[numTracks] volumes = 31 | [ 1.0f, 1.0f, 1.8f, 0.3f, 1.0f, 0.3f, 1.0f ]; 32 | 33 | static immutable float[numTracks] panning = 34 | [ 0.0f, 0.0f, 0.15f, 0.2f, 0.1f, -0.2f, 0.0f ]; 35 | 36 | // Note: game-mixer is not really appropriate to make a drum-machine, but it is for the example. 37 | // Notes would ideally need to be triggered in an audio thread callback not in graphics animation. 38 | // Right now we are dependent on the animation callback being called. 39 | 40 | // A simple drum machine example 41 | class DrumMachineExample : TurtleGame 42 | { 43 | override void load() 44 | { 45 | // Having a clear color with an alpha value different from 255 46 | // will result in a cheap motion blur. 47 | setBackgroundColor( color("#202020") ); 48 | 49 | MixerOptions options; 50 | _mixer = mixerCreate(options); 51 | foreach(n; 0..numTracks) 52 | _samples[n] = _mixer.createSourceFromFile(paths[n]); 53 | } 54 | 55 | ~this() 56 | { 57 | mixerDestroy(_mixer); 58 | } 59 | 60 | override void update(double dt) 61 | { 62 | if (keyboard.isDown("escape")) exitGame; 63 | 64 | double playbackTimeSinceStart = _mixer.playbackTimeInSeconds(); 65 | 66 | // Which step are we in? 67 | double fcurstep = BPM * (playbackTimeSinceStart / 60.0) * (numStepsInLoop / 4); 68 | if (fcurstep < 0) 69 | return; 70 | 71 | int curstep = cast(int)(fcurstep); 72 | double delayBeforePlay = (curstep + 1 - fcurstep) * (60.0 / (BPM * 4)); 73 | 74 | curstep = curstep % numStepsInLoop; 75 | assert(curstep >= 0 && curstep < numStepsInLoop); 76 | 77 | if ((_oldStep != -1) && (_oldStep != curstep)) 78 | { 79 | // A step was changed. 80 | // Schedule all notes that would happen at next step. 81 | int nextStep = (curstep + 1) % numStepsInLoop; 82 | 83 | for (int track = 0; track < numTracks; ++track) 84 | { 85 | if (_steps[track][nextStep]) 86 | { 87 | assert(delayBeforePlay >= 0); 88 | PlayOptions options; 89 | options.volume = 0.5f * volumes[track]; 90 | options.pan = panning[track]; 91 | options.channel = track; 92 | options.delayBeforePlay = delayBeforePlay; 93 | _mixer.play(_samples[track], options); 94 | } 95 | } 96 | } 97 | _oldStep = curstep; 98 | } 99 | 100 | override void resized(float width, float height) 101 | { 102 | float W = windowWidth() * 0.9f; // some margin 103 | float H = windowHeight() * 0.9f; 104 | float padW = W / numStepsInLoop; 105 | float padH = H / numTracks; 106 | _padSize = padW < padH ? padW : padH; 107 | } 108 | 109 | bool getStepAndTrack(float x, float y, out int step, out int track) 110 | { 111 | float W = windowWidth(); 112 | float H = windowHeight(); 113 | step = cast(int) floor( (x - (W / 2)) / _padSize + numStepsInLoop*0.5f); 114 | track = cast(int) floor( (y - (H / 2)) / _padSize + numTracks *0.5f); 115 | return !(step < 0 || track < 0 || step >= numStepsInLoop || track >= numTracks); 116 | } 117 | 118 | override void mousePressed(float x, float y, MouseButton button, int repeat) 119 | { 120 | int step, track; 121 | if (getStepAndTrack(x, y, step, track)) 122 | { 123 | if (button == MouseButton.left) 124 | _steps[track][step] = !_steps[track][step]; 125 | else if (button == MouseButton.right) 126 | { 127 | PlayOptions options; 128 | options.volume = 0.5f * volumes[track]; 129 | options.pan = panning[track]; 130 | options.channel = track; 131 | options.delayBeforePlay = 0; 132 | _mixer.play(_samples[track], options); 133 | } 134 | } 135 | } 136 | 137 | override void draw() 138 | { 139 | float W = windowWidth(); 140 | float H = windowHeight(); 141 | 142 | int mstep = -1; 143 | int mtrack = -1; 144 | getStepAndTrack(mouse.positionX, mouse.positionY, mstep, mtrack); 145 | 146 | // draw pads 147 | for (int track = 0; track < numTracks; ++track) 148 | { 149 | for (int step = 0; step < numStepsInLoop; ++step) 150 | { 151 | float posx = W / 2 + (-numStepsInLoop*0.5f + step) * _padSize; 152 | float posy = H / 2 + (-numTracks*0.5f + track) * _padSize; 153 | 154 | bool intense = _steps[track][step] != 0; 155 | bool yellow = step == _oldStep; 156 | 157 | RGBA color; 158 | if (intense) 159 | { 160 | color = yellow ? RGBA(255, 255, 100, 255) : RGBA(200, 200, 200, 255); 161 | } 162 | else 163 | { 164 | color = yellow ? RGBA(128, 128, 50, 255) : RGBA(100, 100, 100, 255); 165 | } 166 | 167 | if (track == mtrack && step == mstep) 168 | color.b += 55; 169 | 170 | canvas.fillStyle = color; 171 | canvas.fillRect(posx + _padSize * 0.05f, posy + _padSize * 0.05f, 172 | _padSize * 0.9f, _padSize * 0.9f); 173 | } 174 | } 175 | } 176 | 177 | private: 178 | IMixer _mixer; 179 | IAudioSource[numTracks] _samples; 180 | float _padSize; 181 | 182 | int _oldStep = -1; 183 | 184 | int[numStepsInLoop][numTracks] _steps = 185 | [ 186 | [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ], 187 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 188 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 189 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 190 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 191 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 192 | ]; 193 | } -------------------------------------------------------------------------------- /examples/drum-machine/wood.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/drum-machine/wood.wav -------------------------------------------------------------------------------- /examples/loopback/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback", 3 | 4 | "dependencies": 5 | { 6 | "game-mixer": { "path": "../.."} 7 | }, 8 | 9 | "subConfigurations": { 10 | "game-mixer": "only-loopback" 11 | } 12 | } -------------------------------------------------------------------------------- /examples/loopback/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.math; 3 | 4 | import gamemixer; 5 | 6 | void main() 7 | { 8 | // In loopback mode, the IMixer can only be called manually to mix its stereo output. 9 | // It does no I/O itself. 10 | MixerOptions options; 11 | options.isLoopback = true; 12 | IMixer mixer = mixerCreate(options); 13 | IAudioSource music = mixer.createSourceFromFile("lits.xm"); 14 | mixer.play(music); 15 | 16 | // Read 128 first samples 17 | enum int N = 128; 18 | float[N][2] samples; 19 | float*[2] outBuf = [ samples[0].ptr, samples[1].ptr ]; 20 | 21 | mixer.loopbackGenerate(outBuf, N); 22 | 23 | for (int n = 0; n < 100; ++n) 24 | { 25 | writeln("Mixed left samples: ", samples[0]); 26 | writeln("Mixed right samples: ", samples[1]); 27 | } 28 | 29 | mixerDestroy(mixer); 30 | } 31 | -------------------------------------------------------------------------------- /examples/music-playback/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-playback", 3 | 4 | "dependencies": 5 | { 6 | "game-mixer": { "path": "../.."} 7 | } 8 | } -------------------------------------------------------------------------------- /examples/music-playback/first_last.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/music-playback/first_last.mod -------------------------------------------------------------------------------- /examples/music-playback/lits.xm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/examples/music-playback/lits.xm -------------------------------------------------------------------------------- /examples/music-playback/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.math; 3 | 4 | import gamemixer; 5 | 6 | void main() 7 | { 8 | IMixer mixer = mixerCreate(); 9 | 10 | IAudioSource music = mixer.createSourceFromFile("lits.xm"); 11 | 12 | PlayOptions options; 13 | options.channel = 0; // force playing on channel zero 14 | options.crossFadeInSecs = 3.0; // time for a new song to appear when crossfading 15 | options.crossFadeOutSecs = 3.0; // time for an old song to disappear when crossfading 16 | options.fadeInSecs = 3.0; // time for a new song to appear when no other song is playing 17 | mixer.play(music, options); 18 | 19 | writeln("Press ENTER to fade to another song..."); 20 | readln(); 21 | 22 | IAudioSource music2 = mixer.createSourceFromFile("first_last.mod"); 23 | options.pan = 0.2f; 24 | mixer.play(music2, options); 25 | 26 | 27 | writeln("Press ENTER to halt music..."); 28 | readln(); 29 | 30 | mixerDestroy(mixer); 31 | } 32 | -------------------------------------------------------------------------------- /game-mixer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuburnSounds/game-mixer/6a023cfdb7ad583334e8c3b933793c7eb42f8f59/game-mixer.jpg -------------------------------------------------------------------------------- /source/gamemixer/bufferedstream.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Threaded stream decoder. 3 | * 4 | * Copyright: Copyright Guillaume Piolat 2021. 5 | * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module gamemixer.bufferedstream; 8 | 9 | import core.atomic; 10 | import core.stdc.string: memcpy; 11 | 12 | import dplug.core; 13 | import gamemixer.delayline; 14 | import audioformats; 15 | 16 | package: 17 | 18 | // Warning: the sequence of buffering, sample rate change, threading... is pretty complicated to follow. 19 | 20 | // A `BufferedStream` has optional threaded decoding, activated for streams that perform file IO. 21 | class BufferedStream 22 | { 23 | public: 24 | @nogc: 25 | 26 | enum streamingDecodingIncrement = 0.1f; // No more than 100ms in decoded at once. TODO tune 27 | enum streamingBufferDuration = 1.0f; // Seems to be the default buffer size in foobar TODO tune 28 | 29 | this(const(char)[] path) 30 | { 31 | _bufMutex = makeMutex(); 32 | _stream.openFromFile(path); 33 | _channels = _stream.getNumChannels(); 34 | startDecodingThreadIfNeeded(); 35 | } 36 | 37 | this(const(ubyte)[] data) 38 | { 39 | _bufMutex = makeMutex(); 40 | _stream.openFromMemory(data); 41 | _channels = _stream.getNumChannels(); 42 | startDecodingThreadIfNeeded(); 43 | } 44 | 45 | ~this() 46 | { 47 | if (_threaded) 48 | { 49 | atomicStore(_decodeThreadShouldDie, true); 50 | _decodeThread.join(); 51 | _decodeBuffer.reallocBuffer(0); 52 | } 53 | } 54 | 55 | int getNumChannels() nothrow 56 | { 57 | return _stream.getNumChannels(); 58 | } 59 | 60 | float getSamplerate() nothrow 61 | { 62 | return _stream.getSamplerate(); 63 | } 64 | 65 | // return original length in frames, -1 if unknown 66 | long getLengthInFrames() nothrow 67 | { 68 | long len = _stream.getLengthInFrames(); 69 | if (len == audiostreamUnknownLength) 70 | return -1; 71 | return len; 72 | } 73 | 74 | int readSamplesFloat(float* outData, int frames) 75 | { 76 | if (!_threaded) 77 | { 78 | // Non-threaded version 79 | return _stream.readSamplesFloat(outData, frames); 80 | } 81 | 82 | // 83 | int decodedFrames = 0; 84 | 85 | while(true) 86 | { 87 | if (decodedFrames == frames) 88 | break; 89 | 90 | assert(decodedFrames < frames); 91 | 92 | // Get number of frames in ring buffer. 93 | _bufMutex.lock(); 94 | 95 | loop: 96 | 97 | int bufFrames = _bufferLength / _channels; 98 | if (bufFrames == 0) 99 | { 100 | if (atomicLoad(_streamIsFinished)) 101 | { 102 | _bufMutex.unlock(); 103 | break; 104 | } 105 | _bufferIsEmpty.wait(&_bufMutex); 106 | goto loop; // maybe it is filled now,w ait for data 107 | } 108 | int framesNeeded = frames - decodedFrames; 109 | if (bufFrames > framesNeeded) 110 | bufFrames = framesNeeded; 111 | 112 | // PERF: why not read newbufFrames instead of bufFrames ? 113 | int newbufFrames = _bufferLength / _channels; // re-read length to get oldest frame location 114 | const(float)* readPointerOldest = _streamingBuffer.readPointer() - (_bufferLength - 1); 115 | size_t bytes = float.sizeof * bufFrames * _channels; 116 | memcpy(&outData[decodedFrames * _channels], readPointerOldest, bytes); 117 | _bufferLength -= bufFrames * _channels; 118 | assert(_bufferLength >= 0); 119 | 120 | decodedFrames += bufFrames; 121 | 122 | _bufMutex.unlock(); 123 | _bufferIsFull.notifyOne(); // Buffer is probably not full anymore. 124 | 125 | // stream buffer is probably not full anymore 126 | } 127 | 128 | return decodedFrames; 129 | 130 | // 131 | } 132 | 133 | private: 134 | Thread _decodeThread; 135 | AudioStream _stream; 136 | bool _threaded = false; 137 | int _channels; 138 | shared(bool) _decodeThreadShouldDie = false; 139 | shared(bool) _streamIsFinished = false; 140 | 141 | 142 | UncheckedMutex _bufMutex; 143 | ConditionVariable _bufferIsFull; 144 | ConditionVariable _bufferIsEmpty; 145 | 146 | // all protected by _bufMutex too 147 | int _bufferLength; // is counted in individual samples, not frames 148 | int _bufferCapacity; // is counted in individual samples, not frames 149 | Delayline!float _streamingBuffer; 150 | 151 | int _decodeIncrement; // max number of samples to decode at once, to avoid longer mutex hold 152 | float[] _decodeBuffer; // producer-only buffer before-pushgin 153 | 154 | void startDecodingThreadIfNeeded() 155 | { 156 | if (!_stream.realtimeSafe()) 157 | { 158 | _threaded = true; 159 | 160 | _bufferIsEmpty = makeConditionVariable(); 161 | _bufferIsFull = makeConditionVariable(); 162 | 163 | // compute amount of buffer we want 164 | int streamingBufferSamples = cast(int)(streamingBufferDuration * _stream.getSamplerate() * _channels); 165 | 166 | _streamingBuffer.initialize(streamingBufferSamples); 167 | _bufferLength = 0; 168 | _bufferCapacity = streamingBufferSamples; 169 | 170 | _decodeIncrement = cast(int)(streamingDecodingIncrement * _stream.getSamplerate()); 171 | _decodeBuffer.reallocBuffer(_decodeIncrement * _channels); 172 | 173 | // start event thread 174 | _decodeThread = makeThread(&decodeStream); 175 | _decodeThread.start(); 176 | } 177 | } 178 | 179 | void decodeStream() nothrow 180 | { 181 | // 182 | 183 | loop: 184 | while(!atomicLoad(_decodeThreadShouldDie)) 185 | { 186 | // Get available room in the delayline. 187 | _bufMutex.lock(); 188 | 189 | // How much room there is in the streaming buffer? 190 | int roomFrames = ( _bufferCapacity - _bufferLength) / _stream.getNumChannels(); 191 | 192 | assert(roomFrames >= 0); 193 | if (roomFrames > _decodeIncrement) 194 | roomFrames = _decodeIncrement; 195 | 196 | if (roomFrames == 0) 197 | { 198 | // buffer is full, wait on condition 199 | _bufferIsFull.wait(&_bufMutex); 200 | _bufMutex.unlock(); 201 | goto loop; 202 | } 203 | _bufMutex.unlock(); 204 | 205 | assert(roomFrames != 0); 206 | 207 | // Decode that much frames, but without holding the mutex. 208 | int framesRead; 209 | try 210 | { 211 | if (!_stream.isError) 212 | { 213 | framesRead = _stream.readSamplesFloat(_decodeBuffer.ptr, roomFrames); 214 | } 215 | if (_stream.isError) 216 | framesRead = 0; 217 | } 218 | catch(Exception e) 219 | { 220 | // should not happen 221 | framesRead = 0; 222 | } 223 | 224 | bool streamIsFinished = (framesRead != roomFrames); 225 | if (streamIsFinished) 226 | { 227 | atomicStore(_streamIsFinished, true); 228 | } 229 | 230 | if (framesRead) 231 | { 232 | // Re-lock the mutex in order to fill the buffer 233 | _bufMutex.lock(); 234 | int samples = framesRead * _channels; 235 | _streamingBuffer.feedBuffer( _decodeBuffer[0..samples] ); 236 | _bufferLength += samples; 237 | assert(_bufferLength <= _bufferCapacity); 238 | _bufMutex.unlock(); 239 | _bufferIsEmpty.notifyOne(); // stream buffer is probably not empty anymore 240 | } 241 | 242 | if (streamIsFinished) 243 | return; 244 | } 245 | // 246 | } 247 | } -------------------------------------------------------------------------------- /source/gamemixer/chunkedvec.d: -------------------------------------------------------------------------------- 1 | /** 2 | Defines `ChunkedVec`, a grow-only buffer that allocates fixed chunks of memory, 3 | so as to avoid costly `realloc` calls with large sizes. 4 | 5 | Copyright: Guillaume Piolat 2021. 6 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 7 | */ 8 | module gamemixer.chunkedvec; 9 | 10 | import core.stdc.stdlib : malloc, free; 11 | 12 | import dplug.core.vec; 13 | 14 | 15 | nothrow: 16 | @nogc: 17 | 18 | /// Returns: A newly created `ChunkedVec`. 19 | /// Params: 20 | /// chunkLength number of T elements in a chunk. 21 | ChunkedVec!T makeChunkedVec(T)(int chunkLength) nothrow @nogc 22 | { 23 | return ChunkedVec!T(chunkLength); 24 | } 25 | 26 | /// `ChunkedVec` can only grow. 27 | /// `ChunkedVec` has one indirection when indexing. 28 | /// `ChunkedVec` has a fixed-sized allocations list. 29 | struct ChunkedVec(T) 30 | { 31 | nothrow: 32 | @nogc: 33 | public: 34 | 35 | this(int chunkLength) 36 | { 37 | assert(isPowerOfTwo(chunkLength)); 38 | _chunkLength = chunkLength; 39 | _chunkMask = chunkLength - 1; 40 | _shift = iFloorLog2(_chunkLength); 41 | _currentIndex = 0; 42 | _currentChunk = -1; 43 | _len = 0; 44 | assert(1 << _shift == _chunkLength); 45 | } 46 | 47 | ~this() 48 | { 49 | foreach(c; _chunks[]) 50 | { 51 | free(c); 52 | } 53 | } 54 | 55 | @disable this(this); 56 | 57 | ref inout(T) opIndex(size_t n) pure inout 58 | { 59 | size_t chunkIndex = n >>> _shift; 60 | return _chunks[chunkIndex][n & _chunkMask]; 61 | } 62 | 63 | void pushBack(T x) 64 | { 65 | if (_currentIndex == 0) 66 | { 67 | _chunks.pushBack( cast(T*) malloc( T.sizeof * _chunkLength ) ); 68 | _currentChunk += 1; 69 | } 70 | _chunks[_currentChunk][_currentIndex] = x; 71 | _currentIndex = (_currentIndex + 1) & _chunkMask; 72 | _len++; 73 | } 74 | 75 | size_t length() pure const 76 | { 77 | return _len; 78 | } 79 | 80 | static if (is(T == float)) 81 | { 82 | void mixIntoBuffer(float* output, int frames, int frameOffset, const(float)* volumeRamp, float volume) 83 | { 84 | assert(frames != 0); 85 | 86 | // find chunk and index of frame 0 87 | 88 | int globalIndexStart = frameOffset; 89 | int globalIndexEnd = frameOffset + (frames - 1); 90 | 91 | // compute local indices inside the chunks, and which chunks 92 | int chunkStart = globalIndexStart >>> _shift; 93 | int indexStart = globalIndexStart & _chunkMask; 94 | int chunkEnd = globalIndexEnd >>> _shift; 95 | int indexEnd = globalIndexEnd & _chunkMask; 96 | 97 | if (chunkStart == chunkEnd) 98 | { 99 | mixBuffers(&_chunks[chunkStart][indexStart], &volumeRamp[0], &output[0], frames, volume); 100 | } 101 | else 102 | { 103 | int chunk = chunkStart; 104 | 105 | // First chunk 106 | int n = 0; 107 | int len = _chunkLength - indexStart; 108 | mixBuffers(&_chunks[chunkStart][indexStart], &volumeRamp[0], &output[0], len, volume); 109 | n += len; 110 | ++chunk; 111 | 112 | // Middle chunks (full) 113 | while (chunk < chunkEnd) 114 | { 115 | len = _chunkLength; 116 | mixBuffers(&_chunks[chunk][0], &volumeRamp[n], &output[n], len, volume); 117 | n += len; 118 | chunk += 1; 119 | } 120 | 121 | assert(chunk == chunkEnd); 122 | 123 | // Last chunk 124 | len = indexEnd+1; 125 | mixBuffers(&_chunks[chunk][indexEnd], &volumeRamp[n], &output[n], len, volume); 126 | n += len; 127 | 128 | assert(n == frames); 129 | } 130 | } 131 | } 132 | 133 | private: 134 | int _chunkLength; 135 | int _chunkMask; 136 | int _shift; 137 | int _currentIndex; 138 | int _currentChunk; 139 | size_t _len; 140 | Vec!(T*) _chunks; 141 | } 142 | 143 | private static void mixBuffers(float* input, const(float)* volumeRamp, float* output, int frames, float volume) pure 144 | { 145 | // LDC: Optimizing this with inteli has yield inferior results in the past, be careful 146 | for (int n = 0; n < frames; ++n) 147 | { 148 | output[n] += input[n] * (volume * volumeRamp[n]); 149 | } 150 | } 151 | 152 | private int iFloorLog2(int i) pure @safe 153 | { 154 | assert(i >= 1); 155 | int result = 0; 156 | while (i > 1) 157 | { 158 | i = i / 2; 159 | result = result + 1; 160 | } 161 | return result; 162 | } 163 | 164 | private bool isPowerOfTwo(int i) pure @safe 165 | { 166 | assert(i >= 0); 167 | return (i != 0) && ((i & (i - 1)) == 0); 168 | } -------------------------------------------------------------------------------- /source/gamemixer/delayline.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Delay-line implementation. 3 | * Copyright: Guillaume Piolat 2015-2021. 4 | * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 5 | */ 6 | module gamemixer.delayline; 7 | 8 | import core.stdc.string; 9 | 10 | import dplug.core.nogc; 11 | import dplug.core.math; 12 | import dplug.core.vec; 13 | 14 | nothrow @nogc: 15 | 16 | /// Allow to sample signal back in time. 17 | /// This delay-line has a twin write index, so that the read pointer 18 | /// can read a contiguous memory area. 19 | /// ____________________________________________________________________________________ 20 | /// | | _index | | readPointer = _index + half size | | 21 | /// ------------------------------------------------------------------------------------ 22 | /// 23 | struct Delayline(T) 24 | { 25 | public: 26 | nothrow: 27 | @nogc: 28 | 29 | /// Initialize the delay line. Can delay up to count samples. 30 | void initialize(int numSamples) 31 | { 32 | resize(numSamples); 33 | } 34 | 35 | ~this() 36 | { 37 | _data.reallocBuffer(0); 38 | } 39 | 40 | @disable this(this); 41 | 42 | /// Resize the delay line. Can delay up to count samples. 43 | /// The state is cleared. 44 | void resize(int numSamples) 45 | { 46 | if (numSamples < 0) 47 | assert(false); 48 | 49 | // Over-allocate to support POW2 delaylines. 50 | // This wastes memory but allows delay-line of length 0 without tests. 51 | 52 | int toAllocate = nextPow2HigherOrEqual(numSamples + 1); 53 | _data.reallocBuffer(toAllocate * 2); 54 | _half = toAllocate; 55 | _indexMask = toAllocate - 1; 56 | _numSamples = numSamples; 57 | _index = _indexMask; 58 | 59 | _data[] = 0; 60 | } 61 | 62 | /// Combined feed + sampleFull. 63 | /// Uses the delay line as a fixed delay of count samples. 64 | T nextSample(T incoming) pure 65 | { 66 | feedSample(incoming); 67 | return sampleFull(_numSamples); 68 | } 69 | 70 | /// Combined feed + sampleFull. 71 | /// Uses the delay line as a fixed delay of count samples. 72 | /// 73 | /// Note: input and output may overlap. 74 | /// If this was ever optimized, this should preserve that property. 75 | void nextBuffer(const(T)* input, T* output, int frames) pure 76 | { 77 | for(int i = 0; i < frames; ++i) 78 | output[i] = nextSample(input[i]); 79 | } 80 | 81 | /// Adds a new sample at end of delay. 82 | void feedSample(T incoming) pure 83 | { 84 | _index = (_index + 1) & _indexMask; 85 | _data.ptr[_index] = incoming; 86 | _data.ptr[_index + _half] = incoming; 87 | } 88 | 89 | /// Adds several samples at end of delay. 90 | void feedBuffer(const(T)[] incoming) pure 91 | { 92 | int N = cast(int)(incoming.length); 93 | 94 | // this buffer must be smaller than the delay line, 95 | // else we may risk dropping samples immediately 96 | assert(N < _numSamples); 97 | 98 | // remaining samples before end of delayline 99 | int remain = _indexMask - _index; 100 | 101 | if (N <= remain) 102 | { 103 | memcpy( &_data[_index + 1], incoming.ptr, N * T.sizeof ); 104 | memcpy( &_data[_index + 1 + _half], incoming.ptr, N * T.sizeof ); 105 | _index += N; 106 | } 107 | else 108 | { 109 | memcpy( _data.ptr + (_index + 1), incoming.ptr, remain * T.sizeof ); 110 | memcpy( _data.ptr + (_index + 1) + _half, incoming.ptr, remain * T.sizeof ); 111 | size_t numBytes = (N - remain) * T.sizeof; 112 | memcpy( _data.ptr, incoming.ptr + remain, numBytes); 113 | memcpy( _data.ptr + _half, incoming.ptr + remain, numBytes); 114 | _index = (_index + N) & _indexMask; 115 | } 116 | } 117 | 118 | /// Returns: A pointer which allow to get delayed values. 119 | /// readPointer()[0] is the last samples fed, readPointer()[-1] is the penultimate. 120 | /// Warning: it goes backwards, increasing delay => decreasing addressed. 121 | const(T)* readPointer() pure const 122 | { 123 | return _data.ptr + _index + _half; 124 | } 125 | 126 | /// Random access sampling of the delay-line at integer points. 127 | /// Delay 0 = last entered sample with feed(). 128 | T sampleFull(int delay) pure 129 | { 130 | assert(delay >= 0); 131 | return readPointer()[-delay]; 132 | } 133 | 134 | /// Random access sampling of the delay-line at integer points, extract a time slice. 135 | /// Delay 0 = last entered sample with feed(). 136 | void sampleFullBuffer(int delayOfMostRecentSample, float* outBuffer, int frames) pure 137 | { 138 | assert(delayOfMostRecentSample >= 0); 139 | const(T*) p = readPointer(); 140 | const(T*) source = &readPointer[-delayOfMostRecentSample - frames + 1]; 141 | size_t numBytes = frames * T.sizeof; 142 | memcpy(outBuffer, source, numBytes); 143 | } 144 | 145 | static if (is(T == float) || is(T == double)) 146 | { 147 | /// Random access sampling of the delay-line with linear interpolation. 148 | T sampleLinear(float delay) pure const 149 | { 150 | assert(delay > 0); 151 | int iPart; 152 | float fPart; 153 | decomposeFractionalDelay(delay, iPart, fPart); 154 | const(T)* pData = readPointer(); 155 | T x0 = pData[iPart]; 156 | T x1 = pData[iPart + 1]; 157 | return lerp(x0, x1, fPart); 158 | } 159 | 160 | /// Random access sampling of the delay-line with a 3rd order polynomial. 161 | T sampleHermite(float delay) pure const 162 | { 163 | assert(delay > 1); 164 | int iPart; 165 | float fPart; 166 | decomposeFractionalDelay(delay, iPart, fPart); 167 | const(T)* pData = readPointer(); 168 | T xm1 = pData[iPart-1]; 169 | T x0 = pData[iPart ]; 170 | T x1 = pData[iPart+1]; 171 | T x2 = pData[iPart+2]; 172 | return hermiteInterp!T(fPart, xm1, x0, x1, x2); 173 | } 174 | 175 | /// Third-order spline interpolation 176 | /// http://musicdsp.org/showArchiveComment.php?ArchiveID=62 177 | T sampleSpline3(float delay) pure const 178 | { 179 | assert(delay > 1); 180 | int iPart; 181 | float fPart; 182 | decomposeFractionalDelay(delay, iPart, fPart); 183 | assert(fPart >= 0.0f); 184 | assert(fPart <= 1.0f); 185 | const(T)* pData = readPointer(); 186 | T L1 = pData[iPart-1]; 187 | T L0 = pData[iPart ]; 188 | T H0 = pData[iPart+1]; 189 | T H1 = pData[iPart+2]; 190 | 191 | return L0 + 0.5f * 192 | fPart*(H0-L1 + 193 | fPart*(H0 + L0*(-2) + L1 + 194 | fPart*( (H0 - L0)*9 + (L1 - H1)*3 + 195 | fPart*((L0 - H0)*15 + (H1 - L1)*5 + 196 | fPart*((H0 - L0)*6 + (L1 - H1)*2 ))))); 197 | } 198 | 199 | /// 4th order spline interpolation 200 | /// http://musicdsp.org/showArchiveComment.php?ArchiveID=60 201 | T sampleSpline4(float delay) pure const 202 | { 203 | assert(delay > 2); 204 | int iPart; 205 | float fPart; 206 | decomposeFractionalDelay(delay, iPart, fPart); 207 | 208 | align(16) __gshared static immutable float[8][5] MAT = 209 | [ 210 | [ 2.0f / 24, -16.0f / 24, 0.0f / 24, 16.0f / 24, -2.0f / 24, 0.0f / 24, 0.0f, 0.0f ], 211 | [ -1.0f / 24, 16.0f / 24, -30.0f / 24, 16.0f / 24, -1.0f / 24, 0.0f / 24, 0.0f, 0.0f ], 212 | [ -9.0f / 24, 39.0f / 24, -70.0f / 24, 66.0f / 24, -33.0f / 24, 7.0f / 24, 0.0f, 0.0f ], 213 | [ 13.0f / 24, -64.0f / 24, 126.0f / 24, -124.0f / 24, 61.0f / 24, -12.0f / 24, 0.0f, 0.0f ], 214 | [ -5.0f / 24, 25.0f / 24, -50.0f / 24, 50.0f / 24, -25.0f / 24, 5.0f / 24, 0.0f, 0.0f ] 215 | ]; 216 | import inteli.emmintrin; 217 | __m128 pFactor0_3 = _mm_setr_ps(0.0f, 0.0f, 1.0f, 0.0f); 218 | __m128 pFactor4_7 = _mm_setzero_ps(); 219 | 220 | __m128 XMM_fPart = _mm_set1_ps(fPart); 221 | __m128 weight = XMM_fPart; 222 | pFactor0_3 = _mm_add_ps(pFactor0_3, _mm_load_ps(&MAT[0][0]) * weight); 223 | pFactor4_7 = _mm_add_ps(pFactor4_7, _mm_load_ps(&MAT[0][4]) * weight); 224 | weight = _mm_mul_ps(weight, XMM_fPart); 225 | pFactor0_3 = _mm_add_ps(pFactor0_3, _mm_load_ps(&MAT[1][0]) * weight); 226 | pFactor4_7 = _mm_add_ps(pFactor4_7, _mm_load_ps(&MAT[1][4]) * weight); 227 | weight = _mm_mul_ps(weight, XMM_fPart); 228 | pFactor0_3 = _mm_add_ps(pFactor0_3, _mm_load_ps(&MAT[2][0]) * weight); 229 | pFactor4_7 = _mm_add_ps(pFactor4_7, _mm_load_ps(&MAT[2][4]) * weight); 230 | weight = _mm_mul_ps(weight, XMM_fPart); 231 | pFactor0_3 = _mm_add_ps(pFactor0_3, _mm_load_ps(&MAT[3][0]) * weight); 232 | pFactor4_7 = _mm_add_ps(pFactor4_7, _mm_load_ps(&MAT[3][4]) * weight); 233 | weight = _mm_mul_ps(weight, XMM_fPart); 234 | pFactor0_3 = _mm_add_ps(pFactor0_3, _mm_load_ps(&MAT[4][0]) * weight); 235 | pFactor4_7 = _mm_add_ps(pFactor4_7, _mm_load_ps(&MAT[4][4]) * weight); 236 | 237 | float[8] pfactor = void; 238 | _mm_storeu_ps(&pfactor[0], pFactor0_3); 239 | _mm_storeu_ps(&pfactor[4], pFactor4_7); 240 | 241 | T result = 0; 242 | const(T)* pData = readPointer(); 243 | foreach(n; 0..6) 244 | result += pData[iPart-2 + n] * pfactor[n]; 245 | return result; 246 | }; 247 | } 248 | 249 | private: 250 | T[] _data; 251 | int _index; 252 | int _half; // half the size of the data 253 | int _indexMask; 254 | int _numSamples; 255 | 256 | void decomposeFractionalDelay(float delay, 257 | out int outIntegerPart, 258 | out float outFloatPart) pure const 259 | { 260 | // Because float index can yield suprising low precision with interpolation 261 | // So we up the precision to double in order to have a precise fractional part 262 | int offset = cast(int)(_data.length); 263 | double doubleDelayMinus = cast(double)(-delay); 264 | int iPart = cast(int)(doubleDelayMinus + offset); 265 | iPart -= offset; 266 | float fPart = cast(float)(doubleDelayMinus - iPart); 267 | assert(fPart >= 0.0f); 268 | assert(fPart <= 1.0f); 269 | outIntegerPart = iPart; 270 | outFloatPart = fPart; 271 | } 272 | } 273 | 274 | unittest 275 | { 276 | Delayline!float line; 277 | line.initialize(0); // should be possible 278 | assert(line.nextSample(1) == 1); 279 | 280 | Delayline!double line2; 281 | 282 | Delayline!float line3; 283 | line3.initialize(2); 284 | assert(line3.nextSample(1) == 0); 285 | assert(line3.nextSample(2) == 0); 286 | assert(line3.nextSample(3) == 1); 287 | assert(line3.nextSample(42) == 2); 288 | 289 | assert(line3.sampleFull(0) == 42); 290 | assert(line3.sampleFull(1) == 3); 291 | assert(line3.sampleLinear(0.5f) == (3.0f + 42.0f) * 0.5f); 292 | } 293 | 294 | -------------------------------------------------------------------------------- /source/gamemixer/effects.d: -------------------------------------------------------------------------------- 1 | /** 2 | * IAudioEffect API. 3 | * 4 | * Copyright: Copyright Guillaume Piolat 2021. 5 | * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module gamemixer.effects; 8 | 9 | import dplug.core; 10 | import dplug.audio; 11 | 12 | nothrow: 13 | @nogc: 14 | 15 | /// Inherit from `IEffect` to make a custom effect. 16 | class IAudioEffect 17 | { 18 | nothrow: 19 | @nogc: 20 | /// Called before the effect is used in playback. 21 | /// Initialize state here. 22 | abstract void prepareToPlay(float sampleRate, int maxFrames, int numChannels); 23 | 24 | /// Actual effect processing. 25 | abstract void processAudio(ref AudioBuffer!float inoutBuffer, EffectCallbackInfo info); 26 | 27 | /// Get all parameters in this effect. 28 | IParameter[] getParameters() 29 | { 30 | // default: no parameters. 31 | return []; 32 | } 33 | 34 | /// Get number of parameters. 35 | final int numParameters(int index) 36 | { 37 | return cast(int) getParameters().length; 38 | } 39 | 40 | /// Get a parameter by index. 41 | final parameter(int index) 42 | { 43 | return getParameters()[index]; 44 | } 45 | } 46 | 47 | /// 48 | alias EffectCallbackFunction = void function(ref AudioBuffer!float inoutBuffer, EffectCallbackInfo info); 49 | 50 | /// Effect callback info. 51 | struct EffectCallbackInfo 52 | { 53 | float sampleRate; 54 | long timeInFramesSincePlaybackStarted; 55 | void* userData; // only used for EffectCallback, null otherwise 56 | } 57 | 58 | 59 | interface IParameter 60 | { 61 | nothrow: 62 | @nogc: 63 | string getName(); 64 | void setValue(float value); 65 | float getValue(); 66 | } 67 | 68 | package: 69 | 70 | /// You can create custom effect from a function with `EffectCallback`. 71 | /// It's better to create your own IAudioEffect derivative though. 72 | class EffectCallback : IAudioEffect 73 | { 74 | nothrow: 75 | @nogc: 76 | public: 77 | this(EffectCallbackFunction cb, void* userData) 78 | { 79 | _cb = cb; 80 | _userData = userData; 81 | } 82 | 83 | override void prepareToPlay(float sampleRate, int maxFrames, int numChannels) 84 | { 85 | } 86 | 87 | override void processAudio(ref AudioBuffer!float inoutBuffer, EffectCallbackInfo info) 88 | { 89 | info.userData = _userData; 90 | _cb(inoutBuffer, info); 91 | } 92 | 93 | private: 94 | EffectCallbackFunction _cb; 95 | void* _userData; 96 | } 97 | 98 | /// You can create custom effect from a function with `EffectCallback`. 99 | class EffectGain : IAudioEffect 100 | { 101 | nothrow: 102 | @nogc: 103 | public: 104 | this() 105 | { 106 | _params[0] = createLinearFloatParameter("Gain", 0.0f, 1.0f, 1.0f); 107 | } 108 | 109 | override void prepareToPlay(float sampleRate, int maxFrames, int numChannels) 110 | { 111 | _currentGain = 0.0; 112 | _expFactor = expDecayFactor(0.015, sampleRate); 113 | } 114 | 115 | override void processAudio(ref AudioBuffer!float inoutBuffer, EffectCallbackInfo info) 116 | { 117 | int numChans = inoutBuffer.channels(); 118 | int frames = inoutBuffer.frames(); 119 | 120 | float targetLevel = _params[0].getValue(); 121 | for (int n = 0; n < frames; ++n) 122 | { 123 | _currentGain += (targetLevel - _currentGain) * _expFactor; 124 | for (int chan = 0; chan < numChans; ++chan) 125 | { 126 | inoutBuffer[chan][n] *= _currentGain; 127 | } 128 | } 129 | } 130 | 131 | override IParameter[] getParameters() 132 | { 133 | return _params[]; 134 | } 135 | 136 | private: 137 | EffectCallbackFunction _cb; 138 | double _expFactor; 139 | double _currentGain; 140 | IParameter[1] _params; 141 | } 142 | 143 | class LinearFloatParameter : IParameter 144 | { 145 | public: 146 | nothrow: 147 | @nogc: 148 | this(string name, float min, float max, float defaultValue) 149 | { 150 | _name = name; 151 | _min = min; 152 | _max = max; 153 | _value = defaultValue; 154 | } 155 | 156 | override string getName() 157 | { 158 | return _name; 159 | } 160 | 161 | override void setValue(float value) 162 | { 163 | if (value < _min) value = _min; 164 | if (value > _max) value = _max; 165 | _value = value; 166 | } 167 | 168 | override float getValue() 169 | { 170 | return _value; 171 | } 172 | 173 | private: 174 | string _name; 175 | float _value; 176 | float _min, _max; 177 | } 178 | 179 | IParameter createLinearFloatParameter(string name, float min, float max, float defaultValue) 180 | { 181 | return mallocNew!LinearFloatParameter(name, min, max, defaultValue); 182 | } -------------------------------------------------------------------------------- /source/gamemixer/mixer.d: -------------------------------------------------------------------------------- 1 | /** 2 | * `IMixer` API and definition. This is the API entrypoint. 3 | * 4 | * Copyright: Copyright Guillaume Piolat 2021. 5 | * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module gamemixer.mixer; 8 | 9 | import core.thread; 10 | import core.atomic; 11 | import std.math: SQRT2, PI_4; 12 | 13 | import dplug.core; 14 | import dplug.audio; 15 | 16 | import gamemixer.effects; 17 | import gamemixer.source; 18 | 19 | /// Restrict the library to ONLY use loopback 20 | /// Use "loopback" DUB configuration to enable this. 21 | //version = onlyLoopback; 22 | 23 | version(onlyLoopback) 24 | { 25 | } 26 | else 27 | version = hasSoundIO; 28 | 29 | version(hasSoundIO) 30 | { 31 | import soundio; 32 | } 33 | 34 | nothrow: 35 | @nogc: 36 | 37 | /// Create a `Mixer` and start playback. 38 | IMixer mixerCreate(MixerOptions options = MixerOptions.init) 39 | { 40 | return mallocNew!Mixer(options); 41 | } 42 | 43 | /// Stops `playback`. 44 | void mixerDestroy(IMixer mixer) 45 | { 46 | destroyFree(mixer); 47 | } 48 | 49 | /// Options to create the mixer with. 50 | /// You can customize sample-rate or the number of internal tracks. 51 | /// Always stereo. 52 | struct MixerOptions 53 | { 54 | /// If loopback, the mixer is intended not to produce audio. Instead you should call `generateToLoopback`. 55 | /// If `false` (default), the mixer will directly output audio to the OS. 56 | bool isLoopback = false; 57 | 58 | /// Desired output sample rate. 59 | float sampleRate = 48000.0f; 60 | 61 | /// Number of possible sounds to play simultaneously. 62 | int numChannels = 16; 63 | 64 | /// The fade time it takes for one playing channel to change its volume with `setChannelVolume`. 65 | float channelVolumeSecs = 0.040f; 66 | } 67 | 68 | /// Chooses any mixer channel. 69 | enum anyMixerChannel = -1; 70 | 71 | /// Loop the source forever. 72 | enum uint loopForever = uint.max; 73 | 74 | /// Options when playing a source. 75 | struct PlayOptions 76 | { 77 | /// The channel where to play the source. 78 | /// `anyMixerChannel` for the first free unreserved channel. 79 | int channel = anyMixerChannel; 80 | 81 | /// The volume to play the source with. This one volume cannot change during playback. 82 | /// This is multiplied by the channel-specific volume and the master volume, which can change. 83 | float volume = 1.0f; 84 | 85 | /// The angle pan to play the source with. 86 | /// -1 = full left 87 | /// 1 = full right 88 | float pan = 0.0f; 89 | 90 | /// The delay in seconds before which to play. 91 | /// The time reference is the time given by `playbackTimeInSeconds()`. 92 | /// The source starts playing when `playbackTimeInSeconds` has increased by `delayBeforePlay`. 93 | /// Note that it still occupies the channel. 94 | /// Warning: can't use both `delayBeforePlay` and `startTimeSecs` at the same time. 95 | float delayBeforePlay = 0.0f; 96 | 97 | /// Play the sound immediately, starting at a given time in the sample (in mixer time). 98 | /// Warning: can't use both `delayBeforePlay` and `startTimeSecs` at the same time. 99 | float startTimeSecs = 0.0f; 100 | 101 | /// Number of times the source is looped. 102 | uint loopCount = 1; 103 | 104 | /// The time it takes to start the sound if the channel is already busy. 105 | /// If the channel isn't busy, `faceInSecs` is used. 106 | /// Default: 14ms transition. 107 | float crossFadeInSecs = 0.000f; // Default was tuned on drum machine example 108 | 109 | /// The time it takes to halt the existing sound if the channel is already busy. 110 | /// If the channel isn't busy, there is nothing to halt. 111 | /// Default: 40ms transition out. 112 | float crossFadeOutSecs = 0.040f; // Default was tuned on drum machine example. 113 | 114 | /// Fade in time when the channel is free. This can be used to "dull" percussive samples and 115 | /// give them an attack time. 116 | /// Default: no fade in for maximum punch. 117 | float fadeInSecs = 0.0f; 118 | } 119 | 120 | /// Public API for the `Mixer` object. 121 | interface IMixer 122 | { 123 | nothrow: 124 | @nogc: 125 | 126 | /// Create a source from file or memory. 127 | /// (All sources get destroyed automatically when the IMixer is destroyed). 128 | /// Returns: `null` if loading failed 129 | IAudioSource createSourceFromMemory(const(ubyte[]) inputData); 130 | 131 | ///ditto 132 | IAudioSource createSourceFromFile(const(char[]) path); 133 | 134 | /// Play a source. 135 | /// This locks the audio thread for a short while. 136 | void play(IAudioSource source, PlayOptions options); 137 | void play(IAudioSource source, float volume = 1.0f); 138 | 139 | /// Play several source simulatenously, these will be synchronized down to sample accuracy. 140 | /// This locks the audio thread for a short while. 141 | void playSimultaneously(IAudioSource[] sources, PlayOptions[] options); 142 | 143 | /// Stop sound playing on a given channel. 144 | void stopChannel(int channel, float fadeOutSecs = 0.040f); 145 | void stopAllChannels(float fadeOutSecs = 0.040f); 146 | 147 | /// Sets the volume of a particular channel (default is 1.0f). 148 | void setChannelVolume(int channel, float volume); 149 | 150 | /// Sets the volume of the master bus (volume should typically be between 0 and 1). 151 | void setMasterVolume(float volume); 152 | 153 | /// Adds an effect on the master channel (all sounds mixed together). 154 | void addMasterEffect(IAudioEffect effect); 155 | 156 | /// Creates an effect with a custom callback processing function. 157 | /// (All effects get destroyed automatically when the IMixer is destroyed). 158 | IAudioEffect createEffectCustom(EffectCallbackFunction callback, void* userData = null); 159 | 160 | /// Creates an effect with a custom callback processing function. 161 | /// (All effects get destroyed automatically when the IMixer is destroyed). 162 | IAudioEffect createEffectGain(); 163 | 164 | /// Returns: Time in seconds since the beginning of playback. 165 | /// This is equal to `getTimeInFrames() / getSampleRate() - latency`. 166 | /// Warning: Because this subtract known latency, this can return a negative value. 167 | /// BUG: latency reported of libsoundio is too high for WASAPI, so we have an incorrect value here. 168 | double playbackTimeInSeconds(); 169 | 170 | /// Returns: Playback sample rate. 171 | /// Once created, this is guaranteed to never change. 172 | float getSampleRate(); 173 | 174 | /// Returns: `true` if a playback error has been detected. 175 | /// Your best bet is to recreate a `Mixer`. 176 | bool isErrored(); 177 | 178 | /// Returns: An error message for the last error. 179 | /// Warning: only call this if `isErrored()` returns `true`. 180 | const(char)[] lastErrorString(); 181 | 182 | /// Manual output instead of a libsoundio-d stream. 183 | /// You can only call this if the mixer is created with the `isLoopback` option. 184 | void loopbackGenerate(float*[2] outBuffers, int frames); 185 | void loopbackMix(float*[2] inoutBuffers, int frames); ///ditto 186 | } 187 | 188 | package: 189 | 190 | 191 | /// Package API for the `Mixer` object. 192 | interface IMixerInternal 193 | { 194 | nothrow: 195 | @nogc: 196 | float getSampleRate(); 197 | } 198 | 199 | 200 | /// Implementation of `IMixer`. 201 | private final class Mixer : IMixer, IMixerInternal 202 | { 203 | nothrow: 204 | @nogc: 205 | public: 206 | this(MixerOptions options) 207 | { 208 | _isLoopback = options.isLoopback; 209 | 210 | version(onlyLoopback) 211 | { 212 | // If you fail here, you've used a "only loopback" configuration, and then tried to obtain a IMixer with sound I/O. 213 | assert(_isLoopback); 214 | } 215 | 216 | _channelVolumeSecs = options.channelVolumeSecs; 217 | 218 | _channels.resize(options.numChannels); 219 | for (int n = 0; n < options.numChannels; ++n) 220 | _channels[n] = mallocNew!ChannelStatus(n); 221 | 222 | int err = 0; 223 | 224 | version(hasSoundIO) 225 | { 226 | if (!isLoopback) 227 | { 228 | _soundio = soundio_create(); 229 | assert(_soundio !is null); 230 | 231 | err = soundio_connect(_soundio); 232 | if (err != 0) 233 | { 234 | setErrored("Out of memory"); 235 | _lastError = "Out of memory"; 236 | return; 237 | } 238 | 239 | soundio_flush_events(_soundio); 240 | 241 | int default_out_device_index = soundio_default_output_device_index(_soundio); 242 | if (default_out_device_index < 0) 243 | { 244 | setErrored("No output device found"); 245 | return; 246 | } 247 | 248 | _device = soundio_get_output_device(_soundio, default_out_device_index); 249 | if (!_device) 250 | { 251 | setErrored("Out of memory"); 252 | return; 253 | } 254 | 255 | if (!soundio_device_supports_format(_device, SoundIoFormatFloat32NE)) 256 | { 257 | setErrored("Must support 32-bit float output"); 258 | return; 259 | } 260 | } 261 | } 262 | 263 | _masterEffectsMutex = makeMutex(); 264 | _channelsMutex = makeMutex(); 265 | 266 | version(hasSoundIO) 267 | { 268 | if (!isLoopback) 269 | { 270 | _outstream = soundio_outstream_create(_device); 271 | _outstream.format = SoundIoFormatFloat32NE; // little endian floats 272 | _outstream.write_callback = &mixerWriteCallback; 273 | _outstream.userdata = cast(void*)this; 274 | _outstream.sample_rate = cast(int) options.sampleRate; 275 | _outstream.software_latency = 0.010; // 10ms 276 | 277 | err = soundio_outstream_open(_outstream); 278 | 279 | if (err != 0) 280 | { 281 | setErrored("Unable to open device"); 282 | return; 283 | } 284 | 285 | if (_outstream.layout_error) 286 | { 287 | setErrored("Unable to set channel layout"); 288 | return; 289 | } 290 | } 291 | } 292 | 293 | _framesElapsed = 0; 294 | _timeSincePlaybackBegan = 0; 295 | 296 | version(hasSoundIO) 297 | { 298 | if (isLoopback) 299 | _sampleRate = options.sampleRate; 300 | else 301 | _sampleRate = _outstream.sample_rate; 302 | } 303 | else 304 | { 305 | _sampleRate = options.sampleRate; 306 | } 307 | 308 | // TODO: do something better in WASAPI 309 | // do something better when latency reporting works 310 | if (isLoopback) 311 | _softwareLatency = 0; 312 | else 313 | _softwareLatency = (maxInternalBuffering / _sampleRate); 314 | 315 | // The very last effect of the master chain is a global gain. 316 | _masterGainPostFx = createEffectGain(); 317 | _masterGainPostFxContext.initialized = false; 318 | 319 | version(hasSoundIO) 320 | { 321 | if (!isLoopback) 322 | { 323 | err = soundio_outstream_start(_outstream); 324 | if (err != 0) 325 | { 326 | setErrored("Unable to start device"); 327 | return; 328 | } 329 | 330 | // start event thread 331 | _eventThread = makeThread(&waitEvents); 332 | _eventThread.start(); 333 | } 334 | } 335 | } 336 | 337 | ~this() 338 | { 339 | setMasterVolume(0); 340 | 341 | if (!isLoopback) 342 | { 343 | core.thread.Thread.sleep( dur!("msecs")( 200 ) ); 344 | } 345 | 346 | cleanUp(); 347 | } 348 | 349 | /// Returns: Time in seconds since the beginning of playback. 350 | /// This is equal to `getTimeInFrames() / getSampleRate() - softwareLatency()`. 351 | /// Warning: This is returned with some amount of latency. 352 | override double playbackTimeInSeconds() 353 | { 354 | double sr = getSampleRate(); 355 | long t = playbackTimeInFrames(); 356 | return t / sr - _softwareLatency; 357 | } 358 | 359 | /// Returns: Playback sample rate. 360 | override float getSampleRate() 361 | { 362 | return _sampleRate; 363 | } 364 | 365 | override bool isErrored() 366 | { 367 | return _errored; 368 | } 369 | 370 | override const(char)[] lastErrorString() 371 | { 372 | assert(isErrored); 373 | return _lastError; 374 | } 375 | 376 | override void addMasterEffect(IAudioEffect effect) 377 | { 378 | _masterEffectsMutex.lock(); 379 | _masterEffects.pushBack(effect); 380 | _masterEffectsContexts.pushBack(EffectContext(false)); 381 | _masterEffectsMutex.unlock(); 382 | } 383 | 384 | override IAudioEffect createEffectCustom(EffectCallbackFunction callback, void* userData) 385 | { 386 | IAudioEffect fx = mallocNew!EffectCallback(callback, userData); 387 | _allCreatedEffects.pushBack(fx); 388 | return fx; 389 | } 390 | 391 | override IAudioEffect createEffectGain() 392 | { 393 | IAudioEffect fx = mallocNew!EffectGain(); 394 | _allCreatedEffects.pushBack(fx); 395 | return fx; 396 | } 397 | 398 | override IAudioSource createSourceFromMemory(const(ubyte[]) inputData) 399 | { 400 | try 401 | { 402 | IAudioSource s = mallocNew!AudioSource(this, inputData); 403 | _allCreatedSource.pushBack(s); 404 | return s; 405 | } 406 | catch(Exception e) 407 | { 408 | destroyFree(e); // TODO maybe leaks 409 | return null; 410 | } 411 | } 412 | 413 | override IAudioSource createSourceFromFile(const(char[]) path) 414 | { 415 | try 416 | { 417 | IAudioSource s = mallocNew!AudioSource(this, path); 418 | _allCreatedSource.pushBack(s); 419 | return s; 420 | } 421 | catch(Exception e) 422 | { 423 | destroyFree(e); // TODO maybe leaks 424 | return null; 425 | } 426 | } 427 | 428 | override void setMasterVolume(float volume) 429 | { 430 | _masterGainPostFx.parameter(0).setValue(volume); 431 | } 432 | 433 | override void setChannelVolume(int channel, float volume) 434 | { 435 | _channels[channel].setVolume(volume); 436 | } 437 | 438 | override void play(IAudioSource source, float volume) 439 | { 440 | PlayOptions opt; 441 | opt.volume = volume; 442 | play(source, opt); 443 | } 444 | 445 | override void play(IAudioSource source, PlayOptions options) 446 | { 447 | _channelsMutex.lock(); 448 | _channelsMutex.unlock(); 449 | playInternal(source, options); 450 | } 451 | 452 | override void playSimultaneously(IAudioSource[] sources, PlayOptions[] options) 453 | { 454 | _channelsMutex.lock(); 455 | _channelsMutex.unlock(); 456 | assert(sources.length == options.length); 457 | for (int n = 0; n < sources.length; ++n) 458 | playInternal(sources[n], options[n]); 459 | } 460 | 461 | override void stopChannel(int channel, float fadeOutSecs) 462 | { 463 | _channels[channel].stop(fadeOutSecs); 464 | } 465 | 466 | override void stopAllChannels(float fadeOutSecs) 467 | { 468 | for (int chan = 0; chan < _channels.length; ++chan) 469 | _channels[chan].stop(fadeOutSecs); 470 | } 471 | 472 | long playbackTimeInFrames() 473 | { 474 | return atomicLoad(_timeSincePlaybackBegan); 475 | } 476 | 477 | override void loopbackGenerate(float*[2] outBuffers, int frames) 478 | { 479 | // Can't use loopbackGenerate if the IMixer is not created solely for loopback output. 480 | assert(isLoopback); 481 | 482 | const(float*[2]) mixedBuffers = loopbackCallback(frames); 483 | outBuffers[0][0..frames] = mixedBuffers[0][0..frames]; 484 | outBuffers[1][0..frames] = mixedBuffers[1][0..frames]; 485 | } 486 | 487 | override void loopbackMix(float*[2] inoutBuffers, int frames) 488 | { 489 | // Can't use loopbackMix if the IMixer is not created solely for loopback output. 490 | assert(isLoopback); 491 | 492 | const(float*[2]) mixedBuffers = loopbackCallback(frames); 493 | inoutBuffers[0][0..frames] += mixedBuffers[0][0..frames]; 494 | inoutBuffers[1][0..frames] += mixedBuffers[1][0..frames]; 495 | } 496 | 497 | bool isLoopback() 498 | { 499 | return _isLoopback; 500 | } 501 | 502 | private: 503 | bool _isLoopback; 504 | 505 | version(hasSoundIO) 506 | { 507 | SoundIo* _soundio; // null if loopback 508 | SoundIoDevice* _device; // null if loopback 509 | SoundIoOutStream* _outstream; // null if loopback 510 | dplug.core.thread.Thread _eventThread; 511 | } 512 | 513 | long _framesElapsed; 514 | shared(long) _timeSincePlaybackBegan; 515 | float _sampleRate; 516 | double _softwareLatency; 517 | float _channelVolumeSecs; 518 | 519 | static struct EffectContext 520 | { 521 | bool initialized; 522 | } 523 | Vec!EffectContext _masterEffectsContexts; // sync by _masterEffectsMutex 524 | Vec!IAudioEffect _masterEffects; 525 | UncheckedMutex _masterEffectsMutex; 526 | 527 | Vec!IAudioEffect _allCreatedEffects; 528 | Vec!IAudioSource _allCreatedSource; 529 | 530 | IAudioEffect _masterGainPostFx; 531 | EffectContext _masterGainPostFxContext; 532 | 533 | bool _errored; 534 | const(char)[] _lastError; 535 | 536 | AudioBuffer!float _sumBuf; 537 | 538 | shared(bool) _shouldReadEvents = true; 539 | 540 | 541 | Vec!ChannelStatus _channels; 542 | UncheckedMutex _channelsMutex; 543 | 544 | int findFreeChannel() 545 | { 546 | for (int c = 0; c < _channels.length; ++c) 547 | if (_channels[c].isAvailable()) 548 | return c; 549 | return -1; 550 | } 551 | 552 | version(hasSoundIO) 553 | { 554 | void waitEvents() 555 | { 556 | assert(!_isLoopback); // no event thread in loopback mode 557 | 558 | // This function calls ::soundio_flush_events then blocks until another event is ready 559 | // or you call ::soundio_wakeup. Be ready for spurious wakeups. 560 | while (true) 561 | { 562 | bool shouldReadEvents = atomicLoad(_shouldReadEvents); 563 | if (!shouldReadEvents) 564 | break; 565 | soundio_wait_events(_soundio); 566 | } 567 | } 568 | } 569 | 570 | void setErrored(const(char)[] msg) 571 | { 572 | _errored = true; 573 | _lastError = msg; 574 | } 575 | 576 | void playInternal(IAudioSource source, PlayOptions options) 577 | { 578 | int chan = options.channel; 579 | if (chan == -1) 580 | chan = findFreeChannel(); 581 | if (chan == -1) 582 | return; // no free channel 583 | 584 | if (chan >= _channels.length) 585 | { 586 | assert(false); // specified non-existing channel index 587 | } 588 | 589 | float pan = options.pan; 590 | if (pan < -1) pan = -1; 591 | if (pan > 1) pan = 1; 592 | 593 | float volumeL = options.volume * fast_cos((pan + 1) * PI_4) * SQRT2; 594 | float volumeR = options.volume * fast_sin((pan + 1) * PI_4) * SQRT2; 595 | 596 | int delayBeforePlayFrames = cast(int)(0.5 + options.delayBeforePlay * _sampleRate); 597 | int frameOffset = -delayBeforePlayFrames; 598 | 599 | int startTimeFrames = cast(int)(0.5 + options.startTimeSecs * _sampleRate); 600 | if (startTimeFrames != 0) 601 | frameOffset = startTimeFrames; 602 | 603 | // API wrong usage, can't use both delayBeforePlayFrames and startTimeSecs. 604 | assert ((startTimeFrames == 0) || (delayBeforePlayFrames == 0)); 605 | 606 | double crossFadeInSecs = options.crossFadeInSecs; 607 | double crossFadeOutSecs = options.crossFadeOutSecs; 608 | double fadeInSecs = options.fadeInSecs; 609 | _channels[chan].startPlaying(source, volumeL, volumeR, frameOffset, options.loopCount, 610 | crossFadeInSecs, crossFadeOutSecs, fadeInSecs); 611 | 612 | IAudioSourceInternal isource = cast(IAudioSourceInternal) source; 613 | assert(isource); 614 | isource.prepareToPlay(); 615 | } 616 | 617 | 618 | void cleanUp() 619 | { 620 | // remove effects 621 | _masterEffectsMutex.lock(); 622 | _masterEffects.clearContents(); 623 | _masterEffectsMutex.unlock(); 624 | 625 | version(hasSoundIO) 626 | { 627 | if (_outstream !is null) 628 | { 629 | assert(!_isLoopback); 630 | soundio_outstream_destroy(_outstream); 631 | _outstream = null; 632 | } 633 | 634 | if (_eventThread.getThreadID() !is null) 635 | { 636 | atomicStore(_shouldReadEvents, false); 637 | soundio_wakeup(_soundio); 638 | _eventThread.join(); 639 | destroyNoGC(_eventThread); 640 | } 641 | 642 | if (_device !is null) 643 | { 644 | soundio_device_unref(_device); 645 | _device = null; 646 | } 647 | 648 | if (_soundio !is null) 649 | { 650 | soundio_destroy(_soundio); 651 | _soundio = null; 652 | } 653 | } 654 | 655 | // Destroy all effects 656 | foreach(fx; _allCreatedEffects) 657 | { 658 | destroyFree(fx); 659 | } 660 | _allCreatedEffects.clearContents(); 661 | 662 | for (int c = 0; c < _channels.length; ++c) 663 | _channels[c].destroyFree(); 664 | } 665 | 666 | const(float*[2]) loopbackCallback(int frames) 667 | { 668 | // Extend storage if need be. 669 | if (frames > _sumBuf.frames()) 670 | { 671 | _sumBuf.resize(2, frames); 672 | } 673 | 674 | // Take the fisrt `frames` frames as current buf. 675 | AudioBuffer!float masterBuf = _sumBuf.sliceFrames(0, frames); 676 | 677 | // 1. Mix sources in stereo. 678 | masterBuf.fillWithZeroes(); 679 | 680 | float*[2] inoutBuffers; 681 | inoutBuffers[0] = masterBuf.getChannelPointer(0); 682 | inoutBuffers[1] = masterBuf.getChannelPointer(1); 683 | 684 | _channelsMutex.lock(); // to protect from "play" 685 | for (int n = 0; n < _channels.length; ++n) 686 | { 687 | ChannelStatus* cs = &_channels[n]; 688 | cs.produceSound(inoutBuffers, masterBuf.frames(), _sampleRate, this); 689 | } 690 | _channelsMutex.unlock(); 691 | 692 | // 2. Apply master effects 693 | _masterEffectsMutex.lock(); 694 | int numMasterEffects = cast(int) _masterEffects.length; 695 | for (int numFx = 0; numFx < numMasterEffects; ++numFx) 696 | { 697 | applyEffect(masterBuf, _masterEffectsContexts[numFx], _masterEffects[numFx], frames); 698 | } 699 | _masterEffectsMutex.unlock(); 700 | 701 | // Apply post gain effect 702 | applyEffect(masterBuf, _masterGainPostFxContext, _masterGainPostFx, frames); 703 | 704 | _framesElapsed += frames; 705 | 706 | atomicStore(_timeSincePlaybackBegan, _framesElapsed); 707 | 708 | return inoutBuffers; 709 | } 710 | 711 | version(hasSoundIO) 712 | { 713 | void writeCallback(SoundIoOutStream* stream, int frames) 714 | { 715 | assert(stream.sample_rate == _sampleRate); 716 | 717 | SoundIoChannelArea* areas; 718 | 719 | // 1. Generate next `frames` stereo frames. 720 | const(float*[2]) mixedBuffers = loopbackCallback(frames); 721 | 722 | 723 | // 2. Pass the audio to libsoundio 724 | 725 | int frames_left = frames; 726 | 727 | for (;;) 728 | { 729 | int frame_count = frames_left; 730 | if (auto err = soundio_outstream_begin_write(_outstream, &areas, &frame_count)) 731 | { 732 | assert(false, "unrecoverable stream error"); 733 | } 734 | 735 | if (!frame_count) 736 | break; 737 | 738 | const(SoundIoChannelLayout)* layout = &stream.layout; 739 | 740 | for (int frame = 0; frame < frame_count; frame += 1) 741 | { 742 | for (int channel = 0; channel < layout.channel_count; channel += 1) 743 | { 744 | float sample = _sumBuf[channel][frame]; 745 | write_sample_float32ne(areas[channel].ptr, sample); 746 | areas[channel].ptr += areas[channel].step; 747 | } 748 | } 749 | 750 | if (auto err = soundio_outstream_end_write(stream)) 751 | { 752 | if (err == SoundIoError.Underflow) 753 | return; 754 | 755 | setErrored("Unrecoverable stream error"); 756 | return; 757 | } 758 | 759 | frames_left -= frame_count; 760 | if (frames_left <= 0) 761 | break; 762 | } 763 | } 764 | } 765 | 766 | void applyEffect(ref AudioBuffer!float inoutBuf, ref EffectContext ec, IAudioEffect effect, int frames) 767 | { 768 | enum int MAX_FRAMES_FOR_EFFECTS = 512; // TODO: should disappear in favor of maxInternalBuffering 769 | 770 | if (!ec.initialized) 771 | { 772 | effect.prepareToPlay(_sampleRate, MAX_FRAMES_FOR_EFFECTS, 2); 773 | ec.initialized = true; 774 | } 775 | 776 | EffectCallbackInfo info; 777 | info.sampleRate = _sampleRate; 778 | info.userData = null; 779 | 780 | // Buffer-splitting! It is used so that effects can be given a maximum buffer size at init point. 781 | 782 | int framesDone = 0; 783 | foreach( block; inoutBuf.chunkBy(MAX_FRAMES_FOR_EFFECTS)) 784 | { 785 | info.timeInFramesSincePlaybackStarted = _framesElapsed + framesDone; 786 | effect.processAudio(block, info); // apply effect 787 | framesDone += block.frames(); 788 | assert(framesDone <= inoutBuf.frames()); 789 | } 790 | } 791 | } 792 | 793 | private: 794 | 795 | enum int maxInternalBuffering = 1024; // Allows to lower latency with WASAPI 796 | 797 | version(hasSoundIO) 798 | { 799 | extern(C) void mixerWriteCallback(SoundIoOutStream* stream, int frame_count_min, int frame_count_max) 800 | { 801 | Mixer mixer = cast(Mixer)(stream.userdata); 802 | 803 | // Note: WASAPI can have 4 seconds buffers, so we return as frames as following: 804 | // - the highest nearest valid frame count in [frame_count_min .. frame_count_max] that is below 1024. 805 | 806 | int frames = maxInternalBuffering; 807 | if (frames < frame_count_min) frames = frame_count_min; 808 | if (frames > frame_count_max) frames = frame_count_max; 809 | 810 | mixer.writeCallback(stream, frames); 811 | } 812 | 813 | static void write_sample_s16ne(char* ptr, double sample) { 814 | short* buf = cast(short*)ptr; 815 | double range = cast(double)short.max - cast(double)short.min; 816 | double val = sample * range / 2.0; 817 | *buf = cast(short) val; 818 | } 819 | 820 | static void write_sample_s32ne(char* ptr, double sample) { 821 | int* buf = cast(int*)ptr; 822 | double range = cast(double)int.max - cast(double)int.min; 823 | double val = sample * range / 2.0; 824 | *buf = cast(int) val; 825 | } 826 | 827 | static void write_sample_float32ne(char* ptr, double sample) { 828 | float* buf = cast(float*)ptr; 829 | *buf = sample; 830 | } 831 | 832 | static void write_sample_float64ne(char* ptr, double sample) { 833 | double* buf = cast(double*)ptr; 834 | *buf = sample; 835 | } 836 | } 837 | 838 | // A channel can be in one of four states: 839 | enum ChannelState 840 | { 841 | idle, 842 | fadingIn, 843 | normalPlay, 844 | fadingOut 845 | } 846 | 847 | /// Internal status of single channel. 848 | /// In reality, a channel support multiple sounds playing at once, in order to support cross-fades. 849 | final class ChannelStatus 850 | { 851 | nothrow: 852 | @nogc: 853 | public: 854 | 855 | this(int channelIndex) 856 | { 857 | } 858 | 859 | /// Returns: true if no sound is playing or scheduled to play on this channel 860 | bool isAvailable() 861 | { 862 | for (int nsound = 0; nsound < MAX_SOUND_PER_CHANNEL; ++nsound) 863 | { 864 | if (_sounds[nsound].isPlayingOrPending()) 865 | return false; 866 | } 867 | return true; 868 | } 869 | 870 | ~this() 871 | { 872 | _volumeRamp.reallocBuffer(0); 873 | } 874 | 875 | // Change the currently playing source in this channel. 876 | void startPlaying(IAudioSource source, 877 | float volumeL, 878 | float volumeR, 879 | int frameOffset, 880 | uint loopCount, 881 | float crossFadeInSecs, 882 | float crossFadeOutSecs, 883 | float fadeInSecs) 884 | { 885 | // shift sound to keep most recently played 886 | for (int n = MAX_SOUND_PER_CHANNEL - 1; n > 0; --n) 887 | { 888 | _sounds[n] = _sounds[n-1]; 889 | } 890 | 891 | VolumeState _state; 892 | float _currentFadeVolume = 1.0f; 893 | float _fadeInDuration = 0.0f; 894 | float _fadeOutDuration = 0.0f; 895 | 896 | 897 | // Note: _sounds[0] is here to replace _sounds[1]. _sounds[2] and later, if playing, were already fadeouting. 898 | 899 | with (_sounds[0]) 900 | { 901 | _sourcePlaying = source; 902 | _volume[0] = volumeL; 903 | _volume[1] = volumeR; 904 | _frameOffset = frameOffset; 905 | _loopCount = loopCount; 906 | 907 | if (_sounds[1].isPlaying()) 908 | { 909 | // There is another sound already playing, AND it has started 910 | _sounds[1].stopPlayingFadeOut(crossFadeOutSecs); 911 | startFadeIn(crossFadeInSecs); 912 | } 913 | else if (_sounds[1].isPlayingOrPending()) 914 | { 915 | startFadeIn(fadeInSecs); 916 | _sounds[1].stopPlayingImmediately(); 917 | } 918 | else 919 | { 920 | startFadeIn(fadeInSecs); 921 | } 922 | } 923 | } 924 | 925 | void stop(float fadeOutSecs) 926 | { 927 | for (int n = 0; n < MAX_SOUND_PER_CHANNEL; ++n) 928 | { 929 | _sounds[n].stopPlayingFadeOut(fadeOutSecs); 930 | } 931 | } 932 | 933 | void produceSound(float*[2] inoutBuffers, int frames, float sampleRate, Mixer mixer) 934 | { 935 | // Compute channel volume ramp (if any) 936 | bool hasChannelVolumeRamp = (_channelVolume != 1.0f) || (_currentChannelVolume != 1.0f); 937 | bool hasConstantChannelVolume = false; 938 | if (hasChannelVolumeRamp) 939 | { 940 | if (_chanVolumeRamp.length < frames) 941 | _chanVolumeRamp.reallocBuffer(frames); 942 | 943 | float v = _currentChannelVolume; // RACE: technically, should be a raw atomic 944 | float target = _channelVolume; 945 | 946 | float diff = target - v; 947 | 948 | // do we need to recompute the ramp? Or stable. 949 | if (v != target) 950 | { 951 | float channelFaderSecs = mixer._channelVolumeSecs; // time for volume change for the channel 952 | float chanIncrement = 1.0 / (sampleRate * channelFaderSecs); 953 | 954 | for (int n = 0; n < frames; ++n) 955 | { 956 | if (diff > 0) 957 | { 958 | v += chanIncrement; 959 | if (v > target) 960 | v = target; 961 | } 962 | else 963 | { 964 | v -= chanIncrement; 965 | if (v < target) 966 | v = target; 967 | } 968 | _chanVolumeRamp[n] = v; 969 | } 970 | _currentChannelVolume = v; 971 | } 972 | else 973 | { 974 | hasConstantChannelVolume = true; // instead, just multiply by constant volume 975 | } 976 | } 977 | 978 | for (int nsound = 0; nsound < MAX_SOUND_PER_CHANNEL; ++nsound) 979 | { 980 | SoundPlaying* sp = &_sounds[nsound]; 981 | if (sp._loopCount != 0) 982 | { 983 | // deals with negative frameOffset 984 | if (sp._frameOffset + frames <= 0) 985 | { 986 | sp._frameOffset += frames; 987 | } 988 | else 989 | { 990 | if (sp._frameOffset < 0) 991 | { 992 | // Adjust to only a smaller subpart of the beginning of the source. 993 | int skip = -sp._frameOffset; 994 | frames -= skip; 995 | sp._frameOffset = 0; 996 | for (int chan = 0; chan < 2; ++chan) 997 | inoutBuffers[chan] += skip; 998 | } 999 | 1000 | if (_volumeRamp.length < frames) 1001 | _volumeRamp.reallocBuffer(frames); 1002 | 1003 | bool fadeOutFinished = false; 1004 | 1005 | final switch(sp._state) with (VolumeState) 1006 | { 1007 | case VolumeState.fadeIn: 1008 | float fadeInIncrement = 1.0 / (sampleRate * sp._fadeInDuration); 1009 | for (int n = 0; n < frames; ++n) 1010 | { 1011 | _volumeRamp[n] = sp._currentFadeVolume; 1012 | sp._currentFadeVolume += fadeInIncrement; 1013 | if (sp._currentFadeVolume > 1.0f) 1014 | { 1015 | sp._currentFadeVolume = 1.0f; 1016 | sp._state = VolumeState.constant; 1017 | } 1018 | } 1019 | break; 1020 | 1021 | case VolumeState.fadeOut: 1022 | float fadeOutIncrement = 1.0 / (sampleRate * sp._fadeOutDuration); 1023 | for (int n = 0; n < frames; ++n) 1024 | { 1025 | _volumeRamp[n] = sp._currentFadeVolume; 1026 | sp._currentFadeVolume -= fadeOutIncrement; 1027 | if (sp._currentFadeVolume < 0.0f) 1028 | { 1029 | fadeOutFinished = true; 1030 | sp._currentFadeVolume = 0.0f; 1031 | } 1032 | } 1033 | break; 1034 | 1035 | case VolumeState.constant: 1036 | _volumeRamp[0..frames] = 1.0f; 1037 | } 1038 | 1039 | assert(sp._frameOffset >= 0); 1040 | 1041 | // Apply channel volume ramp, if any 1042 | if (hasConstantChannelVolume) 1043 | { 1044 | // PERF: this could be done with the constant term instead 1045 | _volumeRamp[0..frames] *= _currentChannelVolume; 1046 | } 1047 | else if (hasChannelVolumeRamp) 1048 | { 1049 | _volumeRamp[0..frames] *= _chanVolumeRamp[0..frames]; 1050 | } 1051 | 1052 | // Calling this will modify _frameOffset and _loopCount so as to give the newer play position. 1053 | // When loopCount falls to zero, the source has terminated playing. 1054 | IAudioSourceInternal isource = cast(IAudioSourceInternal)(sp._sourcePlaying); 1055 | assert(isource); 1056 | isource.mixIntoBuffer(inoutBuffers, frames, sp._frameOffset, sp._loopCount, _volumeRamp.ptr, sp._volume); 1057 | 1058 | // End of fadeout, stop playing immediately. 1059 | if (fadeOutFinished) 1060 | sp.stopPlayingImmediately(); 1061 | 1062 | if (sp._loopCount == 0) 1063 | { 1064 | sp._sourcePlaying = null; 1065 | } 1066 | } 1067 | } 1068 | } 1069 | } 1070 | 1071 | void setVolume(float chanVolume) 1072 | { 1073 | _channelVolume = chanVolume; 1074 | } 1075 | 1076 | private: 1077 | // 2 Sounds max since the initial use case was cross-fading music on the same channel. 1078 | enum MAX_SOUND_PER_CHANNEL = 2; 1079 | 1080 | SoundPlaying[MAX_SOUND_PER_CHANNEL] _sounds; // item 0 is the currently playing sound, the other ones are the fading out sounds 1081 | float[] _volumeRamp = null; 1082 | float[] _chanVolumeRamp = null; 1083 | 1084 | float _channelVolume = 1.0f; 1085 | float _currentChannelVolume = 1.0f; 1086 | 1087 | enum VolumeState 1088 | { 1089 | fadeIn, 1090 | fadeOut, 1091 | constant, 1092 | } 1093 | 1094 | static struct SoundPlaying 1095 | { 1096 | nothrow: 1097 | @nogc: 1098 | IAudioSource _sourcePlaying; 1099 | float[2] _volume; 1100 | int _frameOffset; // where in the source we are playing, can be negative (for zeroes) 1101 | uint _loopCount; 1102 | 1103 | VolumeState _state; 1104 | float _currentFadeVolume = 1.0f; 1105 | float _fadeInDuration = 0.0f; 1106 | float _fadeOutDuration = 0.0f; 1107 | 1108 | // true if playing 1109 | bool isPlayingOrPending() 1110 | { 1111 | return _loopCount != 0; 1112 | } 1113 | 1114 | // true if playing, or scheduled to play 1115 | bool isPlaying() 1116 | { 1117 | return isPlayingOrPending() && (_frameOffset >= 0); 1118 | } 1119 | 1120 | void startVolumeStateConstant() 1121 | { 1122 | _state = VolumeState.constant; 1123 | _currentFadeVolume = 1.0f; 1124 | } 1125 | 1126 | void startFadeIn(float duration) 1127 | { 1128 | if (duration == 0) 1129 | startVolumeStateConstant(); 1130 | else 1131 | { 1132 | _state = VolumeState.fadeIn; 1133 | _fadeInDuration = duration; 1134 | _currentFadeVolume = 0.0f; 1135 | } 1136 | } 1137 | 1138 | void stopPlayingFadeOut(float duration) 1139 | { 1140 | if (duration == 0) 1141 | { 1142 | stopPlayingImmediately(); 1143 | } 1144 | else 1145 | { 1146 | _state = VolumeState.fadeOut; 1147 | _fadeOutDuration = duration; 1148 | } 1149 | } 1150 | 1151 | void stopPlayingImmediately() 1152 | { 1153 | _loopCount = 0; 1154 | } 1155 | } 1156 | } 1157 | -------------------------------------------------------------------------------- /source/gamemixer/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Public API. 3 | * 4 | * Copyright: Copyright Guillaume Piolat 2021. 5 | * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module gamemixer; 8 | 9 | public import dplug.audio.audiobuffer; 10 | public import gamemixer.mixer; 11 | public import gamemixer.effects; 12 | public import gamemixer.source; 13 | 14 | -------------------------------------------------------------------------------- /source/gamemixer/resampler.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Audio resampler. 3 | * 4 | * Copyright: Copyright Guillaume Piolat 2021. 5 | */ 6 | module gamemixer.resampler; 7 | 8 | /* simple resampler with variety of algorithms 9 | * based on the code by Christopher Snowhill 10 | * 11 | * Permission to use, copy, modify, and/or distribute this software for any 12 | * purpose with or without fee is hereby granted, provided that the above 13 | * copyright notice and this permission notice appear in all copies. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 16 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 17 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 18 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 19 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 20 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 21 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 22 | */ 23 | // AFAIK this was translated to D by Ketmar 24 | 25 | import core.stdc.string : memcpy; 26 | import core.stdc.math : sin, cos, fmod; 27 | import std.math : PI; 28 | import dplug.core.math : fast_fabs, hermiteInterp; 29 | import gamemixer.chunkedvec; 30 | 31 | nothrow: 32 | @nogc: 33 | 34 | /// Audio resampler. 35 | // MAYDO: this might as well use dplug's Delayline 36 | struct AudioResampler 37 | { 38 | nothrow: 39 | @nogc: 40 | public: 41 | 42 | enum Quality 43 | { 44 | Zoh, 45 | Blep, 46 | Linear, 47 | Blam, 48 | Cubic, 49 | Sinc, 50 | } 51 | 52 | void initialize(double srcRate, double destRate, Quality quality = Quality.Sinc) 53 | { 54 | buildAudioResamplerTables(); // no synchronization for this 55 | this = this.init; 56 | rate(srcRate / destRate); 57 | setQuality(quality); 58 | } 59 | 60 | // Feed input samples, get back as much output samples as possible. 61 | // Note: output `Vec` isn't cleared, samples are pushed back. This can reallocate. 62 | void nextBufferPushMode(float* input, int inputFrames, ref ChunkedVec!float output) 63 | { 64 | int framesPushed = 0; 65 | 66 | while (framesPushed < inputFrames) 67 | { 68 | // feed resampler 69 | while ((framesPushed < inputFrames) && freeCount() > 0) 70 | { 71 | writeSample(input[framesPushed++]); 72 | } 73 | 74 | // get data out of resampler 75 | while (sampleCount() > 0) 76 | { 77 | output.pushBack(sampleFloat()); 78 | removeSample(); 79 | } 80 | } 81 | } 82 | 83 | // Must feed zeroes if no more input. 84 | alias PullModeGetSamplesCallback = void delegate(float* buf, int frames); 85 | 86 | void nextBufferPullMode(scope PullModeGetSamplesCallback getSamples, float* output, int frames) 87 | { 88 | float[BufferSize] pulled; 89 | 90 | int framesPulled = 0; 91 | while (framesPulled < frames) 92 | { 93 | int N = freeCount(); 94 | 95 | getSamples(pulled.ptr, N); 96 | 97 | // feed resampler 98 | for(int n = 0; n < N; ++n) 99 | { 100 | writeSample(pulled[n]); 101 | } 102 | 103 | // get data out of resampler 104 | while (sampleCount() > 0) 105 | { 106 | float s = sampleFloat(); 107 | if (framesPulled < frames) 108 | output[framesPulled++] = s; 109 | removeSample(); 110 | } 111 | } 112 | } 113 | 114 | private: 115 | 116 | int writePos = SincWidth - 1; 117 | int writeFilled = 0; 118 | int readPos = 0; 119 | int readFilled = 0; 120 | float phase = 0; 121 | float phaseInc = 0; 122 | float invPhase = 0; 123 | float invPhaseInc = 0; 124 | Quality xquality = Quality.max; 125 | byte delayAdded = -1; 126 | byte delayRemoved = -1; 127 | float lastAmp = 0; 128 | float accum = 0; 129 | float[BufferSize*2] bufferIn = 130 | [ 0.0f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 133 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; 134 | float[BufferSize+SincWidth*2-1] bufferOut = 135 | [ 0.0f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 137 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; 138 | 139 | 140 | // return number of samples that resampler is able to accept before overflowing 141 | int freeCount () 142 | { 143 | return BufferSize-writeFilled; 144 | } 145 | 146 | // return number of samples that resampler is able to emit now 147 | int sampleCount () 148 | { 149 | if (readFilled < 1 && ((xquality != Quality.Blep && xquality != Quality.Blam) || invPhaseInc)) 150 | doFillAndRemoveDelay(); 151 | return readFilled; 152 | } 153 | 154 | /// Set quality of the resampling (default = sinc). 155 | void setQuality(Quality quality) 156 | { 157 | if (xquality != quality) 158 | { 159 | if (quality == Quality.Blep || quality == Quality.Blep || 160 | quality == Quality.Blam || quality == Quality.Blam) 161 | { 162 | readPos = 0; 163 | readFilled = 0; 164 | lastAmp = 0; 165 | accum = 0; 166 | bufferOut[] = 0; 167 | } 168 | delayAdded = -1; 169 | delayRemoved = -1; 170 | } 171 | xquality = quality; 172 | } 173 | 174 | // Get processed sample out of resampler/ 175 | float sampleFloat() 176 | { 177 | if (readFilled < 1 && phaseInc) 178 | doFillAndRemoveDelay(); 179 | if (readFilled < 1) 180 | return 0; 181 | if (xquality == Quality.Blep || xquality == Quality.Blam) 182 | { 183 | return bufferOut.ptr[readPos] + accum; 184 | } else { 185 | return bufferOut.ptr[readPos]; 186 | } 187 | } 188 | 189 | // you should remove sample after getting it's value 190 | // "normal" resampling should decay accum 191 | void removeSample (bool decay=true) { 192 | if (readFilled > 0) { 193 | if (xquality == Quality.Blep || xquality == Quality.Blam) { 194 | accum += bufferOut.ptr[readPos]; 195 | bufferOut.ptr[readPos] = 0; 196 | if (decay) { 197 | accum -= accum*(1.0f/8192.0f); 198 | if (fast_fabs(accum) < 1e-20f) accum = 0; 199 | } 200 | } 201 | --readFilled; 202 | readPos = (readPos+1)%BufferSize; 203 | } 204 | } 205 | 206 | // is resampler ready to emiting output samples? 207 | // note that buffer can contain unread samples, it is ok 208 | bool ready () 209 | { 210 | return (writeFilled > minFilled()); 211 | } 212 | 213 | // set resampling rate (srate/drate) 214 | void rate (double aRateFactor) 215 | { 216 | phaseInc = aRateFactor; 217 | aRateFactor = 1.0/aRateFactor; 218 | invPhaseInc = aRateFactor; 219 | } 220 | 221 | // feed resampler 222 | void writeSample (float s) 223 | { 224 | if (delayAdded < 0) 225 | { 226 | delayAdded = 0; 227 | writeFilled = inputDelay(); 228 | } 229 | if (writeFilled < BufferSize) 230 | { 231 | float s32 = s; 232 | s32 *= 256.0; 233 | bufferIn.ptr[writePos] = s; 234 | bufferIn.ptr[writePos+BufferSize] = s; 235 | ++writeFilled; 236 | writePos = (writePos+1)%BufferSize; 237 | } 238 | } 239 | 240 | enum paddingSize = SincWidth-1; 241 | 242 | // ////////////////////////////////////////////////////////////////////// // 243 | private: 244 | int minFilled () { 245 | switch (xquality) { 246 | default: 247 | case Quality.Zoh: 248 | case Quality.Blep: 249 | return 1; 250 | case Quality.Linear: 251 | case Quality.Blam: 252 | return 2; 253 | case Quality.Cubic: 254 | return 4; 255 | case Quality.Sinc: 256 | return SincWidth*2; 257 | } 258 | } 259 | 260 | int inputDelay () 261 | { 262 | switch (xquality) { 263 | default: 264 | case Quality.Zoh: 265 | case Quality.Blep: 266 | case Quality.Linear: 267 | case Quality.Blam: 268 | return 0; 269 | case Quality.Cubic: 270 | return 1; 271 | case Quality.Sinc: 272 | return SincWidth-1; 273 | } 274 | } 275 | 276 | int outputDelay () 277 | { 278 | switch (xquality) { 279 | default: 280 | case Quality.Zoh: 281 | case Quality.Linear: 282 | case Quality.Cubic: 283 | case Quality.Sinc: 284 | return 0; 285 | case Quality.Blep: 286 | case Quality.Blam: 287 | return SincWidth-1; 288 | } 289 | } 290 | 291 | int doZoh (float** outbuf, float* outbufend) { 292 | int ccinsize = writeFilled; 293 | const(float)* inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 294 | int used = 0; 295 | ccinsize -= 1; 296 | if (ccinsize > 0) { 297 | float* ccoutbuf = *outbuf; 298 | const(float)* ccinbuf = inbuf; 299 | const(float)* ccinbufend = ccinbuf+ccinsize; 300 | float ccPhase = phase; 301 | float ccPhaseInc = phaseInc; 302 | do { 303 | if (ccoutbuf >= outbufend) break; 304 | float sample = *ccinbuf; 305 | *ccoutbuf++ = sample; 306 | ccPhase += ccPhaseInc; 307 | ccinbuf += cast(int)ccPhase; 308 | assert(ccPhase >= 0); 309 | ccPhase = ccPhase - cast(int)ccPhase; 310 | } while (ccinbuf < ccinbufend); 311 | phase = ccPhase; 312 | *outbuf = ccoutbuf; 313 | used = cast(int)(ccinbuf-inbuf); 314 | writeFilled -= used; 315 | } 316 | return used; 317 | } 318 | 319 | int doBlep (float** outbuf, float* outbufend) { 320 | int ccinsize = writeFilled; 321 | const(float)* inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 322 | int used = 0; 323 | ccinsize -= 1; 324 | if (ccinsize > 0) { 325 | float* ccoutbuf = *outbuf; 326 | const(float)* ccinbuf = inbuf; 327 | const(float)* ccinbufend = ccinbuf+ccinsize; 328 | float ccLastAmp = lastAmp; 329 | float ccInvPhase = invPhase; 330 | float ccInvPhaseInc = invPhaseInc; 331 | enum int step = cast(int)(BlepCutoff*Resolution); 332 | enum int winstep = Resolution; 333 | do { 334 | if (ccoutbuf+SincWidth*2 > outbufend) break; 335 | float sample = (*ccinbuf++)-ccLastAmp; 336 | if (sample) { 337 | float[SincWidth*2] kernel = void; 338 | float kernelSum = 0.0f; 339 | int phaseReduced = cast(int)(ccInvPhase*Resolution); 340 | int phaseAdj = phaseReduced*step/Resolution; 341 | int i = SincWidth; 342 | for (; i >= -SincWidth+1; --i) { 343 | int pos = i*step; 344 | int winpos = i*winstep; 345 | kernelSum += kernel.ptr[i+SincWidth-1] = sincLut[abs(phaseAdj-pos)]*windowLut[abs(phaseReduced-winpos)]; 346 | } 347 | ccLastAmp += sample; 348 | sample /= kernelSum; 349 | for (i = 0; i < SincWidth*2; ++i) ccoutbuf[i] += sample*kernel.ptr[i]; 350 | } 351 | ccInvPhase += ccInvPhaseInc; 352 | ccoutbuf += cast(int)ccInvPhase; 353 | ccInvPhase = fmod(ccInvPhase, 1.0f); 354 | } while (ccinbuf < ccinbufend); 355 | invPhase = ccInvPhase; 356 | lastAmp = ccLastAmp; 357 | *outbuf = ccoutbuf; 358 | used = cast(int)(ccinbuf-inbuf); 359 | writeFilled -= used; 360 | } 361 | return used; 362 | } 363 | 364 | int doLinear (float** outbuf, float* outbufend) { 365 | int ccinsize = writeFilled; 366 | const(float)* inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 367 | int used = 0; 368 | ccinsize -= 2; 369 | if (ccinsize > 0) { 370 | float* ccoutbuf = *outbuf; 371 | const(float)* ccinbuf = inbuf; 372 | const(float)* ccinbufend = ccinbuf+ccinsize; 373 | float ccPhase = phase; 374 | float ccPhaseInc = phaseInc; 375 | do { 376 | if (ccoutbuf >= outbufend) break; 377 | float sample = ccinbuf[0]+(ccinbuf[1]-ccinbuf[0])*ccPhase; 378 | *ccoutbuf++ = sample; 379 | ccPhase += ccPhaseInc; 380 | ccinbuf += cast(int)ccPhase; 381 | assert(ccPhase >= 0); 382 | ccPhase = ccPhase - cast(int)ccPhase; 383 | } while (ccinbuf < ccinbufend); 384 | phase = ccPhase; 385 | *outbuf = ccoutbuf; 386 | used = cast(int)(ccinbuf-inbuf); 387 | writeFilled -= used; 388 | } 389 | return used; 390 | } 391 | 392 | int doBlam (float** outbuf, float* outbufend) { 393 | int ccinsize = writeFilled; 394 | const(float)*inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 395 | int used = 0; 396 | ccinsize -= 2; 397 | if (ccinsize > 0) { 398 | float* ccoutbuf = *outbuf; 399 | const(float)* ccinbuf = inbuf; 400 | const(float)* ccinbufend = ccinbuf+ccinsize; 401 | float ccLastAmp = lastAmp; 402 | float ccPhase = phase; 403 | float ccPhaseInc = phaseInc; 404 | float ccInvPhase = invPhase; 405 | float ccInvPhaseInc = invPhaseInc; 406 | enum int step = cast(int)(BlamCutoff*Resolution); 407 | enum int winstep = Resolution; 408 | do { 409 | if (ccoutbuf+SincWidth*2 > outbufend) break; 410 | float sample = ccinbuf[0]; 411 | if (ccPhaseInc < 1.0f) sample += (ccinbuf[1]-ccinbuf[0])*ccPhase; 412 | sample -= ccLastAmp; 413 | if (sample) { 414 | float[SincWidth*2] kernel = void; 415 | float kernelSum = 0.0f; 416 | int phaseReduced = cast(int)(ccInvPhase*Resolution); 417 | int phaseAdj = phaseReduced*step/Resolution; 418 | int i = SincWidth; 419 | for (; i >= -SincWidth+1; --i) { 420 | int pos = i*step; 421 | int winpos = i*winstep; 422 | kernelSum += kernel.ptr[i+SincWidth-1] = sincLut[abs(phaseAdj-pos)]*windowLut[abs(phaseReduced-winpos)]; 423 | } 424 | ccLastAmp += sample; 425 | sample /= kernelSum; 426 | for (i = 0; i < SincWidth*2; ++i) ccoutbuf[i] += sample*kernel.ptr[i]; 427 | } 428 | if (ccInvPhaseInc < 1.0f) { 429 | ++ccinbuf; 430 | ccInvPhase += ccInvPhaseInc; 431 | ccoutbuf += cast(int)ccInvPhase; 432 | ccInvPhase = fmod(ccInvPhase, 1.0f); 433 | } else { 434 | ccPhase += ccPhaseInc; 435 | ++ccoutbuf; 436 | ccinbuf += cast(int)ccPhase; 437 | ccPhase = fmod(ccPhase, 1.0f); 438 | } 439 | } while (ccinbuf < ccinbufend); 440 | phase = ccPhase; 441 | invPhase = ccInvPhase; 442 | lastAmp = ccLastAmp; 443 | *outbuf = ccoutbuf; 444 | used = cast(int)(ccinbuf-inbuf); 445 | writeFilled -= used; 446 | } 447 | return used; 448 | } 449 | 450 | int doCubic (float** outbuf, float* outbufend) { 451 | int ccinsize = writeFilled; 452 | const(float)*inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 453 | int used = 0; 454 | ccinsize -= 4; 455 | if (ccinsize > 0) { 456 | float* ccoutbuf = *outbuf; 457 | const(float)* ccinbuf = inbuf; 458 | const(float)* ccinbufend = ccinbuf+ccinsize; 459 | float ccPhase = phase; 460 | float ccPhaseInc = phaseInc; 461 | 462 | do { 463 | int i; 464 | float sample; 465 | if (ccoutbuf >= outbufend) break; 466 | sample = hermiteInterp!float(ccPhase, ccinbuf[0], ccinbuf[1], ccinbuf[2], ccinbuf[3]); 467 | *ccoutbuf++ = sample; 468 | ccPhase += ccPhaseInc; 469 | ccinbuf += cast(int)ccPhase; 470 | assert(ccPhase >= 0); 471 | ccPhase = ccPhase - cast(int)ccPhase; 472 | } while (ccinbuf < ccinbufend); 473 | phase = ccPhase; 474 | *outbuf = ccoutbuf; 475 | used = cast(int)(ccinbuf-inbuf); 476 | writeFilled -= used; 477 | } 478 | return used; 479 | } 480 | 481 | int doSinc (float** outbuf, float* outbufend) { 482 | int ccinsize = writeFilled; 483 | const(float)*inbuf = bufferIn.ptr+BufferSize+writePos-writeFilled; 484 | int used = 0; 485 | ccinsize -= SincWidth*2; 486 | if (ccinsize > 0) { 487 | float* ccoutbuf = *outbuf; 488 | const(float)* ccinbuf = inbuf; 489 | const(float)* ccinbufend = ccinbuf+ccinsize; 490 | float ccPhase = phase; 491 | float ccPhaseInc = phaseInc; 492 | immutable int step = (ccPhaseInc > 1.0f ? cast(int)(Resolution/ccPhaseInc*SincCutoff) : cast(int)(Resolution*SincCutoff)); 493 | enum int winstep = Resolution; 494 | do { 495 | float[SincWidth*2] kernel = void; 496 | float kernelSum = 0.0; 497 | int i = SincWidth; 498 | int phaseReduced = cast(int)(ccPhase*Resolution); 499 | int phaseAdj = phaseReduced*step/Resolution; 500 | float sample; 501 | if (ccoutbuf >= outbufend) break; 502 | for (; i >= -SincWidth+1; --i) { 503 | int pos = i*step; 504 | int winpos = i*winstep; 505 | kernelSum += kernel.ptr[i+SincWidth-1] = sincLut[abs(phaseAdj-pos)]*windowLut[abs(phaseReduced-winpos)]; 506 | } 507 | for (sample = 0, i = 0; i < SincWidth*2; ++i) sample += ccinbuf[i]*kernel.ptr[i]; 508 | *ccoutbuf++ = cast(float)(sample/kernelSum); 509 | ccPhase += ccPhaseInc; 510 | ccinbuf += cast(int)ccPhase; 511 | assert(ccPhase >= 0); 512 | ccPhase = ccPhase - cast(int)ccPhase; 513 | } while (ccinbuf < ccinbufend); 514 | phase = ccPhase; 515 | *outbuf = ccoutbuf; 516 | used = cast(int)(ccinbuf-inbuf); 517 | writeFilled -= used; 518 | } 519 | return used; 520 | } 521 | 522 | void doFill () 523 | { 524 | int ccMinFilled = minFilled(); 525 | int ccXquality = xquality; 526 | while (writeFilled > ccMinFilled && readFilled < BufferSize) { 527 | int ccWritePos = (readPos+readFilled)%BufferSize; 528 | int ccWriteSize = BufferSize-ccWritePos; 529 | float* ccoutbuf = bufferOut.ptr+ccWritePos; 530 | if (ccWriteSize > BufferSize-readFilled) ccWriteSize = BufferSize-readFilled; 531 | switch (ccXquality) { 532 | case Quality.Zoh: 533 | doZoh(&ccoutbuf, ccoutbuf+ccWriteSize); 534 | break; 535 | case Quality.Blep: 536 | int used; 537 | int ccWriteExtra = 0; 538 | if (ccWritePos >= readPos) ccWriteExtra = readPos; 539 | if (ccWriteExtra > SincWidth*2-1) ccWriteExtra = SincWidth*2-1; 540 | memcpy(bufferOut.ptr+BufferSize, bufferOut.ptr, ccWriteExtra*bufferOut[0].sizeof); 541 | used = doBlep(&ccoutbuf, ccoutbuf+ccWriteSize+ccWriteExtra); 542 | memcpy(bufferOut.ptr, bufferOut.ptr+BufferSize, ccWriteExtra*bufferOut[0].sizeof); 543 | if (!used) return; 544 | break; 545 | case Quality.Linear: 546 | doLinear(&ccoutbuf, ccoutbuf+ccWriteSize); 547 | break; 548 | case Quality.Blam: 549 | float* outbuf = ccoutbuf; 550 | int ccWriteExtra = 0; 551 | if (ccWritePos >= readPos) ccWriteExtra = readPos; 552 | if (ccWriteExtra > SincWidth*2-1) ccWriteExtra = SincWidth*2-1; 553 | memcpy(bufferOut.ptr+BufferSize, bufferOut.ptr, ccWriteExtra*bufferOut[0].sizeof); 554 | doBlam(&ccoutbuf, ccoutbuf+ccWriteSize+ccWriteExtra); 555 | memcpy(bufferOut.ptr, bufferOut.ptr+BufferSize, ccWriteExtra*bufferOut[0].sizeof); 556 | if (ccoutbuf == outbuf) return; 557 | break; 558 | case Quality.Cubic: 559 | doCubic(&ccoutbuf, ccoutbuf+ccWriteSize); 560 | break; 561 | case Quality.Sinc: 562 | doSinc(&ccoutbuf, ccoutbuf+ccWriteSize); 563 | break; 564 | default: assert(0, "wtf?!"); 565 | } 566 | readFilled += ccoutbuf-bufferOut.ptr-ccWritePos; 567 | } 568 | } 569 | 570 | void doFillAndRemoveDelay () { 571 | doFill(); 572 | if (delayRemoved < 0) { 573 | int delay = outputDelay(); 574 | delayRemoved = 0; 575 | while (delay--) removeSample(true); 576 | } 577 | } 578 | 579 | 580 | } 581 | 582 | private 583 | { 584 | enum Shift = 10; 585 | enum ShiftExtra = 8; 586 | enum Resolution = 1<= 0); 118 | 119 | _decodedStream.mixIntoBuffer(inoutChannels, frames, frameOffset, loopCount, volumeRamp, volume, _sampleRate); 120 | } 121 | 122 | // Only meant for command thread 123 | override bool fullDecode() 124 | { 125 | if (_decodedStream.fullyDecoded()) 126 | return true; 127 | 128 | // If you fail here, you have called fullDecode() after play(); this is disallowed. 129 | assert(!_disallowFullDecode); 130 | 131 | if (_disallowFullDecode) 132 | { 133 | return false; // should do it before playing, to avoid races. 134 | } 135 | 136 | _decodedStream.fullDecode(_mixer.getSampleRate()); 137 | return true; // decoding may encounter erorrs, but thisis "fully decoded" 138 | } 139 | 140 | override bool hasKnownLength() 141 | { 142 | return _decodedStream.lengthIsKnown(); 143 | } 144 | 145 | override int lengthInFrames() 146 | { 147 | if (_decodedStream.lengthIsKnown()) 148 | { 149 | return _decodedStream.lengthInFrames(); 150 | } 151 | else 152 | return -1; 153 | } 154 | 155 | double lengthInSeconds() 156 | { 157 | if (_decodedStream.lengthIsKnown()) 158 | { 159 | return cast(double)(_decodedStream.lengthInFrames()) / _mixer.getSampleRate(); 160 | } 161 | else 162 | return -1.0; 163 | } 164 | 165 | int originalLengthInFrames() nothrow 166 | { 167 | return _decodedStream.originalLengthInFrames(); 168 | } 169 | 170 | float sampleRate() nothrow 171 | { 172 | return _decodedStream.originalSampleRate(); 173 | } 174 | 175 | private: 176 | IMixerInternal _mixer; 177 | DecodedStream _decodedStream; 178 | float _sampleRate; 179 | bool _disallowFullDecode = false; 180 | } 181 | 182 | private: 183 | 184 | 185 | bool isChannelCountValid(int channels) 186 | { 187 | return channels == 1 || channels == 2; 188 | } 189 | 190 | // 128kb is approx 300ms of stereo 44100Hz audio float data 191 | // This wasn't tuned. 192 | // The internet says `malloc`/`free` of 128kb should take ~10µs. 193 | // That should be pretty affordable. 194 | enum int CHUNK_SIZE_DECODED = 128 * 1024; 195 | 196 | /// Decode a stream, keeps it in a buffer so that multiple playback are possible. 197 | struct DecodedStream 198 | { 199 | @nogc: 200 | void initializeFromFile(const(char)[] path) 201 | { 202 | _stream = mallocNew!BufferedStream(path); 203 | commonInitialization(); 204 | } 205 | 206 | void initializeFromMemory(const(ubyte)[] inputData) 207 | { 208 | _stream = mallocNew!BufferedStream(inputData); 209 | commonInitialization(); 210 | } 211 | 212 | ~this() 213 | { 214 | destroyFree(_stream); 215 | } 216 | 217 | // Called from command thread 218 | void fullDecode(float sampleRate) nothrow 219 | { 220 | // Simulated normal decoding. 221 | // Because this is done is the command-thread, and the audio thread may play this, this is not thread-safe. 222 | float[64] dummySamples = void; 223 | float[32] volumeRamp = void; 224 | dummySamples[] = 0.0f; 225 | volumeRamp[] = 1.0f; 226 | float*[2] inoutBuffers; 227 | inoutBuffers[0] = &dummySamples[0]; 228 | inoutBuffers[1] = &dummySamples[32]; 229 | int frameOffset = 0; 230 | uint loopCount = 1; 231 | float[2] volume = [0.01f, 0.01f]; 232 | while(!fullyDecoded) 233 | { 234 | mixIntoBuffer(inoutBuffers, 32, frameOffset, loopCount, volumeRamp.ptr, volume, sampleRate); 235 | } 236 | } 237 | 238 | // Mix source[frameOffset..frames+frameOffset] into inoutChannels[0..frames] with volume `volume`, 239 | // decoding more stream if needed. Also extending source virtually if looping. 240 | void mixIntoBuffer(float*[] inoutChannels, 241 | int frames, 242 | ref int frameOffset, 243 | ref uint loopCount, 244 | float* volumeRamp, 245 | float[2] volume, 246 | float sampleRate, // will not change across calls 247 | ) nothrow 248 | { 249 | // Initialize resamplers lazily 250 | if (!_resamplersInitialized) 251 | { 252 | for (int chan = 0; chan < 2; ++chan) 253 | _resamplers[chan].initialize(_stream.getSamplerate(), sampleRate, AudioResampler.Quality.Cubic); 254 | _resamplersInitialized = true; 255 | } 256 | 257 | while (frames != 0) 258 | { 259 | assert(frames >= 0); 260 | 261 | int framesEnd = frames + frameOffset; 262 | 263 | // need to decoder further? 264 | if (_framesDecodedAndResampled < framesEnd) 265 | { 266 | bool finished; 267 | decodeMoreSamples(framesEnd - _framesDecodedAndResampled, sampleRate, finished); 268 | if (!finished) 269 | { 270 | assert(_framesDecodedAndResampled >= framesEnd); 271 | } 272 | } 273 | 274 | if (_lengthIsKnown) 275 | { 276 | // limit mixing to existing samples. 277 | if (framesEnd > _sourceLengthInFrames) 278 | framesEnd = _sourceLengthInFrames; 279 | } 280 | 281 | 282 | int framesToCopy = framesEnd - frameOffset; 283 | if (framesToCopy > 0) 284 | { 285 | // mix into target buffer, upmix mono if needed 286 | for (int chan = 0; chan < 2; ++chan) 287 | { 288 | int sourceChan = chan < _channels ? chan : 0; // only works for mono and stereo sources 289 | _decodedBuffers[sourceChan].mixIntoBuffer(inoutChannels[chan], framesToCopy, frameOffset, volumeRamp, volume[chan]); 290 | } 291 | } 292 | 293 | frames -= framesToCopy; 294 | frameOffset += framesToCopy; 295 | 296 | if (frames != 0) 297 | { 298 | assert(_lengthIsKnown); 299 | if (frameOffset >= _sourceLengthInFrames) 300 | { 301 | frameOffset -= _sourceLengthInFrames; // loop 302 | loopCount -= 1; 303 | if (loopCount == 0) 304 | return; 305 | } 306 | } 307 | } 308 | } 309 | 310 | // Decode in the stream buffers at least `frames` more frames. 311 | // Can possibly decode more than that. 312 | void decodeMoreSamples(int frames, float sampleRate, out bool terminated) nothrow 313 | { 314 | int framesDone = 0; 315 | while (framesDone < frames) 316 | { 317 | bool terminatedResampling = false; 318 | 319 | // Decode any number of frames. 320 | // Return those in _decodedBuffers. 321 | int framesRead = readFromStreamAndResample(sampleRate, terminatedResampling); 322 | _framesDecodedAndResampled += framesRead; 323 | 324 | terminated = terminatedResampling; 325 | 326 | if (terminated) 327 | { 328 | _lengthIsKnown = true; 329 | _sourceLengthInFrames = _framesDecodedAndResampled; 330 | 331 | atomicStore!(MemoryOrder.rel)(_sourceLengthOutside, _framesDecodedAndResampled); 332 | atomicStore!(MemoryOrder.rel)(_lengthIsKnownOutside, true); 333 | 334 | // Fills with zeroes the rest of the buffers, if any output needed. 335 | if (frames > framesDone) 336 | { 337 | int remain = frames - framesDone; 338 | for (int chan = 0; chan < _channels; ++chan) 339 | { 340 | for (int n = 0; n < remain; ++n) 341 | { 342 | _decodedBuffers[chan].pushBack(0.0f); 343 | } 344 | } 345 | framesDone += remain; 346 | } 347 | break; 348 | } 349 | framesDone += framesRead; 350 | } 351 | 352 | assert(framesDone >= frames); 353 | } 354 | 355 | // 356 | bool lengthIsKnown() nothrow 357 | { 358 | return atomicLoad!(MemoryOrder.acq)( _lengthIsKnownOutside); 359 | } 360 | 361 | int lengthInFrames() nothrow 362 | { 363 | return atomicLoad!(MemoryOrder.acq)( _sourceLengthOutside); 364 | } 365 | 366 | bool fullyDecoded() nothrow 367 | { 368 | return lengthIsKnown(); 369 | } 370 | // 371 | 372 | /// Read from stream. Can return any number of frames. 373 | /// Note that "terminated" is not the stream being terminated, but the _resampling output_ being terminated. 374 | /// That happens a few samples later. 375 | int readFromStreamAndResample(float sampleRate, out bool terminated) nothrow 376 | { 377 | // Get more input 378 | int framesDecoded; 379 | if (!_streamIsTerminated) 380 | { 381 | // Read input 382 | try 383 | { 384 | framesDecoded = _stream.readSamplesFloat(_rawDecodeSamples.ptr, chunkFramesDecoder); 385 | _streamIsTerminated = framesDecoded != chunkFramesDecoder; 386 | if (_streamIsTerminated) 387 | _flushResamplingOutput = true; // small state machine 388 | } 389 | catch(Exception e) 390 | { 391 | framesDecoded = 0; 392 | _streamIsTerminated = true; 393 | destroyFree(e); 394 | } 395 | 396 | // Deinterleave 397 | for (int n = 0; n < framesDecoded; ++n) 398 | { 399 | for (int chan = 0; chan < _channels; ++chan) 400 | { 401 | _rawDecodeSamplesDeinterleaved[chan][n] = _rawDecodeSamples[_channels * n + chan]; 402 | } 403 | } 404 | } 405 | else if (_flushResamplingOutput) 406 | { 407 | _flushResamplingOutput = false; 408 | 409 | // Fills with a few empty samples in order to flush the resampler output. 410 | framesDecoded = 128; 411 | 412 | for (int chan = 0; chan < _channels; ++chan) 413 | { 414 | for (int n = 0; n < framesDecoded; ++n) 415 | { 416 | _rawDecodeSamplesDeinterleaved[chan][n] = 0; 417 | } 418 | } 419 | } 420 | else 421 | { 422 | // This is really terminated. No more output form the resampler. 423 | terminated = true; 424 | return 0; 425 | } 426 | 427 | size_t before = _decodedBuffers[0].length; 428 | for (int chan = 0; chan < _channels; ++chan) 429 | { 430 | _resamplers[chan].nextBufferPushMode(_rawDecodeSamplesDeinterleaved[chan].ptr, framesDecoded, _decodedBuffers[chan]); 431 | } 432 | size_t after = _decodedBuffers[0].length; 433 | 434 | // should return same amount of samples for all channels 435 | if (_channels > 1) 436 | { 437 | assert(_decodedBuffers[0].length == _decodedBuffers[1].length); 438 | } 439 | return cast(int) (after - before); 440 | } 441 | 442 | private: 443 | int _channels; 444 | int _framesDecodedAndResampled; // Current number of decoded and resampled frames in _decodedBuffers. 445 | int _sourceLengthInFrames; // Length of the resampled source in frames. 446 | 447 | // Set for consumption by the command thread. 448 | shared(bool) _lengthIsKnownOutside = false; 449 | shared(int) _sourceLengthOutside = -1; 450 | 451 | bool _lengthIsKnown; // true if _sourceLengthInFrames is known. To be used by decode thread only. 452 | bool _streamIsTerminated; // Whether the stream has finished decoding. To be used by decode thread only. 453 | bool _flushResamplingOutput; // Add a few silent sample at the end of decoder output. 454 | bool _resamplersInitialized; // true if resampler initialized. 455 | 456 | BufferedStream _stream; // using a BufferedStream to avoid blocking I/O 457 | AudioResampler[2] _resamplers; 458 | ChunkedVec!float[2] _decodedBuffers; // decoded and resampled _whole_ audio (this can be slow on resize) 459 | 460 | float[chunkFramesDecoder*2] _rawDecodeSamples; // interleaved samples from decoder 461 | float[chunkFramesDecoder][2] _rawDecodeSamplesDeinterleaved; // deinterleaved samples from decoder 462 | 463 | void commonInitialization() 464 | { 465 | _lengthIsKnown = false; 466 | _framesDecodedAndResampled = 0; 467 | _sourceLengthInFrames = -1; 468 | _streamIsTerminated = false; 469 | _channels = _stream.getNumChannels(); 470 | _resamplersInitialized = false; 471 | for(int chan = 0; chan < _channels; ++chan) 472 | _decodedBuffers[chan] = makeChunkedVec!float(CHUNK_SIZE_DECODED); 473 | assert( isChannelCountValid(_stream.getNumChannels()) ); 474 | } 475 | 476 | int originalLengthInFrames() nothrow 477 | { 478 | long len = _stream.getLengthInFrames(); 479 | assert(len >= -1); 480 | 481 | if (len > int.max) 482 | return int.max; // longer lengths not supported by game-mixer 483 | 484 | return cast(int) len; 485 | } 486 | 487 | float originalSampleRate() nothrow 488 | { 489 | return _stream.getSamplerate(); 490 | } 491 | } 492 | 493 | package: 494 | 495 | /// A bit faster than a dynamic cast. 496 | /// This is to avoid TypeInfo look-up. 497 | T unsafeObjectCast(T)(Object obj) 498 | { 499 | return cast(T)(cast(void*)(obj)); 500 | } --------------------------------------------------------------------------------