Skip to content

Commit 95e7b35

Browse files
DeeJayLSPSpartan322
authored andcommitted
Add support for AIFF files and other WAV formats
(cherry picked from commit 8345199b92668a4649b1045607ea454d52fdd63f)
1 parent fd9045f commit 95e7b35

File tree

6 files changed

+8943
-196
lines changed

6 files changed

+8943
-196
lines changed

COPYRIGHT.txt

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

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

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 data from a WAV/AIFF audio file 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, Redot imports WAV files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property.
7+
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.
8+
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.
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: 60 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
#include "core/io/resource_saver.h"
3838
#include "scene/resources/audio_stream_wav.h"
3939

40+
#define DRWAV_IMPLEMENTATION
41+
#define DRWAV_NO_STDIO
42+
#define DR_WAV_LIBSNDFILE_COMPAT
43+
#define DRWAV_MALLOC(sz) memalloc(sz)
44+
#define DRWAV_REALLOC(p, sz) memrealloc(p, sz)
45+
#define DRWAV_FREE(p) \
46+
if (p) \
47+
memfree(p)
48+
49+
#include "thirdparty/dr_libs/dr_wav.h"
50+
4051
const float TRIM_DB_LIMIT = -50;
4152
const int TRIM_FADE_OUT_FRAMES = 500;
4253

@@ -45,11 +56,15 @@ String ResourceImporterWAV::get_importer_name() const {
4556
}
4657

4758
String ResourceImporterWAV::get_visible_name() const {
48-
return "Microsoft WAV";
59+
return "Microsoft WAV/Apple AIFF";
4960
}
5061

5162
void ResourceImporterWAV::get_recognized_extensions(List<String> *p_extensions) const {
5263
p_extensions->push_back("wav");
64+
p_extensions->push_back("wave");
65+
p_extensions->push_back("aif");
66+
p_extensions->push_back("aiff");
67+
p_extensions->push_back("aifc");
5368
}
5469

5570
String ResourceImporterWAV::get_save_extension() const {
@@ -97,219 +112,72 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp
97112
}
98113

99114
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) {
100-
/* STEP 1, READ WAVE FILE */
115+
// STEP 1, READ FILE
101116

102117
Error err;
103118
Ref<FileAccess> file = FileAccess::open(p_source_file, FileAccess::READ, &err);
104119

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

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

112-
if (riff[0] != 'R' || riff[1] != 'I' || riff[2] != 'F' || riff[3] != 'F') {
113-
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()));
114-
}
126+
bytes_to_read = MIN(bytes_to_read, lfile->get_length() - lfile->get_position());
127+
lfile->get_buffer((uint8_t *)p_buffer_out, bytes_to_read);
128+
return bytes_to_read;
129+
};
115130

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

118-
// The file size in header is 8 bytes less than the actual size.
119-
// See https://docs.fileformat.com/audio/wav/
120-
const int FILE_SIZE_HEADER_OFFSET = 8;
121-
uint32_t file_size_header = file->get_32() + FILE_SIZE_HEADER_OFFSET;
122-
uint64_t file_size = file->get_length();
123-
if (file_size != file_size_header) {
124-
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));
125-
}
135+
if (new_offset > lfile->get_length() || (p_offset < 0 && (size_t)-p_offset > lfile->get_position())) {
136+
return DRWAV_FALSE;
137+
}
126138

127-
/* CHECK WAVE */
139+
lfile->seek(new_offset);
140+
return DRWAV_TRUE;
141+
};
128142

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

133-
if (wave[0] != 'W' || wave[1] != 'A' || wave[2] != 'V' || wave[3] != 'E') {
134-
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()));
148+
if (wav.totalPCMFrameCount > INT32_MAX) {
149+
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.");
135150
}
136151

137-
// Let users override potential loop points from the WAV.
138-
// We parse the WAV loop points only with "Detect From WAV" (0).
139-
int import_loop_mode = p_options["edit/loop_mode"];
152+
int format_bits = wav.bitsPerSample;
153+
int format_channels = wav.channels;
154+
int format_freq = wav.sampleRate;
155+
int frames = wav.totalPCMFrameCount;
140156

141-
int format_bits = 0;
142-
int format_channels = 0;
157+
int import_loop_mode = p_options["edit/loop_mode"];
143158

144-
AudioStreamWAV::LoopMode loop_mode = AudioStreamWAV::LOOP_DISABLED;
145-
uint16_t compression_code = 1;
146-
bool format_found = false;
147-
bool data_found = false;
148-
int format_freq = 0;
149159
int loop_begin = 0;
150160
int loop_end = 0;
151-
int frames = 0;
152-
153-
Vector<float> data;
154-
155-
while (!file->eof_reached()) {
156-
/* chunk */
157-
char chunkID[4];
158-
file->get_buffer((uint8_t *)&chunkID, 4); //RIFF
159-
160-
/* chunk size */
161-
uint32_t chunksize = file->get_32();
162-
uint32_t file_pos = file->get_position(); //save file pos, so we can skip to next chunk safely
163-
164-
if (file->eof_reached()) {
165-
//ERR_PRINT("EOF REACH");
166-
break;
167-
}
168-
169-
if (chunkID[0] == 'f' && chunkID[1] == 'm' && chunkID[2] == 't' && chunkID[3] == ' ' && !format_found) {
170-
/* IS FORMAT CHUNK */
171-
172-
//Issue: #7755 : Not a bug - usage of other formats (format codes) are unsupported in current importer version.
173-
//Consider revision for engine version 3.0
174-
compression_code = file->get_16();
175-
if (compression_code != 1 && compression_code != 3) {
176-
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.");
177-
}
178-
179-
format_channels = file->get_16();
180-
if (format_channels != 1 && format_channels != 2) {
181-
ERR_FAIL_V_MSG(ERR_INVALID_DATA, "Format not supported for WAVE file (not stereo or mono).");
182-
}
183-
184-
format_freq = file->get_32(); //sampling rate
185-
186-
file->get_32(); // average bits/second (unused)
187-
file->get_16(); // block align (unused)
188-
format_bits = file->get_16(); // bits per sample
189-
190-
if (format_bits % 8 || format_bits == 0) {
191-
ERR_FAIL_V_MSG(ERR_INVALID_DATA, "Invalid amount of bits in the sample (should be one of 8, 16, 24 or 32).");
192-
}
193-
194-
if (compression_code == 3 && format_bits % 32) {
195-
ERR_FAIL_V_MSG(ERR_INVALID_DATA, "Invalid amount of bits in the IEEE float sample (should be 32 or 64).");
196-
}
197-
198-
/* Don't need anything else, continue */
199-
format_found = true;
200-
}
201-
202-
if (chunkID[0] == 'd' && chunkID[1] == 'a' && chunkID[2] == 't' && chunkID[3] == 'a' && !data_found) {
203-
/* IS DATA CHUNK */
204-
data_found = true;
205-
206-
if (!format_found) {
207-
ERR_PRINT("'data' chunk before 'format' chunk found.");
161+
AudioStreamWAV::LoopMode loop_mode = AudioStreamWAV::LOOP_DISABLED;
162+
if (import_loop_mode == 0) {
163+
for (uint32_t meta = 0; meta < wav.metadataCount; ++meta) {
164+
drwav_metadata md = wav.pMetadata[meta];
165+
if (md.type == drwav_metadata_type_smpl && md.data.smpl.sampleLoopCount > 0) {
166+
drwav_smpl_loop loop = md.data.smpl.pLoops[0];
167+
loop_mode = (AudioStreamWAV::LoopMode)(loop.type + 1);
168+
loop_begin = loop.firstSampleByteOffset;
169+
loop_end = loop.lastSampleByteOffset;
208170
break;
209171
}
210-
211-
uint64_t remaining_bytes = file_size - file_pos;
212-
frames = chunksize;
213-
if (remaining_bytes < chunksize) {
214-
WARN_PRINT(vformat("Data chunk size is smaller than expected. Proceeding with actual data size. (%s)", p_source_file));
215-
frames = remaining_bytes;
216-
}
217-
218-
ERR_FAIL_COND_V(format_channels == 0, ERR_INVALID_DATA);
219-
frames /= format_channels;
220-
frames /= (format_bits >> 3);
221-
222-
/*print_line("chunksize: "+itos(chunksize));
223-
print_line("channels: "+itos(format_channels));
224-
print_line("bits: "+itos(format_bits));
225-
*/
226-
227-
data.resize(frames * format_channels);
228-
229-
if (compression_code == 1) {
230-
if (format_bits == 8) {
231-
for (int i = 0; i < frames * format_channels; i++) {
232-
// 8 bit samples are UNSIGNED
233-
234-
data.write[i] = int8_t(file->get_8() - 128) / 128.f;
235-
}
236-
} else if (format_bits == 16) {
237-
for (int i = 0; i < frames * format_channels; i++) {
238-
//16 bit SIGNED
239-
240-
data.write[i] = int16_t(file->get_16()) / 32768.f;
241-
}
242-
} else {
243-
for (int i = 0; i < frames * format_channels; i++) {
244-
//16+ bits samples are SIGNED
245-
// if sample is > 16 bits, just read extra bytes
246-
247-
uint32_t s = 0;
248-
for (int b = 0; b < (format_bits >> 3); b++) {
249-
s |= ((uint32_t)file->get_8()) << (b * 8);
250-
}
251-
s <<= (32 - format_bits);
252-
253-
data.write[i] = (int32_t(s) >> 16) / 32768.f;
254-
}
255-
}
256-
} else if (compression_code == 3) {
257-
if (format_bits == 32) {
258-
for (int i = 0; i < frames * format_channels; i++) {
259-
//32 bit IEEE Float
260-
261-
data.write[i] = file->get_float();
262-
}
263-
} else if (format_bits == 64) {
264-
for (int i = 0; i < frames * format_channels; i++) {
265-
//64 bit IEEE Float
266-
267-
data.write[i] = file->get_double();
268-
}
269-
}
270-
}
271-
272-
if (file->eof_reached()) {
273-
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Premature end of file.");
274-
}
275172
}
173+
}
276174

277-
if (import_loop_mode == 0 && chunkID[0] == 's' && chunkID[1] == 'm' && chunkID[2] == 'p' && chunkID[3] == 'l') {
278-
// Loop point info!
279-
280-
/**
281-
* Consider exploring next document:
282-
* http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/RIFFNEW.pdf
283-
* Especially on page:
284-
* 16 - 17
285-
* Timestamp:
286-
* 22:38 06.07.2017 GMT
287-
**/
288-
289-
for (int i = 0; i < 10; i++) {
290-
file->get_32(); // i wish to know why should i do this... no doc!
291-
}
175+
Vector<float> data;
176+
data.resize(frames * format_channels);
177+
drwav_read_pcm_frames_f32(&wav, frames, data.ptrw());
292178

293-
// only read 0x00 (loop forward), 0x01 (loop ping-pong) and 0x02 (loop backward)
294-
// Skip anything else because it's not supported, reserved for future uses or sampler specific
295-
// from https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#smpl (loop type values table)
296-
int loop_type = file->get_32();
297-
if (loop_type == 0x00 || loop_type == 0x01 || loop_type == 0x02) {
298-
if (loop_type == 0x00) {
299-
loop_mode = AudioStreamWAV::LOOP_FORWARD;
300-
} else if (loop_type == 0x01) {
301-
loop_mode = AudioStreamWAV::LOOP_PINGPONG;
302-
} else if (loop_type == 0x02) {
303-
loop_mode = AudioStreamWAV::LOOP_BACKWARD;
304-
}
305-
loop_begin = file->get_32();
306-
loop_end = file->get_32();
307-
}
308-
}
309-
// Move to the start of the next chunk. Note that RIFF requires a padding byte for odd
310-
// chunk sizes.
311-
file->seek(file_pos + chunksize + (chunksize & 1));
312-
}
179+
drwav_uninit(&wav);
180+
file->close();
313181

314182
// STEP 2, APPLY CONVERSIONS
315183

thirdparty/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ Files extracted from upstream source:
186186
- `LICENSE.txt`
187187

188188

189+
## dr_libs
190+
191+
- Upstream: https://github.com/mackron/dr_libs
192+
- Version: git (da35f9d6c7374a95353fd1df1d394d44ab66cf01, 2024)
193+
- License: Public Domain or Unlicense or MIT
194+
195+
Files extracted from upstream source:
196+
197+
- `dr_wav.h`
198+
- `LICENSE`
199+
200+
189201
## embree
190202

191203
- Upstream: https://github.com/embree/embree

0 commit comments

Comments
 (0)