Skip to content

Commit

Permalink
Add support for AIFF and other WAV formats
Browse files Browse the repository at this point in the history
  • Loading branch information
DeeJayLSP committed Dec 4, 2024
1 parent 47bc374 commit 659c57c
Show file tree
Hide file tree
Showing 8 changed files with 8,924 additions and 205 deletions.
5 changes: 5 additions & 0 deletions COPYRIGHT.txt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ Comment: doctest
Copyright: 2016-2023, Viktor Kirilov
License: Expat

Files: ./thirdparty/dr_libs/
Comment: dr_libs
Copyright: 2024 David Reid
License: public-domain or Unlicense or Expat

Files: ./thirdparty/embree/
Comment: Embree
Copyright: 2009-2021 Intel Corporation
Expand Down
4 changes: 2 additions & 2 deletions doc/classes/AudioStreamWAV.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamWAV" inherits="AudioStream" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Stores audio data loaded from WAV files.
Stores audio data loaded from WAV or AIFF files.
</brief_description>
<description>
AudioStreamWAV stores sound samples loaded from WAV files. To play the stored sound, use an [AudioStreamPlayer] (for non-positional audio) or [AudioStreamPlayer2D]/[AudioStreamPlayer3D] (for positional audio). The sound can be looped.
AudioStreamWAV stores sound samples loaded from WAV or AIFF files. To play the stored sound, use an [AudioStreamPlayer] (for non-positional audio) or [AudioStreamPlayer2D]/[AudioStreamPlayer3D] (for positional audio). The sound can be looped.
This class can also be used to store dynamically-generated PCM audio data. See also [AudioStreamGenerator] for procedural audio generation.
</description>
<tutorials>
Expand Down
8 changes: 4 additions & 4 deletions doc/classes/ResourceImporterWAV.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="ResourceImporterWAV" inherits="ResourceImporter" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Imports a WAV audio file for playback.
Imports data from a WAV/AIFF audio file for playback.
</brief_description>
<description>
WAV is an uncompressed format, which can provide higher quality compared to Ogg Vorbis and MP3. It also has the lowest CPU cost to decode. This means high numbers of WAV sounds can be played at the same time, even on low-end devices.
By default, Godot imports WAV files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property.
WAV/AIFF is an uncompressed format, which can provide higher quality compared to Ogg Vorbis and MP3. It also has the lowest CPU cost to decode. This means a high number of sounds can be played at the same time, even on low-end devices.
By default, Godot imports WAV/AIFF files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property.
</description>
<tutorials>
<link title="Importing audio samples">$DOCS_URL/tutorials/assets_pipeline/importing_audio_samples.html</link>
Expand All @@ -25,7 +25,7 @@
</member>
<member name="edit/loop_mode" type="int" setter="" getter="" default="0">
Controls how audio should loop.
- [b]Detect From WAV:[/b] Uses loop information from the WAV metadata.
- [b]Detect From WAV:[/b] Uses loop information from the WAV metadata (AIFF does not support this).
- [b]Disabled:[/b] Don't loop audio, even if the metadata indicates the file playback should loop.
- [b]Forward:[/b] Standard audio looping. Plays the audio forward from the beginning to [member edit/loop_end], then returns to [member edit/loop_begin] and repeats.
- [b]Ping-Pong:[/b] Plays the audio forward until [member edit/loop_end], then backwards to [member edit/loop_begin], repeating this cycle.
Expand Down
4 changes: 4 additions & 0 deletions editor/import/resource_importer_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ String ResourceImporterWAV::get_visible_name() const {

void ResourceImporterWAV::get_recognized_extensions(List<String> *p_extensions) const {
p_extensions->push_back("wav");
p_extensions->push_back("wave");
p_extensions->push_back("aif");
p_extensions->push_back("aiff");
p_extensions->push_back("aifc");
}

String ResourceImporterWAV::get_save_extension() const {
Expand Down
234 changes: 35 additions & 199 deletions scene/resources/audio_stream_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
#include "core/io/file_access_memory.h"
#include "core/io/marshalls.h"

#define DRWAV_IMPLEMENTATION
#define DRWAV_NO_STDIO
#define DR_WAV_LIBSNDFILE_COMPAT
#define DRWAV_MALLOC(sz) memalloc(sz)
#define DRWAV_REALLOC(p, sz) memrealloc(p, sz)
#define DRWAV_FREE(p) \
if (p) \
memfree(p)

#include "thirdparty/dr_libs/dr_wav.h"

const float TRIM_DB_LIMIT = -50;
const int TRIM_FADE_OUT_FRAMES = 500;

Expand Down Expand Up @@ -770,220 +781,45 @@ void AudioStreamWAV::_bind_methods() {
}

Ref<AudioStreamWAV> AudioStreamWAV::load_from_buffer(const Vector<uint8_t> &p_file_data, const Dictionary &p_options) {
// /* STEP 1, READ WAVE FILE */

Ref<FileAccessMemory> file;
file.instantiate();
Error err = file->open_custom(p_file_data.ptr(), p_file_data.size());
ERR_FAIL_COND_V_MSG(err != OK, Ref<AudioStreamWAV>(), "Cannot create memfile for WAV file buffer.");
// STEP 1, READ FILE

/* CHECK RIFF */
char riff[5];
riff[4] = 0;
file->get_buffer((uint8_t *)&riff, 4); //RIFF

if (riff[0] != 'R' || riff[1] != 'I' || riff[2] != 'F' || riff[3] != 'F') {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), vformat("Not a WAV file. File should start with 'RIFF', but found '%s', in file of size %d bytes", riff, file->get_length()));
drwav wav;
if (!drwav_init_memory_with_metadata(&wav, p_file_data.ptr(), p_file_data.size(), DRWAV_WITH_METADATA, nullptr)) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Audio data is invalid, corrupted or an unsupported format.");
}

/* GET FILESIZE */

// The file size in header is 8 bytes less than the actual size.
// See https://docs.fileformat.com/audio/wav/
const int FILE_SIZE_HEADER_OFFSET = 8;
uint32_t file_size_header = file->get_32() + FILE_SIZE_HEADER_OFFSET;
uint64_t file_size = file->get_length();
if (file_size != file_size_header) {
WARN_PRINT(vformat("File size %d is %s than the expected size %d.", file_size, file_size > file_size_header ? "larger" : "smaller", file_size_header));
if (wav.totalPCMFrameCount > INT32_MAX) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Audio data exceeds maximum supported size of 2,147,483,647 frames.");
}

/* CHECK WAVE */
int format_bits = wav.bitsPerSample;
int format_channels = wav.channels;
int format_freq = wav.sampleRate;
int frames = wav.totalPCMFrameCount;

char wave[5];
wave[4] = 0;
file->get_buffer((uint8_t *)&wave, 4); //WAVE

if (wave[0] != 'W' || wave[1] != 'A' || wave[2] != 'V' || wave[3] != 'E') {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), vformat("Not a WAV file. Header should contain 'WAVE', but found '%s', in file of size %d bytes", wave, file->get_length()));
}

// Let users override potential loop points from the WAV.
// We parse the WAV loop points only with "Detect From WAV" (0).
int import_loop_mode = p_options["edit/loop_mode"];

int format_bits = 0;
int format_channels = 0;

AudioStreamWAV::LoopMode loop_mode = AudioStreamWAV::LOOP_DISABLED;
uint16_t compression_code = 1;
bool format_found = false;
bool data_found = false;
int format_freq = 0;
int loop_begin = 0;
int loop_end = 0;
int frames = 0;

Vector<float> data;

while (!file->eof_reached()) {
/* chunk */
char chunk_id[4];
file->get_buffer((uint8_t *)&chunk_id, 4); //RIFF

/* chunk size */
uint32_t chunksize = file->get_32();
uint32_t file_pos = file->get_position(); //save file pos, so we can skip to next chunk safely

if (file->eof_reached()) {
//ERR_PRINT("EOF REACH");
break;
}

if (chunk_id[0] == 'f' && chunk_id[1] == 'm' && chunk_id[2] == 't' && chunk_id[3] == ' ' && !format_found) {
/* IS FORMAT CHUNK */

//Issue: #7755 : Not a bug - usage of other formats (format codes) are unsupported in current importer version.
//Consider revision for engine version 3.0
compression_code = file->get_16();
if (compression_code != 1 && compression_code != 3) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Format not supported for WAVE file (not PCM). Save WAVE files as uncompressed PCM or IEEE float instead.");
}

format_channels = file->get_16();
if (format_channels != 1 && format_channels != 2) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Format not supported for WAVE file (not stereo or mono).");
}

format_freq = file->get_32(); //sampling rate

file->get_32(); // average bits/second (unused)
file->get_16(); // block align (unused)
format_bits = file->get_16(); // bits per sample

if (format_bits % 8 || format_bits == 0) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Invalid amount of bits in the sample (should be one of 8, 16, 24 or 32).");
}

if (compression_code == 3 && format_bits % 32) {
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Invalid amount of bits in the IEEE float sample (should be 32 or 64).");
}

/* Don't need anything else, continue */
format_found = true;
}

if (chunk_id[0] == 'd' && chunk_id[1] == 'a' && chunk_id[2] == 't' && chunk_id[3] == 'a' && !data_found) {
/* IS DATA CHUNK */
data_found = true;

if (!format_found) {
ERR_PRINT("'data' chunk before 'format' chunk found.");
AudioStreamWAV::LoopMode loop_mode = AudioStreamWAV::LOOP_DISABLED;
if (import_loop_mode == 0) {
for (uint32_t meta = 0; meta < wav.metadataCount; ++meta) {
drwav_metadata md = wav.pMetadata[meta];
if (md.type == drwav_metadata_type_smpl && md.data.smpl.sampleLoopCount > 0) {
drwav_smpl_loop loop = md.data.smpl.pLoops[0];
loop_mode = (AudioStreamWAV::LoopMode)(loop.type + 1);
loop_begin = loop.firstSampleByteOffset;
loop_end = loop.lastSampleByteOffset;
break;
}

uint64_t remaining_bytes = file_size - file_pos;
frames = chunksize;
if (remaining_bytes < chunksize) {
WARN_PRINT("Data chunk size is smaller than expected. Proceeding with actual data size.");
frames = remaining_bytes;
}

ERR_FAIL_COND_V(format_channels == 0, Ref<AudioStreamWAV>());
frames /= format_channels;
frames /= (format_bits >> 3);

/*print_line("chunksize: "+itos(chunksize));
print_line("channels: "+itos(format_channels));
print_line("bits: "+itos(format_bits));
*/

data.resize(frames * format_channels);

if (compression_code == 1) {
if (format_bits == 8) {
for (int i = 0; i < frames * format_channels; i++) {
// 8 bit samples are UNSIGNED

data.write[i] = int8_t(file->get_8() - 128) / 128.f;
}
} else if (format_bits == 16) {
for (int i = 0; i < frames * format_channels; i++) {
//16 bit SIGNED

data.write[i] = int16_t(file->get_16()) / 32768.f;
}
} else {
for (int i = 0; i < frames * format_channels; i++) {
//16+ bits samples are SIGNED
// if sample is > 16 bits, just read extra bytes

uint32_t s = 0;
for (int b = 0; b < (format_bits >> 3); b++) {
s |= ((uint32_t)file->get_8()) << (b * 8);
}
s <<= (32 - format_bits);

data.write[i] = (int32_t(s) >> 16) / 32768.f;
}
}
} else if (compression_code == 3) {
if (format_bits == 32) {
for (int i = 0; i < frames * format_channels; i++) {
//32 bit IEEE Float

data.write[i] = file->get_float();
}
} else if (format_bits == 64) {
for (int i = 0; i < frames * format_channels; i++) {
//64 bit IEEE Float

data.write[i] = file->get_double();
}
}
}

// This is commented out due to some weird edge case seemingly in FileAccessMemory, doesn't seem to have any side effects though.
// if (file->eof_reached()) {
// ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Premature end of file.");
// }
}
}

if (import_loop_mode == 0 && chunk_id[0] == 's' && chunk_id[1] == 'm' && chunk_id[2] == 'p' && chunk_id[3] == 'l') {
// Loop point info!

/**
* Consider exploring next document:
* http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/RIFFNEW.pdf
* Especially on page:
* 16 - 17
* Timestamp:
* 22:38 06.07.2017 GMT
**/

for (int i = 0; i < 10; i++) {
file->get_32(); // i wish to know why should i do this... no doc!
}
Vector<float> data;
data.resize(frames * format_channels);
drwav_read_pcm_frames_f32(&wav, frames, data.ptrw());

// only read 0x00 (loop forward), 0x01 (loop ping-pong) and 0x02 (loop backward)
// Skip anything else because it's not supported, reserved for future uses or sampler specific
// from https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#smpl (loop type values table)
int loop_type = file->get_32();
if (loop_type == 0x00 || loop_type == 0x01 || loop_type == 0x02) {
if (loop_type == 0x00) {
loop_mode = AudioStreamWAV::LOOP_FORWARD;
} else if (loop_type == 0x01) {
loop_mode = AudioStreamWAV::LOOP_PINGPONG;
} else if (loop_type == 0x02) {
loop_mode = AudioStreamWAV::LOOP_BACKWARD;
}
loop_begin = file->get_32();
loop_end = file->get_32();
}
}
// Move to the start of the next chunk. Note that RIFF requires a padding byte for odd
// chunk sizes.
file->seek(file_pos + chunksize + (chunksize & 1));
}
drwav_uninit(&wav);

// STEP 2, APPLY CONVERSIONS

Expand Down
12 changes: 12 additions & 0 deletions thirdparty/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ Files extracted from upstream source:
- `LICENSE.txt`


## dr_libs

- Upstream: https://github.com/mackron/dr_libs
- Version: git (da35f9d6c7374a95353fd1df1d394d44ab66cf01, 2024)
- License: Public Domain or Unlicense or MIT

Files extracted from upstream source:

- `dr_wav.h`
- `LICENSE`


## embree

- Upstream: https://github.com/embree/embree
Expand Down
47 changes: 47 additions & 0 deletions thirdparty/dr_libs/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
This software is available as a choice of the following licenses. Choose
whichever you prefer.

===============================================================================
ALTERNATIVE 1 - Public Domain (www.unlicense.org)
===============================================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>

===============================================================================
ALTERNATIVE 2 - MIT No Attribution
===============================================================================
Copyright 2020 David Reid

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading

0 comments on commit 659c57c

Please sign in to comment.