Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for AIFF files and other WAV formats #877

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions COPYRIGHT.txt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,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, Redot 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, Redot 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
252 changes: 60 additions & 192 deletions editor/import/resource_importer_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
#include "core/io/resource_saver.h"
#include "scene/resources/audio_stream_wav.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 All @@ -45,11 +56,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 @@ -97,219 +112,72 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp
}

Error ResourceImporterWAV::import(ResourceUID::ID p_source_id, 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);

ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_OPEN, "Cannot open file '" + p_source_file + "'.");
ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot open file '" + p_source_file + "'.");

/* CHECK RIFF */
char riff[5];
riff[4] = 0;
file->get_buffer((uint8_t *)&riff, 4); //RIFF
// Lambdas allow dr_wav to use FileAccess
drwav_read_proc read_fa = [](void *p_user_data, void *p_buffer_out, size_t bytes_to_read) -> size_t {
Ref<FileAccess> lfile = *(Ref<FileAccess> *)p_user_data;

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()));
}
bytes_to_read = MIN(bytes_to_read, lfile->get_length() - lfile->get_position());
lfile->get_buffer((uint8_t *)p_buffer_out, bytes_to_read);
return bytes_to_read;
};

/* GET FILESIZE */
drwav_seek_proc seek_fa = [](void *p_user_data, int p_offset, drwav_seek_origin origin) -> drwav_bool32 {
Ref<FileAccess> lfile = *(Ref<FileAccess> *)p_user_data;
uint64_t new_offset = p_offset + (origin == drwav_seek_origin_current ? lfile->get_position() : 0);

// 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. (%s)", file_size, file_size > file_size_header ? "larger" : "smaller", file_size_header, p_source_file));
}
if (new_offset > lfile->get_length() || (p_offset < 0 && (size_t)-p_offset > lfile->get_position())) {
return DRWAV_FALSE;
}

/* CHECK WAVE */
lfile->seek(new_offset);
return DRWAV_TRUE;
};

char wave[5];
wave[4] = 0;
file->get_buffer((uint8_t *)&wave, 4); //WAVE
drwav wav;
if (!drwav_init_with_metadata(&wav, read_fa, seek_fa, &file, DRWAV_WITH_METADATA, nullptr)) {
ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Could not read file '" + p_source_file + "'. Invalid/corrupted data or unsupported format.");
}

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

int format_bits = 0;
int format_channels = 0;
int import_loop_mode = p_options["edit/loop_mode"];

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 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.");
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(vformat("Data chunk size is smaller than expected. Proceeding with actual data size. (%s)", p_source_file));
frames = remaining_bytes;
}

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!
}
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);
file->close();

// 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
Loading