speed control and other fun things

This commit is contained in:
Adam D. Ruppe 2023-01-31 20:35:16 -05:00
parent b4ada541d3
commit 76142d0bd9
1 changed files with 220 additions and 53 deletions

View File

@ -197,6 +197,8 @@ void main() {
/++
Provides an interface to control a sound.
All methods on this interface execute asynchronously
History:
Added December 23, 2020
+/
@ -241,12 +243,11 @@ interface SampleController {
History:
Added November 20, 2022 (dub v10.10)
Bugs:
Only implemented for mp3 and ogg at this time.
+/
// FIXME: this is clearly wrong on mp3s in some way.
void seek(float where);
// FIXME: would be cool to add volume and playback speed control methods too.
/++
Duration of the sample, in seconds. Please note it may be nan if unknown or inf if infinite looping.
You should check for both conditions.
@ -254,7 +255,48 @@ interface SampleController {
History:
Added November 20, 2022 (dub v10.10)
+/
// float duration();
float duration();
/++
Controls the volume of this particular sample, as a multiplier of its
original perceptual volume.
If unimplemented, the setter will return `float.nan` and the getter will
always return 1.0.
History:
Added November 26, 2020 (dub v10.10)
Bugs:
Not implemented for any type in simpleaudio at this time.
+/
float volume();
/// ditto
float volume(float multiplierOfOriginal);
/++
Controls the playback speed of this particular sample, as a multiplier
of its original speed. Setting it to 0.0 is liable to crash.
If unimplemented, the getter will always return 1.0. This is nearly always the
case if you compile with `-version=without_resampler`.
Please note that all members, [position], [duration], and any
others that relate to time will always return original times;
that is, as if `playbackSpeed == 1.0`.
Note that this is going to change the pitch of the sample; it
isn't a tempo change.
History:
Added November 26, 2020 (dub v10.10)
+/
float playbackSpeed();
/// ditto
void playbackSpeed(float multiplierOfOriginal);
/+
/++
Sets a delegate that will be called on the audio thread when the sample is finished
@ -265,11 +307,62 @@ interface SampleController {
to do in it is to simply send a message back to your main thread where it deals
with whatever you want to do.
)
History:
Added November 26, 2020 (dub v10.10)
+/
void onfinished(void delegate() shared callback);
/++
Sets a delegate that will pre-process any buffer before it is passed to the audio device
when playing, or your waveform delegate when using [getWaveform]. You can modify data
in the buffer if you want, or copy it out somewhere else, but remember this may be called
on the audio thread.
I didn't mark the delegate param `scope` but I might. Copying the actual pointer is super
iffy because the buffer can be reused by the audio thread as soon as this function returns.
History:
Added November 27, 2020 (dub v10.10)
+/
void setBufferDelegate(void delegate(short[] buffer, int sampleRate, int numberOfChannels) shared callback);
/++
Plays the sample on the given audio device. You can only ever play it on one device at a time.
Returns:
`true` if it was able to play on the given device, `false` if not.
Among the reasons it may be unable to play is if it is already playing
elsewhere or if it is already used up.
History:
Added November 27, 2020 (dub v10.10)
+/
bool playOn(AudioOutputThread where);
/++
Plays it to your delegate which emulates an audio device with the given sample rate and number of channels. It will call your delegate with interleaved signed 16 bit samples.
Returns:
`true` if it called your delegate at least once.
Among the reasons it might be `false`:
$(LIST
* The sample is already playing on another device.
* You compiled with `-version=without_resampler` and the sample rate didn't match the sample's capabilities.
* The number of channels requested is incompatible with the implementation.
)
History:
Added November 27, 2020 (dub v10.10)
+/
bool getWaveform(int sampleRate, int numberOfChannels, scope void delegate(scope short[] buffer) dg);
+/
//void onfinished(void delegate() shared callback);
}
private class DummySample : SampleController {
class DummySample : SampleController {
void pause() {}
void resume() {}
void stop() {}
@ -277,10 +370,17 @@ private class DummySample : SampleController {
bool finished() { return true; }
bool paused() { return true; }
float duration() { return float.init; }
float volume() { return 1.0; }
float volume(float v) { return float.init; }
float playbackSpeed() { return 1.0; }
void playbackSpeed(float v) { }
void seek(float where) {}
}
private class SampleControlFlags : SampleController {
private final class SampleControlFlags : SampleController {
void pause() { paused_ = true; }
void resume() { paused_ = false; }
void stop() { paused_ = false; stopped = true; }
@ -297,6 +397,44 @@ private class SampleControlFlags : SampleController {
float currentPosition = 0.0;
float requestedSeek = float.init;
float detectedDuration;
float duration() { return detectedDuration; }
// FIXME: these aren't implemented
float volume() { return 1.0; }
float volume(float v) { return float.init; }
float playbackSpeed_ = 1.0;
float requestedPlaybackSpeed;
float playbackSpeed() { return playbackSpeed_; }
void playbackSpeed(float v) { requestedPlaybackSpeed = v; }
void pollUserChanges(
scope bool delegate(float) executeSeek,
scope bool delegate(float) executePlaybackSpeed,
) {
// should I synchronize it after all?
synchronized(this) {
if(this.requestedSeek !is float.init) {
if(executeSeek !is null && executeSeek(this.requestedSeek)) {
this.currentPosition = this.requestedSeek;
}
this.requestedSeek = float.init;
}
if(this.requestedPlaybackSpeed !is float.init) {
if(executePlaybackSpeed !is null && executePlaybackSpeed(this.playbackSpeed_)) {
this.playbackSpeed_ = this.requestedPlaybackSpeed;
}
this.requestedPlaybackSpeed = float.init;
}
}
}
}
/++
@ -854,6 +992,7 @@ final class AudioPcmOutThreadImplementation : Thread {
auto scf = new SampleControlFlags;
auto player = new OPLPlayer(this.SampleRate, true, channels == 2);
// FIXME: populate the duration, support seek etc.
player.looped = loop;
player.load(data);
player.play();
@ -924,6 +1063,7 @@ final class AudioPcmOutThreadImplementation : Thread {
/* private */ SampleController playOgg(VorbisDecoder)(VorbisDecoder v, bool loop = false) {
auto scf = new SampleControlFlags;
scf.detectedDuration = v.streamLengthInSeconds;
/+
If you want 2 channels:
@ -934,8 +1074,8 @@ final class AudioPcmOutThreadImplementation : Thread {
if the file has 2, average them.
+/
if(v.sampleRate == SampleRate && v.chans == channels) {
plain_fallback:
void plainFallback() {
//if(false && v.sampleRate == SampleRate && v.chans == channels) {
addChannel(
delegate bool(short[] buffer) {
if(scf.paused) {
@ -945,14 +1085,12 @@ final class AudioPcmOutThreadImplementation : Thread {
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
synchronized(scf)
if(scf.requestedSeek !is float.init) {
if(v.seek(cast(uint) (scf.requestedSeek * v.sampleRate))) {
scf.currentPosition = scf.requestedSeek;
}
scf.requestedSeek = float.init;
}
scf.pollUserChanges(
delegate bool(float requestedSeek) {
return !!v.seek(cast(uint) (scf.requestedSeek * v.sampleRate));
},
null, // can't change speed without the resampler
);
plain:
auto got = v.getSamplesShortInterleaved(2, buffer.ptr, cast(int) buffer.length);
@ -973,7 +1111,9 @@ final class AudioPcmOutThreadImplementation : Thread {
return !scf.stopped;
}
);
} else {
}
void withResampler() {
version(with_resampler) {
auto resampleContext = new class ResamplingContext {
this() {
@ -985,14 +1125,16 @@ final class AudioPcmOutThreadImplementation : Thread {
tmp[0] = buffersIn[0].ptr;
tmp[1] = buffersIn[1].ptr;
synchronized(scf)
if(scf.requestedSeek !is float.init) {
if(v.seekFrame(cast(uint) (scf.requestedSeek * v.sampleRate))) {
scf.currentPosition = scf.requestedSeek;
}
scf.requestedSeek = float.init;
}
scf.pollUserChanges(
delegate bool(float requestedSeek) {
return !!v.seekFrame(cast(uint) (scf.requestedSeek * v.sampleRate));
},
delegate bool(float requestedPlaybackSpeed) {
this.changePlaybackSpeed(requestedPlaybackSpeed);
return true;
},
);
loop:
auto actuallyGot = v.getSamplesFloat(v.chans, tmp.ptr, cast(int) buffersIn[0].length);
@ -1009,9 +1151,11 @@ final class AudioPcmOutThreadImplementation : Thread {
};
addChannel(&resampleContext.fillBuffer);
} else goto plain_fallback;
} else plainFallback();
}
withResampler();
return scf;
}
@ -1068,7 +1212,7 @@ final class AudioPcmOutThreadImplementation : Thread {
}
// no compatibility guarantees, I can change this overload at any time!
/* private */ SampleController playMp3()(int delegate(ubyte[]) reader, int delegate(size_t) seeker) {
/* private */ SampleController playMp3()(int delegate(ubyte[]) reader, int delegate(ulong) seeker) {
import arsd.mp3;
auto mp3 = new MP3Decoder(reader, seeker);
@ -1076,9 +1220,11 @@ final class AudioPcmOutThreadImplementation : Thread {
throw new Exception("file not valid");
auto scf = new SampleControlFlags;
scf.detectedDuration = mp3.duration;
if(mp3.sampleRate == SampleRate && mp3.channels == channels) {
plain_fallback:
void plainFallback() {
// if these aren't true this will not work right but im not gonna require it per se
// if(mp3.sampleRate == SampleRate && mp3.channels == channels) { ... }
auto next = mp3.frameSamples;
@ -1092,26 +1238,24 @@ final class AudioPcmOutThreadImplementation : Thread {
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
synchronized(scf)
if(scf.requestedSeek !is float.init) {
if(mp3.seek(cast(uint) (scf.requestedSeek * mp3.sampleRate * mp3.channels))) {
scf.currentPosition = scf.requestedSeek;
}
scf.requestedSeek = float.init;
}
scf.pollUserChanges(
delegate bool(float requestedSeek) {
return mp3.seek(cast(uint) (requestedSeek * mp3.sampleRate * mp3.channels));
},
null, // can't change speed without the resampler
);
more:
if(next.length >= buffer.length) {
buffer[] = next[0 .. buffer.length];
next = next[buffer.length .. $];
scf.currentPosition += cast(float) buffer.length / mp3.sampleRate / mp3.channels;
scf.currentPosition += cast(float) buffer.length / mp3.sampleRate / mp3.channels * scf.playbackSpeed;
} else {
buffer[0 .. next.length] = next[];
buffer = buffer[next.length .. $];
scf.currentPosition += cast(float) next.length / mp3.sampleRate / mp3.channels;
scf.currentPosition += cast(float) next.length / mp3.sampleRate / mp3.channels * scf.playbackSpeed;
next = next[$..$];
@ -1134,8 +1278,11 @@ final class AudioPcmOutThreadImplementation : Thread {
return !scf.stopped;
}
);
} else {
}
void resamplingVersion() {
version(with_resampler) {
mp3.decodeNextFrame();
auto next = mp3.frameSamples;
auto resampleContext = new class ResamplingContext {
@ -1145,16 +1292,15 @@ final class AudioPcmOutThreadImplementation : Thread {
override void loadMoreSamples() {
synchronized(scf)
if(scf.requestedSeek !is float.init) {
if(mp3.seek(cast(uint) (scf.requestedSeek * mp3.sampleRate * mp3.channels))) {
scf.currentPosition = scf.requestedSeek;
}
scf.requestedSeek = float.init;
}
scf.pollUserChanges(
delegate bool(float requestedSeek) {
return mp3.seek(cast(uint) (requestedSeek * mp3.sampleRate * mp3.channels));
},
delegate bool(float requestedPlaybackSpeed) {
this.changePlaybackSpeed(requestedPlaybackSpeed);
return true;
},
);
if(mp3.channels == 1) {
int actuallyGot;
@ -1197,9 +1343,11 @@ final class AudioPcmOutThreadImplementation : Thread {
addChannel(&resampleContext.fillBuffer);
} else goto plain_fallback;
} else plainFallback();
}
resamplingVersion();
return scf;
}
@ -1221,6 +1369,7 @@ final class AudioPcmOutThreadImplementation : Thread {
+/
SampleController playWav(R)(R filename_or_data) if(is(R == string) /* filename */ || is(R == immutable(ubyte)[]) /* data */ ) {
auto scf = new SampleControlFlags;
// FIXME: support seeking
version(with_resampler) {
auto resampleContext = new class ResamplingContext {
import arsd.wav;
@ -1229,6 +1378,8 @@ final class AudioPcmOutThreadImplementation : Thread {
reader = wavReader(filename_or_data);
next = reader.front;
scf.detectedDuration = reader.duration;
super(scf, reader.sampleRate, SampleRate, reader.numberOfChannels, channels);
}
@ -1237,6 +1388,8 @@ final class AudioPcmOutThreadImplementation : Thread {
override void loadMoreSamples() {
// FIXME: pollUserChanges once seek is implemented
bool moar() {
if(next.length == 0) {
if(reader.empty)
@ -1336,6 +1489,8 @@ final class AudioPcmOutThreadImplementation : Thread {
this.ao = ao;
}
private AudioPcmOutThreadImplementation ao;
// prolly want a tree of things that can be simultaneous sounds or sequential sounds
}
/// ditto
@ -4434,6 +4589,17 @@ abstract class ResamplingContext {
throw new Exception("ugh");
resamplerRight.setup(1, inputSampleRate, outputSampleRate, 5);
processNewRate();
}
void changePlaybackSpeed(float newMultiplier) {
resamplerLeft.setRate(cast(uint) (inputSampleRate * newMultiplier), outputSampleRate);
resamplerRight.setRate(cast(uint) (inputSampleRate * newMultiplier), outputSampleRate);
processNewRate();
}
void processNewRate() {
resamplerLeft.getRatio(rateNum, rateDem);
int add = (rateNum % rateDem) ? 1 : 0;
@ -4444,6 +4610,7 @@ abstract class ResamplingContext {
buffersIn[1] = new float[](BUFFER_SIZE_FRAMES * rateNum / rateDem + add);
buffersOut[1] = new float[](BUFFER_SIZE_FRAMES);
}
}
/+
@ -4514,7 +4681,7 @@ abstract class ResamplingContext {
}
}
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels;
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels * scflags.playbackSpeed;
} else if(outputChannels == 2) {
foreach(idx, ref s; buffer) {
if(resamplerDataLeft.dataOut.length == 0) {
@ -4539,7 +4706,7 @@ abstract class ResamplingContext {
}
}
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels;
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels * scflags.playbackSpeed;
} else assert(0);
if(scflags.stopped)