Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/audiodocs/docs/inputs/audio-recorder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -947,11 +947,14 @@ enum FileFormat {
Caf,
M4A,
Flac,
Adts,
}
```

`Adts` writes raw AAC-LC in ADTS framing (`.aac` extension). Every frame carries its own sync word and length header, so a recording interrupted by a crash or force-kill stays decodable up to the last complete frame, like FLAC but at lossy-AAC file sizes. Bitrate and quality follow the same AAC properties as `M4A`.

:::caution Android + FFmpeg
On Android, encoded file output for `M4A`, `FLAC`, and `CAF` uses FFmpeg. When FFmpeg is disabled in the build, only **WAV** recording to file is supported. iOS uses system AVFoundation for all listed formats. See [Runtime flags](../other/runtime-flags.mdx#where-ffmpeg-is-used).
On Android, encoded file output for `M4A`, `FLAC`, `CAF`, and `ADTS` uses FFmpeg. When FFmpeg is disabled in the build, only **WAV** recording to file is supported. iOS uses system AVFoundation for all listed formats. See [Runtime flags](../other/runtime-flags.mdx#where-ffmpeg-is-used).
:::

#### FileInfo
Expand Down
2 changes: 1 addition & 1 deletion packages/audiodocs/docs/other/runtime-flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ if (!isFfmpegEnabled()) {
| :---: | :---: | :---- |
| Streaming | [`Audio tag`](../sources/audio-tag.mdx), `createFileSource` | Remote URL streaming (HTTP byte ranges) and HLS (`.m3u8`) |
| Metadata | [`getAudioDuration`](../utils/decoding.mdx#getaudioduration), [`<Audio>`](../sources/audio-tag.mdx) `preload="metadata"` | Remote `http(s):` URL duration probing |
| Recording | [`AudioRecorder`](../inputs/audio-recorder.mdx) file output | **Android only:** `M4A`, `FLAC`, and `CAF` (WAV uses miniaudio). iOS uses system AVFoundation instead. |
| Recording | [`AudioRecorder`](../inputs/audio-recorder.mdx) file output | **Android only:** `M4A`, `FLAC`, `CAF`, and `ADTS` (WAV uses miniaudio). iOS uses system AVFoundation instead. |
| File utils | [`concatAudioFiles`](../utils/file-concatenation.mdx#concataudiofiles) | M4A concatenation (WAV uses miniaudio) |

Batch decoding ([`decodeAudioData`](../utils/decoding.mdx#decodeaudiodata)) uses OS APIs on mobile (and miniaudio for formats like Ogg/Opus) — not FFmpeg. Without FFmpeg, remote progressive sources can still be fully downloaded in JS and decoded that way; HLS and in-place URL streaming remain unavailable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ std::string getFileExtension(const std::shared_ptr<AudioFileProperties> &propert
return "m4a";
case AudioFileProperties::Format::FLAC:
return "flac";
case AudioFileProperties::Format::ADTS:
return "aac";
default:
return "m4a";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,35 @@ extern "C" {

#include <sys/stat.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdio>
#include <iterator>
#include <memory>
#include <string>
#include <utility>

constexpr int fallbackFIFOSize = 8192;
constexpr int fallbackFrameSize = 512;
constexpr int defaultFlushInterval = 100;
constexpr int adtsHeaderSize = 7;

namespace {

/// @brief MPEG-4 sampling frequency index for the ADTS header, or -1 if the
/// rate has no index (ADTS cannot express it).
int adtsFreqIndex(int sampleRate) {
static constexpr int rates[] = {
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350};
for (size_t i = 0; i < std::size(rates); i++) {
if (rates[i] == sampleRate) {
return static_cast<int>(i);
}
}
return -1;
}

} // namespace

namespace audioapi::android::ffmpeg {

Expand Down Expand Up @@ -81,20 +101,38 @@ OpenFileResult FFmpegAudioFileWriter::openFile(
return OpenFileResult::Err("Unsupported codec for the given file format");
}

return initializeFormatContext(codec)
.and_then([this, codec](auto) { return configureAndOpenCodec(codec); })
.and_then([this](auto) { return initializeStream(); })
.and_then([this](auto) { return openIOAndWriteHeader(); })
// ADTS is muxer-less: raw AAC packets each get a hand-built 7-byte header
// (see writeAdtsPacket), so only the encoder and a plain AVIOContext are
// needed. This also sidesteps the prebuilt FFmpeg, which does not compile
// in the adts muxer.
isAdts_ = fileProperties_->format == AudioFileProperties::Format::ADTS;

auto containerInit = isAdts_
? configureAndOpenCodec(codec).and_then([this](auto) { return openAdtsIO(); })
: initializeFormatContext(codec)
.and_then([this, codec](auto) { return configureAndOpenCodec(codec); })
.and_then([this](auto) { return initializeStream(); })
.and_then([this](auto) { return openIOAndWriteHeader(); });

// `and_then` is rvalue-ref-qualified (Result.hpp), so the accumulated chain
// must be consumed as an rvalue — `containerInit` is a named lvalue here.
// Any failure in the chain rolls back every resource acquired so far (codec,
// format/ADTS IO, resampler, pool), so a late failure cannot leak an open
// AVIOContext or a partial output file.
return std::move(containerInit)
.and_then(
[this](auto) { return initializeResampler(streamSampleRate_, streamChannelCount_); })
.and_then([this](auto) {
initializeBuffers(streamMaxBufferSize_);
if (!initializePreallocatedInputPool()) {
rollbackFailedOpen();
return OpenFileResult::Err("Failed to preallocate Android file writer buffers");
}
isFileOpen_.store(true, std::memory_order_release);
return OpenFileResult::Ok(filePath_);
})
.or_else([this](std::string error) {
rollbackFailedOpen();
return OpenFileResult::Err(std::move(error));
});
}

Expand Down Expand Up @@ -245,7 +283,39 @@ Result<NoneType, std::string> FFmpegAudioFileWriter::openIOAndWriteHeader() {
return Result<NoneType, std::string>::Ok(None);
}

/// @brief Opens the raw byte output for ADTS (no muxer, no container header).
/// @returns Success status or Error status with message.
Result<NoneType, std::string> FFmpegAudioFileWriter::openAdtsIO() {
if (adtsFreqIndex(static_cast<int>(encoderCtx_->sample_rate)) < 0) {
return Result<NoneType, std::string>::Err(
"Sample rate not expressible in an ADTS header: " +
std::to_string(encoderCtx_->sample_rate));
}

// channel_configuration is a 3-bit field: 1..6 map to the channel count,
// 7 means 7.1 and 0 means "defined by an in-band PCE", which the header
// written here never carries.
const int channelCount = encoderCtx_->ch_layout.nb_channels;
if (channelCount < 1 || channelCount > 6) {
return Result<NoneType, std::string>::Err(
"Channel count not expressible in an ADTS header: " + std::to_string(channelCount));
}

int result = avio_open(&adtsIo_, filePath_.c_str(), AVIO_FLAG_WRITE);

if (result < 0) {
return Result<NoneType, std::string>::Err(
"Failed to open output file with error: " + parseErrorCode(result));
}

return Result<NoneType, std::string>::Ok(None);
}

size_t FFmpegAudioFileWriter::getFileSizeBytes() const {
if (adtsIo_ != nullptr) {
return static_cast<size_t>(avio_tell(adtsIo_));
}

if (formatCtx_ == nullptr) {
return 0;
}
Expand Down Expand Up @@ -431,17 +501,22 @@ int FFmpegAudioFileWriter::writeEncodedPackets() {
return result;
}

av_packet_rescale_ts(packet_.get(), encoderCtx_->time_base, stream_->time_base);
packet_->stream_index = stream_->index;
if (isAdts_) {
result = writeAdtsPacket();
} else {
av_packet_rescale_ts(packet_.get(), encoderCtx_->time_base, stream_->time_base);
packet_->stream_index = stream_->index;

result = av_interleaved_write_frame(formatCtx_.get(), packet_.get());
result = av_interleaved_write_frame(formatCtx_.get(), packet_.get());
}

AVIOContext *io = isAdts_ ? adtsIo_ : formatCtx_->pb;
auto now = std::chrono::steady_clock::now();
auto elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastFlushTime_).count();

if (formatCtx_->pb && elapsedMs >= flushIntervalMs_) {
avio_flush(formatCtx_->pb);
if (io && elapsedMs >= flushIntervalMs_) {
avio_flush(io);
lastFlushTime_ = now;
}

Expand All @@ -451,21 +526,69 @@ int FFmpegAudioFileWriter::writeEncodedPackets() {
}
}

/// @brief Writes one encoded AAC packet as a self-delimiting ADTS frame:
/// a 7-byte header (syncword, profile, sample-rate index, channel config,
/// frame length) followed by the raw packet bytes. Because every frame is
/// self-contained, a recording truncated by a crash or force-kill stays
/// decodable up to the last complete frame — no trailer required.
/// @returns 0 on success, AV_ERROR code on failure
int FFmpegAudioFileWriter::writeAdtsPacket() {
const int freqIdx = adtsFreqIndex(static_cast<int>(encoderCtx_->sample_rate));
const int chanCfg = encoderCtx_->ch_layout.nb_channels;
const int frameLen = packet_->size + adtsHeaderSize;

// 13-bit frame length is the format's hard ceiling; an AAC-LC packet
// (max 768 bytes/channel) never reaches it, but fail closed regardless.
if (frameLen >= (1 << 13)) {
av_packet_unref(packet_.get());
return AVERROR(EINVAL);
}

std::array<uint8_t, adtsHeaderSize> header{};
header[0] = 0xFF; // syncword high
header[1] = 0xF1; // syncword low, MPEG-4, layer 0, no CRC
// profile (AAC-LC = 2, stored as profile - 1), sampling index, channel cfg high bit
header[2] = static_cast<uint8_t>(
(1U << 6) | (static_cast<unsigned>(freqIdx) << 2) |
((static_cast<unsigned>(chanCfg) >> 2) & 0x1U));
header[3] = static_cast<uint8_t>(
((static_cast<unsigned>(chanCfg) & 0x3U) << 6) |
((static_cast<unsigned>(frameLen) >> 11) & 0x3U));
header[4] = static_cast<uint8_t>((static_cast<unsigned>(frameLen) >> 3) & 0xFFU);
header[5] = static_cast<uint8_t>(((static_cast<unsigned>(frameLen) & 0x7U) << 5) | 0x1FU);
header[6] = 0xFC; // buffer fullness low bits (0x7FF = VBR), 1 frame per ADTS frame

avio_write(adtsIo_, header.data(), adtsHeaderSize);
avio_write(adtsIo_, packet_->data, packet_->size);
av_packet_unref(packet_.get());

return adtsIo_->error < 0 ? adtsIo_->error : 0;
}

/// @brief Closes the currently opened audio file, flushing any remaining data and finalizing the file.
/// Method checks the file size and duration for convenience.
/// @returns CloseFileResult indicating success or error details
CloseFileResult FFmpegAudioFileWriter::finalizeOutput() {
int result = av_write_trailer(formatCtx_.get());
double fileSizeInMB = 0;

if (result < 0) {
return CloseFileResult::Err("Failed to write trailer: " + parseErrorCode(result));
}
if (isAdts_) {
// No trailer — an ADTS stream is complete after its last frame.
if (adtsIo_) {
avio_flush(adtsIo_);
fileSizeInMB = static_cast<double>(avio_tell(adtsIo_)) / MB_IN_BYTES;
avio_closep(&adtsIo_);
}
} else {
int result = av_write_trailer(formatCtx_.get());

double fileSizeInMB = 0;
if (result < 0) {
return CloseFileResult::Err("Failed to write trailer: " + parseErrorCode(result));
}

if (formatCtx_->pb) {
fileSizeInMB = static_cast<double>(avio_size(formatCtx_->pb)) / MB_IN_BYTES;
avio_closep(&formatCtx_->pb);
if (formatCtx_->pb) {
fileSizeInMB = static_cast<double>(avio_size(formatCtx_->pb)) / MB_IN_BYTES;
avio_closep(&formatCtx_->pb);
}
}

double durationInSeconds = 0.0;
Expand All @@ -485,6 +608,11 @@ void FFmpegAudioFileWriter::rollbackFailedOpen() {
avio_closep(&formatCtx_->pb);
}

if (adtsIo_ != nullptr) {
avio_closep(&adtsIo_);
}
isAdts_ = false;

encoderCtx_.reset();
formatCtx_.reset();
resampleCtx_.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct AVPacket;
struct AVAudioFifo;
struct SwrContext;
struct AVStream;
struct AVIOContext;

namespace audioapi {

Expand Down Expand Up @@ -50,6 +51,13 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend {
av_unique_ptr<AVFrame> writingFrame_{nullptr};
AVStream *stream_{nullptr};

// ADTS output bypasses the muxer layer entirely: the prebuilt FFmpeg is
// built without the adts muxer (rn-audio-libs: --enable-muxer=wav,mp4,flac,caf),
// and ADTS needs none — each frame is a 7-byte header + raw AAC packet,
// written straight to this AVIOContext.
bool isAdts_{false};
AVIOContext *adtsIo_{nullptr};

unsigned int nextPts_{0};

std::chrono::steady_clock::time_point lastFlushTime_ = std::chrono::steady_clock::now();
Expand All @@ -60,6 +68,7 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend {
Result<NoneType, std::string> configureAndOpenCodec(const AVCodec *codec);
Result<NoneType, std::string> initializeStream();
Result<NoneType, std::string> openIOAndWriteHeader();
Result<NoneType, std::string> openAdtsIO();
// TODO: rewrite to use r8brain resampler
Result<NoneType, std::string> initializeResampler(float inputRate, int inputChannels);
void initializeBuffers(int32_t maxBufferSize);
Expand All @@ -68,6 +77,7 @@ class FFmpegAudioFileWriter : public AndroidFileWriterBackend {
bool resampleAndPushToFifo(void *data, int numFrames);
int processFifo(bool flush);
int writeEncodedPackets();
int writeAdtsPacket();

// Finalization helper methods
CloseFileResult finalizeOutput();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ AVCodecID getCodecID(const std::shared_ptr<AudioFileProperties> &properties) {
case AudioFileProperties::Format::CAF:
return getPCMCodecID(properties);
case AudioFileProperties::Format::M4A:
case AudioFileProperties::Format::ADTS:
return AV_CODEC_ID_AAC;
case AudioFileProperties::Format::FLAC:
return AV_CODEC_ID_FLAC;
Expand All @@ -53,7 +54,8 @@ AVCodecID getCodecID(const std::shared_ptr<AudioFileProperties> &properties) {
/// @param properties The audio file properties.
/// @return The corresponding AVSampleFormat.
AVSampleFormat getSampleFormat(const std::shared_ptr<AudioFileProperties> &properties) {
if (properties->format == AudioFileProperties::Format::M4A) {
if (properties->format == AudioFileProperties::Format::M4A ||
properties->format == AudioFileProperties::Format::ADTS) {
return AV_SAMPLE_FMT_FLTP;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AudioFileProperties {
CAF = 1,
M4A = 2,
FLAC = 3,
ADTS = 4,
};

enum class IOSAudioQuality {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ AudioFormatID getFormat(const std::shared_ptr<AudioFileProperties> &properties)
case AudioFileProperties::Format::FLAC:
return kAudioFormatFLAC;

case AudioFileProperties::Format::ADTS:
return kAudioFormatMPEG4AAC;

default:
return kAudioFormatLinearPCM;
}
Expand Down Expand Up @@ -81,6 +84,9 @@ NSInteger getFlacCompressionLevel(const std::shared_ptr<AudioFileProperties> &pr
case AudioFileProperties::Format::FLAC:
return @"flac";

case AudioFileProperties::Format::ADTS:
return @"aac";

default:
return @"wav";
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-audio-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export enum FileFormat {
Caf = 1,
M4A = 2,
Flac = 3,
Adts = 4,
}

export enum IOSAudioQuality {
Expand Down
Loading