Audio File Writing rounding issue

When converting between different file types (original is pcm 24 bit) the output file is not bit-for-bit (CAF -> WAV). The issue is in the convertFloatsToInts function that calls roundToInt. This method is annotated for being inaccurate at times. This is probably fine for 99.999% of use cases and agree that there will be no audible difference (but clients expect exact copies). Is it possible to have a flag (or even #define) somewhere that could use roundf instead (regular round or rint (depending on rounding mode)) also have an issue)?

roundf appears to do the right thing (at least right enough). Ideally it would be great if it was never converted to floating point in the first place, but looks like Juce internally does much with floating point.

In times of C++ I would probably prefer std::round() over roundf().

I think the most users prefer the benefit of headroom security of floats over fixed point audio, where you always have the danger of digital overs during processing.
There might be some valid use cases for it though…

1 Like

I totally agree with your float statement and has been great for most cases (this is an edge case which can still work with proper rounding). Unfortunately, std::round() gives the wrong value while roundf works great in this case.

I am aware of the numerical challenges when converting float numbers, and I trust you if you say std::round() does the wrong thing.
Out of curiosity, can you give the examples, when std::round() gives wrong results?

Interesting enough, it seems like roundf is doing the wrong thing but seems to output the correct files in this scenario. I guess there could be an error on the input side (converting the CAF 24bit to float first) and this is compensating for it.
int a = roundf(1337538815.37716);
int b = std::round(1337538815.37716);
a = 1337538816 and b=1337538815

So it looks like the issue is that the input for this (in convertFloatsToInts()) is actually a double. If I pass a float to the std::round, I get the same result. There is not enough precision in a float to represent this number (as well as others). Since truncating to float first gives me the correct file, I wonder if this is compensating for a similar error on the input (from 24 bit int to float).

The CoreAudioFormat reader appears to just use the underlying calls to core audio to convert to float. Could it be that this is using floating point and by using double precision upon writing we get these edge case errors occasionally. Is double needed inside of convertFloatsToInts?

Also, looking through the code, the float data is converted to a 32 bit int first, then further converted. Is there a format which uses more than 24 bits as an integer? If not, why not just first convert to this case which would eliminate some further conversions if you have selected 24 bit as your destination.

Because the way it’s done, you can write a lossless function to operate on any pair of file formats by supporting just two code-paths for float and 32-bit ints.

If data could arrive as 16-bit, 24-bit, 8-bit, float etc, then users would have to write many more code-paths, and all the integer ones end up being exactly the same thing with different types.

Fair enough. Do you see any way around my issue of writing a byte for byte compatible file without rolling my own writeFromAudioReader?

If you’re converting to float to do some processing, then why do you care about the rounding? If you’re modifying the data at all, then rounding errors will be lost in the other noise.

And if you’re not actually processing the data, then just leave it as 32-bit int all the way through to writing it back, and it’ll be lossless. That’s how you do things like trimming a file or other operations that don’t modify anything.

Internally, Juce’s CoreAudioReader always gives back the audio as a float. Need to go from CAF to wav (and visa vera). All audio is 24bit pcm.

Ah right, well I’ve no strong feelings about the exact rounding used in there. If you can suggest a way to do the conversion that works in your tests but is pure C++ rather than calling roundf then we’d certainly consider it

Just using a float instead of a double inside of convertFloatToInts seems to work (although very strange truncation). But, double does seem to make more sense here for more accuracy, it just “messes up” the bit for bit thing.
Ex: casting the double 1337538815.37716 to a float gives you 1337538816.0).

Not sure of the correct solution here. Possibly pass in the max value as a param (using enum, class, etc.) for the rounding but then this would mess up the WriteHelper (but possibly speed things up a bit since you would need to do this conversion only once (would need a bit of a re-write in the WriteHelper to just do byte alignment stuff)).