arsd/simpleaudio.d

4705 lines
137 KiB
D

// FIXME: add a query devices thing
// FIXME: add the alsa sequencer interface cuz then i don't need the virtual raw midi sigh. or at elast load "virtual" and auto connect it somehow
// bindings: https://gist.github.com/pbackus/5eadddb1de8a8c5b24f5016a365c5942
// FIXME: 3d sound samples - basically you can assign a position to a thing you are playing in terms of a angle and distance from teh observe and do a bit of lag and left/right balance adjustments, then tell it its own speed for doppler shifts
/**
The purpose of this module is to provide audio functions for
things like playback, capture, and volume on both Windows
(via the mmsystem calls) and Linux (through ALSA).
It is only aimed at the basics, and will be filled in as I want
a particular feature. I don't generally need super configurability
and see it as a minus, since I don't generally care either, so I'm
going to be going for defaults that just work. If you need more though,
you can hack the source or maybe just use it for the operating system
bindings.
For example, I'm starting this because I want to write a volume
control program for my linux box, so that's what is going first.
That will consist of a listening callback for volume changes and
being able to get/set the volume.
TODO:
* pre-resampler that loads a clip and prepares it for repeated fast use
* controls so you can tell a particular thing to keep looping until you tell it to stop, or stop after the next loop, etc (think a phaser sound as long as you hold the button down)
* playFile function that detects automatically. basically:
if(args[1].endsWith("ogg"))
a.playOgg(args[1]);
else if(args[1].endsWith("wav"))
a.playWav(args[1]);
else if(mp3)
a.playMp3(args[1]);
* play audio high level with options to wait until completion or return immediately
* midi mid-level stuff but see [arsd.midi]!
* some kind of encoder???????
I will probably NOT do OSS anymore, since my computer doesn't even work with it now.
Ditto for Macintosh, as I don't have one and don't really care about them.
License:
GPL3 unless you compile with `-version=without_resampler` and don't use the `playEmulatedOpl3Midi`,
in which case it is BSL-1.0.
*/
module arsd.simpleaudio;
// hacking around https://issues.dlang.org/show_bug.cgi?id=23595
import core.stdc.config;
version(Posix)
import core.sys.posix.sys.types;
// done with hack around compiler bug
// http://webcache.googleusercontent.com/search?q=cache:NqveBqL0AOUJ:https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html&hl=en&gl=us&strip=1&vwsrc=0
version(without_resampler) {
} else {
version(X86)
version=with_resampler;
version(X86_64)
version=with_resampler;
}
enum BUFFER_SIZE_FRAMES = 1024;//512;//2048;
enum BUFFER_SIZE_SHORT = BUFFER_SIZE_FRAMES * 2;
/// A reasonable default volume for an individual sample. It doesn't need to be large; in fact it needs to not be large so mixing doesn't clip too much.
enum DEFAULT_VOLUME = 20;
version(Demo_simpleaudio)
void main() {
/+
version(none) {
import iv.stb.vorbis;
int channels;
short* decoded;
auto v = new VorbisDecoder("test.ogg");
auto ao = AudioOutput(0);
ao.fillData = (short[] buffer) {
auto got = v.getSamplesShortInterleaved(2, buffer.ptr, buffer.length);
if(got == 0) {
ao.stop();
}
};
ao.play();
return;
}
auto thread = new AudioPcmOutThread();
thread.start();
thread.playOgg("test.ogg");
Thread.sleep(5.seconds);
//Thread.sleep(150.msecs);
thread.beep();
Thread.sleep(250.msecs);
thread.blip();
Thread.sleep(250.msecs);
thread.boop();
Thread.sleep(1000.msecs);
/*
thread.beep(800, 500);
Thread.sleep(500.msecs);
thread.beep(366, 500);
Thread.sleep(600.msecs);
thread.beep(800, 500);
thread.beep(366, 500);
Thread.sleep(500.msecs);
Thread.sleep(150.msecs);
thread.beep(200);
Thread.sleep(150.msecs);
thread.beep(100);
Thread.sleep(150.msecs);
thread.noise();
Thread.sleep(150.msecs);
*/
thread.stop();
thread.join();
return;
/*
auto aio = AudioMixer(0);
import std.stdio;
writeln(aio.muteMaster);
*/
/*
mciSendStringA("play test.wav", null, 0, null);
Sleep(3000);
import std.stdio;
if(auto err = mciSendStringA("play test2.wav", null, 0, null))
writeln(err);
Sleep(6000);
return;
*/
// output about a second of random noise to demo PCM
auto ao = AudioOutput(0);
short[BUFFER_SIZE_SHORT] randomSpam = void;
import core.stdc.stdlib;
foreach(ref s; randomSpam)
s = cast(short)((cast(short) rand()) - short.max / 2);
int loopCount = 40;
//import std.stdio;
//writeln("Should be about ", loopCount * BUFFER_SIZE_FRAMES * 1000 / SampleRate, " microseconds");
int loops = 0;
// only do simple stuff in here like fill the data, set simple
// variables, or call stop anything else might cause deadlock
ao.fillData = (short[] buffer) {
buffer[] = randomSpam[0 .. buffer.length];
loops++;
if(loops == loopCount)
ao.stop();
};
ao.play();
return;
+/
// Play a C major scale on the piano to demonstrate midi
auto midi = MidiOutput(0);
ubyte[16] buffer = void;
ubyte[] where = buffer[];
midi.writeRawMessageData(where.midiProgramChange(1, 1));
for(ubyte note = MidiNote.C; note <= MidiNote.C + 12; note++) {
where = buffer[];
midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
import core.thread;
Thread.sleep(dur!"msecs"(500));
midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
if(note != 76 && note != 83)
note++;
}
import core.thread;
Thread.sleep(dur!"msecs"(500)); // give the last note a chance to finish
}
/++
Provides an interface to control a sound.
All methods on this interface execute asynchronously
History:
Added December 23, 2020
+/
interface SampleController {
/++
Pauses playback, keeping its position. Use [resume] to pick up where it left off.
+/
void pause();
/++
Resumes playback after a call to [pause].
+/
void resume();
/++
Stops playback. Once stopped, it cannot be restarted
except by creating a new sample from the [AudioOutputThread]
object.
+/
void stop();
/++
Reports the current stream position, in seconds, if available (NaN if not).
+/
float position();
/++
If the sample has finished playing. Happens when it runs out or if it is stopped.
+/
bool finished();
/++
If the sample has been paused.
History:
Added May 26, 2021 (dub v10.0)
+/
bool paused();
/++
Seeks to a point in the sample, if possible. If impossible, this function does nothing.
Params:
where = point to seek to, in seconds
History:
Added November 20, 2022 (dub v10.10)
Bugs:
Only implemented for mp3 and ogg at this time.
+/
void seek(float where);
/++
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.
History:
Added November 20, 2022 (dub v10.10)
+/
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
playing; immediately after [finished] becomes `true`.
$(PITFALL
Very important: your callback is called on the audio thread. The safest thing
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);
+/
}
class DummySample : SampleController {
void pause() {}
void resume() {}
void stop() {}
float position() { return float.init; }
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 final class SampleControlFlags : SampleController {
void pause() { paused_ = true; }
void resume() { paused_ = false; }
void stop() { paused_ = false; stopped = true; }
bool paused_;
bool stopped;
bool finished_;
float position() { return currentPosition; }
bool finished() { return finished_; }
bool paused() { return paused_; }
void seek(float where) { synchronized(this) {if(where < 0) where = 0; requestedSeek = where;} }
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;
}
}
}
}
/++
Wraps [AudioPcmOutThreadImplementation] with RAII semantics for better
error handling and disposal than the old way.
DO NOT USE THE `new` OPERATOR ON THIS! Just construct it inline:
---
auto audio = AudioOutputThread(true);
audio.beep();
---
History:
Added May 9, 2020 to replace the old [AudioPcmOutThread] class
that proved pretty difficult to use correctly.
+/
struct AudioOutputThread {
@disable this();
static if(__VERSION__ < 2098)
mixin(q{ @disable new(size_t); }); // gdc9 requires the arg fyi, but i mix it in because dmd deprecates before semantic so it can't be versioned out ugh
else
@disable new(); // but new dmd is strict about not allowing it
@disable void start() {} // you aren't supposed to control the thread yourself!
/++
You should call `exit` instead of join. It will signal the thread to exit and then call join for you.
If you absolutely must call join, use [rawJoin] instead.
History:
Disabled on December 30, 2021
+/
@disable void join(bool a = false) {} // you aren't supposed to control the thread yourself!
/++
Don't call this unless you're sure you know what you're doing.
You should use `audioOutputThread.exit();` instead.
+/
Throwable rawJoin(bool rethrow = true) {
if(impl is null)
return null;
return impl.join(rethrow);
}
/++
Pass `true` to enable the audio thread. Otherwise, it will
just live as a dummy mock object that you should not actually
try to use.
History:
Parameter `default` added on Nov 8, 2020.
The sample rate parameter was not correctly applied to the device on Linux until December 24, 2020.
+/
this(bool enable, int SampleRate = 44100, int channels = 2, string device = "default") {
if(enable) {
impl = new AudioPcmOutThreadImplementation(SampleRate, channels, device);
impl.refcount++;
impl.start();
impl.waitForInitialization();
impl.priority = Thread.PRIORITY_MAX;
}
}
/// ditto
this(bool enable, string device, int SampleRate = 44100, int channels = 2) {
this(enable, SampleRate, channels, device);
}
/// Keeps an internal refcount.
this(this) {
if(impl)
impl.refcount++;
}
/// When the internal refcount reaches zero, it stops the audio and rejoins the thread, throwing any pending exception (yes the dtor can throw! extremely unlikely though).
~this() {
if(impl) {
impl.refcount--;
if(impl.refcount == 0) {
impl.exit(true);
}
}
}
/++
Returns true if the output is suspended. Use `suspend` and `unsuspend` to change this.
History:
Added December 21, 2021 (dub v10.5)
+/
bool suspended() {
if(impl)
return impl.suspended();
return true;
}
/++
This allows you to check `if(audio)` to see if it is enabled.
+/
bool opCast(T : bool)() {
return impl !is null;
}
/++
Other methods are forwarded to the implementation of type
[AudioPcmOutThreadImplementation]. See that for more information
on what you can do.
This opDispatch template will forward all other methods directly
to that [AudioPcmOutThreadImplementation] if this is live, otherwise
it does nothing.
+/
template opDispatch(string name) {
static if(is(typeof(__traits(getMember, impl, name)) Params == __parameters))
auto opDispatch(Params params) {
if(impl)
return __traits(getMember, impl, name)(params);
static if(!is(typeof(return) == void))
return typeof(return).init;
}
else static assert(0);
}
// manual forward of thse since the opDispatch doesn't do the variadic
alias Sample = AudioPcmOutThreadImplementation.Sample;
void addSample(Sample[] samples...) {
if(impl !is null)
impl.addSample(samples);
}
// since these are templates, the opDispatch won't trigger them, so I have to do it differently.
// the dummysample is good anyway.
SampleController playEmulatedOpl3Midi()(string filename) {
if(impl)
return impl.playEmulatedOpl3Midi(filename);
return new DummySample;
}
SampleController playEmulatedOpl3Midi()(immutable(ubyte)[] data) {
if(impl)
return impl.playEmulatedOpl3Midi(data);
return new DummySample;
}
SampleController playOgg()(string filename, bool loop = false) {
if(impl)
return impl.playOgg(filename, loop);
return new DummySample;
}
SampleController playOgg()(immutable(ubyte)[] data, bool loop = false) {
if(impl)
return impl.playOgg(data, loop);
return new DummySample;
}
SampleController playMp3()(string filename) {
if(impl)
return impl.playMp3(filename);
return new DummySample;
}
SampleController playMp3()(immutable(ubyte)[] data) {
if(impl)
return impl.playMp3(data);
return new DummySample;
}
SampleController playWav()(string filename) {
if(impl)
return impl.playWav(filename);
return new DummySample;
}
SampleController playWav()(immutable(ubyte)[] data) {
if(impl)
return impl.playWav(data);
return new DummySample;
}
/// provides automatic [arsd.jsvar] script wrapping capability. Make sure the
/// script also finishes before this goes out of scope or it may end up talking
/// to a dead object....
auto toArsdJsvar() {
return impl;
}
/+
alias getImpl this;
AudioPcmOutThreadImplementation getImpl() {
assert(impl !is null);
return impl;
}
+/
private AudioPcmOutThreadImplementation impl;
}
/++
Old thread implementation. I decided to deprecate it in favor of [AudioOutputThread] because
RAII semantics make it easier to get right at the usage point. See that to go forward.
History:
Deprecated on May 9, 2020.
+/
deprecated("Use AudioOutputThread instead.") class AudioPcmOutThread {}
/+
/++
+/
void mmsleep(Duration time) {
version(Windows) {
static HANDLE timerQueue;
static HANDLE event;
if(event is null)
event = CreateEvent(null, false, false, null);
extern(Windows)
static void cb(PVOID ev, BOOLEAN) {
HANDLE e = cast(HANDLE) ev;
SetEvent(e);
}
//if(timerQueue is null)
//timerQueue = CreateTimerQueue();
// DeleteTimerQueueEx(timerQueue, null);
HANDLE nt;
auto ret = CreateTimerQueueTimer(&nt, timerQueue, &cb, event /+ param +/, cast(DWORD) time.total!"msecs", 0 /* period */, WT_EXECUTEDEFAULT);
if(!ret)
throw new Exception("fail");
//DeleteTimerQueueTimer(timerQueue, nt, INVALID_HANDLE_VALUE);
WaitForSingleObject(event, 1000);
}
}
+/
/++
A clock you can use for multimedia applications. It compares time elapsed against
a position variable you pass in to figure out how long to wait to get to that point.
Very similar to Phobos' [std.datetime.stopwatch.StopWatch|StopWatch] but with built-in
wait capabilities.
For example, suppose you want something to happen 60 frames per second:
---
MMClock clock;
Duration frame;
clock.restart();
while(running) {
frame += 1.seconds / 60;
bool onSchedule = clock.waitUntil(frame);
do_essential_frame_work();
if(onSchedule) {
// if we're on time, do other work too.
// but if we weren't on time, skipping this
// might help catch back up to where we're
// supposed to be.
do_would_be_nice_frame_work();
}
}
---
+/
struct MMClock {
import core.time;
private Duration position;
private MonoTime lastPositionUpdate;
private bool paused;
int speed = 1000; /// 1000 = 1.0, 2000 = 2.0, 500 = 0.5, etc.
private void updatePosition() {
auto now = MonoTime.currTime;
position += (now - lastPositionUpdate) * speed / 1000;
lastPositionUpdate = now;
}
/++
Restarts the clock from position zero.
+/
void restart() {
position = Duration.init;
lastPositionUpdate = MonoTime.currTime;
}
/++
Pauses the clock.
+/
void pause() {
if(paused) return;
updatePosition();
paused = true;
}
void unpause() {
if(!paused) return;
lastPositionUpdate = MonoTime.currTime;
paused = false;
}
/++
Goes to sleep until the real clock catches up to the given
`position`.
Returns: `true` if you're on schedule, returns false if the
given `position` is already in the past. In that case,
you might want to consider skipping some work to get back
on time.
+/
bool waitUntil(Duration position) {
auto diff = timeUntil(position);
if(diff < 0.msecs)
return false;
if(diff == 0.msecs)
return true;
import core.thread;
Thread.sleep(diff);
return true;
}
/++
+/
Duration timeUntil(Duration position) {
updatePosition();
return (position - this.position) * 1000 / speed;
}
/++
Returns the current time on the clock since the
last call to [restart], excluding times when the
clock was paused.
+/
Duration currentPosition() {
updatePosition();
return position;
}
}
import core.thread;
/++
Makes an audio thread for you that you can make
various sounds on and it will mix them with good
enough latency for simple games.
DO NOT USE THIS DIRECTLY. Instead, access it through
[AudioOutputThread].
---
auto audio = AudioOutputThread(true);
audio.beep();
// you need to keep the main program alive long enough
// to keep this thread going to hear anything
Thread.sleep(1.seconds);
---
+/
final class AudioPcmOutThreadImplementation : Thread {
private this(int SampleRate, int channels, string device = "default") {
this.isDaemon = true;
this.SampleRate = SampleRate;
this.channels = channels;
this.device = device;
super(&run);
}
private int SampleRate;
private int channels;
private int refcount;
private string device;
private void waitForInitialization() {
shared(AudioOutput*)* ao = cast(shared(AudioOutput*)*) &this.ao;
//int wait = 0;
while(isRunning && *ao is null) {
Thread.sleep(5.msecs);
//wait += 5;
}
//import std.stdio; writeln(wait);
if(*ao is null) {
exit(true);
}
}
/++
Asks the device to pause/unpause. This may not actually do anything on some systems.
You should probably use [suspend] and [unsuspend] instead.
+/
@scriptable
void pause() {
if(ao) {
ao.pause();
}
}
/// ditto
@scriptable
void unpause() {
if(ao) {
ao.unpause();
}
}
/++
Stops the output thread. Using the object after it is stopped is not recommended which is why
this is now deprecated.
You probably want [suspend] or [exit] instead. Use [suspend] if you want to stop playing, and
close the output device, but keep the thread alive so you can [unsuspend] later. After calling
[suspend], you can call [unsuspend] and then continue using the other method normally again.
Use [exit] if you want to stop playing, close the output device, and terminate the worker thread.
After calling [exit], you may not call any methods on the thread again.
The one exception is if you are inside an audio callback and want to stop the thread and prepare
it to be [AudioOutputThread.rawJoin]ed. Preferably, you'd avoid doing this - the channels can
simply return false to indicate that they are done. But if you must do that, call [rawStop] instead.
History:
`stop` was deprecated and `rawStop` added on December 30, 2021 (dub v10.5)
+/
deprecated("You want to use either suspend or exit instead, or rawStop if you must but see the docs.")
void stop() {
if(ao) {
ao.stop();
}
}
/// ditto
void rawStop() {
if(ao) { ao.stop(); }
}
/++
Makes some old-school style sound effects. Play with them to see what they actually sound like.
Params:
freq = frequency of the wave in hertz
dur = duration in milliseconds
volume = amplitude of the wave, between 0 and 100
balance = stereo balance. 50 = both speakers equally, 0 = all to the left, none to the right, 100 = all to the right, none to the left.
attack = a parameter to the change of frequency
freqBase = the base frequency in the sound effect algorithm
History:
The `balance` argument was added on December 13, 2021 (dub v10.5)
+/
@scriptable
void beep(int freq = 900, int dur = 150, int volume = DEFAULT_VOLUME, int balance = 50) {
Sample s;
s.operation = 0; // square wave
s.frequency = SampleRate / freq;
s.duration = dur * SampleRate / 1000;
s.volume = volume;
s.balance = balance;
addSample(s);
}
/// ditto
@scriptable
void noise(int dur = 150, int volume = DEFAULT_VOLUME, int balance = 50) {
Sample s;
s.operation = 1; // noise
s.frequency = 0;
s.volume = volume;
s.duration = dur * SampleRate / 1000;
s.balance = balance;
addSample(s);
}
/// ditto
@scriptable
void boop(float attack = 8, int freqBase = 500, int dur = 150, int volume = DEFAULT_VOLUME, int balance = 50) {
Sample s;
s.operation = 5; // custom
s.volume = volume;
s.duration = dur * SampleRate / 1000;
s.balance = balance;
s.f = delegate short(int x) {
auto currentFrequency = cast(float) freqBase / (1 + cast(float) x / (cast(float) SampleRate / attack));
import std.math;
auto freq = 2 * PI / (cast(float) SampleRate / currentFrequency);
return cast(short) (sin(cast(float) freq * cast(float) x) * short.max * volume / 100);
};
addSample(s);
}
/// ditto
@scriptable
void blip(float attack = 6, int freqBase = 800, int dur = 150, int volume = DEFAULT_VOLUME, int balance = 50) {
Sample s;
s.operation = 5; // custom
s.volume = volume;
s.duration = dur * SampleRate / 1000;
s.balance = balance;
s.f = delegate short(int x) {
auto currentFrequency = cast(float) freqBase * (1 + cast(float) x / (cast(float) SampleRate / attack));
import std.math;
auto freq = 2 * PI / (cast(float) SampleRate / currentFrequency);
return cast(short) (sin(cast(float) freq * cast(float) x) * short.max * volume / 100);
};
addSample(s);
}
version(none)
void custom(int dur = 150, int volume = DEFAULT_VOLUME) {
Sample s;
s.operation = 5; // custom
s.volume = volume;
s.duration = dur * SampleRate / 1000;
s.f = delegate short(int x) {
auto currentFrequency = 500.0 / (1 + cast(float) x / (cast(float) SampleRate / 8));
import std.math;
auto freq = 2 * PI / (cast(float) SampleRate / currentFrequency);
return cast(short) (sin(cast(float) freq * cast(float) x) * short.max * volume / 100);
};
addSample(s);
}
/++
Plays the given midi files with the nuked opl3 emulator.
Requires nukedopl3.d (module [arsd.nukedopl3]) to be compiled in, which is GPL.
History:
Added December 24, 2020.
License:
If you use this function, you are opting into the GPL version 2 or later.
Authors:
Based on ketmar's code.
Bugs:
The seek method is not yet implemented.
+/
SampleController playEmulatedOpl3Midi()(string filename, bool loop = false) {
import std.file;
auto bytes = cast(immutable(ubyte)[]) std.file.read(filename);
return playEmulatedOpl3Midi(bytes);
}
/// ditto
SampleController playEmulatedOpl3Midi()(immutable(ubyte)[] data, bool loop = false) {
import arsd.nukedopl3;
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();
addChannel(
delegate bool(short[] buffer) {
if(scf.paused) {
buffer[] = 0;
return true;
}
if(!player.playing) {
scf.finished_ = true;
return false;
}
auto pos = player.generate(buffer[]);
scf.currentPosition += cast(float) buffer.length / SampleRate/ channels;
if(pos == 0 || scf.stopped) {
scf.finished_ = true;
return false;
}
return !scf.stopped;
}
);
return scf;
}
/++
Requires vorbis.d to be compiled in (module arsd.vorbis)
Returns:
An implementation of [SampleController] which lets you pause, etc., the file.
Please note that the static type may change in the future. It will always be a subtype of [SampleController], but it may be more specialized as I add more features and this will not necessarily match its sister functions, [playMp3] and [playWav], though all three will share an ancestor in [SampleController]. Therefore, if you use `auto`, there's no guarantee the static type won't change in future versions and I will NOT consider that a breaking change since the base interface will remain compatible.
History:
Automatic resampling support added Nov 7, 2020.
Return value changed from `void` to a sample control object on December 23, 2020.
+/
SampleController playOgg()(string filename, bool loop = false) {
import arsd.vorbis;
auto v = new VorbisDecoder(filename);
return playOgg(v, loop);
}
/// ditto
SampleController playOgg()(immutable(ubyte)[] data, bool loop = false) {
import arsd.vorbis;
auto v = new VorbisDecoder(cast(int) data.length, delegate int(void[] buffer, uint ofs, VorbisDecoder vb) nothrow @nogc {
if(buffer is null)
return 0;
ubyte[] buf = cast(ubyte[]) buffer;
if(ofs + buf.length <= data.length) {
buf[] = data[ofs .. ofs + buf.length];
return cast(int) buf.length;
} else {
buf[0 .. data.length - ofs] = data[ofs .. $];
return cast(int) data.length - ofs;
}
});
return playOgg(v, loop);
}
// no compatibility guarantees, I can change this overload at any time!
/* private */ SampleController playOgg(VorbisDecoder)(VorbisDecoder v, bool loop = false) {
auto scf = new SampleControlFlags;
scf.detectedDuration = v.streamLengthInSeconds;
/+
If you want 2 channels:
if the file has 2+, use them.
If the file has 1, duplicate it for the two outputs.
If you want 1 channel:
if the file has 1, use it
if the file has 2, average them.
+/
void plainFallback() {
//if(false && v.sampleRate == SampleRate && v.chans == channels) {
addChannel(
delegate bool(short[] buffer) {
if(scf.paused) {
buffer[] = 0;
return true;
}
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
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);
if(got == 0) {
if(loop) {
v.seekStart();
scf.currentPosition = 0;
return true;
}
scf.finished_ = true;
return false;
} else {
scf.currentPosition += cast(float) got / v.sampleRate;
}
if(scf.stopped)
scf.finished_ = true;
return !scf.stopped;
}
);
}
void withResampler() {
version(with_resampler) {
auto resampleContext = new class ResamplingContext {
this() {
super(scf, v.sampleRate, SampleRate, v.chans, channels);
}
override void loadMoreSamples() {
float*[2] tmp;
tmp[0] = buffersIn[0].ptr;
tmp[1] = buffersIn[1].ptr;
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);
if(actuallyGot == 0 && loop) {
v.seekStart();
scf.currentPosition = 0;
goto loop;
}
resamplerDataLeft.dataIn = buffersIn[0][0 .. actuallyGot];
if(v.chans > 1)
resamplerDataRight.dataIn = buffersIn[1][0 .. actuallyGot];
}
};
addChannel(&resampleContext.fillBuffer);
} else plainFallback();
}
withResampler();
return scf;
}
/++
Requires mp3.d to be compiled in (module [arsd.mp3]).
Returns:
An implementation of [SampleController] which lets you pause, etc., the file.
Please note that the static type may change in the future. It will always be a subtype of [SampleController], but it may be more specialized as I add more features and this will not necessarily match its sister functions, [playOgg] and [playWav], though all three will share an ancestor in [SampleController]. Therefore, if you use `auto`, there's no guarantee the static type won't change in future versions and I will NOT consider that a breaking change since the base interface will remain compatible.
History:
Automatic resampling support added Nov 7, 2020.
Return value changed from `void` to a sample control object on December 23, 2020.
The `immutable(ubyte)[]` overload was added December 30, 2020.
The implementation of arsd.mp3 was completely changed on November 20, 2022, adding loop and seek support.
+/
SampleController playMp3()(string filename) {
import std.stdio;
auto fi = new File(filename); // just let the GC close it... otherwise random segfaults happen... blargh
auto reader = delegate(ubyte[] buf) {
return cast(int) fi.rawRead(buf[]).length;
};
return playMp3(reader, (ulong pos) {
fi.seek(pos);
return 0;
});
}
/// ditto
SampleController playMp3()(immutable(ubyte)[] data) {
auto originalData = data;
return playMp3( (ubyte[] buffer) {
ubyte[] buf = cast(ubyte[]) buffer;
if(data.length >= buf.length) {
buf[] = data[0 .. buf.length];
data = data[buf.length .. $];
return cast(int) buf.length;
} else {
auto it = data.length;
buf[0 .. data.length] = data[];
buf[data.length .. $] = 0;
data = data[$ .. $];
return cast(int) it;
}
}, (ulong pos) {
data = originalData[pos .. $];
return 0;
});
}
// no compatibility guarantees, I can change this overload at any time!
/* private */ SampleController playMp3()(int delegate(ubyte[]) reader, int delegate(ulong) seeker) {
import arsd.mp3;
auto mp3 = new MP3Decoder(reader, seeker);
if(!mp3.valid)
throw new Exception("file not valid");
auto scf = new SampleControlFlags;
scf.detectedDuration = mp3.duration;
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;
addChannel(
delegate bool(short[] buffer) {
if(scf.paused) {
buffer[] = 0;
return true;
}
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
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.playbackSpeed;
} else {
buffer[0 .. next.length] = next[];
buffer = buffer[next.length .. $];
scf.currentPosition += cast(float) next.length / mp3.sampleRate / mp3.channels * scf.playbackSpeed;
next = next[$..$];
if(buffer.length) {
if(mp3.valid) {
mp3.decodeNextFrame();
next = mp3.frameSamples;
goto more;
} else {
buffer[] = 0;
scf.finished_ = true;
return false;
}
}
}
if(scf.stopped) {
scf.finished_ = true;
}
return !scf.stopped;
}
);
}
void resamplingVersion() {
version(with_resampler) {
mp3.decodeNextFrame();
auto next = mp3.frameSamples;
auto resampleContext = new class ResamplingContext {
this() {
super(scf, mp3.sampleRate, SampleRate, mp3.channels, channels);
}
override void loadMoreSamples() {
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;
foreach(ref b; buffersIn[0]) {
if(next.length == 0) break;
b = cast(float) next[0] / short.max;
next = next[1 .. $];
if(next.length == 0) {
mp3.decodeNextFrame();
next = mp3.frameSamples;
}
actuallyGot++;
}
resamplerDataLeft.dataIn = buffersIn[0][0 .. actuallyGot];
} else {
int actuallyGot;
foreach(idx, ref b; buffersIn[0]) {
if(next.length == 0) break;
b = cast(float) next[0] / short.max;
next = next[1 .. $];
if(next.length == 0) {
mp3.decodeNextFrame();
next = mp3.frameSamples;
}
buffersIn[1][idx] = cast(float) next[0] / short.max;
next = next[1 .. $];
if(next.length == 0) {
mp3.decodeNextFrame();
next = mp3.frameSamples;
}
actuallyGot++;
}
resamplerDataLeft.dataIn = buffersIn[0][0 .. actuallyGot];
resamplerDataRight.dataIn = buffersIn[1][0 .. actuallyGot];
}
}
};
addChannel(&resampleContext.fillBuffer);
} else plainFallback();
}
resamplingVersion();
return scf;
}
/++
Requires [arsd.wav]. Only supports simple 8 or 16 bit wav files, no extensible or float formats at this time.
Also requires the resampler to be compiled in at this time, but that may change in the future, I was just lazy.
Returns:
An implementation of [SampleController] which lets you pause, etc., the file.
Please note that the static type may change in the future. It will always be a subtype of [SampleController], but it may be more specialized as I add more features and this will not necessarily match its sister functions, [playMp3] and [playOgg], though all three will share an ancestor in [SampleController]. Therefore, if you use `auto`, there's no guarantee the static type won't change in future versions and I will NOT consider that a breaking change since the base interface will remain compatible.
Bugs:
The seek method is not yet implemented.
History:
Added Nov 8, 2020.
Return value changed from `void` to a sample control object on December 23, 2020.
+/
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;
this() {
reader = wavReader(filename_or_data);
next = reader.front;
scf.detectedDuration = reader.duration;
super(scf, reader.sampleRate, SampleRate, reader.numberOfChannels, channels);
}
typeof(wavReader(filename_or_data)) reader;
const(ubyte)[] next;
override void loadMoreSamples() {
// FIXME: pollUserChanges once seek is implemented
bool moar() {
if(next.length == 0) {
if(reader.empty)
return false;
reader.popFront;
next = reader.front;
if(next.length == 0)
return false;
}
return true;
}
if(reader.numberOfChannels == 1) {
int actuallyGot;
foreach(ref b; buffersIn[0]) {
if(!moar) break;
if(reader.bitsPerSample == 8) {
b = (cast(float) next[0] - 128.0f) / 127.0f;
next = next[1 .. $];
} else if(reader.bitsPerSample == 16) {
short n = next[0];
next = next[1 .. $];
if(!moar) break;
n |= cast(ushort)(next[0]) << 8;
next = next[1 .. $];
b = (cast(float) n) / short.max;
} else assert(0);
actuallyGot++;
}
resamplerDataLeft.dataIn = buffersIn[0][0 .. actuallyGot];
} else {
int actuallyGot;
foreach(idx, ref b; buffersIn[0]) {
if(!moar) break;
if(reader.bitsPerSample == 8) {
b = (cast(float) next[0] - 128.0f) / 127.0f;
next = next[1 .. $];
if(!moar) break;
buffersIn[1][idx] = (cast(float) next[0] - 128.0f) / 127.0f;
next = next[1 .. $];
} else if(reader.bitsPerSample == 16) {
short n = next[0];
next = next[1 .. $];
if(!moar) break;
n |= cast(ushort)(next[0]) << 8;
next = next[1 .. $];
b = (cast(float) n) / short.max;
if(!moar) break;
n = next[0];
next = next[1 .. $];
if(!moar) break;
n |= cast(ushort)(next[0]) << 8;
next = next[1 .. $];
buffersIn[1][idx] = (cast(float) n) / short.max;
} else assert(0);
actuallyGot++;
}
resamplerDataLeft.dataIn = buffersIn[0][0 .. actuallyGot];
resamplerDataRight.dataIn = buffersIn[1][0 .. actuallyGot];
}
}
};
addChannel(&resampleContext.fillBuffer);
} else static assert(0, "I was lazy and didn't implement straight-through playing");
return scf;
}
/++
A helper object to create synthesized sound samples.
Construct it with the [synth] function.
History:
Added October 29, 2022 (dub v10.10)
Examples:
---
AudioOutputThread ao = AudioOutputThread(true);
with(ao.synth) ao.playSynth(beep, boop, blip);
---
+/
static struct SynthBuilder {
private this(AudioPcmOutThreadImplementation ao) {
this.ao = ao;
}
private AudioPcmOutThreadImplementation ao;
// prolly want a tree of things that can be simultaneous sounds or sequential sounds
}
/// ditto
SynthBuilder synth() {
return SynthBuilder(this);
}
static struct Sample {
enum Operation {
squareWave = 0,
noise = 1,
triangleWave = 2,
sawtoothWave = 3,
sineWave = 4,
customFunction = 5
}
/+
static Sample opDispatch(string operation)(int frequency) if(__traits(hasMember, Operation, operation)) {
Sample s;
s.operation = cast(int) __traits(getMember, Operation, operation);
s.frequency = frequency;
return s;
}
+/
int operation;
int frequency; /* in samples */
int duration; /* in samples */
int volume = DEFAULT_VOLUME; /* between 1 and 100. You should generally shoot for something lowish, like 20. */
int delay; /* in samples */
int balance = 50; /* between 0 and 100 */
/+
// volume envelope
int attack;
int decay;
int sustainLevel;
int release;
// change in frequency
int frequencyAttack;
int vibratoRange; // change of frequency as it sustains
int vibratoSpeed; // how fast it cycles through the vibratoRange
+/
int x;
short delegate(int x) f;
}
// FIXME: go ahead and make this return a SampleController too
final void addSample(Sample[] samples...) {
if(samples.length == 0)
return;
Sample currentSample = samples[0];
samples = samples[1 .. $];
if(samples.length)
samples = samples.dup; // ensure it isn't in stack memory that might get smashed when the delegate is passed to the other thread
int frequencyCounter;
short val = cast(short) (cast(int) short.max * currentSample.volume / 100);
enum divisor = 50;
int leftMultiplier = 50 + (50 - currentSample.balance);
int rightMultiplier = 50 + (currentSample.balance - 50);
bool left = true;
addChannel(
delegate bool (short[] buffer) {
newsample:
if(currentSample.duration) {
size_t i = 0;
if(currentSample.delay) {
if(buffer.length <= currentSample.delay * 2) {
// whole buffer consumed by delay
buffer[] = 0;
currentSample.delay -= buffer.length / 2;
} else {
i = currentSample.delay * 2;
buffer[0 .. i] = 0;
currentSample.delay = 0;
}
}
if(currentSample.delay > 0)
return true;
size_t sampleFinish;
if(currentSample.duration * 2 <= buffer.length) {
sampleFinish = currentSample.duration * 2;
currentSample.duration = 0;
} else {
sampleFinish = buffer.length;
currentSample.duration -= buffer.length / 2;
}
switch(currentSample.operation) {
case 0: // square wave
for(; i < sampleFinish; i++) {
buffer[i] = cast(short)((val * (left ? leftMultiplier : rightMultiplier)) / divisor);
left = !left;
// left and right do the same thing so we only count
// every other sample
if(i & 1) {
if(frequencyCounter)
frequencyCounter--;
if(frequencyCounter == 0) {
// are you kidding me dmd? random casts suck
val = cast(short) -cast(int)(val);
frequencyCounter = currentSample.frequency / 2;
}
}
}
break;
case 1: // noise
for(; i < sampleFinish; i++) {
import std.random;
buffer[i] = cast(short)((left ? leftMultiplier : rightMultiplier) * uniform(cast(short) -cast(int)val, val) / divisor);
left = !left;
}
break;
/+
case 2: // triangle wave
short[] tone;
tone.length = 22050 * len / 1000;
short valmax = cast(short) (cast(int) volume * short.max / 100);
int wavelength = 22050 / freq;
wavelength /= 2;
int da = valmax / wavelength;
int val = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = cast(short) val;
val+= da;
if(da > 0 && val >= valmax)
da *= -1;
if(da < 0 && val <= -valmax)
da *= -1;
}
data ~= tone;
for(; i < sampleFinish; i++) {
buffer[i] = val;
// left and right do the same thing so we only count
// every other sample
if(i & 1) {
if(frequencyCounter)
frequencyCounter--;
if(frequencyCounter == 0) {
val = 0;
frequencyCounter = currentSample.frequency / 2;
}
}
}
break;
case 3: // sawtooth wave
short[] tone;
tone.length = 22050 * len / 1000;
int valmax = volume * short.max / 100;
int wavelength = 22050 / freq;
int da = valmax / wavelength;
short val = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = val;
val+= da;
if(val >= valmax)
val = 0;
}
data ~= tone;
case 4: // sine wave
short[] tone;
tone.length = 22050 * len / 1000;
int valmax = volume * short.max / 100;
int val = 0;
float i = 2*PI / (22050/freq);
float f = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = cast(short) (valmax * sin(f));
f += i;
if(f>= 2*PI)
f -= 2*PI;
}
data ~= tone;
+/
case 5: // custom function
val = currentSample.f(currentSample.x);
for(; i < sampleFinish; i++) {
buffer[i] = cast(short)(val * (left ? leftMultiplier : rightMultiplier) / divisor);
left = !left;
if(i & 1) {
currentSample.x++;
val = currentSample.f(currentSample.x);
}
}
break;
default: // unknown; use silence
currentSample.duration = 0;
}
if(i < buffer.length)
buffer[i .. $] = 0;
return currentSample.duration > 0 || samples.length;
} else if(samples.length) {
currentSample = samples[0];
samples = samples[1 .. $];
frequencyCounter = 0;
val = cast(short) (cast(int) short.max * currentSample.volume / 100);
leftMultiplier = 50 + (50 - currentSample.balance);
rightMultiplier = 50 + (currentSample.balance - 50);
left = true;
goto newsample;
} else {
return false;
}
}
);
}
/++
The delegate returns false when it is finished (true means keep going).
It must fill the buffer with waveform data on demand and must be latency
sensitive; as fast as possible.
+/
public void addChannel(bool delegate(short[] buffer) dg) {
synchronized(this) {
// silently drops info if we don't have room in the buffer...
// don't do a lot of long running things lol
if(fillDatasLength < fillDatas.length)
fillDatas[fillDatasLength++] = dg;
}
}
private {
AudioOutput* ao;
bool delegate(short[] buffer)[32] fillDatas;
int fillDatasLength = 0;
}
private bool suspendWanted;
private bool exiting;
private bool suspended_;
/++
Stops playing and closes the audio device, but keeps the worker thread
alive and waiting for a call to [unsuspend], which will re-open everything
and pick up (close to; a couple buffers may be discarded) where it left off.
This is more reliable than [pause] and [unpause] since it doesn't require
the system/hardware to cooperate.
History:
Added December 30, 2021 (dub v10.5)
+/
public void suspend() {
suspended_ = true;
suspendWanted = true;
if(ao)
ao.stop();
}
/// ditto
public void unsuspend() {
suspended_ = false;
suspendWanted = false;
static if(__traits(hasMember, event, "setIfInitialized"))
event.setIfInitialized();
else
event.set();
}
/// ditto
public bool suspended() {
return suspended_;
}
/++
Stops playback and unsupends if necessary and exits.
Call this instead of join.
Please note: you should never call this from inside an audio
callback, as it will crash or deadlock. Instead, just return false
from your buffer fill function to indicate that you are done.
History:
Added December 30, 2021 (dub v10.5)
+/
public Throwable exit(bool rethrow = false) {
exiting = true;
unsuspend();
if(ao)
ao.stop();
return join(rethrow);
}
private void run() {
version(linux) {
// this thread has no business intercepting signals from the main thread,
// so gonna block a couple of them
import core.sys.posix.signal;
sigset_t sigset;
auto err = sigemptyset(&sigset);
assert(!err);
err = sigaddset(&sigset, SIGINT); assert(!err);
err = sigaddset(&sigset, SIGCHLD); assert(!err);
err = sigprocmask(SIG_BLOCK, &sigset, null);
assert(!err);
}
AudioOutput ao = AudioOutput(device, SampleRate, channels);
this.ao = &ao;
scope(exit) this.ao = null;
auto omg = this;
ao.fillData = (short[] buffer) {
short[BUFFER_SIZE_SHORT] bfr;
bool first = true;
if(fillDatasLength) {
for(int idx = 0; idx < fillDatasLength; idx++) {
auto dg = fillDatas[idx];
auto ret = dg(bfr[0 .. buffer.length][]);
foreach(i, v; bfr[0 .. buffer.length][]) {
int val;
if(first)
val = 0;
else
val = buffer[i];
int a = val;
int b = v;
int cap = a + b;
if(cap > short.max) cap = short.max;
else if(cap < short.min) cap = short.min;
val = cast(short) cap;
buffer[i] = cast(short) val;
}
if(!ret) {
// it returned false meaning this one is finished...
synchronized(omg) {
fillDatas[idx] = fillDatas[fillDatasLength - 1];
fillDatasLength--;
}
idx--;
}
first = false;
}
} else {
buffer[] = 0;
}
};
//try
resume_from_suspend:
ao.play();
/+
catch(Throwable t) {
import std.stdio;
writeln(t);
}
+/
if(suspendWanted) {
ao.close();
event.initialize(true, false);
if(event.wait() && !exiting) {
event.reset();
ao.open();
goto resume_from_suspend;
}
}
event.terminate();
}
static if(__VERSION__ > 2080) {
import core.sync.event;
} else {
// bad emulation of the Event but meh
static struct Event {
void terminate() {}
void initialize(bool, bool) {}
bool isSet;
void set() { isSet = true; }
void reset() { isSet = false; }
bool wait() {
while(!isSet) {
Thread.sleep(500.msecs);
}
isSet = false;
return true;
}
}
}
Event event;
}
import core.stdc.config;
version(linux) version=ALSA;
version(Windows) version=WinMM;
version(ALSA) {
// this is the virtual rawmidi device on my computer at least
// maybe later i'll make it probe
//
// Getting midi to actually play on Linux is a bit of a pain.
// Here's what I did:
/*
# load the kernel driver, if amidi -l gives ioctl error,
# you haven't done this yet!
modprobe snd-virmidi
# start a software synth. timidity -iA is also an option
fluidsynth soundfont.sf2
# connect the virtual hardware port to the synthesizer
aconnect 24:0 128:0
I might also add a snd_seq client here which is a bit
easier to setup but for now I'm using the rawmidi so you
gotta get them connected somehow.
*/
// fyi raw midi dump: amidi -d --port hw:4,0
// connect my midi out to fluidsynth: aconnect 28:0 128:0
// and my keyboard to it: aconnect 32:0 128:0
}
/// Thrown on audio failures.
/// Subclass this to provide OS-specific exceptions
class AudioException : Exception {
this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
super(message, file, line, next);
}
}
/++
Gives PCM input access (such as a microphone).
History:
Windows support added May 10, 2020 and the API got overhauled too.
+/
struct AudioInput {
version(ALSA) {
snd_pcm_t* handle;
} else version(WinMM) {
HWAVEIN handle;
HANDLE event;
} else static assert(0);
@disable this();
@disable this(this);
int channels;
int SampleRate;
/// Always pass card == 0.
this(int card, int SampleRate = 44100, int channels = 2) {
assert(card == 0);
this("default", SampleRate, channels);
}
/++
`device` is a device name. On Linux, it is the ALSA string.
On Windows, it is currently ignored, so you should pass "default"
or null so when it does get implemented your code won't break.
History:
Added Nov 8, 2020.
+/
this(string device, int SampleRate = 44100, int channels = 2) {
assert(channels == 1 || channels == 2);
this.channels = channels;
this.SampleRate = SampleRate;
version(ALSA) {
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_CAPTURE, SampleRate, channels, device);
} else version(WinMM) {
event = CreateEvent(null, false /* manual reset */, false /* initially triggered */, null);
WAVEFORMATEX format;
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = 2;
format.nSamplesPerSec = SampleRate;
format.nAvgBytesPerSec = SampleRate * channels * 2; // two channels, two bytes per sample
format.nBlockAlign = 4;
format.wBitsPerSample = 16;
format.cbSize = 0;
if(auto err = waveInOpen(&handle, WAVE_MAPPER, &format, cast(DWORD_PTR) event, cast(DWORD_PTR) &this, CALLBACK_EVENT))
throw new WinMMException("wave in open", err);
} else static assert(0);
}
/// Data is delivered as interleaved stereo, LE 16 bit, 44.1 kHz
/// Each item in the array thus alternates between left and right channel
/// and it takes a total of 88,200 items to make one second of sound.
///
/// Returns the slice of the buffer actually read into
///
/// LINUX ONLY. You should prolly use [record] instead
version(ALSA)
short[] read(short[] buffer) {
snd_pcm_sframes_t read;
read = snd_pcm_readi(handle, buffer.ptr, buffer.length / channels /* div number of channels apparently */);
if(read < 0) {
read = snd_pcm_recover(handle, cast(int) read, 0);
if(read < 0)
throw new AlsaException("pcm read", cast(int)read);
return null;
}
return buffer[0 .. read * channels];
}
/// passes a buffer of data to fill
///
/// Data is delivered as interleaved stereo, LE 16 bit, 44.1 kHz
/// Each item in the array thus alternates between left and right channel
/// and it takes a total of 88,200 items to make one second of sound.
void delegate(short[]) receiveData;
///
void stop() {
recording = false;
}
/// First, set [receiveData], then call this.
void record() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
assert(receiveData !is null);
recording = true;
version(ALSA) {
short[BUFFER_SIZE_SHORT] buffer;
while(recording) {
auto got = read(buffer);
receiveData(got);
}
} else version(WinMM) {
enum numBuffers = 2; // use a lot of buffers to get smooth output with Sleep, see below
short[BUFFER_SIZE_SHORT][numBuffers] buffers;
WAVEHDR[numBuffers] headers;
foreach(i, ref header; headers) {
auto buffer = buffers[i][];
header.lpData = cast(char*) buffer.ptr;
header.dwBufferLength = cast(int) buffer.length * cast(int) short.sizeof;
header.dwFlags = 0;// WHDR_BEGINLOOP | WHDR_ENDLOOP;
header.dwLoops = 0;
if(auto err = waveInPrepareHeader(handle, &header, header.sizeof))
throw new WinMMException("prepare header", err);
header.dwUser = 1; // mark that the driver is using it
if(auto err = waveInAddBuffer(handle, &header, header.sizeof))
throw new WinMMException("wave in read", err);
}
waveInStart(handle);
scope(failure) waveInReset(handle);
while(recording) {
if(auto err = WaitForSingleObject(event, INFINITE))
throw new Exception("WaitForSingleObject");
if(!recording)
break;
foreach(ref header; headers) {
if(!(header.dwFlags & WHDR_DONE)) continue;
receiveData((cast(short*) header.lpData)[0 .. header.dwBytesRecorded / short.sizeof]);
if(!recording) break;
header.dwUser = 1; // mark that the driver is using it
if(auto err = waveInAddBuffer(handle, &header, header.sizeof)) {
throw new WinMMException("waveInAddBuffer", err);
}
}
}
/*
if(auto err = waveInStop(handle))
throw new WinMMException("wave in stop", err);
*/
if(auto err = waveInReset(handle)) {
throw new WinMMException("wave in reset", err);
}
still_in_use:
foreach(idx, header; headers)
if(!(header.dwFlags & WHDR_DONE)) {
Sleep(1);
goto still_in_use;
}
foreach(ref header; headers)
if(auto err = waveInUnprepareHeader(handle, &header, header.sizeof)) {
throw new WinMMException("unprepare header", err);
}
ResetEvent(event);
} else static assert(0);
}
private bool recording;
~this() {
receiveData = null;
version(ALSA) {
snd_pcm_close(handle);
} else version(WinMM) {
if(auto err = waveInClose(handle))
throw new WinMMException("close", err);
CloseHandle(event);
// in wine (though not Windows nor winedbg as far as I can tell)
// this randomly segfaults. the sleep prevents it. idk why.
Sleep(5);
} else static assert(0);
}
}
///
enum SampleRateFull = 44100;
/// Gives PCM output access (such as the speakers).
struct AudioOutput {
version(ALSA) {
snd_pcm_t* handle;
} else version(WinMM) {
HWAVEOUT handle;
}
@disable this();
// This struct must NEVER be moved or copied, a pointer to it may
// be passed to a device driver and stored!
@disable this(this);
private int SampleRate;
private int channels;
private string device;
/++
`device` is a device name. On Linux, it is the ALSA string.
On Windows, it is currently ignored, so you should pass "default"
or null so when it does get implemented your code won't break.
History:
Added Nov 8, 2020.
+/
this(string device, int SampleRate = 44100, int channels = 2) {
assert(channels == 1 || channels == 2);
this.SampleRate = SampleRate;
this.channels = channels;
this.device = device;
open();
}
/// Always pass card == 0.
this(int card, int SampleRate = 44100, int channels = 2) {
assert(card == 0);
this("default", SampleRate, channels);
}
/// passes a buffer of data to fill
///
/// Data is assumed to be interleaved stereo, LE 16 bit, 44.1 kHz (unless you change that in the ctor)
/// Each item in the array thus alternates between left and right channel (unless you change that in the ctor)
/// and it takes a total of 88,200 items to make one second of sound.
void delegate(short[]) fillData;
shared(bool) playing = false; // considered to be volatile
/// Starts playing, loops until stop is called
void play() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
if(handle is null)
open();
assert(fillData !is null);
playing = true;
version(ALSA) {
short[BUFFER_SIZE_SHORT] buffer;
while(playing) {
auto err = snd_pcm_wait(handle, 500);
if(err < 0) {
// see: https://stackoverflow.com/a/59400592/1457000
err = snd_pcm_recover(handle, err, 0);
if(err)
throw new AlsaException("pcm recover failed after pcm_wait did ", err);
//throw new AlsaException("uh oh", err);
continue;
}
if(err == 0)
continue;
// err == 0 means timeout
// err == 1 means ready
auto ready = snd_pcm_avail_update(handle);
if(ready < 0) {
//import std.stdio; writeln("recover");
// actually it seems ok to just try again..
// err = snd_pcm_recover(handle, err, 0);
//if(err)
//throw new AlsaException("avail", cast(int)ready);
continue;
}
if(ready > BUFFER_SIZE_FRAMES)
ready = BUFFER_SIZE_FRAMES;
//import std.stdio; writeln("filling ", ready);
fillData(buffer[0 .. ready * channels]);
if(playing) {
snd_pcm_sframes_t written;
auto data = buffer[0 .. ready * channels];
while(data.length) {
written = snd_pcm_writei(handle, data.ptr, data.length / channels);
if(written < 0) {
//import std.stdio; writeln(written);
written = snd_pcm_recover(handle, cast(int)written, 0);
//import std.stdio; writeln("recover ", written);
if (written < 0) throw new AlsaException("pcm write", cast(int)written);
}
data = data[written * channels .. $];
}
}
}
} else version(WinMM) {
enum numBuffers = 4; // use a lot of buffers to get smooth output with Sleep, see below
short[BUFFER_SIZE_SHORT][numBuffers] buffers;
WAVEHDR[numBuffers] headers;
foreach(i, ref header; headers) {
// since this is wave out, it promises not to write...
auto buffer = buffers[i][];
header.lpData = cast(char*) buffer.ptr;
header.dwBufferLength = cast(int) buffer.length * cast(int) short.sizeof;
header.dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
header.dwLoops = 1;
if(auto err = waveOutPrepareHeader(handle, &header, header.sizeof))
throw new WinMMException("prepare header", err);
// prime it
fillData(buffer[]);
// indicate that they are filled and good to go
header.dwUser = 1;
}
while(playing) {
// and queue both to be played, if they are ready
foreach(ref header; headers)
if(header.dwUser) {
if(auto err = waveOutWrite(handle, &header, header.sizeof))
throw new WinMMException("wave out write", err);
header.dwUser = 0;
}
Sleep(1);
// the system resolution may be lower than this sleep. To avoid gaps
// in output, we use multiple buffers. Might introduce latency, not
// sure how best to fix. I don't want to busy loop...
}
// wait for the system to finish with our buffers
bool anyInUse = true;
while(anyInUse) {
anyInUse = false;
foreach(header; headers) {
if(!header.dwUser) {
anyInUse = true;
break;
}
}
if(anyInUse)
Sleep(1);
}
foreach(ref header; headers)
if(auto err = waveOutUnprepareHeader(handle, &header, header.sizeof))
throw new WinMMException("unprepare", err);
} else static assert(0);
close();
}
/// Breaks the play loop
void stop() {
playing = false;
}
///
void pause() {
version(WinMM)
waveOutPause(handle);
else version(ALSA)
snd_pcm_pause(handle, 1);
}
///
void unpause() {
version(WinMM)
waveOutRestart(handle);
else version(ALSA)
snd_pcm_pause(handle, 0);
}
version(WinMM) {
extern(Windows)
static void mmCallback(HWAVEOUT handle, UINT msg, void* userData, WAVEHDR* header, DWORD_PTR param2) {
AudioOutput* ao = cast(AudioOutput*) userData;
if(msg == WOM_DONE) {
// we want to bounce back and forth between two buffers
// to keep the sound going all the time
if(ao.playing) {
ao.fillData((cast(short*) header.lpData)[0 .. header.dwBufferLength / short.sizeof]);
}
header.dwUser = 1;
}
}
}
/++
Re-opens the audio device that you have previously [close]d.
History:
Added December 30, 2021
+/
void open() {
assert(handle is null);
assert(!playing);
version(ALSA) {
handle = openAlsaPcm(snd_pcm_stream_t.SND_PCM_STREAM_PLAYBACK, SampleRate, channels, device);
} else version(WinMM) {
WAVEFORMATEX format;
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = cast(ushort) channels;
format.nSamplesPerSec = SampleRate;
format.nAvgBytesPerSec = SampleRate * channels * 2; // two channels, two bytes per sample
format.nBlockAlign = cast(short)(channels * 2);
format.wBitsPerSample = 16;
format.cbSize = 0;
if(auto err = waveOutOpen(&handle, WAVE_MAPPER, &format, cast(DWORD_PTR) &mmCallback, cast(DWORD_PTR) &this, CALLBACK_FUNCTION))
throw new WinMMException("wave out open", err);
} else static assert(0);
}
/++
Closes the audio device. You MUST call [stop] before calling this.
History:
Added December 30, 2021
+/
void close() {
if(!handle)
return;
assert(!playing);
version(ALSA) {
snd_pcm_close(handle);
handle = null;
} else version(WinMM) {
waveOutClose(handle);
handle = null;
} else static assert(0);
}
// FIXME: add async function hooks
~this() {
close();
}
}
/++
For reading midi events from hardware, for example, an electronic piano keyboard
attached to the computer.
+/
struct MidiInput {
// reading midi devices...
version(ALSA) {
snd_rawmidi_t* handle;
} else version(WinMM) {
HMIDIIN handle;
}
@disable this();
@disable this(this);
/+
B0 40 7F # pedal on
B0 40 00 # sustain pedal off
+/
/// Always pass card == 0.
this(int card) {
assert(card == 0);
this("default"); // "hw:4,0"
}
/++
`device` is a device name. On Linux, it is the ALSA string.
On Windows, it is currently ignored, so you should pass "default"
or null so when it does get implemented your code won't break.
History:
Added Nov 8, 2020.
+/
this(string device) {
version(ALSA) {
if(auto err = snd_rawmidi_open(&handle, null, device.toStringz, 0))
throw new AlsaException("rawmidi open", err);
} else version(WinMM) {
if(auto err = midiInOpen(&handle, 0, cast(DWORD_PTR) &mmCallback, cast(DWORD_PTR) &this, CALLBACK_FUNCTION))
throw new WinMMException("midi in open", err);
} else static assert(0);
}
private bool recording = false;
///
void stop() {
recording = false;
}
/++
Records raw midi input data from the device.
The timestamp is given in milliseconds since recording
began (if you keep this program running for 23ish days
it might overflow! so... don't do that.). The other bytes
are the midi messages.
$(PITFALL Do not call any other multimedia functions from the callback!)
+/
void record(void delegate(uint timestamp, ubyte b1, ubyte b2, ubyte b3) dg) {
version(ALSA) {
recording = true;
ubyte[1024] data;
import core.time;
auto start = MonoTime.currTime;
while(recording) {
auto read = snd_rawmidi_read(handle, data.ptr, data.length);
if(read < 0)
throw new AlsaException("midi read", cast(int) read);
auto got = data[0 .. read];
while(got.length) {
// FIXME some messages are fewer bytes....
dg(cast(uint) (MonoTime.currTime - start).total!"msecs", got[0], got[1], got[2]);
got = got[3 .. $];
}
}
} else version(WinMM) {
recording = true;
this.dg = dg;
scope(exit)
this.dg = null;
midiInStart(handle);
scope(exit)
midiInReset(handle);
while(recording) {
Sleep(1);
}
} else static assert(0);
}
version(WinMM)
private void delegate(uint timestamp, ubyte b1, ubyte b2, ubyte b3) dg;
version(WinMM)
extern(Windows)
static
void mmCallback(HMIDIIN handle, UINT msg, DWORD_PTR user, DWORD_PTR param1, DWORD_PTR param2) {
MidiInput* mi = cast(MidiInput*) user;
if(msg == MIM_DATA) {
mi.dg(
cast(uint) param2,
param1 & 0xff,
(param1 >> 8) & 0xff,
(param1 >> 16) & 0xff
);
}
}
~this() {
version(ALSA) {
snd_rawmidi_close(handle);
} else version(WinMM) {
midiInClose(handle);
} else static assert(0);
}
}
/// Gives MIDI output access.
struct MidiOutput {
version(ALSA) {
snd_rawmidi_t* handle;
} else version(WinMM) {
HMIDIOUT handle;
}
@disable this();
@disable this(this);
/// Always pass card == 0.
this(int card) {
assert(card == 0);
this("default"); // "hw:3,0"
}
/++
`device` is a device name. On Linux, it is the ALSA string.
On Windows, it is currently ignored, so you should pass "default"
or null so when it does get implemented your code won't break.
If you pass the string "DUMMY", it will not actually open a device
and simply be a do-nothing mock object;
History:
Added Nov 8, 2020.
Support for the "DUMMY" device was added on January 2, 2022.
+/
this(string device) {
if(device == "DUMMY")
return;
version(ALSA) {
if(auto err = snd_rawmidi_open(null, &handle, device.toStringz, 0))
throw new AlsaException("rawmidi open", err);
} else version(WinMM) {
if(auto err = midiOutOpen(&handle, 0, 0, 0, CALLBACK_NULL))
throw new WinMMException("midi out open", err);
} else static assert(0);
}
void silenceAllNotes() {
foreach(a; 0 .. 16)
writeMidiMessage((0x0b << 4)|a /*MIDI_EVENT_CONTROLLER*/, 123, 0);
}
/// Send a reset message, silencing all notes
void reset() {
if(!handle) return;
version(ALSA) {
silenceAllNotes();
static immutable ubyte[1] resetCmd = [0xff];
writeRawMessageData(resetCmd[]);
// and flush it immediately
snd_rawmidi_drain(handle);
} else version(WinMM) {
if(auto error = midiOutReset(handle))
throw new WinMMException("midi reset", error);
} else static assert(0);
}
/// Writes a single low-level midi message
/// Timing and sending sane data is your responsibility!
void writeMidiMessage(int status, int param1, int param2) {
if(!handle) return;
version(ALSA) {
ubyte[3] dataBuffer;
dataBuffer[0] = cast(ubyte) status;
dataBuffer[1] = cast(ubyte) param1;
dataBuffer[2] = cast(ubyte) param2;
auto msg = status >> 4;
ubyte[] data;
if(msg == MidiEvent.ProgramChange || msg == MidiEvent.ChannelAftertouch)
data = dataBuffer[0 .. 2];
else
data = dataBuffer[];
writeRawMessageData(data);
} else version(WinMM) {
DWORD word = (param2 << 16) | (param1 << 8) | status;
if(auto error = midiOutShortMsg(handle, word))
throw new WinMMException("midi out", error);
} else static assert(0);
}
/// Writes a series of individual raw messages.
/// Timing and sending sane data is your responsibility!
/// The data should NOT include any timestamp bytes - each midi message should be 2 or 3 bytes.
void writeRawMessageData(scope const(ubyte)[] data) {
if(!handle) return;
if(data.length == 0)
return;
version(ALSA) {
ssize_t written;
while(data.length) {
written = snd_rawmidi_write(handle, data.ptr, data.length);
if(written < 0)
throw new AlsaException("midi write", cast(int) written);
data = data[cast(int) written .. $];
}
} else version(WinMM) {
while(data.length) {
auto msg = data[0] >> 4;
if(msg == MidiEvent.ProgramChange || msg == MidiEvent.ChannelAftertouch) {
writeMidiMessage(data[0], data[1], 0);
data = data[2 .. $];
} else {
writeMidiMessage(data[0], data[1], data[2]);
data = data[3 .. $];
}
}
} else static assert(0);
}
~this() {
if(!handle) return;
version(ALSA) {
snd_rawmidi_close(handle);
} else version(WinMM) {
midiOutClose(handle);
} else static assert(0);
}
}
// FIXME: maybe add a PC speaker beep function for completeness
/// Interfaces with the default sound card. You should only have a single instance of this and it should
/// be stack allocated, so its destructor cleans up after it.
///
/// A mixer gives access to things like volume controls and mute buttons. It should also give a
/// callback feature to alert you of when the settings are changed by another program.
version(ALSA) // FIXME
struct AudioMixer {
// To port to a new OS: put the data in the right version blocks
// then implement each function. Leave else static assert(0) at the
// end of each version group in a function so it is easier to implement elsewhere later.
//
// If a function is only relevant on your OS, put the whole function in a version block
// and give it an OS specific name of some sort.
//
// Feel free to do that btw without worrying about lowest common denominator: we want low level access when we want it.
//
// Put necessary bindings at the end of the file, or use an import if you like, but I prefer these files to be standalone.
version(ALSA) {
snd_mixer_t* handle;
snd_mixer_selem_id_t* sid;
snd_mixer_elem_t* selem;
c_long maxVolume, minVolume; // these are ok to use if you are writing ALSA specific code i guess
enum selemName = "Master";
}
@disable this();
@disable this(this);
/// Only cardId == 0 is supported
this(int cardId) {
assert(cardId == 0, "Pass 0 to use default sound card.");
this("default");
}
/++
`device` is a device name. On Linux, it is the ALSA string.
On Windows, it is currently ignored, so you should pass "default"
or null so when it does get implemented your code won't break.
History:
Added Nov 8, 2020.
+/
this(string device) {
version(ALSA) {
if(auto err = snd_mixer_open(&handle, 0))
throw new AlsaException("open sound", err);
scope(failure)
snd_mixer_close(handle);
if(auto err = snd_mixer_attach(handle, device.toStringz))
throw new AlsaException("attach to sound card", err);
if(auto err = snd_mixer_selem_register(handle, null, null))
throw new AlsaException("register mixer", err);
if(auto err = snd_mixer_load(handle))
throw new AlsaException("load mixer", err);
if(auto err = snd_mixer_selem_id_malloc(&sid))
throw new AlsaException("master channel open", err);
scope(failure)
snd_mixer_selem_id_free(sid);
snd_mixer_selem_id_set_index(sid, 0);
snd_mixer_selem_id_set_name(sid, selemName);
selem = snd_mixer_find_selem(handle, sid);
if(selem is null)
throw new AlsaException("find master element", 0);
if(auto err = snd_mixer_selem_get_playback_volume_range(selem, &minVolume, &maxVolume))
throw new AlsaException("get volume range", err);
version(with_eventloop) {
import arsd.eventloop;
addFileEventListeners(getAlsaFileDescriptors()[0], &eventListener, null, null);
setAlsaElemCallback(&alsaCallback);
}
} else static assert(0);
}
~this() {
version(ALSA) {
version(with_eventloop) {
import arsd.eventloop;
removeFileEventListeners(getAlsaFileDescriptors()[0]);
}
snd_mixer_selem_id_free(sid);
snd_mixer_close(handle);
} else static assert(0);
}
version(ALSA)
version(with_eventloop) {
static struct MixerEvent {}
nothrow @nogc
extern(C) static int alsaCallback(snd_mixer_elem_t*, uint) {
import arsd.eventloop;
try
send(MixerEvent());
catch(Exception)
return 1;
return 0;
}
void eventListener(int fd) {
handleAlsaEvents();
}
}
/// Gets the master channel's mute state
/// Note: this affects shared system state and you should not use it unless the end user wants you to.
@property bool muteMaster() {
version(ALSA) {
int result;
if(auto err = snd_mixer_selem_get_playback_switch(selem, 0, &result))
throw new AlsaException("get mute state", err);
return result == 0;
} else static assert(0);
}
/// Mutes or unmutes the master channel
/// Note: this affects shared system state and you should not use it unless the end user wants you to.
@property void muteMaster(bool mute) {
version(ALSA) {
if(auto err = snd_mixer_selem_set_playback_switch_all(selem, mute ? 0 : 1))
throw new AlsaException("set mute state", err);
} else static assert(0);
}
/// returns a percentage, between 0 and 100 (inclusive)
int getMasterVolume() {
version(ALSA) {
auto volume = getMasterVolumeExact();
return cast(int)(volume * 100 / (maxVolume - minVolume));
} else static assert(0);
}
/// Gets the exact value returned from the operating system. The range may vary.
int getMasterVolumeExact() {
version(ALSA) {
c_long volume;
snd_mixer_selem_get_playback_volume(selem, 0, &volume);
return cast(int)volume;
} else static assert(0);
}
/// sets a percentage on the volume, so it must be 0 <= volume <= 100
/// Note: this affects shared system state and you should not use it unless the end user wants you to.
void setMasterVolume(int volume) {
version(ALSA) {
assert(volume >= 0 && volume <= 100);
setMasterVolumeExact(cast(int)(volume * (maxVolume - minVolume) / 100));
} else static assert(0);
}
/// Sets an exact volume. Must be in range of the OS provided min and max.
void setMasterVolumeExact(int volume) {
version(ALSA) {
if(auto err = snd_mixer_selem_set_playback_volume_all(selem, volume))
throw new AlsaException("set volume", err);
} else static assert(0);
}
version(ALSA) {
/// Gets the ALSA descriptors which you can watch for events
/// on using regular select, poll, epoll, etc.
int[] getAlsaFileDescriptors() {
import core.sys.posix.poll;
pollfd[32] descriptors = void;
int got = snd_mixer_poll_descriptors(handle, descriptors.ptr, descriptors.length);
int[] result;
result.length = got;
foreach(i, desc; descriptors[0 .. got])
result[i] = desc.fd;
return result;
}
/// When the FD is ready, call this to let ALSA do its thing.
void handleAlsaEvents() {
snd_mixer_handle_events(handle);
}
/// Set a callback for the master volume change events.
void setAlsaElemCallback(snd_mixer_elem_callback_t dg) {
snd_mixer_elem_set_callback(selem, dg);
}
}
}
// ****************
// Midi helpers
// ****************
// FIXME: code the .mid file format, read and write
enum MidiEvent {
NoteOff = 0x08,
NoteOn = 0x09,
NoteAftertouch = 0x0a,
Controller = 0x0b,
ProgramChange = 0x0c, // one param
ChannelAftertouch = 0x0d, // one param
PitchBend = 0x0e,
}
enum MidiNote : ubyte {
middleC = 60,
A = 69, // 440 Hz
As = 70,
B = 71,
C = 72,
Cs = 73,
D = 74,
Ds = 75,
E = 76,
F = 77,
Fs = 78,
G = 79,
Gs = 80,
}
/// Puts a note on at the beginning of the passed slice, advancing it by the amount of the message size.
/// Returns the message slice.
///
/// See: http://www.midi.org/techspecs/midimessages.php
ubyte[] midiNoteOn(ref ubyte[] where, ubyte channel, byte note, byte velocity) {
where[0] = (MidiEvent.NoteOn << 4) | (channel&0x0f);
where[1] = note;
where[2] = velocity;
auto it = where[0 .. 3];
where = where[3 .. $];
return it;
}
/// Note off.
ubyte[] midiNoteOff(ref ubyte[] where, ubyte channel, byte note, byte velocity) {
where[0] = (MidiEvent.NoteOff << 4) | (channel&0x0f);
where[1] = note;
where[2] = velocity;
auto it = where[0 .. 3];
where = where[3 .. $];
return it;
}
/// Aftertouch.
ubyte[] midiNoteAftertouch(ref ubyte[] where, ubyte channel, byte note, byte pressure) {
where[0] = (MidiEvent.NoteAftertouch << 4) | (channel&0x0f);
where[1] = note;
where[2] = pressure;
auto it = where[0 .. 3];
where = where[3 .. $];
return it;
}
/// Controller.
ubyte[] midiNoteController(ref ubyte[] where, ubyte channel, byte controllerNumber, byte controllerValue) {
where[0] = (MidiEvent.Controller << 4) | (channel&0x0f);
where[1] = controllerNumber;
where[2] = controllerValue;
auto it = where[0 .. 3];
where = where[3 .. $];
return it;
}
/// Program change.
ubyte[] midiProgramChange(ref ubyte[] where, ubyte channel, byte program) {
where[0] = (MidiEvent.ProgramChange << 4) | (channel&0x0f);
where[1] = program;
auto it = where[0 .. 2];
where = where[2 .. $];
return it;
}
/// Channel aftertouch.
ubyte[] midiChannelAftertouch(ref ubyte[] where, ubyte channel, byte amount) {
where[0] = (MidiEvent.ProgramChange << 4) | (channel&0x0f);
where[1] = amount;
auto it = where[0 .. 2];
where = where[2 .. $];
return it;
}
/// Pitch bend. FIXME doesn't work right
ubyte[] midiNotePitchBend(ref ubyte[] where, ubyte channel, short change) {
/*
first byte is llllll
second byte is mmmmmm
Pitch Bend Change. 0mmmmmmm This message is sent to indicate a change in the pitch bender (wheel or lever, typically). The pitch bender is measured by a fourteen bit value. Center (no pitch change) is 2000H. Sensitivity is a function of the transmitter. (llllll) are the least significant 7 bits. (mmmmmm) are the most significant 7 bits.
*/
where[0] = (MidiEvent.PitchBend << 4) | (channel&0x0f);
// FIXME
where[1] = 0;
where[2] = 0;
auto it = where[0 .. 3];
where = where[3 .. $];
return it;
}
// ****************
// Wav helpers
// ****************
// FIXME: the .wav file format should be here, read and write (at least basics)
// as well as some kind helpers to generate some sounds.
// ****************
// OS specific helper stuff follows
// ****************
private const(char)* toStringz(string s) {
return s.ptr; // FIXME jic
}
version(ALSA)
// Opens the PCM device with default settings: stereo, 16 bit, 44.1 kHz, interleaved R/W.
snd_pcm_t* openAlsaPcm(snd_pcm_stream_t direction, int SampleRate, int channels, string cardName = "default") {
snd_pcm_t* handle;
snd_pcm_hw_params_t* hwParams;
/* Open PCM and initialize hardware */
// import arsd.core;
// writeln("before");
if (auto err = snd_pcm_open(&handle, cardName.toStringz, direction, 0))
throw new AlsaException("open device", err);
// writeln("after");
scope(failure)
snd_pcm_close(handle);
if (auto err = snd_pcm_hw_params_malloc(&hwParams))
throw new AlsaException("params malloc", err);
scope(exit)
snd_pcm_hw_params_free(hwParams);
if (auto err = snd_pcm_hw_params_any(handle, hwParams))
// can actually survive a failure here, we will just move forward
{} // throw new AlsaException("params init", err);
if (auto err = snd_pcm_hw_params_set_access(handle, hwParams, snd_pcm_access_t.SND_PCM_ACCESS_RW_INTERLEAVED))
throw new AlsaException("params access", err);
if (auto err = snd_pcm_hw_params_set_format(handle, hwParams, snd_pcm_format.SND_PCM_FORMAT_S16_LE))
throw new AlsaException("params format", err);
uint rate = SampleRate;
int dir = 0;
if (auto err = snd_pcm_hw_params_set_rate_near(handle, hwParams, &rate, &dir))
throw new AlsaException("params rate", err);
assert(rate == SampleRate); // cheap me
if (auto err = snd_pcm_hw_params_set_channels(handle, hwParams, channels))
throw new AlsaException("params channels", err);
uint periods = 4;
{
auto err = snd_pcm_hw_params_set_periods_near(handle, hwParams, &periods, 0);
if(err < 0)
throw new AlsaException("periods", err);
// import std.stdio; writeln(periods);
snd_pcm_uframes_t sz = (BUFFER_SIZE_FRAMES * periods);
err = snd_pcm_hw_params_set_buffer_size_near(handle, hwParams, &sz);
if(err < 0)
throw new AlsaException("buffer size", err);
}
if (auto err = snd_pcm_hw_params(handle, hwParams))
throw new AlsaException("params install", err);
/* Setting up the callbacks */
snd_pcm_sw_params_t* swparams;
if(auto err = snd_pcm_sw_params_malloc(&swparams))
throw new AlsaException("sw malloc", err);
scope(exit)
snd_pcm_sw_params_free(swparams);
if(auto err = snd_pcm_sw_params_current(handle, swparams))
throw new AlsaException("sw set", err);
if(auto err = snd_pcm_sw_params_set_avail_min(handle, swparams, BUFFER_SIZE_FRAMES))
throw new AlsaException("sw min", err);
if(auto err = snd_pcm_sw_params_set_start_threshold(handle, swparams, 0))
throw new AlsaException("sw threshold", err);
if(auto err = snd_pcm_sw_params(handle, swparams))
throw new AlsaException("sw params", err);
/* finish setup */
// writeln("prepare");
if (auto err = snd_pcm_prepare(handle))
throw new AlsaException("prepare", err);
// writeln("done");
assert(handle !is null);
return handle;
}
version(ALSA)
class AlsaException : AudioException {
this(string message, int error, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
auto msg = snd_strerror(error);
import core.stdc.string;
super(cast(string) (message ~ ": " ~ msg[0 .. strlen(msg)]), file, line, next);
}
}
version(WinMM)
class WinMMException : AudioException {
this(string message, int error, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
// FIXME: format the error
// midiOutGetErrorText, etc.
super(message, file, line, next);
}
}
// ****************
// Bindings follow
// ****************
version(ALSA) {
extern(C):
@nogc nothrow:
pragma(lib, "asound");
private import core.sys.posix.poll;
const(char)* snd_strerror(int);
// pcm
enum snd_pcm_stream_t {
SND_PCM_STREAM_PLAYBACK,
SND_PCM_STREAM_CAPTURE
}
enum snd_pcm_access_t {
/** mmap access with simple interleaved channels */
SND_PCM_ACCESS_MMAP_INTERLEAVED = 0,
/** mmap access with simple non interleaved channels */
SND_PCM_ACCESS_MMAP_NONINTERLEAVED,
/** mmap access with complex placement */
SND_PCM_ACCESS_MMAP_COMPLEX,
/** snd_pcm_readi/snd_pcm_writei access */
SND_PCM_ACCESS_RW_INTERLEAVED,
/** snd_pcm_readn/snd_pcm_writen access */
SND_PCM_ACCESS_RW_NONINTERLEAVED,
SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
}
enum snd_pcm_format {
/** Unknown */
SND_PCM_FORMAT_UNKNOWN = -1,
/** Signed 8 bit */
SND_PCM_FORMAT_S8 = 0,
/** Unsigned 8 bit */
SND_PCM_FORMAT_U8,
/** Signed 16 bit Little Endian */
SND_PCM_FORMAT_S16_LE,
/** Signed 16 bit Big Endian */
SND_PCM_FORMAT_S16_BE,
/** Unsigned 16 bit Little Endian */
SND_PCM_FORMAT_U16_LE,
/** Unsigned 16 bit Big Endian */
SND_PCM_FORMAT_U16_BE,
/** Signed 24 bit Little Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_S24_LE,
/** Signed 24 bit Big Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_S24_BE,
/** Unsigned 24 bit Little Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_U24_LE,
/** Unsigned 24 bit Big Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_U24_BE,
/** Signed 32 bit Little Endian */
SND_PCM_FORMAT_S32_LE,
/** Signed 32 bit Big Endian */
SND_PCM_FORMAT_S32_BE,
/** Unsigned 32 bit Little Endian */
SND_PCM_FORMAT_U32_LE,
/** Unsigned 32 bit Big Endian */
SND_PCM_FORMAT_U32_BE,
/** Float 32 bit Little Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT_LE,
/** Float 32 bit Big Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT_BE,
/** Float 64 bit Little Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT64_LE,
/** Float 64 bit Big Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT64_BE,
/** IEC-958 Little Endian */
SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
/** IEC-958 Big Endian */
SND_PCM_FORMAT_IEC958_SUBFRAME_BE,
/** Mu-Law */
SND_PCM_FORMAT_MU_LAW,
/** A-Law */
SND_PCM_FORMAT_A_LAW,
/** Ima-ADPCM */
SND_PCM_FORMAT_IMA_ADPCM,
/** MPEG */
SND_PCM_FORMAT_MPEG,
/** GSM */
SND_PCM_FORMAT_GSM,
/** Special */
SND_PCM_FORMAT_SPECIAL = 31,
/** Signed 24bit Little Endian in 3bytes format */
SND_PCM_FORMAT_S24_3LE = 32,
/** Signed 24bit Big Endian in 3bytes format */
SND_PCM_FORMAT_S24_3BE,
/** Unsigned 24bit Little Endian in 3bytes format */
SND_PCM_FORMAT_U24_3LE,
/** Unsigned 24bit Big Endian in 3bytes format */
SND_PCM_FORMAT_U24_3BE,
/** Signed 20bit Little Endian in 3bytes format */
SND_PCM_FORMAT_S20_3LE,
/** Signed 20bit Big Endian in 3bytes format */
SND_PCM_FORMAT_S20_3BE,
/** Unsigned 20bit Little Endian in 3bytes format */
SND_PCM_FORMAT_U20_3LE,
/** Unsigned 20bit Big Endian in 3bytes format */
SND_PCM_FORMAT_U20_3BE,
/** Signed 18bit Little Endian in 3bytes format */
SND_PCM_FORMAT_S18_3LE,
/** Signed 18bit Big Endian in 3bytes format */
SND_PCM_FORMAT_S18_3BE,
/** Unsigned 18bit Little Endian in 3bytes format */
SND_PCM_FORMAT_U18_3LE,
/** Unsigned 18bit Big Endian in 3bytes format */
SND_PCM_FORMAT_U18_3BE,
/* G.723 (ADPCM) 24 kbit/s, 8 samples in 3 bytes */
SND_PCM_FORMAT_G723_24,
/* G.723 (ADPCM) 24 kbit/s, 1 sample in 1 byte */
SND_PCM_FORMAT_G723_24_1B,
/* G.723 (ADPCM) 40 kbit/s, 8 samples in 3 bytes */
SND_PCM_FORMAT_G723_40,
/* G.723 (ADPCM) 40 kbit/s, 1 sample in 1 byte */
SND_PCM_FORMAT_G723_40_1B,
/* Direct Stream Digital (DSD) in 1-byte samples (x8) */
SND_PCM_FORMAT_DSD_U8,
/* Direct Stream Digital (DSD) in 2-byte samples (x16) */
SND_PCM_FORMAT_DSD_U16_LE,
SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_DSD_U16_LE,
// I snipped a bunch of endian-specific ones!
}
struct snd_pcm_t {}
struct snd_pcm_hw_params_t {}
struct snd_pcm_sw_params_t {}
int snd_pcm_open(snd_pcm_t**, const char*, snd_pcm_stream_t, int);
int snd_pcm_close(snd_pcm_t*);
int snd_pcm_pause(snd_pcm_t*, int);
int snd_pcm_prepare(snd_pcm_t*);
int snd_pcm_hw_params(snd_pcm_t*, snd_pcm_hw_params_t*);
int snd_pcm_hw_params_set_periods(snd_pcm_t*, snd_pcm_hw_params_t*, uint, int);
int snd_pcm_hw_params_set_periods_near(snd_pcm_t*, snd_pcm_hw_params_t*, uint*, int);
int snd_pcm_hw_params_set_buffer_size(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_uframes_t);
int snd_pcm_hw_params_set_buffer_size_near(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_uframes_t*);
int snd_pcm_hw_params_set_channels(snd_pcm_t*, snd_pcm_hw_params_t*, uint);
int snd_pcm_hw_params_malloc(snd_pcm_hw_params_t**);
void snd_pcm_hw_params_free(snd_pcm_hw_params_t*);
int snd_pcm_hw_params_any(snd_pcm_t*, snd_pcm_hw_params_t*);
int snd_pcm_hw_params_set_access(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_access_t);
int snd_pcm_hw_params_set_format(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_format);
int snd_pcm_hw_params_set_rate_near(snd_pcm_t*, snd_pcm_hw_params_t*, uint*, int*);
int snd_pcm_sw_params_malloc(snd_pcm_sw_params_t**);
void snd_pcm_sw_params_free(snd_pcm_sw_params_t*);
int snd_pcm_sw_params_current(snd_pcm_t *pcm, snd_pcm_sw_params_t *params);
int snd_pcm_sw_params(snd_pcm_t *pcm, snd_pcm_sw_params_t *params);
int snd_pcm_sw_params_set_avail_min(snd_pcm_t*, snd_pcm_sw_params_t*, snd_pcm_uframes_t);
int snd_pcm_sw_params_set_start_threshold(snd_pcm_t*, snd_pcm_sw_params_t*, snd_pcm_uframes_t);
int snd_pcm_sw_params_set_stop_threshold(snd_pcm_t*, snd_pcm_sw_params_t*, snd_pcm_uframes_t);
alias snd_pcm_sframes_t = c_long;
alias snd_pcm_uframes_t = c_ulong;
snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t*, const void*, snd_pcm_uframes_t size);
snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t*, void*, snd_pcm_uframes_t size);
int snd_pcm_wait(snd_pcm_t *pcm, int timeout);
snd_pcm_sframes_t snd_pcm_avail(snd_pcm_t *pcm);
snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);
int snd_pcm_recover (snd_pcm_t* pcm, int err, int silent);
alias snd_lib_error_handler_t = void function (const(char)* file, int line, const(char)* function_, int err, const(char)* fmt, ...);
int snd_lib_error_set_handler (snd_lib_error_handler_t handler);
import core.stdc.stdarg;
private void alsa_message_silencer (const(char)* file, int line, const(char)* function_, int err, const(char)* fmt, ...) @system {}
//k8: ALSAlib loves to trash stderr; shut it up
void silence_alsa_messages () { snd_lib_error_set_handler(&alsa_message_silencer); }
extern(D) shared static this () { silence_alsa_messages(); }
// raw midi
static if(is(size_t == uint))
alias ssize_t = int;
else
alias ssize_t = long;
struct snd_rawmidi_t {}
int snd_rawmidi_open(snd_rawmidi_t**, snd_rawmidi_t**, const char*, int);
int snd_rawmidi_close(snd_rawmidi_t*);
int snd_rawmidi_drain(snd_rawmidi_t*);
ssize_t snd_rawmidi_write(snd_rawmidi_t*, const void*, size_t);
ssize_t snd_rawmidi_read(snd_rawmidi_t*, void*, size_t);
// mixer
struct snd_mixer_t {}
struct snd_mixer_elem_t {}
struct snd_mixer_selem_id_t {}
alias snd_mixer_elem_callback_t = int function(snd_mixer_elem_t*, uint);
int snd_mixer_open(snd_mixer_t**, int mode);
int snd_mixer_close(snd_mixer_t*);
int snd_mixer_attach(snd_mixer_t*, const char*);
int snd_mixer_load(snd_mixer_t*);
// FIXME: those aren't actually void*
int snd_mixer_selem_register(snd_mixer_t*, void*, void*);
int snd_mixer_selem_id_malloc(snd_mixer_selem_id_t**);
void snd_mixer_selem_id_free(snd_mixer_selem_id_t*);
void snd_mixer_selem_id_set_index(snd_mixer_selem_id_t*, uint);
void snd_mixer_selem_id_set_name(snd_mixer_selem_id_t*, const char*);
snd_mixer_elem_t* snd_mixer_find_selem(snd_mixer_t*, const scope snd_mixer_selem_id_t*);
// FIXME: the int should be an enum for channel identifier
int snd_mixer_selem_get_playback_volume(snd_mixer_elem_t*, int, c_long*);
int snd_mixer_selem_get_playback_volume_range(snd_mixer_elem_t*, c_long*, c_long*);
int snd_mixer_selem_set_playback_volume_all(snd_mixer_elem_t*, c_long);
void snd_mixer_elem_set_callback(snd_mixer_elem_t*, snd_mixer_elem_callback_t);
int snd_mixer_poll_descriptors(snd_mixer_t*, pollfd*, uint space);
int snd_mixer_handle_events(snd_mixer_t*);
// FIXME: the first int should be an enum for channel identifier
int snd_mixer_selem_get_playback_switch(snd_mixer_elem_t*, int, int* value);
int snd_mixer_selem_set_playback_switch_all(snd_mixer_elem_t*, int);
}
version(WinMM) {
extern(Windows):
@nogc nothrow:
pragma(lib, "winmm");
import core.sys.windows.windows;
/*
Windows functions include:
http://msdn.microsoft.com/en-us/library/ms713762%28VS.85%29.aspx
http://msdn.microsoft.com/en-us/library/ms713504%28v=vs.85%29.aspx
http://msdn.microsoft.com/en-us/library/windows/desktop/dd798480%28v=vs.85%29.aspx#
http://msdn.microsoft.com/en-US/subscriptions/ms712109.aspx
*/
// pcm
// midi
/+
alias HMIDIOUT = HANDLE;
alias MMRESULT = UINT;
MMRESULT midiOutOpen(HMIDIOUT*, UINT, DWORD, DWORD, DWORD);
MMRESULT midiOutClose(HMIDIOUT);
MMRESULT midiOutReset(HMIDIOUT);
MMRESULT midiOutShortMsg(HMIDIOUT, DWORD);
alias HWAVEOUT = HANDLE;
struct WAVEFORMATEX {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
}
struct WAVEHDR {
void* lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD dwUser;
DWORD dwFlags;
DWORD dwLoops;
WAVEHDR *lpNext;
DWORD reserved;
}
enum UINT WAVE_MAPPER= -1;
MMRESULT waveOutOpen(HWAVEOUT*, UINT_PTR, WAVEFORMATEX*, void* callback, void*, DWORD);
MMRESULT waveOutClose(HWAVEOUT);
MMRESULT waveOutPrepareHeader(HWAVEOUT, WAVEHDR*, UINT);
MMRESULT waveOutUnprepareHeader(HWAVEOUT, WAVEHDR*, UINT);
MMRESULT waveOutWrite(HWAVEOUT, WAVEHDR*, UINT);
MMRESULT waveOutGetVolume(HWAVEOUT, PDWORD);
MMRESULT waveOutSetVolume(HWAVEOUT, DWORD);
enum CALLBACK_TYPEMASK = 0x70000;
enum CALLBACK_NULL = 0;
enum CALLBACK_WINDOW = 0x10000;
enum CALLBACK_TASK = 0x20000;
enum CALLBACK_FUNCTION = 0x30000;
enum CALLBACK_THREAD = CALLBACK_TASK;
enum CALLBACK_EVENT = 0x50000;
enum WAVE_FORMAT_PCM = 1;
enum WHDR_PREPARED = 2;
enum WHDR_BEGINLOOP = 4;
enum WHDR_ENDLOOP = 8;
enum WHDR_INQUEUE = 16;
enum WinMMMessage : UINT {
MM_JOY1MOVE = 0x3A0,
MM_JOY2MOVE,
MM_JOY1ZMOVE,
MM_JOY2ZMOVE, // = 0x3A3
MM_JOY1BUTTONDOWN = 0x3B5,
MM_JOY2BUTTONDOWN,
MM_JOY1BUTTONUP,
MM_JOY2BUTTONUP,
MM_MCINOTIFY, // = 0x3B9
MM_WOM_OPEN = 0x3BB,
MM_WOM_CLOSE,
MM_WOM_DONE,
MM_WIM_OPEN,
MM_WIM_CLOSE,
MM_WIM_DATA,
MM_MIM_OPEN,
MM_MIM_CLOSE,
MM_MIM_DATA,
MM_MIM_LONGDATA,
MM_MIM_ERROR,
MM_MIM_LONGERROR,
MM_MOM_OPEN,
MM_MOM_CLOSE,
MM_MOM_DONE, // = 0x3C9
MM_DRVM_OPEN = 0x3D0,
MM_DRVM_CLOSE,
MM_DRVM_DATA,
MM_DRVM_ERROR,
MM_STREAM_OPEN,
MM_STREAM_CLOSE,
MM_STREAM_DONE,
MM_STREAM_ERROR, // = 0x3D7
MM_MOM_POSITIONCB = 0x3CA,
MM_MCISIGNAL,
MM_MIM_MOREDATA, // = 0x3CC
MM_MIXM_LINE_CHANGE = 0x3D0,
MM_MIXM_CONTROL_CHANGE = 0x3D1
}
enum WOM_OPEN = WinMMMessage.MM_WOM_OPEN;
enum WOM_CLOSE = WinMMMessage.MM_WOM_CLOSE;
enum WOM_DONE = WinMMMessage.MM_WOM_DONE;
enum WIM_OPEN = WinMMMessage.MM_WIM_OPEN;
enum WIM_CLOSE = WinMMMessage.MM_WIM_CLOSE;
enum WIM_DATA = WinMMMessage.MM_WIM_DATA;
uint mciSendStringA(const scope char*,char*,uint,void*);
+/
}
version(with_resampler) {
/* Copyright (C) 2007-2008 Jean-Marc Valin
* Copyright (C) 2008 Thorvald Natvig
* D port by Ketmar // Invisible Vector
*
* Arbitrary resampling code
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/* A-a-a-and now... D port is covered by the following license!
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//module iv.follin.resampler /*is aliced*/;
//import iv.alice;
/*
The design goals of this code are:
- Very fast algorithm
- SIMD-friendly algorithm
- Low memory requirement
- Good *perceptual* quality (and not best SNR)
Warning: This resampler is relatively new. Although I think I got rid of
all the major bugs and I don't expect the API to change anymore, there
may be something I've missed. So use with caution.
This algorithm is based on this original resampling algorithm:
Smith, Julius O. Digital Audio Resampling Home Page
Center for Computer Research in Music and Acoustics (CCRMA),
Stanford University, 2007.
Web published at http://www-ccrma.stanford.edu/~jos/resample/.
There is one main difference, though. This resampler uses cubic
interpolation instead of linear interpolation in the above paper. This
makes the table much smaller and makes it possible to compute that table
on a per-stream basis. In turn, being able to tweak the table for each
stream makes it possible to both reduce complexity on simple ratios
(e.g. 2/3), and get rid of the rounding operations in the inner loop.
The latter both reduces CPU time and makes the algorithm more SIMD-friendly.
*/
version = sincresample_use_full_table;
version(X86) {
version(sincresample_disable_sse) {
} else {
version(D_PIC) {} else version = sincresample_use_sse;
}
}
// ////////////////////////////////////////////////////////////////////////// //
public struct SpeexResampler {
public:
alias Quality = int;
enum : uint {
Fastest = 0,
Voip = 3,
Default = 4,
Desktop = 5,
Music = 8,
Best = 10,
}
enum Error {
OK = 0,
NoMemory,
BadState,
BadArgument,
BadData,
}
private:
nothrow @trusted @nogc:
alias ResamplerFn = int function (ref SpeexResampler st, uint chanIdx, const(float)* indata, uint *indataLen, float *outdata, uint *outdataLen);
private:
uint inRate;
uint outRate;
uint numRate; // from
uint denRate; // to
Quality srQuality;
uint chanCount;
uint filterLen;
uint memAllocSize;
uint bufferSize;
int intAdvance;
int fracAdvance;
float cutoff;
uint oversample;
bool started;
// these are per-channel
int[64] lastSample;
uint[64] sampFracNum;
uint[64] magicSamples;
float* mem;
uint realMemLen; // how much memory really allocated
float* sincTable;
uint sincTableLen;
uint realSincTableLen; // how much memory really allocated
ResamplerFn resampler;
int inStride;
int outStride;
public:
static string errorStr (int err) {
switch (err) with (Error) {
case OK: return "success";
case NoMemory: return "memory allocation failed";
case BadState: return "bad resampler state";
case BadArgument: return "invalid argument";
case BadData: return "bad data passed";
default:
}
return "unknown error";
}
public:
@disable this (this);
~this () { deinit(); }
@property bool inited () const pure { return (resampler !is null); }
void deinit () {
import core.stdc.stdlib : free;
if (mem !is null) { free(mem); mem = null; }
if (sincTable !is null) { free(sincTable); sincTable = null; }
/*
memAllocSize = realMemLen = 0;
sincTableLen = realSincTableLen = 0;
resampler = null;
started = false;
*/
inRate = outRate = numRate = denRate = 0;
srQuality = cast(Quality)666;
chanCount = 0;
filterLen = 0;
memAllocSize = 0;
bufferSize = 0;
intAdvance = 0;
fracAdvance = 0;
cutoff = 0;
oversample = 0;
started = 0;
mem = null;
realMemLen = 0; // how much memory really allocated
sincTable = null;
sincTableLen = 0;
realSincTableLen = 0; // how much memory really allocated
resampler = null;
inStride = outStride = 0;
}
/** Create a new resampler with integer input and output rates.
*
* Params:
* chans = Number of channels to be processed
* inRate = Input sampling rate (integer number of Hz).
* outRate = Output sampling rate (integer number of Hz).
* aquality = Resampling quality between 0 and 10, where 0 has poor quality and 10 has very high quality.
*
* Returns:
* 0 or error code
*/
Error setup (uint chans, uint ainRate, uint aoutRate, Quality aquality/*, usize line=__LINE__*/) {
//{ import core.stdc.stdio; printf("init: %u -> %u at %u\n", ainRate, aoutRate, cast(uint)line); }
import core.stdc.stdlib : malloc, free;
deinit();
if (aquality < 0) aquality = 0;
if (aquality > SpeexResampler.Best) aquality = SpeexResampler.Best;
if (chans < 1 || chans > 16) return Error.BadArgument;
started = false;
inRate = 0;
outRate = 0;
numRate = 0;
denRate = 0;
srQuality = cast(Quality)666; // it's ok
sincTableLen = 0;
memAllocSize = 0;
filterLen = 0;
mem = null;
resampler = null;
cutoff = 1.0f;
chanCount = chans;
inStride = 1;
outStride = 1;
bufferSize = 160;
// per channel data
lastSample[] = 0;
magicSamples[] = 0;
sampFracNum[] = 0;
setQuality(aquality);
setRate(ainRate, aoutRate);
if (auto filterErr = updateFilter()) { deinit(); return filterErr; }
skipZeros(); // make sure that the first samples to go out of the resamplers don't have leading zeros
return Error.OK;
}
/** Set (change) the input/output sampling rates (integer value).
*
* Params:
* ainRate = Input sampling rate (integer number of Hz).
* aoutRate = Output sampling rate (integer number of Hz).
*
* Returns:
* 0 or error code
*/
Error setRate (uint ainRate, uint aoutRate/*, usize line=__LINE__*/) {
//{ import core.stdc.stdio; printf("changing rate: %u -> %u at %u\n", ainRate, aoutRate, cast(uint)line); }
if (inRate == ainRate && outRate == aoutRate) return Error.OK;
//{ import core.stdc.stdio; printf("changing rate: %u -> %u at %u\n", ratioNum, ratioDen, cast(uint)line); }
uint oldDen = denRate;
inRate = ainRate;
outRate = aoutRate;
auto div = gcd(ainRate, aoutRate);
numRate = ainRate/div;
denRate = aoutRate/div;
if (oldDen > 0) {
foreach (ref v; sampFracNum.ptr[0..chanCount]) {
v = v*denRate/oldDen;
// safety net
if (v >= denRate) v = denRate-1;
}
}
return (inited ? updateFilter() : Error.OK);
}
/** Get the current input/output sampling rates (integer value).
*
* Params:
* ainRate = Input sampling rate (integer number of Hz) copied.
* aoutRate = Output sampling rate (integer number of Hz) copied.
*/
void getRate (out uint ainRate, out uint aoutRate) {
ainRate = inRate;
aoutRate = outRate;
}
@property uint getInRate () { return inRate; }
@property uint getOutRate () { return outRate; }
@property uint getChans () { return chanCount; }
/** Get the current resampling ratio. This will be reduced to the least common denominator.
*
* Params:
* ratioNum = Numerator of the sampling rate ratio copied
* ratioDen = Denominator of the sampling rate ratio copied
*/
void getRatio (out uint ratioNum, out uint ratioDen) {
ratioNum = numRate;
ratioDen = denRate;
}
/** Set (change) the conversion quality.
*
* Params:
* quality = Resampling quality between 0 and 10, where 0 has poor quality and 10 has very high quality.
*
* Returns:
* 0 or error code
*/
Error setQuality (Quality aquality) {
if (aquality < 0) aquality = 0;
if (aquality > SpeexResampler.Best) aquality = SpeexResampler.Best;
if (srQuality == aquality) return Error.OK;
srQuality = aquality;
return (inited ? updateFilter() : Error.OK);
}
/** Get the conversion quality.
*
* Returns:
* Resampling quality between 0 and 10, where 0 has poor quality and 10 has very high quality.
*/
int getQuality () { return srQuality; }
/** Get the latency introduced by the resampler measured in input samples.
*
* Returns:
* Input latency;
*/
int inputLatency () { return filterLen/2; }
/** Get the latency introduced by the resampler measured in output samples.
*
* Returns:
* Output latency.
*/
int outputLatency () { return ((filterLen/2)*denRate+(numRate>>1))/numRate; }
/* Make sure that the first samples to go out of the resamplers don't have
* leading zeros. This is only useful before starting to use a newly created
* resampler. It is recommended to use that when resampling an audio file, as
* it will generate a file with the same length. For real-time processing,
* it is probably easier not to use this call (so that the output duration
* is the same for the first frame).
*
* Setup/reset sequence will automatically call this, so it is private.
*/
private void skipZeros () { foreach (immutable i; 0..chanCount) lastSample.ptr[i] = filterLen/2; }
static struct Data {
const(float)[] dataIn;
float[] dataOut;
uint inputSamplesUsed; // out value, in samples (i.e. multiplied by channel count)
uint outputSamplesUsed; // out value, in samples (i.e. multiplied by channel count)
}
/** Resample (an interleaved) float array. The input and output buffers must *not* overlap.
* `data.dataIn` can be empty, but `data.dataOut` can't.
* Function will return number of consumed samples (*not* *frames*!) in `data.inputSamplesUsed`,
* and number of produced samples in `data.outputSamplesUsed`.
* You should provide enough samples for all channels, and all channels will be processed.
*
* Params:
* data = input and output buffers, number of frames consumed and produced
*
* Returns:
* 0 or error code
*/
Error process(string mode="interleaved") (ref Data data) {
static assert(mode == "interleaved" || mode == "sequential");
data.inputSamplesUsed = data.outputSamplesUsed = 0;
if (!inited) return Error.BadState;
if (data.dataIn.length%chanCount || data.dataOut.length < 1 || data.dataOut.length%chanCount) return Error.BadData;
if (data.dataIn.length > uint.max/4 || data.dataOut.length > uint.max/4) return Error.BadData;
static if (mode == "interleaved") {
inStride = outStride = chanCount;
} else {
inStride = outStride = 1;
}
uint iofs = 0, oofs = 0;
immutable uint idclen = cast(uint)(data.dataIn.length/chanCount);
immutable uint odclen = cast(uint)(data.dataOut.length/chanCount);
foreach (immutable i; 0..chanCount) {
data.inputSamplesUsed = idclen;
data.outputSamplesUsed = odclen;
if (data.dataIn.length) {
processOneChannel(i, data.dataIn.ptr+iofs, &data.inputSamplesUsed, data.dataOut.ptr+oofs, &data.outputSamplesUsed);
} else {
processOneChannel(i, null, &data.inputSamplesUsed, data.dataOut.ptr+oofs, &data.outputSamplesUsed);
}
static if (mode == "interleaved") {
++iofs;
++oofs;
} else {
iofs += idclen;
oofs += odclen;
}
}
data.inputSamplesUsed *= chanCount;
data.outputSamplesUsed *= chanCount;
return Error.OK;
}
//HACK for libswresample
// return -1 or number of outframes
int swrconvert (float** outbuf, int outframes, const(float)**inbuf, int inframes) {
if (!inited || outframes < 1 || inframes < 0) return -1;
inStride = outStride = 1;
Data data;
foreach (immutable i; 0..chanCount) {
data.dataIn = (inframes ? inbuf[i][0..inframes] : null);
data.dataOut = (outframes ? outbuf[i][0..outframes] : null);
data.inputSamplesUsed = inframes;
data.outputSamplesUsed = outframes;
if (inframes > 0) {
processOneChannel(i, data.dataIn.ptr, &data.inputSamplesUsed, data.dataOut.ptr, &data.outputSamplesUsed);
} else {
processOneChannel(i, null, &data.inputSamplesUsed, data.dataOut.ptr, &data.outputSamplesUsed);
}
}
return data.outputSamplesUsed;
}
/// Reset a resampler so a new (unrelated) stream can be processed.
void reset () {
lastSample[] = 0;
magicSamples[] = 0;
sampFracNum[] = 0;
//foreach (immutable i; 0..chanCount*(filterLen-1)) mem[i] = 0;
if (mem !is null) mem[0..chanCount*(filterLen-1)] = 0;
skipZeros(); // make sure that the first samples to go out of the resamplers don't have leading zeros
}
private:
Error processOneChannel (uint chanIdx, const(float)* indata, uint* indataLen, float* outdata, uint* outdataLen) {
uint ilen = *indataLen;
uint olen = *outdataLen;
float* x = mem+chanIdx*memAllocSize;
immutable int filterOfs = filterLen-1;
immutable uint xlen = memAllocSize-filterOfs;
immutable int istride = inStride;
if (magicSamples.ptr[chanIdx]) olen -= magic(chanIdx, &outdata, olen);
if (!magicSamples.ptr[chanIdx]) {
while (ilen && olen) {
uint ichunk = (ilen > xlen ? xlen : ilen);
uint ochunk = olen;
if (indata !is null) {
//foreach (immutable j; 0..ichunk) x[j+filterOfs] = indata[j*istride];
if (istride == 1) {
x[filterOfs..filterOfs+ichunk] = indata[0..ichunk];
} else {
auto sp = indata;
auto dp = x+filterOfs;
foreach (immutable j; 0..ichunk) { *dp++ = *sp; sp += istride; }
}
} else {
//foreach (immutable j; 0..ichunk) x[j+filterOfs] = 0;
x[filterOfs..filterOfs+ichunk] = 0;
}
processNative(chanIdx, &ichunk, outdata, &ochunk);
ilen -= ichunk;
olen -= ochunk;
outdata += ochunk*outStride;
if (indata !is null) indata += ichunk*istride;
}
}
*indataLen -= ilen;
*outdataLen -= olen;
return Error.OK;
}
Error processNative (uint chanIdx, uint* indataLen, float* outdata, uint* outdataLen) {
immutable N = filterLen;
int outSample = 0;
float* x = mem+chanIdx*memAllocSize;
uint ilen;
started = true;
// call the right resampler through the function ptr
outSample = resampler(this, chanIdx, x, indataLen, outdata, outdataLen);
if (lastSample.ptr[chanIdx] < cast(int)*indataLen) *indataLen = lastSample.ptr[chanIdx];
*outdataLen = outSample;
lastSample.ptr[chanIdx] -= *indataLen;
ilen = *indataLen;
foreach (immutable j; 0..N-1) x[j] = x[j+ilen];
return Error.OK;
}
int magic (uint chanIdx, float **outdata, uint outdataLen) {
uint tempInLen = magicSamples.ptr[chanIdx];
float* x = mem+chanIdx*memAllocSize;
processNative(chanIdx, &tempInLen, *outdata, &outdataLen);
magicSamples.ptr[chanIdx] -= tempInLen;
// if we couldn't process all "magic" input samples, save the rest for next time
if (magicSamples.ptr[chanIdx]) {
immutable N = filterLen;
foreach (immutable i; 0..magicSamples.ptr[chanIdx]) x[N-1+i] = x[N-1+i+tempInLen];
}
*outdata += outdataLen*outStride;
return outdataLen;
}
Error updateFilter () {
uint oldFilterLen = filterLen;
uint oldAllocSize = memAllocSize;
bool useDirect;
uint minSincTableLen;
uint minAllocSize;
intAdvance = numRate/denRate;
fracAdvance = numRate%denRate;
oversample = qualityMap.ptr[srQuality].oversample;
filterLen = qualityMap.ptr[srQuality].baseLength;
if (numRate > denRate) {
// down-sampling
cutoff = qualityMap.ptr[srQuality].downsampleBandwidth*denRate/numRate;
// FIXME: divide the numerator and denominator by a certain amount if they're too large
filterLen = filterLen*numRate/denRate;
// round up to make sure we have a multiple of 8 for SSE
filterLen = ((filterLen-1)&(~0x7))+8;
if (2*denRate < numRate) oversample >>= 1;
if (4*denRate < numRate) oversample >>= 1;
if (8*denRate < numRate) oversample >>= 1;
if (16*denRate < numRate) oversample >>= 1;
if (oversample < 1) oversample = 1;
} else {
// up-sampling
cutoff = qualityMap.ptr[srQuality].upsampleBandwidth;
}
// choose the resampling type that requires the least amount of memory
version(sincresample_use_full_table) {
useDirect = true;
if (int.max/float.sizeof/denRate < filterLen) goto fail;
} else {
useDirect = (filterLen*denRate <= filterLen*oversample+8 && int.max/float.sizeof/denRate >= filterLen);
}
if (useDirect) {
minSincTableLen = filterLen*denRate;
} else {
if ((int.max/float.sizeof-8)/oversample < filterLen) goto fail;
minSincTableLen = filterLen*oversample+8;
}
if (sincTableLen < minSincTableLen) {
import core.stdc.stdlib : realloc;
auto nslen = cast(uint)(minSincTableLen*float.sizeof);
if (nslen > realSincTableLen) {
if (nslen < 512*1024) nslen = 512*1024; // inc to 3 mb?
auto x = cast(float*)realloc(sincTable, nslen);
if (!x) goto fail;
sincTable = x;
realSincTableLen = nslen;
}
sincTableLen = minSincTableLen;
}
if (useDirect) {
foreach (int i; 0..denRate) {
foreach (int j; 0..filterLen) {
sincTable[i*filterLen+j] = sinc(cutoff, ((j-cast(int)filterLen/2+1)-(cast(float)i)/denRate), filterLen, qualityMap.ptr[srQuality].windowFunc);
}
}
if (srQuality > 8) {
resampler = &resamplerBasicDirect!double;
} else {
resampler = &resamplerBasicDirect!float;
}
} else {
foreach (immutable int i; -4..cast(int)(oversample*filterLen+4)) {
sincTable[i+4] = sinc(cutoff, (i/cast(float)oversample-filterLen/2), filterLen, qualityMap.ptr[srQuality].windowFunc);
}
if (srQuality > 8) {
resampler = &resamplerBasicInterpolate!double;
} else {
resampler = &resamplerBasicInterpolate!float;
}
}
/* Here's the place where we update the filter memory to take into account
the change in filter length. It's probably the messiest part of the code
due to handling of lots of corner cases. */
// adding bufferSize to filterLen won't overflow here because filterLen could be multiplied by float.sizeof above
minAllocSize = filterLen-1+bufferSize;
if (minAllocSize > memAllocSize) {
import core.stdc.stdlib : realloc;
if (int.max/float.sizeof/chanCount < minAllocSize) goto fail;
auto nslen = cast(uint)(chanCount*minAllocSize*mem[0].sizeof);
if (nslen > realMemLen) {
if (nslen < 16384) nslen = 16384;
auto x = cast(float*)realloc(mem, nslen);
if (x is null) goto fail;
mem = x;
realMemLen = nslen;
}
memAllocSize = minAllocSize;
}
if (!started) {
//foreach (i=0;i<chanCount*memAllocSize;i++) mem[i] = 0;
mem[0..chanCount*memAllocSize] = 0;
} else if (filterLen > oldFilterLen) {
// increase the filter length
foreach_reverse (uint i; 0..chanCount) {
uint j;
uint olen = oldFilterLen;
{
// try and remove the magic samples as if nothing had happened
//FIXME: this is wrong but for now we need it to avoid going over the array bounds
olen = oldFilterLen+2*magicSamples.ptr[i];
for (j = oldFilterLen-1+magicSamples.ptr[i]; j--; ) mem[i*memAllocSize+j+magicSamples.ptr[i]] = mem[i*oldAllocSize+j];
//for (j = 0; j < magicSamples.ptr[i]; ++j) mem[i*memAllocSize+j] = 0;
mem[i*memAllocSize..i*memAllocSize+magicSamples.ptr[i]] = 0;
magicSamples.ptr[i] = 0;
}
if (filterLen > olen) {
// if the new filter length is still bigger than the "augmented" length
// copy data going backward
for (j = 0; j < olen-1; ++j) mem[i*memAllocSize+(filterLen-2-j)] = mem[i*memAllocSize+(olen-2-j)];
// then put zeros for lack of anything better
for (; j < filterLen-1; ++j) mem[i*memAllocSize+(filterLen-2-j)] = 0;
// adjust lastSample
lastSample.ptr[i] += (filterLen-olen)/2;
} else {
// put back some of the magic!
magicSamples.ptr[i] = (olen-filterLen)/2;
for (j = 0; j < filterLen-1+magicSamples.ptr[i]; ++j) mem[i*memAllocSize+j] = mem[i*memAllocSize+j+magicSamples.ptr[i]];
}
}
} else if (filterLen < oldFilterLen) {
// reduce filter length, this a bit tricky
// we need to store some of the memory as "magic" samples so they can be used directly as input the next time(s)
foreach (immutable i; 0..chanCount) {
uint j;
uint oldMagic = magicSamples.ptr[i];
magicSamples.ptr[i] = (oldFilterLen-filterLen)/2;
// we must copy some of the memory that's no longer used
// copy data going backward
for (j = 0; j < filterLen-1+magicSamples.ptr[i]+oldMagic; ++j) {
mem[i*memAllocSize+j] = mem[i*memAllocSize+j+magicSamples.ptr[i]];
}
magicSamples.ptr[i] += oldMagic;
}
}
return Error.OK;
fail:
resampler = null;
/* mem may still contain consumed input samples for the filter.
Restore filterLen so that filterLen-1 still points to the position after
the last of these samples. */
filterLen = oldFilterLen;
return Error.NoMemory;
}
}
// ////////////////////////////////////////////////////////////////////////// //
static immutable double[68] kaiser12Table = [
0.99859849, 1.00000000, 0.99859849, 0.99440475, 0.98745105, 0.97779076,
0.96549770, 0.95066529, 0.93340547, 0.91384741, 0.89213598, 0.86843014,
0.84290116, 0.81573067, 0.78710866, 0.75723148, 0.72629970, 0.69451601,
0.66208321, 0.62920216, 0.59606986, 0.56287762, 0.52980938, 0.49704014,
0.46473455, 0.43304576, 0.40211431, 0.37206735, 0.34301800, 0.31506490,
0.28829195, 0.26276832, 0.23854851, 0.21567274, 0.19416736, 0.17404546,
0.15530766, 0.13794294, 0.12192957, 0.10723616, 0.09382272, 0.08164178,
0.07063950, 0.06075685, 0.05193064, 0.04409466, 0.03718069, 0.03111947,
0.02584161, 0.02127838, 0.01736250, 0.01402878, 0.01121463, 0.00886058,
0.00691064, 0.00531256, 0.00401805, 0.00298291, 0.00216702, 0.00153438,
0.00105297, 0.00069463, 0.00043489, 0.00025272, 0.00013031, 0.0000527734,
0.00001000, 0.00000000];
static immutable double[36] kaiser10Table = [
0.99537781, 1.00000000, 0.99537781, 0.98162644, 0.95908712, 0.92831446,
0.89005583, 0.84522401, 0.79486424, 0.74011713, 0.68217934, 0.62226347,
0.56155915, 0.50119680, 0.44221549, 0.38553619, 0.33194107, 0.28205962,
0.23636152, 0.19515633, 0.15859932, 0.12670280, 0.09935205, 0.07632451,
0.05731132, 0.04193980, 0.02979584, 0.02044510, 0.01345224, 0.00839739,
0.00488951, 0.00257636, 0.00115101, 0.00035515, 0.00000000, 0.00000000];
static immutable double[36] kaiser8Table = [
0.99635258, 1.00000000, 0.99635258, 0.98548012, 0.96759014, 0.94302200,
0.91223751, 0.87580811, 0.83439927, 0.78875245, 0.73966538, 0.68797126,
0.63451750, 0.58014482, 0.52566725, 0.47185369, 0.41941150, 0.36897272,
0.32108304, 0.27619388, 0.23465776, 0.19672670, 0.16255380, 0.13219758,
0.10562887, 0.08273982, 0.06335451, 0.04724088, 0.03412321, 0.02369490,
0.01563093, 0.00959968, 0.00527363, 0.00233883, 0.00050000, 0.00000000];
static immutable double[36] kaiser6Table = [
0.99733006, 1.00000000, 0.99733006, 0.98935595, 0.97618418, 0.95799003,
0.93501423, 0.90755855, 0.87598009, 0.84068475, 0.80211977, 0.76076565,
0.71712752, 0.67172623, 0.62508937, 0.57774224, 0.53019925, 0.48295561,
0.43647969, 0.39120616, 0.34752997, 0.30580127, 0.26632152, 0.22934058,
0.19505503, 0.16360756, 0.13508755, 0.10953262, 0.08693120, 0.06722600,
0.05031820, 0.03607231, 0.02432151, 0.01487334, 0.00752000, 0.00000000];
struct FuncDef {
immutable(double)* table;
int oversample;
}
static immutable FuncDef Kaiser12 = FuncDef(kaiser12Table.ptr, 64);
static immutable FuncDef Kaiser10 = FuncDef(kaiser10Table.ptr, 32);
static immutable FuncDef Kaiser8 = FuncDef(kaiser8Table.ptr, 32);
static immutable FuncDef Kaiser6 = FuncDef(kaiser6Table.ptr, 32);
struct QualityMapping {
int baseLength;
int oversample;
float downsampleBandwidth;
float upsampleBandwidth;
immutable FuncDef* windowFunc;
}
/* This table maps conversion quality to internal parameters. There are two
reasons that explain why the up-sampling bandwidth is larger than the
down-sampling bandwidth:
1) When up-sampling, we can assume that the spectrum is already attenuated
close to the Nyquist rate (from an A/D or a previous resampling filter)
2) Any aliasing that occurs very close to the Nyquist rate will be masked
by the sinusoids/noise just below the Nyquist rate (guaranteed only for
up-sampling).
*/
static immutable QualityMapping[11] qualityMap = [
QualityMapping( 8, 4, 0.830f, 0.860f, &Kaiser6 ), /* Q0 */
QualityMapping( 16, 4, 0.850f, 0.880f, &Kaiser6 ), /* Q1 */
QualityMapping( 32, 4, 0.882f, 0.910f, &Kaiser6 ), /* Q2 */ /* 82.3% cutoff ( ~60 dB stop) 6 */
QualityMapping( 48, 8, 0.895f, 0.917f, &Kaiser8 ), /* Q3 */ /* 84.9% cutoff ( ~80 dB stop) 8 */
QualityMapping( 64, 8, 0.921f, 0.940f, &Kaiser8 ), /* Q4 */ /* 88.7% cutoff ( ~80 dB stop) 8 */
QualityMapping( 80, 16, 0.922f, 0.940f, &Kaiser10), /* Q5 */ /* 89.1% cutoff (~100 dB stop) 10 */
QualityMapping( 96, 16, 0.940f, 0.945f, &Kaiser10), /* Q6 */ /* 91.5% cutoff (~100 dB stop) 10 */
QualityMapping(128, 16, 0.950f, 0.950f, &Kaiser10), /* Q7 */ /* 93.1% cutoff (~100 dB stop) 10 */
QualityMapping(160, 16, 0.960f, 0.960f, &Kaiser10), /* Q8 */ /* 94.5% cutoff (~100 dB stop) 10 */
QualityMapping(192, 32, 0.968f, 0.968f, &Kaiser12), /* Q9 */ /* 95.5% cutoff (~100 dB stop) 10 */
QualityMapping(256, 32, 0.975f, 0.975f, &Kaiser12), /* Q10 */ /* 96.6% cutoff (~100 dB stop) 10 */
];
nothrow @trusted @nogc:
/*8, 24, 40, 56, 80, 104, 128, 160, 200, 256, 320*/
double computeFunc (float x, immutable FuncDef* func) {
version(Posix) import core.stdc.math : lrintf;
import std.math : floor;
//double[4] interp;
float y = x*func.oversample;
version(Posix) {
int ind = cast(int)lrintf(floor(y));
} else {
int ind = cast(int)(floor(y));
}
float frac = (y-ind);
immutable f2 = frac*frac;
immutable f3 = f2*frac;
double interp3 = -0.1666666667*frac+0.1666666667*(f3);
double interp2 = frac+0.5*(f2)-0.5*(f3);
//double interp2 = 1.0f-0.5f*frac-f2+0.5f*f3;
double interp0 = -0.3333333333*frac+0.5*(f2)-0.1666666667*(f3);
// just to make sure we don't have rounding problems
double interp1 = 1.0f-interp3-interp2-interp0;
//sum = frac*accum[1]+(1-frac)*accum[2];
return interp0*func.table[ind]+interp1*func.table[ind+1]+interp2*func.table[ind+2]+interp3*func.table[ind+3];
}
// the slow way of computing a sinc for the table; should improve that some day
float sinc (float cutoff, float x, int N, immutable FuncDef *windowFunc) {
version(LittleEndian) {
align(1) union temp_float { align(1): float f; uint n; }
} else {
static T fabs(T) (T n) pure { static if (__VERSION__ > 2067) pragma(inline, true); return (n < 0 ? -n : n); }
}
import std.math : sin, PI;
version(LittleEndian) {
temp_float txx = void;
txx.f = x;
txx.n &= 0x7fff_ffff; // abs
if (txx.f < 1.0e-6f) return cutoff;
if (txx.f > 0.5f*N) return 0;
} else {
if (fabs(x) < 1.0e-6f) return cutoff;
if (fabs(x) > 0.5f*N) return 0;
}
//FIXME: can it really be any slower than this?
immutable float xx = x*cutoff;
immutable pixx = PI*xx;
version(LittleEndian) {
return cutoff*sin(pixx)/pixx*computeFunc(2.0*txx.f/N, windowFunc);
} else {
return cutoff*sin(pixx)/pixx*computeFunc(fabs(2.0*x/N), windowFunc);
}
}
void cubicCoef (in float frac, float* interp) {
immutable f2 = frac*frac;
immutable f3 = f2*frac;
// compute interpolation coefficients; i'm not sure whether this corresponds to cubic interpolation but I know it's MMSE-optimal on a sinc
interp[0] = -0.16667f*frac+0.16667f*f3;
interp[1] = frac+0.5f*f2-0.5f*f3;
//interp[2] = 1.0f-0.5f*frac-f2+0.5f*f3;
interp[3] = -0.33333f*frac+0.5f*f2-0.16667f*f3;
// just to make sure we don't have rounding problems
interp[2] = 1.0-interp[0]-interp[1]-interp[3];
}
// ////////////////////////////////////////////////////////////////////////// //
int resamplerBasicDirect(T) (ref SpeexResampler st, uint chanIdx, const(float)* indata, uint* indataLen, float* outdata, uint* outdataLen)
if (is(T == float) || is(T == double))
{
auto N = st.filterLen;
static if (is(T == double)) assert(N%4 == 0);
int outSample = 0;
int lastSample = st.lastSample.ptr[chanIdx];
uint sampFracNum = st.sampFracNum.ptr[chanIdx];
const(float)* sincTable = st.sincTable;
immutable outStride = st.outStride;
immutable intAdvance = st.intAdvance;
immutable fracAdvance = st.fracAdvance;
immutable denRate = st.denRate;
T sum = void;
while (!(lastSample >= cast(int)(*indataLen) || outSample >= cast(int)(*outdataLen))) {
const(float)* sinct = &sincTable[sampFracNum*N];
const(float)* iptr = &indata[lastSample];
static if (is(T == float)) {
// at least 2x speedup with SSE here (but for unrolled loop)
if (N%4 == 0) {
version(sincresample_use_sse) {
//align(64) __gshared float[4] zero = 0;
align(64) __gshared float[4+128] zeroesBuf = 0; // dmd cannot into such aligns, alas
__gshared uint zeroesptr = 0;
if (zeroesptr == 0) {
zeroesptr = cast(uint)zeroesBuf.ptr;
if (zeroesptr&0x3f) zeroesptr = (zeroesptr|0x3f)+1;
}
//assert((zeroesptr&0x3f) == 0, "wtf?!");
asm nothrow @safe @nogc {
mov ECX,[N];
shr ECX,2;
mov EAX,[zeroesptr];
movaps XMM0,[EAX];
mov EAX,[sinct];
mov EBX,[iptr];
mov EDX,16;
align 8;
rbdseeloop:
movups XMM1,[EAX];
movups XMM2,[EBX];
mulps XMM1,XMM2;
addps XMM0,XMM1;
add EAX,EDX;
add EBX,EDX;
dec ECX;
jnz rbdseeloop;
// store result in sum
movhlps XMM1,XMM0; // now low part of XMM1 contains high part of XMM0
addps XMM0,XMM1; // low part of XMM0 is ok
movaps XMM1,XMM0;
shufps XMM1,XMM0,0b_01_01_01_01; // 2nd float of XMM0 goes to the 1st float of XMM1
addss XMM0,XMM1;
movss [sum],XMM0;
}
/*
float sum1 = 0;
foreach (immutable j; 0..N) sum1 += sinct[j]*iptr[j];
import std.math;
if (fabs(sum-sum1) > 0.000001f) {
import core.stdc.stdio;
printf("sum=%f; sum1=%f\n", sum, sum1);
assert(0);
}
*/
} else {
// no SSE; for my i3 unrolled loop is almost of the speed of SSE code
T[4] accum = 0;
foreach (immutable j; 0..N/4) {
accum.ptr[0] += *sinct++ * *iptr++;
accum.ptr[1] += *sinct++ * *iptr++;
accum.ptr[2] += *sinct++ * *iptr++;
accum.ptr[3] += *sinct++ * *iptr++;
}
sum = accum.ptr[0]+accum.ptr[1]+accum.ptr[2]+accum.ptr[3];
}
} else {
sum = 0;
foreach (immutable j; 0..N) sum += *sinct++ * *iptr++;
}
outdata[outStride*outSample++] = sum;
} else {
if (N%4 == 0) {
//TODO: write SSE code here!
// for my i3 unrolled loop is ~2 times faster
T[4] accum = 0;
foreach (immutable j; 0..N/4) {
accum.ptr[0] += cast(double)*sinct++ * cast(double)*iptr++;
accum.ptr[1] += cast(double)*sinct++ * cast(double)*iptr++;
accum.ptr[2] += cast(double)*sinct++ * cast(double)*iptr++;
accum.ptr[3] += cast(double)*sinct++ * cast(double)*iptr++;
}
sum = accum.ptr[0]+accum.ptr[1]+accum.ptr[2]+accum.ptr[3];
} else {
sum = 0;
foreach (immutable j; 0..N) sum += cast(double)*sinct++ * cast(double)*iptr++;
}
outdata[outStride*outSample++] = cast(float)sum;
}
lastSample += intAdvance;
sampFracNum += fracAdvance;
if (sampFracNum >= denRate) {
sampFracNum -= denRate;
++lastSample;
}
}
st.lastSample.ptr[chanIdx] = lastSample;
st.sampFracNum.ptr[chanIdx] = sampFracNum;
return outSample;
}
int resamplerBasicInterpolate(T) (ref SpeexResampler st, uint chanIdx, const(float)* indata, uint *indataLen, float *outdata, uint *outdataLen)
if (is(T == float) || is(T == double))
{
immutable N = st.filterLen;
assert(N%4 == 0);
int outSample = 0;
int lastSample = st.lastSample.ptr[chanIdx];
uint sampFracNum = st.sampFracNum.ptr[chanIdx];
immutable outStride = st.outStride;
immutable intAdvance = st.intAdvance;
immutable fracAdvance = st.fracAdvance;
immutable denRate = st.denRate;
float sum;
float[4] interp = void;
T[4] accum = void;
while (!(lastSample >= cast(int)(*indataLen) || outSample >= cast(int)(*outdataLen))) {
const(float)* iptr = &indata[lastSample];
const int offset = sampFracNum*st.oversample/st.denRate;
const float frac = (cast(float)((sampFracNum*st.oversample)%st.denRate))/st.denRate;
accum[] = 0;
//TODO: optimize!
foreach (immutable j; 0..N) {
immutable T currIn = iptr[j];
accum.ptr[0] += currIn*(st.sincTable[4+(j+1)*st.oversample-offset-2]);
accum.ptr[1] += currIn*(st.sincTable[4+(j+1)*st.oversample-offset-1]);
accum.ptr[2] += currIn*(st.sincTable[4+(j+1)*st.oversample-offset]);
accum.ptr[3] += currIn*(st.sincTable[4+(j+1)*st.oversample-offset+1]);
}
cubicCoef(frac, interp.ptr);
sum = (interp.ptr[0]*accum.ptr[0])+(interp.ptr[1]*accum.ptr[1])+(interp.ptr[2]*accum.ptr[2])+(interp.ptr[3]*accum.ptr[3]);
outdata[outStride*outSample++] = sum;
lastSample += intAdvance;
sampFracNum += fracAdvance;
if (sampFracNum >= denRate) {
sampFracNum -= denRate;
++lastSample;
}
}
st.lastSample.ptr[chanIdx] = lastSample;
st.sampFracNum.ptr[chanIdx] = sampFracNum;
return outSample;
}
// ////////////////////////////////////////////////////////////////////////// //
uint gcd (uint a, uint b) pure {
if (a == 0) return b;
if (b == 0) return a;
for (;;) {
if (a > b) {
a %= b;
if (a == 0) return b;
if (a == 1) return 1;
} else {
b %= a;
if (b == 0) return a;
if (b == 1) return 1;
}
}
}
// ////////////////////////////////////////////////////////////////////////// //
// very simple and cheap cubic upsampler
struct CubicUpsampler {
public:
nothrow @trusted @nogc:
float[2] curposfrac; // current position offset [0..1)
float step; // how long we should move on one step?
float[4][2] data; // -1..3
uint[2] drain;
void reset () {
curposfrac[] = 0.0f;
foreach (ref d; data) d[] = 0.0f;
drain[] = 0;
}
bool setup (float astep) {
if (astep >= 1.0f) return false;
step = astep;
return true;
}
/*
static struct Data {
const(float)[] dataIn;
float[] dataOut;
uint inputSamplesUsed; // out value, in samples (i.e. multiplied by channel count)
uint outputSamplesUsed; // out value, in samples (i.e. multiplied by channel count)
}
*/
SpeexResampler.Error process (ref SpeexResampler.Data d) {
d.inputSamplesUsed = d.outputSamplesUsed = 0;
if (d.dataOut.length < 2) return SpeexResampler.Error.OK;
foreach (uint cidx; 0..2) {
uint inleft = cast(uint)d.dataIn.length/2;
uint outleft = cast(uint)d.dataOut.length/2;
processChannel(inleft, outleft, (d.dataIn.length ? d.dataIn.ptr+cidx : null), (d.dataOut.length ? d.dataOut.ptr+cidx : null), cidx);
d.outputSamplesUsed += cast(uint)(d.dataOut.length/2)-outleft;
d.inputSamplesUsed += cast(uint)(d.dataIn.length/2)-inleft;
}
return SpeexResampler.Error.OK;
}
private void processChannel (ref uint inleft, ref uint outleft, const(float)* dataIn, float* dataOut, uint cidx) {
if (outleft == 0) return;
if (inleft == 0 && drain.ptr[cidx] <= 1) return;
auto dt = data.ptr[cidx].ptr;
auto drn = drain.ptr+cidx;
auto cpf = curposfrac.ptr+cidx;
immutable float st = step;
for (;;) {
// fill buffer
while ((*drn) < 4) {
if (inleft == 0) return;
dt[(*drn)++] = *dataIn;
dataIn += 2;
--inleft;
}
if (outleft == 0) return;
--outleft;
// cubic interpolation
/*version(none)*/ {
// interpolate between y1 and y2
immutable float mu = (*cpf); // how far we are moved from y1 to y2
immutable float mu2 = mu*mu; // wow
immutable float y0 = dt[0], y1 = dt[1], y2 = dt[2], y3 = dt[3];
version(complex_cubic) {
immutable float z0 = 0.5*y3;
immutable float z1 = 0.5*y0;
immutable float a0 = 1.5*y1-z1-1.5*y2+z0;
immutable float a1 = y0-2.5*y1+2*y2-z0;
immutable float a2 = 0.5*y2-z1;
} else {
immutable float a0 = y3-y2-y0+y1;
immutable float a1 = y0-y1-a0;
immutable float a2 = y2-y0;
}
*dataOut = a0*mu*mu2+a1*mu2+a2*mu+y1;
}// else *dataOut = dt[1];
dataOut += 2;
if (((*cpf) += st) >= 1.0f) {
(*cpf) -= 1.0f;
dt[0] = dt[1];
dt[1] = dt[2];
dt[2] = dt[3];
dt[3] = 0.0f;
--(*drn); // will request more input bytes
}
}
}
}
}
version(with_resampler)
abstract class ResamplingContext {
int inputSampleRate;
int outputSampleRate;
int inputChannels;
int outputChannels;
SpeexResampler resamplerLeft;
SpeexResampler resamplerRight;
SpeexResampler.Data resamplerDataLeft;
SpeexResampler.Data resamplerDataRight;
float[][2] buffersIn;
float[][2] buffersOut;
uint rateNum;
uint rateDem;
float[][2] dataReady;
SampleControlFlags scflags;
this(SampleControlFlags scflags, int inputSampleRate, int outputSampleRate, int inputChannels, int outputChannels) {
this.scflags = scflags;
this.inputSampleRate = inputSampleRate;
this.outputSampleRate = outputSampleRate;
this.inputChannels = inputChannels;
this.outputChannels = outputChannels;
if(auto err = resamplerLeft.setup(1, inputSampleRate, outputSampleRate, 5))
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;
buffersIn[0] = new float[](BUFFER_SIZE_FRAMES * rateNum / rateDem + add);
buffersOut[0] = new float[](BUFFER_SIZE_FRAMES);
if(inputChannels > 1) {
buffersIn[1] = new float[](BUFFER_SIZE_FRAMES * rateNum / rateDem + add);
buffersOut[1] = new float[](BUFFER_SIZE_FRAMES);
}
}
/+
float*[2] tmp;
tmp[0] = buffersIn[0].ptr;
tmp[1] = buffersIn[1].ptr;
auto actuallyGot = v.getSamplesFloat(v.chans, tmp.ptr, cast(int) buffersIn[0].length);
resamplerDataLeft.dataIn should be a slice of buffersIn[0] that is filled up
ditto for resamplerDataRight if the source has two channels
+/
abstract void loadMoreSamples();
bool loadMore() {
resamplerDataLeft.dataIn = buffersIn[0];
resamplerDataLeft.dataOut = buffersOut[0];
resamplerDataRight.dataIn = buffersIn[1];
resamplerDataRight.dataOut = buffersOut[1];
loadMoreSamples();
//resamplerLeft.reset();
if(auto err = resamplerLeft.process(resamplerDataLeft))
throw new Exception("ugh");
if(inputChannels > 1)
//resamplerRight.reset();
resamplerRight.process(resamplerDataRight);
resamplerDataLeft.dataOut = resamplerDataLeft.dataOut[0 .. resamplerDataLeft.outputSamplesUsed];
resamplerDataRight.dataOut = resamplerDataRight.dataOut[0 .. resamplerDataRight.outputSamplesUsed];
if(resamplerDataLeft.dataOut.length == 0) {
return true;
}
return false;
}
bool fillBuffer(short[] buffer) {
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
if(scflags.paused) {
buffer[] = 0;
return true;
}
if(outputChannels == 1) {
foreach(ref s; buffer) {
if(resamplerDataLeft.dataOut.length == 0) {
if(loadMore()) {
scflags.finished_ = true;
return false;
}
}
if(inputChannels == 1) {
s = cast(short) (resamplerDataLeft.dataOut[0] * short.max);
resamplerDataLeft.dataOut = resamplerDataLeft.dataOut[1 .. $];
} else {
s = cast(short) ((resamplerDataLeft.dataOut[0] + resamplerDataRight.dataOut[0]) * short.max / 2);
resamplerDataLeft.dataOut = resamplerDataLeft.dataOut[1 .. $];
resamplerDataRight.dataOut = resamplerDataRight.dataOut[1 .. $];
}
}
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels * scflags.playbackSpeed;
} else if(outputChannels == 2) {
foreach(idx, ref s; buffer) {
if(resamplerDataLeft.dataOut.length == 0) {
if(loadMore()) {
scflags.finished_ = true;
return false;
}
}
if(inputChannels == 1) {
s = cast(short) (resamplerDataLeft.dataOut[0] * short.max);
if(idx & 1)
resamplerDataLeft.dataOut = resamplerDataLeft.dataOut[1 .. $];
} else {
if(idx & 1) {
s = cast(short) (resamplerDataRight.dataOut[0] * short.max);
resamplerDataRight.dataOut = resamplerDataRight.dataOut[1 .. $];
} else {
s = cast(short) (resamplerDataLeft.dataOut[0] * short.max);
resamplerDataLeft.dataOut = resamplerDataLeft.dataOut[1 .. $];
}
}
}
scflags.currentPosition += cast(float) buffer.length / outputSampleRate / outputChannels * scflags.playbackSpeed;
} else assert(0);
if(scflags.stopped)
scflags.finished_ = true;
return !scflags.stopped;
}
}
private enum scriptable = "arsd_jsvar_compatible";