Skip to content

Commit f1355f8

Browse files
committed
Add support for AIFF files and other WAV formats
1 parent 791c87e commit f1355f8

File tree

10 files changed

+9000
-209
lines changed

10 files changed

+9000
-209
lines changed

COPYRIGHT.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ Comment: doctest
220220
Copyright: 2016-2023, Viktor Kirilov
221221
License: Expat
222222

223+
Files: thirdparty/dr_libs/*
224+
Comment: dr_libs
225+
Copyright: 2024 David Reid
226+
License: public-domain or Unlicense or Expat
227+
223228
Files: thirdparty/embree/*
224229
Comment: Embree
225230
Copyright: 2009-2021 Intel Corporation

doc/classes/AudioStreamWAV.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<class name="AudioStreamWAV" inherits="AudioStream" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
33
<brief_description>
4-
Stores audio data loaded from WAV files.
4+
Stores audio data loaded from WAV and AIFF files.
55
</brief_description>
66
<description>
7-
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.
7+
AudioStreamWAV stores sound samples loaded from WAV and 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.
88
This class can also be used to store dynamically-generated PCM audio data. See also [AudioStreamGenerator] for procedural audio generation.
99
</description>
1010
<tutorials>

doc/classes/ResourceImporterWAV.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<class name="ResourceImporterWAV" inherits="ResourceImporter" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
33
<brief_description>
4-
Imports a WAV audio file for playback.
4+
Imports WAV and AIFF audio files for playback.
55
</brief_description>
66
<description>
7-
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.
8-
By default, Godot imports WAV files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property.
7+
WAV and AIFF are uncompressed formats, 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 and AIFF sounds can be played at the same time, even on low-end devices.
8+
By default, Godot imports WAV and AIFF files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property.
99
</description>
1010
<tutorials>
1111
<link title="Importing audio samples">$DOCS_URL/tutorials/assets_pipeline/importing_audio_samples.html</link>
@@ -25,7 +25,7 @@
2525
</member>
2626
<member name="edit/loop_mode" type="int" setter="" getter="" default="0">
2727
Controls how audio should loop.
28-
- [b]Detect From WAV:[/b] Uses loop information from the WAV metadata.
28+
- [b]Detect From WAV:[/b] Uses loop information from the WAV metadata (AIFF does not support this).
2929
- [b]Disabled:[/b] Don't loop audio, even if the metadata indicates the file playback should loop.
3030
- [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.
3131
- [b]Ping-Pong:[/b] Plays the audio forward until [member edit/loop_end], then backwards to [member edit/loop_begin], repeating this cycle.

editor/import/resource_importer_wav.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ String ResourceImporterWAV::get_importer_name() const {
3737
}
3838

3939
String ResourceImporterWAV::get_visible_name() const {
40-
return "Microsoft WAV";
40+
return "Microsoft WAV/Apple AIFF";
4141
}
4242

4343
void ResourceImporterWAV::get_recognized_extensions(List<String> *p_extensions) const {
4444
p_extensions->push_back("wav");
45+
p_extensions->push_back("wave");
46+
p_extensions->push_back("aif");
47+
p_extensions->push_back("aiff");
48+
p_extensions->push_back("aifc");
4549
}
4650

4751
String ResourceImporterWAV::get_save_extension() const {

scene/resources/audio_stream_wav.cpp

Lines changed: 53 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
2929
/**************************************************************************/
3030

31-
#include "audio_stream_wav.h"
32-
33-
#include "core/io/file_access_memory.h"
3431
#include "core/io/marshalls.h"
32+
#include "thirdparty/dr_libs/dr_wav_defines.h"
33+
34+
#include "audio_stream_wav.h"
3535

3636
const float TRIM_DB_LIMIT = -50;
3737
const int TRIM_FADE_OUT_FRAMES = 500;
@@ -724,221 +724,78 @@ Ref<AudioSample> AudioStreamWAV::generate_sample() const {
724724
}
725725

726726
Ref<AudioStreamWAV> AudioStreamWAV::load_from_buffer(const Vector<uint8_t> &p_stream_data, const Dictionary &p_options) {
727-
// /* STEP 1, READ WAVE FILE */
728-
729-
Ref<FileAccessMemory> file;
730-
file.instantiate();
731-
Error err = file->open_custom(p_stream_data.ptr(), p_stream_data.size());
732-
ERR_FAIL_COND_V_MSG(err != OK, Ref<AudioStreamWAV>(), "Cannot create memfile for WAV file buffer.");
733-
734-
/* CHECK RIFF */
735-
char riff[5];
736-
riff[4] = 0;
737-
file->get_buffer((uint8_t *)&riff, 4); //RIFF
738-
739-
if (riff[0] != 'R' || riff[1] != 'I' || riff[2] != 'F' || riff[3] != 'F') {
740-
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()));
741-
}
742-
743-
/* GET FILESIZE */
727+
drwav wav;
744728

745-
// The file size in header is 8 bytes less than the actual size.
746-
// See https://docs.fileformat.com/audio/wav/
747-
const int FILE_SIZE_HEADER_OFFSET = 8;
748-
uint32_t file_size_header = file->get_32() + FILE_SIZE_HEADER_OFFSET;
749-
uint64_t file_size = file->get_length();
750-
if (file_size != file_size_header) {
751-
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));
729+
if (!drwav_init_memory_with_metadata(&wav, p_stream_data.ptr(), p_stream_data.size(), DRWAV_WITH_METADATA, nullptr)) {
730+
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Audio data is invalid, corrupted, or an unsupported format.");
752731
}
753732

754-
/* CHECK WAVE */
755-
756-
char wave[5];
757-
wave[4] = 0;
758-
file->get_buffer((uint8_t *)&wave, 4); //WAVE
759-
760-
if (wave[0] != 'W' || wave[1] != 'A' || wave[2] != 'V' || wave[3] != 'E') {
761-
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()));
762-
}
763-
764-
// Let users override potential loop points from the WAV.
765-
// We parse the WAV loop points only with "Detect From WAV" (0).
766-
int import_loop_mode = p_options["edit/loop_mode"];
767-
768-
int format_bits = 0;
769-
int format_channels = 0;
733+
return _load_from_dr_wav(wav, p_options);
734+
}
770735

771-
AudioStreamWAV::LoopMode loop_mode = AudioStreamWAV::LOOP_DISABLED;
772-
uint16_t compression_code = 1;
773-
bool format_found = false;
774-
bool data_found = false;
775-
int format_freq = 0;
776-
int loop_begin = 0;
777-
int loop_end = 0;
778-
int frames = 0;
736+
Ref<AudioStreamWAV> AudioStreamWAV::load_from_file(const String &p_path, const Dictionary &p_options) {
737+
Error err;
779738

780-
Vector<float> data;
739+
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ, &err);
740+
ERR_FAIL_COND_V_MSG(err != OK, Ref<AudioStreamWAV>(), vformat("Cannot open file '%s'.", p_path));
781741

782-
while (!file->eof_reached()) {
783-
/* chunk */
784-
char chunk_id[4];
785-
file->get_buffer((uint8_t *)&chunk_id, 4); //RIFF
742+
drwav_read_proc read_fa = [](void *p_user_data, void *p_out, size_t to_read) -> size_t {
743+
Ref<FileAccess> lfile = *(Ref<FileAccess> *)p_user_data;
744+
return lfile->get_buffer((uint8_t *)p_out, to_read);
745+
};
786746

787-
/* chunk size */
788-
uint32_t chunksize = file->get_32();
789-
uint32_t file_pos = file->get_position(); //save file pos, so we can skip to next chunk safely
747+
drwav_seek_proc seek_fa = [](void *p_user_data, int p_offset, drwav_seek_origin origin) -> drwav_bool32 {
748+
Ref<FileAccess> lfile = *(Ref<FileAccess> *)p_user_data;
749+
uint64_t new_offset = p_offset + (origin == drwav_seek_origin_current ? lfile->get_position() : 0);
790750

791-
if (file->eof_reached()) {
792-
//ERR_PRINT("EOF REACH");
793-
break;
751+
if (new_offset > lfile->get_length()) {
752+
return DRWAV_FALSE;
794753
}
795754

796-
if (chunk_id[0] == 'f' && chunk_id[1] == 'm' && chunk_id[2] == 't' && chunk_id[3] == ' ' && !format_found) {
797-
/* IS FORMAT CHUNK */
755+
lfile->seek(new_offset);
756+
return DRWAV_TRUE;
757+
};
798758

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

806-
format_channels = file->get_16();
807-
if (format_channels != 1 && format_channels != 2) {
808-
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Format not supported for WAVE file (not stereo or mono).");
809-
}
810-
811-
format_freq = file->get_32(); //sampling rate
812-
813-
file->get_32(); // average bits/second (unused)
814-
file->get_16(); // block align (unused)
815-
format_bits = file->get_16(); // bits per sample
761+
if (!drwav_init_with_metadata(&wav, read_fa, seek_fa, &file, DRWAV_WITH_METADATA, nullptr)) {
762+
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Cannot read data from file '" + p_path + "'. Data is invalid, corrupted, or an unsupported format.");
763+
}
816764

817-
if (format_bits % 8 || format_bits == 0) {
818-
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Invalid amount of bits in the sample (should be one of 8, 16, 24 or 32).");
819-
}
765+
return _load_from_dr_wav(wav, p_options);
766+
}
820767

821-
if (compression_code == 3 && format_bits % 32) {
822-
ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Invalid amount of bits in the IEEE float sample (should be 32 or 64).");
823-
}
768+
Ref<AudioStreamWAV> AudioStreamWAV::_load_from_dr_wav(drwav &p_wav, const Dictionary &p_options) {
769+
// STEP 1, READ DATA
824770

825-
/* Don't need anything else, continue */
826-
format_found = true;
827-
}
771+
int format_bits = p_wav.bitsPerSample;
772+
int format_channels = p_wav.channels;
773+
int format_freq = p_wav.sampleRate;
774+
int frames = p_wav.totalPCMFrameCount;
828775

829-
if (chunk_id[0] == 'd' && chunk_id[1] == 'a' && chunk_id[2] == 't' && chunk_id[3] == 'a' && !data_found) {
830-
/* IS DATA CHUNK */
831-
data_found = true;
776+
int import_loop_mode = p_options["edit/loop_mode"];
832777

833-
if (!format_found) {
834-
ERR_PRINT("'data' chunk before 'format' chunk found.");
778+
int loop_begin = 0;
779+
int loop_end = 0;
780+
LoopMode loop_mode = LOOP_DISABLED;
781+
if (import_loop_mode == 0) {
782+
for (uint32_t meta = 0; meta < p_wav.metadataCount; ++meta) {
783+
drwav_metadata md = p_wav.pMetadata[meta];
784+
if (md.type == drwav_metadata_type_smpl && md.data.smpl.sampleLoopCount > 0) {
785+
drwav_smpl_loop loop = md.data.smpl.pLoops[0];
786+
loop_mode = (LoopMode)(loop.type + 1);
787+
loop_begin = loop.firstSampleByteOffset;
788+
loop_end = loop.lastSampleByteOffset;
835789
break;
836790
}
837-
838-
uint64_t remaining_bytes = file_size - file_pos;
839-
frames = chunksize;
840-
if (remaining_bytes < chunksize) {
841-
WARN_PRINT("Data chunk size is smaller than expected. Proceeding with actual data size.");
842-
frames = remaining_bytes;
843-
}
844-
845-
ERR_FAIL_COND_V(format_channels == 0, Ref<AudioStreamWAV>());
846-
frames /= format_channels;
847-
frames /= (format_bits >> 3);
848-
849-
/*print_line("chunksize: "+itos(chunksize));
850-
print_line("channels: "+itos(format_channels));
851-
print_line("bits: "+itos(format_bits));
852-
*/
853-
854-
data.resize(frames * format_channels);
855-
856-
if (compression_code == 1) {
857-
if (format_bits == 8) {
858-
for (int i = 0; i < frames * format_channels; i++) {
859-
// 8 bit samples are UNSIGNED
860-
861-
data.write[i] = int8_t(file->get_8() - 128) / 128.f;
862-
}
863-
} else if (format_bits == 16) {
864-
for (int i = 0; i < frames * format_channels; i++) {
865-
//16 bit SIGNED
866-
867-
data.write[i] = int16_t(file->get_16()) / 32768.f;
868-
}
869-
} else {
870-
for (int i = 0; i < frames * format_channels; i++) {
871-
//16+ bits samples are SIGNED
872-
// if sample is > 16 bits, just read extra bytes
873-
874-
uint32_t s = 0;
875-
for (int b = 0; b < (format_bits >> 3); b++) {
876-
s |= ((uint32_t)file->get_8()) << (b * 8);
877-
}
878-
s <<= (32 - format_bits);
879-
880-
data.write[i] = (int32_t(s) >> 16) / 32768.f;
881-
}
882-
}
883-
} else if (compression_code == 3) {
884-
if (format_bits == 32) {
885-
for (int i = 0; i < frames * format_channels; i++) {
886-
//32 bit IEEE Float
887-
888-
data.write[i] = file->get_float();
889-
}
890-
} else if (format_bits == 64) {
891-
for (int i = 0; i < frames * format_channels; i++) {
892-
//64 bit IEEE Float
893-
894-
data.write[i] = file->get_double();
895-
}
896-
}
897-
}
898-
899-
// This is commented out due to some weird edge case seemingly in FileAccessMemory, doesn't seem to have any side effects though.
900-
// if (file->eof_reached()) {
901-
// ERR_FAIL_V_MSG(Ref<AudioStreamWAV>(), "Premature end of file.");
902-
// }
903791
}
904-
905-
if (import_loop_mode == 0 && chunk_id[0] == 's' && chunk_id[1] == 'm' && chunk_id[2] == 'p' && chunk_id[3] == 'l') {
906-
// Loop point info!
907-
908-
/**
909-
* Consider exploring next document:
910-
* http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/RIFFNEW.pdf
911-
* Especially on page:
912-
* 16 - 17
913-
* Timestamp:
914-
* 22:38 06.07.2017 GMT
915-
**/
916-
917-
for (int i = 0; i < 10; i++) {
918-
file->get_32(); // i wish to know why should i do this... no doc!
919-
}
920-
921-
// only read 0x00 (loop forward), 0x01 (loop ping-pong) and 0x02 (loop backward)
922-
// Skip anything else because it's not supported, reserved for future uses or sampler specific
923-
// from https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#smpl (loop type values table)
924-
int loop_type = file->get_32();
925-
if (loop_type == 0x00 || loop_type == 0x01 || loop_type == 0x02) {
926-
if (loop_type == 0x00) {
927-
loop_mode = AudioStreamWAV::LOOP_FORWARD;
928-
} else if (loop_type == 0x01) {
929-
loop_mode = AudioStreamWAV::LOOP_PINGPONG;
930-
} else if (loop_type == 0x02) {
931-
loop_mode = AudioStreamWAV::LOOP_BACKWARD;
932-
}
933-
loop_begin = file->get_32();
934-
loop_end = file->get_32();
935-
}
936-
}
937-
// Move to the start of the next chunk. Note that RIFF requires a padding byte for odd
938-
// chunk sizes.
939-
file->seek(file_pos + chunksize + (chunksize & 1));
940792
}
941793

794+
Vector<float> data;
795+
data.resize(frames * format_channels);
796+
drwav_read_pcm_frames_f32(&p_wav, frames, data.ptrw());
797+
drwav_uninit(&p_wav);
798+
942799
// STEP 2, APPLY CONVERSIONS
943800

944801
bool is16 = format_bits != 8;
@@ -1176,12 +1033,6 @@ Ref<AudioStreamWAV> AudioStreamWAV::load_from_buffer(const Vector<uint8_t> &p_st
11761033
return sample;
11771034
}
11781035

1179-
Ref<AudioStreamWAV> AudioStreamWAV::load_from_file(const String &p_path, const Dictionary &p_options) {
1180-
const Vector<uint8_t> stream_data = FileAccess::get_file_as_bytes(p_path);
1181-
ERR_FAIL_COND_V_MSG(stream_data.is_empty(), Ref<AudioStreamWAV>(), vformat("Cannot open file '%s'.", p_path));
1182-
return load_from_buffer(stream_data, p_options);
1183-
}
1184-
11851036
void AudioStreamWAV::_bind_methods() {
11861037
ClassDB::bind_static_method("AudioStreamWAV", D_METHOD("load_from_buffer", "stream_data", "options"), &AudioStreamWAV::load_from_buffer, DEFVAL(Dictionary()));
11871038
ClassDB::bind_static_method("AudioStreamWAV", D_METHOD("load_from_file", "path", "options"), &AudioStreamWAV::load_from_file, DEFVAL(Dictionary()));

scene/resources/audio_stream_wav.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include "servers/audio/audio_stream.h"
3535

36+
#include "thirdparty/dr_libs/dr_wav.h"
3637
#include "thirdparty/misc/qoa.h"
3738

3839
class AudioStreamWAV;
@@ -138,6 +139,8 @@ class AudioStreamWAV : public AudioStream {
138139
LocalVector<uint8_t> data;
139140
uint32_t data_bytes = 0;
140141

142+
static Ref<AudioStreamWAV> _load_from_dr_wav(drwav &p_wav, const Dictionary &p_options);
143+
141144
protected:
142145
static void _bind_methods();
143146

0 commit comments

Comments
 (0)