Skip to content

Commit

Permalink
ResourceImporterWAV: Use dr_wav to read file data
Browse files Browse the repository at this point in the history
  • Loading branch information
DeeJayLSP committed Sep 15, 2024
1 parent e2dd56b commit 67fe879
Show file tree
Hide file tree
Showing 7 changed files with 8,940 additions and 189 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
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 high numbers of WAV/AIFF 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 loop information).
- [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
229 changes: 44 additions & 185 deletions editor/import/resource_importer_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
#include "core/io/resource_saver.h"
#include "scene/resources/audio_stream_wav.h"

#define DRWAV_IMPLEMENTATION
#define DR_WAV_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) memfree(p)

#include "thirdparty/dr_libs/dr_wav.h"

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

Expand All @@ -43,11 +52,15 @@ String ResourceImporterWAV::get_importer_name() const {
}

String ResourceImporterWAV::get_visible_name() const {
return "Microsoft WAV";
return "Microsoft WAV/Apple AIFF";
}

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 Expand Up @@ -95,209 +108,55 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp
}

Error ResourceImporterWAV::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
/* STEP 1, READ WAVE FILE */
// STEP 1, READ FILE

Error err;
Ref<FileAccess> file = FileAccess::open(p_source_file, FileAccess::READ, &err);
Vector<uint8_t> file = FileAccess::get_file_as_bytes(p_source_file, &err);

ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_OPEN, "Cannot open file '" + p_source_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(ERR_FILE_UNRECOGNIZED, 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, file.ptr(), file.size(), DRWAV_WITH_METADATA, nullptr)) {
ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Could not read file '" + p_source_file + "'. Invalid/corrupted data or unsupported format.");
}

/* GET FILESIZE */
file->get_32(); // filesize

/* CHECK WAVE */

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(ERR_FILE_UNRECOGNIZED, vformat("Not a WAV file. Header should contain 'WAVE', but found '%s', in file of size %d bytes", wave, file->get_length()));
if (wav.totalPCMFrameCount > INT32_MAX) {
ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Could not read file '" + p_source_file + "'. Audio data exceeds maximum size of 2,147,483,647 frames.");
}

// 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;
int format_bits = wav.bitsPerSample;
int format_channels = wav.channels;
int format_freq = wav.sampleRate;
int frames = wav.totalPCMFrameCount;

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 import_loop_mode = p_options["edit/loop_mode"];
int loop_begin = 0;
int loop_end = 0;
int frames = 0;

Vector<float> data;

while (!file->eof_reached()) {
/* chunk */
char chunkID[4];
file->get_buffer((uint8_t *)&chunkID, 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 (chunkID[0] == 'f' && chunkID[1] == 'm' && chunkID[2] == 't' && chunkID[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(ERR_INVALID_DATA, "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(ERR_INVALID_DATA, "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(ERR_INVALID_DATA, "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(ERR_INVALID_DATA, "Invalid amount of bits in the IEEE float sample (should be 32 or 64).");
}

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

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

if (!format_found) {
ERR_PRINT("'data' chunk before 'format' chunk found.");
break;
}

frames = chunksize;

if (format_channels == 0) {
ERR_FAIL_COND_V(format_channels == 0, ERR_INVALID_DATA);
}
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();
}
}
}

if (file->eof_reached()) {
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Premature end of file.");
}
}

if (import_loop_mode == 0 && chunkID[0] == 's' && chunkID[1] == 'm' && chunkID[2] == 'p' && chunkID[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!
}

// 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) {
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) {
drwav_smpl_loop loop = md.data.smpl.pLoops[0];
if (loop.type == drwav_smpl_loop_type_forward)
loop_mode = AudioStreamWAV::LOOP_FORWARD;
} else if (loop_type == 0x01) {
else if (loop.type == drwav_smpl_loop_type_pingpong)
loop_mode = AudioStreamWAV::LOOP_PINGPONG;
} else if (loop_type == 0x02) {
else if (loop.type == drwav_smpl_loop_type_backward)
loop_mode = AudioStreamWAV::LOOP_BACKWARD;
}
loop_begin = file->get_32();
loop_end = file->get_32();
loop_begin = loop.firstSampleByteOffset;
loop_end = loop.lastSampleByteOffset;
}
}
// 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));
}

Vector<float> data;
data.resize(wav.totalPCMFrameCount * wav.channels);
drwav_read_pcm_frames_f32(&wav, wav.totalPCMFrameCount, data.ptrw());

drwav_uninit(&wav);
file.clear();

// STEP 2, APPLY CONVERSIONS

bool is16 = format_bits != 8;
Expand Down
12 changes: 12 additions & 0 deletions thirdparty/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ Files extracted from upstream source:
- `doctest/doctest.h` as `doctest.h`
- `LICENSE.txt`

## dr_libs

- Upstream: https://github.com/mackron/dr_libs
- Version: git (da35f9d6c7374a95353fd1df1d394d44ab66cf01, 2024)
- License: Public Domain or Unlicense or MIT
- Modifications: Added pointer check before free.

Files extracted from upstream source:

- `dr_wav.h`
- `LICENSE`


## 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 67fe879

Please sign in to comment.