Skip to content

Comments

dr_wav: Support for full metadata read/write#170

Merged
mackron merged 53 commits intomackron:dev-wav-0.13from
SamWindell:dev
Jun 11, 2021
Merged

dr_wav: Support for full metadata read/write#170
mackron merged 53 commits intomackron:dev-wav-0.13from
SamWindell:dev

Conversation

@SamWindell
Copy link
Contributor

This patch adds support for some extra wav metadata. These chunks are often useful for audio-editing software, software synthesizers and samplers. They are read and stored in the same fashion as the smpl was.

In addition to this, each of these new chunks (and the smpl chunk) now have a bool to say if the chunk was found and correctly read. Previously there was no way to tell if the chunk was valid or not.

I'd like to be able to get the LIST chunk, (with its subchunks) with dr_wav too. https://sites.google.com/site/musicgapi/technical-documents/wav-file-format

But it will require string allocations probably, or use fixed buffers (which is a perhaps a little limiting). Any ideas on how to go about that?

This patch adds support for some extra wav metadata. These chunks are often useful for audio-editing software, software synthesizers and samplers. They are read and stored in the same fashion as the smpl was.

In addition to this, each of these new chunks (and the smpl chunk) now have a bool to say if the chunk was found and correctly read. Previously there was no way to tell if the chunk was valid or not.
@mackron
Copy link
Owner

mackron commented Nov 30, 2020

Thanks for this! I'll review this ASAP. Just a quick question - I saw you've limited the cue count to a maximum of 4. Is that a realistic scenario? (I don't do anything with cue points.) Certainly in the past I've rejected the idea of explicitly supporting cue points because I was concerned at the requirement for a heap allocation in the initialization routine.

@SamWindell
Copy link
Contributor Author

Yeah, I was just thinking that the user would set their own define for the max cue points. But thinking about it further it's probably not going to be a good-enough solution. It could be anywhere from 1 to 50 in extreme cases. The smpl loop isn't ideal either though, it's almost always 1 but occasionally 2 or 3.

Perhaps this is all a bit ad-hoc for handling these chunks. Maybe a whole new system should be made. An extra step would be to have this new system allow for writing of chunks too.

Something like an opt-in system where the user says what types of chunks they are interested in. Then a single allocation is done and filled with all the asked-for chunk data and saved for them to access later on. What do you think? I might have a go at this.

This system requires iterating through the chunks twice. The first time to work out the maximum memory needed to store the metadata, and the second time to read the data into a nice format. It does however mean that only one allocation is done, and the data is store contiguously in memory.

The type of metadata is chosen through a bitset. Only the types of metadata that you require are read and allocated.
…vements

The metadata functions were moved down in the file so they could make use of drwav__on_read. As well as this, the code now makes consistent use of u64 ints for chunk size.
This macro comes from Nuklear
Some test files are needed before enabling the other paths. The way the LIST sub-chunks are handled might need reviewing before formats other than standard riff are supported.
This reduces the amount of function parameters for all the metadata read functions. It also decouples the metadata from the metadata parsing so we can use metadata for a writer
The chunk objects are the first things assigned to the memory block, and malloc always returns aligned memory so we can skip this.
The old variable 'mallocBlockForMetadata' is redundant as it will always be the same as the metadata pointer. The take_ownership function simply nulls the drwav's metadata so that it is not freed during uninit.
For riff and rf64 the bytes for the id "data" and the u32 for the data size where not included. Not sure files worked without this!
@SamWindell SamWindell changed the title dr_wav: Add support for inst, cue and acid chunks. dr_wav: Support for full metadata read/write Dec 10, 2020
@SamWindell
Copy link
Contributor Author

Ok here we go. In total, these changes add support for all the common types of wav metadata to be read and written. This is only for standard wav format and rf64 wav. Honestly, I haven't delved into WAV64 at all.

For reading metadata, there are new drwav_init functions that end with _with_metadata. These functions take a u32 int which is a bit-field used to represent the types of metadata that you are interested in. So by 'bitwise-or'ing together the drwav_metadata_type constants you can specify that you only care about smpl and cue metadata for example; this saves a time and memory.

The reading works by first iterating over all the chunks in the wav file in a 'counting' mode - counting the number of bytes the parser will need to malloc for the metadata. This data is then allocated in a single block. It then resets the cursor back to the start and iterates over the chunks again in a 'reading' mode, filling up the memory with the data from the chunks. This 2-pass approach I don't think is too slow and it has the advantage of only needing a single allocation which is easy to manage for the user.

The rough structure of this malloced block is that the drwav_metadata objects come first, and then after it are the various other required bits of data such as null-terminated strings. The drwav_metadata struct contains a union of all possible metadata types so can be easily handled as an array.

After the init_with_metadata call, the user just reads the metadata from a field in the drwav object. I found that I wanted this metadata to persist longer than drwav, so I added an API for take_ownership. If you don't take ownership of it, it is freed in drwav_unint.

Writing is pretty simple. There is a new function, drwav_init_write_with_metadata. You pass an array of metadata to it. It writes all metadata items into the file. It doesn't change the data and doesn't free anything.

A couple of unrelated things: apologies, a whitespace edit got committed into this. My editor removed the trailing whitespace from all lines. I also found a bug I believe - one that I am pretty sure is not related to these metadata changes. The bug is to do with calculating the size of a riff file. a066b8b. This fix should change the already existing utility function, drwav_target_write_size_bytes. Do you have any tests you can run as well?

I'm happy to make changes based on your review.

@mackron
Copy link
Owner

mackron commented Dec 27, 2020

Thanks a lot for this work and your writeup - I appreciate the amount of work that went into this. I've only taken a cursory look so far, but there's a few styling things that will need to change. Just little things like open curly braces on functions and structs not being on a separate line, and "numMetadata" naming instead of "metadataCount" which is the convention used in all of dr_libs.

I'm happy with the _with_metadata() idea and it's consistent with dr_flac. Just a thought, though - I wonder if we really need the onChunk callback in this case considering the existence of drwav_unknown_metadata which, I'm assuming, acts as the catch-all fallback? If we were to remove that, the only different between the _ex and non-_ex versions is the flags parameter, in which case I reckon we could just not bother about the _ex version entirely and move the flags parameter over to the non-ex version. (I still want to keep the _ex version as-is for the base drwav_init() function, though.) Your thoughts on that?

I'm also happy with the malloc(), so long as it's just one bulk allocation (which it looks like you've done) and it's restricted only to _with_metadata(). It looks like drwav_init__private() is checking pWav->allowedMetadataTypes for non-0 which is the guard for this, I'm assuming? So the idea is that drwav_init() will just set this to 0, thereby skipping the metadata parsing stuff and in turn the malloc()? The 2-pass approach is fine and is my preference.

No worries about the whitespace stuff - I should probably do a whitespace strip of all my libraries anyway. Also no worries about the W64 stuff. That's just a niche thing that can be dealt with when the need arises. There's also RF64 which is the same deal.

There's one thing I'm still undecided on, though, and that's the metadata type filtering. Is the performance stuff an actual real-world problem with measurable and significant consequences, or is it a handwavey hypothetical problem? I'm not entirely sure, but I usually err towards the latter with that kind of stuff. I don't completely hate the idea or anything, it's just that API simplicity is always at the top (or near the top) of my priorities.

Just a quick error a noticed in my scan. The drwav_init_with_metadata() declaration in the header section is missing the drwav_uint64 allowedMetadataTypes parameter (last parameter).

As I go through this in detail I'll go ahead and push changes to your branch - I assume you're fine with that? And I assume you're fine with all of this going into public domain?

@SamWindell
Copy link
Contributor Author

Thanks a lot for this work and your writeup - I appreciate the amount of work that went into this. I've only taken a cursory look so far, but there's a few styling things that will need to change. Just little things like open curly braces on functions and structs not being on a separate line, and "numMetadata" naming instead of "metadataCount" which is the convention used in all of the dr_libs.

No worries, thanks for writing the dr_libs!

I'm happy with the _with_metadata() idea and it's consistent with dr_flac. Just a thought, though - I wonder if we really need the onChunk callback in this case considering the existence of drwav_unknown_metadata which, I'm assuming, acts as the catch-all fallback? If we were to remove that, the only different between the _ex and non-_ex versions is the flags parameter, in which case I reckon we could just not bother about the _ex version entirely and move the flags parameter over to the non-ex version. (I still want to keep the _ex version as-is for the base drwav_init() function, though.) Your thoughts on that?

You're right - let's simplify that a little and drop the _ex version. The unknown metadata type is a good alternative to onChunk. Is the flags parameter just used for specifying sequential mode? If so, then it actually won't serve any purpose because the metadata parser can't work if it's not allowed to seek backwards through the file.

I'm also happy with the malloc(), so long as it's just one bulk allocation (which it looks like you've done) and it's restricted only to _with_metadata(). It looks like drwav_init__private() is checking pWav->allowedMetadataTypes for non-0 which is the guard for this, I'm assuming? So the idea is that drwav_init() will just set this to 0, thereby skipping the metadata parsing stuff and in turn the malloc()? The 2-pass approach is fine and is my preference.

All correct!

There's one thing I'm still undecided on, though, and that's the metadata type filtering. Is the performance stuff an actual real-world problem with measurable and significant consequences, or is it a handwavey hypothetical problem? I'm not entirely sure, but I usually err towards the latter with that kind of stuff. I don't completely hate the idea or anything, it's just that API simplicity is always at the top (or near the top) of my priorities.

Yeah we could do away with it and it would simplify the API. I'm happy for that to happen. The performance aspect is indeed hypothetical, there are a few large-ish chunks out there but I doubt it will make any difference at all. I guess one nice thing about filtering the types is that you can specify which types you understand and care about and then after you have done processing a wav file, you can pass that same metadata back to the write function, without having to filter out the things you don't care about yourself. Perhaps a niche case though! I believe we will still need to keep an enum of all possible metadata types somewhere though. It will be needed for determining the metadata union type.

Just a quick error a noticed in my scan. The drwav_init_with_metadata() declaration in the header section is missing the drwav_uint64 allowedMetadataTypes parameter (last parameter).

Yep sounds like something I'd missed.

As I go through this in detail I'll go ahead and push changes to your branch - I assume you're fine with that? And I assume you're fine with all of this going into public domain?

Yep to both! Let me know if you need anything else.

@mackron
Copy link
Owner

mackron commented Dec 28, 2020

OK, cool, it looks like we agree on most things. I'm still undecided on the filtering thing, but I'll make a decision. Will review and update this branch as soon as I get a chance.

It does not like the anonymous struct version of the alignof macro. This code comes from public domain library Nuklear.
@SamWindell
Copy link
Contributor Author

BTW, just fixed a little compile error to do with the alignof macro when compiling the header as C++ with Clang.

@mackron
Copy link
Owner

mackron commented Jan 8, 2021

I haven't yet had a chance to look at this, I'm sorry (my mind is focused on some miniaudio stuff right now). However, that DRWAV_ALIGNOF macro is not something I'll be accepting into the code base. I know it's not your fault or anything, but that C++ code is absolutely disgusting and is something I refuse to maintain and accept into my code base. What's that macro used for, and why is it necessary? I would rather come up with a solution to remove it entirely.

@SamWindell
Copy link
Contributor Author

No rush on my part, I'm happy using my own fork for the time being.

Fair enough. I'd personally just use the C++11 or C11 alignof operator, but I understand this library needs to work on older compilers too. The macro is used to ensure the structs are correctly aligned within the single malloc block for the metadata. The block contains strings that can be any length as well as structs so I'm pretty sure some sort of alignment is needed. I guess the allocations for strings could be rounded up to a multiple of 16 bytes? I can't say for sure if that would cut it though.

mackron added 8 commits April 6, 2021 09:23
This is replaced with a generic value that should satisfy both 32- and
64-bit platforms.
  * drwav_init_file_with_metadata()
  * drwav_init_file_with_metadata_w()
  * drwav_init_memory_with_metadata()
This offers a little bit more code coverage.
@mackron
Copy link
Owner

mackron commented Apr 6, 2021

I've finally got around to reviewing this one. I've synchronized this with the dev branch which had some conflicts. Hopefully I didn't accidentally break anything with that. I've made the following changes:

  • Metadata filtering has been removed - all metadata types will be processed.
  • drwav_init_with_metadata_ex() has been removed. The onChunk callback is redundant and the flags parameter has been moved to drwav_init_with_metadata().
  • DRWAV_ALIGNOF() has been removed and replaced with a generic value that should satisfy alignment requirements on both 32- and 64-bit platforms.
  • Structure members have been renamed for consistency with my coding style.
  • General coding style changes for consistency with the rest of the library

There will be changes here that will affect your own code base so just be careful about synchronizing until you've got a proper chance to take a look. I'll probably push a few more little updates to this branch soon.

Thanks for your work on this.

@SamWindell
Copy link
Contributor Author

Great stuff! Thanks for reviewing it.

I'll add this new version into my project probably on the weekend and have a test.

@SamWindell
Copy link
Contributor Author

All seems to be working well. I've added your changes to my project and run my tests. They certainly don't cover every aspect of testing the metadata, but it's something.

I found a bug and fixed it. It wasn't related to your changes.

@mackron mackron changed the base branch from dev to dev-wav-0.13 April 10, 2021 23:50
@mackron
Copy link
Owner

mackron commented Apr 11, 2021

Thanks for giving that a test. It's no problem if we can't test every case - the community can let us know of any issues. This changes a public facing API so I'll be bumping the minor version number. I've created a branch for version 0.13 called dev-wav-0.13 which is where this PR will be merged into. It shouldn't be too long before I release this proper.

@mackron mackron merged commit 022284c into mackron:dev-wav-0.13 Jun 11, 2021
@mackron
Copy link
Owner

mackron commented Jun 12, 2021

I've merged this work into the dev branch. The next release of dr_wav will be version 0.13.0, and it'll include this. Indeed, this is probably going to be the only new thing in that release. Thanks again for your work on this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants