Unverified Commit 5217885f authored by Jake Stine's avatar Jake Stine Committed by GitHub
Browse files

audio: fix decoding of non-canonical wavefile headers. (#192)

* Added new build types: Debug, Tool, and Player.

 Debug is -O0 compiled with all assertions enabled.
 Tool is -O1 compiled with debug assertions disabled entirely.
 Player is -O3 compiled with tool assertions disabled, and all ignorable assertions demoted to log-only.

Implementation of ignorable asserts is still partial, but as it stands this change is OK to go live (can add features later). On the todo list:
  - add popup to allow content creator to ignore the assert
  - add lua stacktrace information when lua context seems relevant

* GHActions updated to be aware of new player and tool build configs

* makefile: allow changing config from CLI without having to make clean

Also force rebuild of vcxproj target since dep checking is currently insufficient. it should be checking dirs of all globbed source file includes for modifications, but it's not. and this is a really fast step, no point in not running it for now

* Added lutro_errorf and lutro_alertf for logging errors and popup alerts, respectively.

 - Cleans up macros and keeps assertions clearly in the domain of Debug and Tooling builds.
 - removed play_assert and friends, which were both confusing terminology and not practical in design

* WAV: fix decoding of non-canonical wave headers.

In short, we can't assume anything about these pesky subchunks.

* printf formatting fixed
parent a5ab4d4d
......@@ -33,7 +33,8 @@ void decOgg_destroy(dec_OggData *data)
//
bool decOgg_init(dec_OggData *data, const char *filename)
{
data->info = NULL;
memset(data, 0, sizeof(*data));
if (ov_fopen(filename, &data->vf) < 0)
{
// only print for file not found errors.
......@@ -204,8 +205,7 @@ void decWav_destroy(dec_WavData *data)
//
bool decWav_init(dec_WavData *data, const char *filename)
{
data->fp = NULL;
data->pos = 0;
memset(data, 0, sizeof(*data));
FILE *fp = fopen(filename, "rb");
if (!fp)
......@@ -216,7 +216,7 @@ bool decWav_init(dec_WavData *data, const char *filename)
// caller's intent might be to silently try opening "optional" files until one succeeeds.
if (errno == ENOENT)
{
fprintf(stderr, "wavfile not found '%s'\n", filename);
fprintf(stderr, "wavfile not found: %s\n", filename);
return 0;
}
......@@ -224,22 +224,62 @@ bool decWav_init(dec_WavData *data, const char *filename)
return 0;
}
if (fread(&data->head, WAV_HEADER_SIZE, 1, fp) == 0)
{
fread(&data->headc1, WAV_HEADER_CHUNK1_SIZE, 1, fp);
if (feof(fp)
|| memcmp(data->headc1.ChunkID, "RIFF", 4)
|| memcmp(data->headc1.Format, "WAVE", 4)
|| memcmp(data->headc1.Subchunk1ID, "fmt ", 4)
) {
lutro_errorf("%s is not a valid wav file or is truncated.", filename);
fclose(fp);
fclose(fp);
return 0;
}
data->fp = fp;
return 1;
if (data->headc1.Subchunk1Size < 16)
{
lutro_errorf("%s has invalid subchunk size=%u. Expected size >= 16.", filename, data->headc1.Subchunk1Size);
fclose(fp);
return 0;
}
if (data->headc1.Subchunk1Size != 16)
{
int extra = data->headc1.Subchunk1Size - 16;
fseek(fp, extra, SEEK_CUR);
}
while (1)
{
if (fread(&data->headc2, WAV_HEADER_CHUNK2_SIZE, 1, fp) == 0)
{
lutro_errorf("%s is not a supported wav file. No data subchunk was found.", filename);
return 0;
}
if (memcmp(data->headc2.Subchunk2ID, "data", 4) == 0)
{
data->seekPosSubChunk2 = ftell(fp);
data->fp = fp;
return 1;
}
fseek(fp, data->headc2.Subchunk2Size, SEEK_CUR);
}
dbg_assumef(false, "unreachable");
return 0;
}
int decWav_CalcOffsetDataStart(const dec_WavData* wavData)
{
return wavData ? wavData->seekPosSubChunk2 : 0;
}
//
bool decWav_seek(dec_WavData *data, intmax_t samplepos)
{
int bps = ((data->head.BitsPerSample + 7) / 8) * data->head.NumChannels;
int numSamples = data->head.Subchunk2Size;
int bps = ((data->headc1.BitsPerSample + 7) / 8) * data->headc1.NumChannels;
int numSamples = data->headc2.Subchunk2Size / bps;
// fseek will let us seek past the end of file without returning an error.
// So it is best to verify positions against the know sample size.
......@@ -255,16 +295,17 @@ bool decWav_seek(dec_WavData *data, intmax_t samplepos)
}
intmax_t bytepos = samplepos * bps;
intmax_t seekpos = decWav_CalcOffsetDataStart(data) + bytepos;
// spurious calls to fseek have overhead, so early out if the internal managed
// seek pos matches
if (data->pos == bytepos)
{
tool_assert(ftell(data->fp) == WAV_HEADER_SIZE + bytepos);
tool_assert(ftell(data->fp) == seekpos);
return 1;
}
if (fseek(data->fp, WAV_HEADER_SIZE + bytepos, SEEK_SET))
if (fseek(data->fp, seekpos, SEEK_SET))
{
// logging here could be unnecessarily spammy. If we add a log it should be gated by
// some audio diagnostic output switch/mode.
......@@ -278,18 +319,18 @@ bool decWav_seek(dec_WavData *data, intmax_t samplepos)
//
intmax_t decWav_sampleTell(dec_WavData *data)
{
int bps = ((data->head.BitsPerSample + 7) / 8) * data->head.NumChannels;
int bps = ((data->headc1.BitsPerSample + 7) / 8) * data->headc1.NumChannels;
intmax_t ret = ftell(data->fp) - WAV_HEADER_SIZE;
intmax_t ret = ftell(data->fp) - decWav_CalcOffsetDataStart(data);
if (ret >= 0)
{
if ((ret % bps) != 0)
{
// print size, it helps identify the offender.
fprintf(stderr, "Unaligned read position in wav decoder stream. size=%d bps=%d channels=%d pos=%jd\n",
data->head.Subchunk2Size,
data->head.BitsPerSample,
data->head.NumChannels,
data->headc2.Subchunk2Size,
data->headc1.BitsPerSample,
data->headc1.NumChannels,
ret
);
}
......@@ -317,19 +358,20 @@ static __always_inline bool _inl_decode_wav(dec_WavData *data, intmax_t bufsz, m
// 8-bit wav is scaled up to 16 bit and then normalized using 16-bit divisor.
float mul_volume_and_normalize = volume / 32767;
int numSamples = data->head.Subchunk2Size;
int numSamples = data->headc2.Subchunk2Size / bytesPerSample;
for (int j = 0; j < bufsz; j++, data->pos += (bytesPerSample * chan_src))
{
uint8_t sample_raw[8];
int readResult = 0;
if (data->pos < numSamples)
readResult = fread(sample_raw, bytesPerSample * chan_src, 1, data->fp);
readResult = (int)fread(sample_raw, bytesPerSample * chan_src, 1, data->fp);
if (!readResult)
{
dbg_assertf(data->pos == ftell(data->fp) - WAV_HEADER_SIZE, "numSamples=%jd dataPos=%jd and ftell=%jd",
numSamples, (intmax_t)data->pos, ftell(data->fp)
intmax_t seekpos = decWav_CalcOffsetDataStart(data) + data->pos;
dbg_assertf(ftell(data->fp) == seekpos, "numSamples=%jd dataPos=%jd and ftell=%jd",
(intmax_t)numSamples, (intmax_t)data->pos, ftell(data->fp)
);
if (!loop)
......@@ -341,7 +383,7 @@ static __always_inline bool _inl_decode_wav(dec_WavData *data, intmax_t bufsz, m
}
data->pos = 0;
fseek(data->fp, WAV_HEADER_SIZE + data->pos, SEEK_SET);
fseek(data->fp, seekpos, SEEK_SET);
--j; continue; // attempt to re-read sample.
}
......@@ -375,8 +417,8 @@ static __always_inline bool _inl_decode_wav(dec_WavData *data, intmax_t bufsz, m
}
}
dbg_assertf(data->pos == ftell(data->fp) - WAV_HEADER_SIZE, "numSamples=%jd dataPos=%jd and ftell=%jd",
numSamples, (intmax_t)data->pos, ftell(data->fp)
dbg_assertf(ftell(data->fp) == decWav_CalcOffsetDataStart(data) + data->pos, "numSamples=%jd dataPos=%jd and ftell=%jd",
(intmax_t)numSamples, (intmax_t)data->pos, ftell(data->fp)
);
return 0;
}
......@@ -394,15 +436,15 @@ bool decWav_decode(dec_WavData *data, presaturate_buffer_desc *buffer, float vol
// to avoid "hiss" that plagues 8-bit at low volumes. Therefore, as a rule of thumb, 8-bit samples
// mixed at half volume will "match" better with 16-bit samples mixed at full volume. --jstine
if (data->head.BitsPerSample == 8 && data->head.NumChannels == 2 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 1, 2, 2, volume, loop);
if (data->head.BitsPerSample == 8 && data->head.NumChannels == 2 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 1, 2, 1, volume, loop);
if (data->head.BitsPerSample == 8 && data->head.NumChannels == 1 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 1, 1, 2, volume, loop);
if (data->head.BitsPerSample == 8 && data->head.NumChannels == 1 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 1, 1, 1, volume, loop);
if (data->headc1.BitsPerSample == 8 && data->headc1.NumChannels == 2 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 1, 2, 2, volume, loop);
if (data->headc1.BitsPerSample == 8 && data->headc1.NumChannels == 2 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 1, 2, 1, volume, loop);
if (data->headc1.BitsPerSample == 8 && data->headc1.NumChannels == 1 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 1, 1, 2, volume, loop);
if (data->headc1.BitsPerSample == 8 && data->headc1.NumChannels == 1 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 1, 1, 1, volume, loop);
if (data->head.BitsPerSample == 16 && data->head.NumChannels == 2 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 2, 2, 2, volume, loop);
if (data->head.BitsPerSample == 16 && data->head.NumChannels == 2 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 2, 2, 1, volume, loop);
if (data->head.BitsPerSample == 16 && data->head.NumChannels == 1 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 2, 1, 2, volume, loop);
if (data->head.BitsPerSample == 16 && data->head.NumChannels == 1 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 2, 1, 1, volume, loop);
if (data->headc1.BitsPerSample == 16 && data->headc1.NumChannels == 2 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 2, 2, 2, volume, loop);
if (data->headc1.BitsPerSample == 16 && data->headc1.NumChannels == 2 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 2, 2, 1, volume, loop);
if (data->headc1.BitsPerSample == 16 && data->headc1.NumChannels == 1 && buffer->channels == 2) return _inl_decode_wav(data, bufsz, dst, 2, 1, 2, volume, loop);
if (data->headc1.BitsPerSample == 16 && data->headc1.NumChannels == 1 && buffer->channels == 1) return _inl_decode_wav(data, bufsz, dst, 2, 1, 1, volume, loop);
return 0;
}
......@@ -4,14 +4,15 @@
#include <vorbis/vorbisfile.h>
#include "audio_mixer.h"
#define WAV_HEADER_SIZE 44
#define WAV_HEADER_CHUNK1_SIZE 36 // minimum size. the chunk can be larger. See Subchunk1Size.
#define WAV_HEADER_CHUNK2_SIZE 8
typedef struct
{
char ChunkID[4];
char ChunkID[4];
uint32_t ChunkSize;
char Format[4];
char Subchunk1ID[4];
char Format[4];
char Subchunk1ID[4];
uint32_t Subchunk1Size;
uint16_t AudioFormat;
uint16_t NumChannels;
......@@ -19,9 +20,13 @@ typedef struct
uint32_t ByteRate;
uint16_t BlockAlign;
uint16_t BitsPerSample;
} wavhead_t;
typedef struct
{
char Subchunk2ID[4];
uint32_t Subchunk2Size;
} wavhead_t;
} wav_subchunk2_t;
typedef struct
{
......@@ -31,9 +36,11 @@ typedef struct
typedef struct
{
void* fp;
intmax_t pos;
wavhead_t head;
void* fp;
intmax_t pos;
wavhead_t headc1; // RIFF header and Chunk 1
wav_subchunk2_t headc2; // data chunk header (other headers are not tracked)
intmax_t seekPosSubChunk2; // data subchunk could be anywhere in this evil file format.
} dec_WavData;
bool decWav_init(dec_WavData *data, const char *filename);
......
......@@ -63,8 +63,8 @@ int snd_newSoundData(lua_State *L)
{
dec_WavData wavData;
decWav_init(&wavData, asset.fullpath);
self->numSamples = wavData.head.Subchunk2Size / ((wavData.head.BitsPerSample/8) * wavData.head.NumChannels);
self->numChannels = wavData.head.NumChannels;
self->numSamples = wavData.headc2.Subchunk2Size / ((wavData.headc1.BitsPerSample/8) * wavData.headc1.NumChannels);
self->numChannels = wavData.headc1.NumChannels;
self->data = calloc(1, sizeof(mixer_presaturate_t) * self->numSamples * self->numChannels);
bufdesc.data = self->data;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment