├── .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 | }
--------------------------------------------------------------------------------