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 QOA (Quite OK Audio) as a WAV compression mode #91014

Merged
merged 1 commit into from
May 2, 2024
Merged
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 @@ -412,6 +412,11 @@ Comment: PolyPartition / Triangulator
Copyright: 2011-2021, Ivan Fratric and contributors
License: Expat

Files: ./thirdparty/misc/qoa.h
Comment: Quite OK Audio Format
Copyright: 2023, Dominic Szablewski
License: Expat

Files: ./thirdparty/misc/r128.c
./thirdparty/misc/r128.h
Comment: r128 library
Expand Down
5 changes: 4 additions & 1 deletion doc/classes/AudioStreamWAV.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<return type="int" enum="Error" />
<param index="0" name="path" type="String" />
<description>
Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM format can't be saved.
Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM or QOA formats can't be saved.
[b]Note:[/b] A [code].wav[/code] extension is automatically appended to [param path] if it is missing.
</description>
</method>
Expand Down Expand Up @@ -56,6 +56,9 @@
<constant name="FORMAT_IMA_ADPCM" value="2" enum="Format">
Audio is compressed using IMA ADPCM.
</constant>
<constant name="FORMAT_QOA" value="3" enum="Format">
Audio is compressed as QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]).
</constant>
<constant name="LOOP_DISABLED" value="0" enum="LoopMode">
Audio does not loop.
</constant>
Expand Down
1 change: 1 addition & 0 deletions doc/classes/ResourceImporterWAV.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
The compression mode to use on import.
[b]Disabled:[/b] Imports audio data without any compression. This results in the highest possible quality.
[b]RAM (Ima-ADPCM):[/b] Performs fast lossy compression on import. Low CPU cost, but quality is noticeably decreased compared to Ogg Vorbis or even MP3.
[b]QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]):[/b] Performs lossy compression on import. CPU cost is slightly higher than IMA-ADPCM, but quality is much higher.
</member>
<member name="edit/loop_begin" type="int" setter="" getter="" default="0">
The begin loop point to use when [member edit/loop_mode] is [b]Forward[/b], [b]Ping-Pong[/b] or [b]Backward[/b]. This is set in seconds after the beginning of the audio file.
Expand Down
34 changes: 26 additions & 8 deletions editor/import/resource_importer_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_mode", PROPERTY_HINT_ENUM, "Detect From WAV,Disabled,Forward,Ping-Pong,Backward", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_begin"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_end"), -1));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM)"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM),QOA (Quite OK Audio)"), 0));
}

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) {
Expand Down Expand Up @@ -456,13 +456,13 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
is16 = false;
}

Vector<uint8_t> dst_data;
Vector<uint8_t> pcm_data;
AudioStreamWAV::Format dst_format;

if (compression == 1) {
dst_format = AudioStreamWAV::FORMAT_IMA_ADPCM;
if (format_channels == 1) {
_compress_ima_adpcm(data, dst_data);
_compress_ima_adpcm(data, pcm_data);
} else {
//byte interleave
Vector<float> left;
Expand All @@ -484,9 +484,9 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
_compress_ima_adpcm(right, bright);

int dl = bleft.size();
dst_data.resize(dl * 2);
pcm_data.resize(dl * 2);

uint8_t *w = dst_data.ptrw();
uint8_t *w = pcm_data.ptrw();
const uint8_t *rl = bleft.ptr();
const uint8_t *rr = bright.ptr();

Expand All @@ -498,13 +498,14 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s

} else {
dst_format = is16 ? AudioStreamWAV::FORMAT_16_BITS : AudioStreamWAV::FORMAT_8_BITS;
dst_data.resize(data.size() * (is16 ? 2 : 1));
bool enforce16 = is16 || compression == 2;
pcm_data.resize(data.size() * (enforce16 ? 2 : 1));
{
uint8_t *w = dst_data.ptrw();
uint8_t *w = pcm_data.ptrw();

int ds = data.size();
for (int i = 0; i < ds; i++) {
if (is16) {
if (enforce16) {
int16_t v = CLAMP(data[i] * 32768, -32768, 32767);
encode_uint16(v, &w[i * 2]);
} else {
Expand All @@ -515,6 +516,23 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
}
}

Vector<uint8_t> dst_data;
if (compression == 2) {
dst_format = AudioStreamWAV::FORMAT_QOA;
qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
uint32_t qoa_len = 0;

desc.samplerate = rate;
desc.samples = frames;
desc.channels = format_channels;

void *encoded = qoa_encode((short *)pcm_data.ptrw(), &desc, &qoa_len);
dst_data.resize(qoa_len);
memcpy(dst_data.ptrw(), encoded, qoa_len);
} else {
dst_data = pcm_data;
}

Ref<AudioStreamWAV> sample;
sample.instantiate();
sample->set_data(dst_data);
Expand Down
144 changes: 113 additions & 31 deletions scene/resources/audio_stream_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ void AudioStreamPlaybackWAV::seek(double p_time) {
offset = uint64_t(p_time * base->mix_rate) << MIX_FRAC_BITS;
}

template <typename Depth, bool is_stereo, bool is_ima_adpcm>
void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm) {
template <typename Depth, bool is_stereo, bool is_ima_adpcm, bool is_qoa>
void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa) {
// this function will be compiled branchless by any decent compiler

int32_t final, final_r, next, next_r;
int32_t final = 0, final_r = 0, next = 0, next_r = 0;
while (p_amount) {
p_amount--;
int64_t pos = p_offset >> MIX_FRAC_BITS;
if (is_stereo && !is_ima_adpcm) {
if (is_stereo && !is_ima_adpcm && !is_qoa) {
pos <<= 1;
}

Expand Down Expand Up @@ -175,32 +175,77 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst,
}

} else {
final = p_src[pos];
if (is_stereo) {
final_r = p_src[pos + 1];
}
if (is_qoa) {
if (pos != p_qoa->cache_pos) { // Prevents triple decoding on lower mix rates.
for (int i = 0; i < 2; i++) {
// Sign operations prevent triple decoding on backward loops, maxing prevents pop.
uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc->samples - 1);
uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa->frame_len;

if (p_qoa->data_ofs != new_data_ofs) {
p_qoa->data_ofs = new_data_ofs;
const uint8_t *src_ptr = (const uint8_t *)base->data;
src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD;
qoa_decode_frame(src_ptr, p_qoa->frame_len, p_qoa->desc, p_qoa->dec, &p_qoa->dec_len);
}

if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */
final <<= 8;
uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc->channels;

if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) {
final = p_qoa->dec[dec_idx];
p_qoa->cache[0] = final;
if (is_stereo) {
final_r = p_qoa->dec[dec_idx + 1];
p_qoa->cache_r[0] = final_r;
}
} else {
next = p_qoa->dec[dec_idx];
p_qoa->cache[1] = next;
if (is_stereo) {
next_r = p_qoa->dec[dec_idx + 1];
p_qoa->cache_r[1] = next_r;
}
}
}
p_qoa->cache_pos = pos;
} else {
final = p_qoa->cache[0];
if (is_stereo) {
final_r = p_qoa->cache_r[0];
}

next = p_qoa->cache[1];
if (is_stereo) {
next_r = p_qoa->cache_r[1];
}
}
DeeJayLSP marked this conversation as resolved.
Show resolved Hide resolved
} else {
final = p_src[pos];
if (is_stereo) {
final_r <<= 8;
final_r = p_src[pos + 1];
}
}

if (is_stereo) {
next = p_src[pos + 2];
next_r = p_src[pos + 3];
} else {
next = p_src[pos + 1];
}
if constexpr (sizeof(Depth) == 1) { /* conditions will not exist anymore when compiled! */
final <<= 8;
if (is_stereo) {
final_r <<= 8;
}
}

if constexpr (sizeof(Depth) == 1) {
next <<= 8;
if (is_stereo) {
next_r <<= 8;
next = p_src[pos + 2];
next_r = p_src[pos + 3];
} else {
next = p_src[pos + 1];
}
}

if constexpr (sizeof(Depth) == 1) {
next <<= 8;
if (is_stereo) {
next_r <<= 8;
}
}
}
int32_t frac = int64_t(p_offset & MIX_FRAC_MASK);

final = final + ((next - final) * frac >> MIX_FRAC_BITS);
Expand Down Expand Up @@ -240,6 +285,9 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_
case AudioStreamWAV::FORMAT_IMA_ADPCM:
len *= 2;
break;
case AudioStreamWAV::FORMAT_QOA:
len = qoa.desc->samples * qoa.desc->channels;
break;
}

if (base->stereo) {
Expand Down Expand Up @@ -368,27 +416,34 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_
switch (base->format) {
case AudioStreamWAV::FORMAT_8_BITS: {
if (is_stereo) {
do_resample<int8_t, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int8_t, true, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
do_resample<int8_t, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int8_t, false, false, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}
} break;
case AudioStreamWAV::FORMAT_16_BITS: {
if (is_stereo) {
do_resample<int16_t, true, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int16_t, true, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
do_resample<int16_t, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int16_t, false, false, false>((int16_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}

} break;
case AudioStreamWAV::FORMAT_IMA_ADPCM: {
if (is_stereo) {
do_resample<int8_t, true, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int8_t, true, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
do_resample<int8_t, false, true>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm);
do_resample<int8_t, false, true, false>((int8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}

} break;
case AudioStreamWAV::FORMAT_QOA: {
if (is_stereo) {
do_resample<uint8_t, true, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
} else {
do_resample<uint8_t, false, false, true>((uint8_t *)data, dst_buff, offset, increment, target, ima_adpcm, &qoa);
}
} break;
}

dst_buff += target;
Expand All @@ -412,6 +467,16 @@ void AudioStreamPlaybackWAV::tag_used_streams() {

AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {}

AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() {
if (qoa.desc) {
memfree(qoa.desc);
}

if (qoa.dec) {
memfree(qoa.dec);
}
}

/////////////////////

void AudioStreamWAV::set_format(Format p_format) {
Expand Down Expand Up @@ -475,6 +540,10 @@ double AudioStreamWAV::get_length() const {
case AudioStreamWAV::FORMAT_IMA_ADPCM:
len *= 2;
break;
case AudioStreamWAV::FORMAT_QOA:
qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, &desc);
len = desc.samples * desc.channels;
}

if (stereo) {
Expand Down Expand Up @@ -526,8 +595,8 @@ Vector<uint8_t> AudioStreamWAV::get_data() const {
}

Error AudioStreamWAV::save_to_wav(const String &p_path) {
if (format == AudioStreamWAV::FORMAT_IMA_ADPCM) {
WARN_PRINT("Saving IMA_ADPC samples are not supported yet");
if (format == AudioStreamWAV::FORMAT_IMA_ADPCM || format == AudioStreamWAV::FORMAT_QOA) {
WARN_PRINT("Saving IMA_ADPCM and QOA samples is not supported yet");
return ERR_UNAVAILABLE;
}

Expand All @@ -548,6 +617,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) {
byte_pr_sample = 1;
break;
case AudioStreamWAV::FORMAT_16_BITS:
case AudioStreamWAV::FORMAT_QOA:
byte_pr_sample = 2;
break;
case AudioStreamWAV::FORMAT_IMA_ADPCM:
Expand Down Expand Up @@ -590,6 +660,7 @@ Error AudioStreamWAV::save_to_wav(const String &p_path) {
}
break;
case AudioStreamWAV::FORMAT_16_BITS:
case AudioStreamWAV::FORMAT_QOA:
for (unsigned int i = 0; i < data_bytes / 2; i++) {
uint16_t data_point = decode_uint16(&read_data[i * 2]);
file->store_16(data_point);
Expand All @@ -607,6 +678,16 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() {
Ref<AudioStreamPlaybackWAV> sample;
sample.instantiate();
sample->base = Ref<AudioStreamWAV>(this);

if (format == AudioStreamWAV::FORMAT_QOA) {
sample->qoa.desc = (qoa_desc *)memalloc(sizeof(qoa_desc));
qoa_decode_header((uint8_t *)data + DATA_PAD, QOA_MIN_FILESIZE, sample->qoa.desc);
sample->qoa.frame_len = qoa_max_frame_size(sample->qoa.desc);
int samples_len = (sample->qoa.desc->samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc->samples);
int alloc_len = sample->qoa.desc->channels * samples_len * sizeof(int16_t);
sample->qoa.dec = (int16_t *)memalloc(alloc_len);
}

return sample;
}

Expand Down Expand Up @@ -639,7 +720,7 @@ void AudioStreamWAV::_bind_methods() {
ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav);

ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data");
ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM"), "set_format", "get_format");
ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM,QOA"), "set_format", "get_format");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "Disabled,Forward,Ping-Pong,Backward"), "set_loop_mode", "get_loop_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_begin"), "set_loop_begin", "get_loop_begin");
ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end");
Expand All @@ -649,6 +730,7 @@ void AudioStreamWAV::_bind_methods() {
BIND_ENUM_CONSTANT(FORMAT_8_BITS);
BIND_ENUM_CONSTANT(FORMAT_16_BITS);
BIND_ENUM_CONSTANT(FORMAT_IMA_ADPCM);
BIND_ENUM_CONSTANT(FORMAT_QOA);

BIND_ENUM_CONSTANT(LOOP_DISABLED);
BIND_ENUM_CONSTANT(LOOP_FORWARD);
Expand Down
Loading
Loading