Conversation
e40c122 to
b51c06a
Compare
1ab7cdc to
6f00ef3
Compare
6f00ef3 to
4794aa5
Compare
835e8f2 to
7a26449
Compare
| }; | ||
|
|
||
| template <typename Out, typename Num> | ||
| constexpr Out clampInt(Num value) |
There was a problem hiding this comment.
Any reason not to use std::clamp? Quick search indicates std::clamp is used elsewhere in SFML.
Ignore me, I just re-read what this is doing
eXpl0it3r
left a comment
There was a problem hiding this comment.
Thanks for tackling this! 🙂
I went through the whole diff and I think, I found a few correctness issues in the writer, mostly in the encoding itself.
Instead of fixing those one by one, I believe it would make sense to vendor qoa.h similar to how we do it with QOI or STB in extlibs/headers. The reader and writer then only need to deal with the streams, the framing and the channel mapping.
Additionally, the lists of supported formats in the sf::SoundBuffer, sf::Music and sf::InputSoundFile documentation need to be updated.
| std::vector<std::uint8_t> buffer; | ||
| if (const auto error = seekAndWriteHeader(static_cast<std::uint32_t>(samplesPerChannel), buffer)) | ||
| { | ||
| err() << "Failed to write QOA file header: " << *error << std::endl; | ||
| return; | ||
| } |
There was a problem hiding this comment.
write() can be called multiple times, sf::OutputSoundFile::write forwards every call as is. Here every call seeks back to 0, writes the header with only the sample count of that call, creates a fresh LMS state and writes the frames right after the header. If someone writes in chunks, only the last chunk survives.
The header should be written once in open() with a placeholder, the total sample count and the LMS state need to be members, incomplete frames need to be buffered, and the sample count gets patched when closing the file, similar to what the WAV writer does.
| for (std::size_t i = 0; i < channelCount; ++i) | ||
| m_remapTable[i] = static_cast<std::uint8_t>( | ||
| std::find(targetChannelMap.begin(), targetChannelMap.end(), channelMap[i]) - targetChannelMap.begin()); | ||
|
|
There was a problem hiding this comment.
The remap table is the wrong way around. This stores the QOA position for input channel i, but in writeFrame it's used as "input index for output position i". Have a look at SoundFileWriterWav.cpp, there it's std::find(channelMap.begin(), channelMap.end(), targetChannelMap[i]).
Additionally, the slices of output channel k are encoded with lms.channels[m_remapTable[k]], while writeLmsState writes the states in index order. With any channel map that isn't the identity, the decoder ends up with the wrong predictor state from the second frame on.
| const auto frameBodySizeByte = qoaFile::getFrameSizeByte(m_frameSharedData->numChannels) - | ||
| qoaFile::frameHeaderSizeByte::value; |
There was a problem hiding this comment.
This always uses the size of a full frame with 256 slices, while the frame header declares the actual size. For the last frame, which is usually shorter, the whole buffer is written anyways, so the file ends with a bunch of zero bytes past the declared frame size. getFrameSizeByte(numChannels, slicesPerChannel) should be used here as well.
|
|
||
| bool isChannelCountValid(std::uint8_t channelCount) | ||
| { | ||
| return channelCount <= maxChannels; |
There was a problem hiding this comment.
This accepts 0 channels, which should fail like for other formats. In the writer count % numChannels then divides by zero, and on the reader side a crafted file gets through check() and open(), and ends up at sampleOffset / m_channelCount in seek().
| } | ||
|
|
||
| const auto sampleOffset = static_cast<std::uint32_t>(rawSampleOffset); | ||
| const auto isPastEnd = sampleOffset / m_channelCount >= m_samplesPerChannel; |
There was a problem hiding this comment.
What's the plan for streaming files, i.e. samplesPerChannel == 0? The PR description says they're not supported, but open() succeeds with a sample count of 0 and read() does decode them. Here however every offset is considered past the end, so seek(0) jumps to EOF.
I'd either reject them in open() with a clear error message or support them properly. Have you checked other how other formats do it?
| #include <SFML/Audio/SoundFileReaderQoa.hpp> | ||
|
|
||
| #include <SFML/System/Err.hpp> | ||
| #include <SFML/System/FileInputStream.hpp> |
There was a problem hiding this comment.
I think FileInputStream.hpp isn't used, while <algorithm> is missing for std::min and std::max.
| m_channelCount = firstFrameHeader->numChannels; | ||
| m_sampleRate = firstFrameHeader->sampleRate; | ||
| m_samplesPerChannel = headerContent->samplesPerChannel; | ||
| m_currentFrameSamples.clear(); |
There was a problem hiding this comment.
m_decodedSamplesPerChannel isn't reset here, unlike the other members.
| if (remainingSamplesInFrame <= 0) | ||
| return 0; | ||
| const auto readSamplesCount = static_cast<std::uint16_t>(std::min<std::uint32_t>(remainingSamplesInFrame, maxCount)); | ||
| std::memcpy(samples, m_currentFrameSamples.data() + m_currentFrameNextSampleIndex, readSamplesCount * 2); |
There was a problem hiding this comment.
sizeof(std::int16_t) instead of the magic 2, or just std::copy_n.
| std::vector<std::int16_t> m_currentFrameSamples; //!< Decoded samples of last processed frame | ||
| std::uint16_t m_currentFrameNextSampleIndex{}; //!< Index of next unread sample in the last processed frame | ||
| InputStream* m_inputStream{}; //!< The input stream received in the open method | ||
| std::size_t m_streamFirstFramePosition{}; |
There was a problem hiding this comment.
Missing documentation comment for m_streamFirstFramePosition.
| if (!std::is_permutation(channelMap.begin(), channelMap.end(), targetChannelMap.begin())) | ||
| { | ||
| err() << "Unsupported channel when writing QOA file" << std::endl; | ||
| return false; | ||
| } |
There was a problem hiding this comment.
With the three iterator overload this reads out of bounds, if channelMap.size() doesn't match channelCount. Use the overload that takes both end iterators.
This PR adds support for QOA sound format. It was requested in #2969.
Since there is no dependency, some files containing common utilities for reader and writer were added (
SoundFileQoa.hppandSoundFileQoa.cpp).Known limitations/remarks:
Tasks
How to test this PR?