feat: encoder - #1183
Conversation
Resolves conflicts between the encoder work and the OS-APIs decoding refactor (#1177): - AudioFileConcatenator: main's decoder-factory WAV path + encoder's OS remux (M4A/MP4) path; all FFmpeg remux code removed - AudioEventHandlerRegistry -> IAudioEventHandlerRegistry (#1212) in encoder file writers - AndroidEncoding/AndroidRemux moved to android/src/main/cpp/audioapi/android/ to match AndroidDecoding placement - docs/flags updated: recording and concatAudioFiles are FFmpeg-free Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WPT non-regression comparisonERROR — the comparison did not produce a report; the test run itself likely failed. Workflow run · this comment is updated on every push. |
closetcaiman
left a comment
There was a problem hiding this comment.
Please add description to the PR for fast-access reference to the scope of these changes.
| // Packs @p numberOfChannels planar channel pointers into one channel-interleaved buffer. | ||
| // @p outputInterleaved must hold numberOfFrames * numberOfChannels floats. | ||
| void interleave( | ||
| const float *const *inputChannels, | ||
| size_t numberOfChannels, | ||
| float *outputInterleaved, | ||
| size_t numberOfFrames); | ||
|
|
||
| // Splits a channel-interleaved buffer into @p numberOfChannels planar channel pointers, | ||
| // each of which must hold numberOfFrames floats. | ||
| void deinterleave( | ||
| const float *inputInterleaved, | ||
| float *const *outputChannels, | ||
| size_t numberOfChannels, | ||
| size_t numberOfFrames); | ||
|
|
||
| } // namespace audioapi::dsp |
There was a problem hiding this comment.
Is this intended to be reused for (de)interleaving AudioBuffers?
| void RotatingFileWriter::writeAudioData(const float *interleavedFrames, int numFrames) { | ||
| if (currentWriter_ == nullptr) { | ||
| return; | ||
| } | ||
|
|
||
| currentWriter_->writeAudioData(interleavedFrames, numFrames); | ||
|
|
||
| writesSinceLastCheck_++; | ||
| if (writesSinceLastCheck_ >= FILE_SIZE_CHECK_WRITE_INTERVAL) { | ||
| writesSinceLastCheck_ = 0; | ||
| if (currentWriter_->getFileSizeBytes() > rotateIntervalBytes_) { | ||
| rotateFiles(); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Why we ignore the result here? Should we fire an event? This seems really dangerous to me.
| std::vector<EncoderOutputSpec> EncoderCapabilities::probe() { | ||
| #ifdef __APPLE__ | ||
| return { | ||
| {.container = AudioContainer::WAV, .codec = AudioCodec::PCM, .extension = "wav"}, | ||
| {.container = AudioContainer::CAF, .codec = AudioCodec::PCM, .extension = "caf"}, | ||
| {.container = AudioContainer::AIFF, .codec = AudioCodec::PCM, .extension = "aiff"}, | ||
| {.container = AudioContainer::M4A, .codec = AudioCodec::AAC, .extension = "m4a"}, | ||
| {.container = AudioContainer::M4A, .codec = AudioCodec::ALAC, .extension = "m4a"}, | ||
| {.container = AudioContainer::FLAC, .codec = AudioCodec::FLAC, .extension = "flac"}, | ||
| {.container = AudioContainer::WAV, .codec = AudioCodec::ULAW, .extension = "wav"}, | ||
| {.container = AudioContainer::WAV, .codec = AudioCodec::ALAW, .extension = "wav"}, | ||
| }; | ||
| #elif defined(__ANDROID__) | ||
| return { | ||
| {.container = AudioContainer::WAV, .codec = AudioCodec::PCM, .extension = "wav"}, | ||
| {.container = AudioContainer::M4A, .codec = AudioCodec::AAC, .extension = "m4a"}, | ||
| {.container = AudioContainer::FLAC, .codec = AudioCodec::FLAC, .extension = "flac"}, | ||
| {.container = AudioContainer::OGG, .codec = AudioCodec::OPUS, .extension = "ogg"}, | ||
| {.container = AudioContainer::WEBM, .codec = AudioCodec::OPUS, .extension = "webm"}, | ||
| {.container = AudioContainer::WEBM, .codec = AudioCodec::VORBIS, .extension = "webm"}, | ||
| }; | ||
| #else | ||
| return {}; | ||
| #endif | ||
| } |
There was a problem hiding this comment.
probe name seems misleading as we are not actually probing anything here. Maybe this could be just a static field?
| EncoderOutputSpec EncoderCapabilities::specForFormat(Format format) { | ||
| switch (format) { | ||
| case Format::WAV: | ||
| return {.container = AudioContainer::WAV, .codec = AudioCodec::PCM, .extension = "wav"}; | ||
| case Format::CAF: | ||
| return {.container = AudioContainer::CAF, .codec = AudioCodec::PCM, .extension = "caf"}; | ||
| case Format::M4A: | ||
| return {.container = AudioContainer::M4A, .codec = AudioCodec::AAC, .extension = "m4a"}; | ||
| case Format::FLAC: | ||
| return {.container = AudioContainer::FLAC, .codec = AudioCodec::FLAC, .extension = "flac"}; | ||
| case Format::AIFF: | ||
| return {.container = AudioContainer::AIFF, .codec = AudioCodec::PCM, .extension = "aiff"}; | ||
| case Format::ALAC: | ||
| return {.container = AudioContainer::M4A, .codec = AudioCodec::ALAC, .extension = "m4a"}; | ||
| case Format::OPUS_OGG: | ||
| return {.container = AudioContainer::OGG, .codec = AudioCodec::OPUS, .extension = "ogg"}; | ||
| case Format::OPUS_WEBM: | ||
| return {.container = AudioContainer::WEBM, .codec = AudioCodec::OPUS, .extension = "webm"}; | ||
| case Format::VORBIS_WEBM: | ||
| return {.container = AudioContainer::WEBM, .codec = AudioCodec::VORBIS, .extension = "webm"}; | ||
| case Format::ULAW: | ||
| return {.container = AudioContainer::WAV, .codec = AudioCodec::ULAW, .extension = "wav"}; | ||
| case Format::ALAW: | ||
| return {.container = AudioContainer::WAV, .codec = AudioCodec::ALAW, .extension = "wav"}; | ||
| } | ||
| return {.container = AudioContainer::WAV, .codec = AudioCodec::PCM, .extension = "wav"}; | ||
| } | ||
|
|
There was a problem hiding this comment.
Same case as with probe. We can just hardcode this, then EncoderCapabilites can probably be just a namespace.
There was a problem hiding this comment.
getFormat is not used anymore, same for getFileSettings.
| OpenFileResult RotatingFileWriter::openFile( | ||
| float streamSampleRate, | ||
| int32_t streamChannelCount, | ||
| int32_t maxFramesPerBuffer, | ||
| const std::string &fileNameOverride) { | ||
| streamSampleRate_ = streamSampleRate; | ||
| streamChannelCount_ = streamChannelCount; | ||
| maxFramesPerBuffer_ = maxFramesPerBuffer; | ||
|
|
||
| if (!fileNameOverride.empty()) { | ||
| fileProperties_->fileNamePrefix = fileNameOverride + fileProperties_->fileNamePrefix; | ||
| } | ||
| if (currentWriter_ == nullptr) { | ||
| currentWriter_ = writerFactory_(fileProperties_); | ||
| } | ||
|
|
||
| return openInnerWriter(); | ||
| } | ||
|
|
There was a problem hiding this comment.
This is not idempotent in terms of fileProperties_->fileNamePrefix. If I understand the flow correctly, re-preparing can create a weird-concatented name.
| if (usesFileOutput()) { | ||
| auto fileWriterLock = Locker::tryLock(fileWriterMutex_); | ||
| if (fileWriterLock && fileWriter_) { | ||
| // if we are inside the lock and the fileWriter_ is valid we can be sure that nobody will interrupt us in the middle | ||
| fileWriter_->writeAudioData(interleavedFrames, numFrames); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The call chain causes the file rotation to be invoked on AudioThread.
maciejmakowski2003
left a comment
There was a problem hiding this comment.
could you add an overview of refactor? would be great to get some diagram
Closes #
Introduced changes
Checklist