From 09301ba8da32222b7738e6464f07463eda18dd3a Mon Sep 17 00:00:00 2001 From: Guillaume Piolat Date: Fri, 10 Feb 2023 15:26:07 +0100 Subject: [PATCH] Add toWAV and saveAsWAV, functions that allow to output a WAV in file or slice form, in one single line. --- source/audioformats/package.d | 118 ++++++++++++++++++++++++++++++++++ source/audioformats/stream.d | 30 +++++++-- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/source/audioformats/package.d b/source/audioformats/package.d index 280637c..67e09d9 100644 --- a/source/audioformats/package.d +++ b/source/audioformats/package.d @@ -13,10 +13,128 @@ public import audioformats.stream; public import audioformats.internals: AudioFormatsException; +import core.stdc.stdlib: free; /// Frees an exception thrown by audio-formats. void destroyAudioFormatException(AudioFormatsException e) nothrow @nogc { import audioformats.internals; destroyFree!AudioFormatsException(e); +} + + +/// Encode a slice to a WAV file. +/// +/// Returns: `true` on success. +bool saveAsWAV(const(float)[] data, + const(char)[] filePath, + int numChannels = 1, + float sampleRate = 44100.0f, + EncodingOptions options = EncodingOptions.init) nothrow @nogc +{ + return saveAsWAVImpl!float(data, filePath, numChannels, sampleRate, options); +} +///ditto +bool saveAsWAV(const(double)[] data, + const(char)[] filePath, + int numChannels = 1, + float sampleRate = 44100.0f, + EncodingOptions options = EncodingOptions.init) nothrow @nogc +{ + return saveAsWAVImpl!double(data, filePath, numChannels, sampleRate, options); +} + + +/// Encode a slice to a WAV in memory. +/// The returned slice MUST be freed with `freeEncodedAudio`. +/// +/// Returns: `null` in case of error. +const(ubyte)[] toWAV(const(float)[] data, + int numChannels = 1, + float sampleRate = 44100.0f, + EncodingOptions options = EncodingOptions.init) nothrow @nogc +{ + return toWAVImpl!float(data, numChannels, sampleRate, options); +} +///ditto +const(ubyte)[] toWAV(const(double)[] data, + int numChannels = 1, + float sampleRate = 44100.0f, + EncodingOptions options = EncodingOptions.init) nothrow @nogc +{ + return toWAVImpl!double(data, numChannels, sampleRate, options); +} + + +/// Disowned audio buffers (with eg. `encodeToWAV`) must be freed with this function. +void freeEncodedAudio(const(ubyte)[] encoded) +{ + free(cast(void*)encoded.ptr); +} + + +private: + + +const(ubyte)[] toWAVImpl(T)(const(T)[] data, int numChannels, float sampleRate, EncodingOptions options) nothrow @nogc +{ + assert(data !is null); + import core.stdc.string: strlen; + + try + { + AudioStream encoder; + encoder.openToBuffer(AudioFileFormat.wav, sampleRate, numChannels, options); + static if (is(T == float)) + encoder.writeSamplesFloat(data); + else + encoder.writeSamplesDouble(data); + const(ubyte)[] r = encoder.finalizeAndGetEncodedResultDisown(); + return r; + } + catch (AudioFormatsException e) + { + destroyAudioFormatException(e); + return null; + } + catch(Exception e) + { + return null; + } +} + +bool saveAsWAVImpl(T)(const(T)[] data, + const(char)[] filePath, + int numChannels, + float sampleRate, + EncodingOptions options) nothrow @nogc +{ + if (data is null) + return false; + if (filePath is null) + return false; + + import core.stdc.string: strlen; + + try + { + AudioStream encoder; + encoder.openToFile(filePath, AudioFileFormat.wav, sampleRate, numChannels, options); + static if (is(T == float)) + encoder.writeSamplesFloat(data); + else + encoder.writeSamplesDouble(data); + encoder.flush(); + encoder.finalizeEncoding(); + } + catch (AudioFormatsException e) + { + destroyAudioFormatException(e); + return false; + } + catch(Exception e) + { + return false; + } + return true; } \ No newline at end of file diff --git a/source/audioformats/stream.d b/source/audioformats/stream.d index de79ad4..1597441 100644 --- a/source/audioformats/stream.d +++ b/source/audioformats/stream.d @@ -637,7 +637,7 @@ public: // This is also part of the public API /// /// Returns: Number of actually written frames. Multiply by `getNumChannels()` to get the number of written samples. /// When that number is less than `frames`, it means the stream had a write error. - int writeSamplesFloat(float* inData, int frames) nothrow @nogc + int writeSamplesFloat(const(float)* inData, int frames) nothrow @nogc { assert(_io && _io.write !is null); @@ -667,7 +667,7 @@ public: // This is also part of the public API } } ///ditto - int writeSamplesFloat(float[] inData) nothrow @nogc + int writeSamplesFloat(const(float)[] inData) nothrow @nogc { assert( (inData.length % _numChannels) == 0); return writeSamplesFloat(inData.ptr, cast(int)(inData.length / _numChannels)); @@ -687,11 +687,11 @@ public: // This is also part of the public API /// /// Returns: Number of actually written frames. Multiply by `getNumChannels()` to get the number of written samples. /// When that number is less than `frames`, it means the stream had a write error. - int writeSamplesDouble(double* inData, int frames) nothrow @nogc + int writeSamplesDouble(const(double)* inData, int frames) nothrow @nogc { - assert(_io && _io.write !is null); + assert (_io && _io.write !is null); - switch(_format) + switch (_format) { case AudioFileFormat.unknown: // One shouldn't ever get there @@ -721,7 +721,7 @@ public: // This is also part of the public API } } ///ditto - int writeSamplesDouble(double[] inData) nothrow @nogc + int writeSamplesDouble(const(double)[] inData) nothrow @nogc { assert( (inData.length % _numChannels) == 0); return writeSamplesDouble(inData.ptr, cast(int)(inData.length / _numChannels)); @@ -1106,6 +1106,16 @@ public: // This is also part of the public API return memoryContext.buffer[0..memoryContext.size]; } + // Finalize encoding and get internal buffer, which is disowned by the `AudioStream`. + // The caller has to call `freeEncodedAudio` manually. + // This can be exactly one time, if a growable owned buffer was used. + const(ubyte)[] finalizeAndGetEncodedResultDisown() @nogc + { + const(ubyte)[] buf = finalizeAndGetEncodedResult(); + memoryContext.disownBuffer(); + return buf; + } + private: IOCallbacks* _io; @@ -1729,6 +1739,14 @@ struct MemoryContext capacity = 0; } + // caller guarantees the buffer will be freed with `free`. + void disownBuffer() nothrow @nogc + { + assert(bufferIsOwned); + bufferIsOwned = false; + bufferCanGrow = false; + } + ~this() { if (bufferIsOwned)