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
6 changes: 5 additions & 1 deletion .claude/skills/build-compilation-dependencies/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ react-native-audio-api/
│ │ └── CMakeLists.txt # Actual Android C++ build target
│ ├── common/cpp/audioapi/ # Shared C++ (used by all platforms)
│ │ ├── decoding/ # Decoder factory, backends, SeekDecoderDaemon, AudioDecoding
│ │ ├── encoding/ # AudioEncoder interface, EncoderCapabilities, OS encoder/remux selector headers, AudioFileConcatenator
│ │ ├── encoding/ # AudioEncoder interface, EncoderCapabilities, OS encoder/remux selector headers, AudioFileConcatenator, AdtsHeader (ADTS framing)
│ │ ├── libs/ # Third-party wrappers (FFmpeg, miniaudio, pffft, …)
│ │ └── external/ # Prebuilt binaries per platform
│ │ ├── android/ # .a static libs (Opus, Ogg, Vorbis, OpenSSL)
Expand Down Expand Up @@ -382,6 +382,10 @@ Resolution pitfalls learned the hard way (both handled inside `package-root.js`)
| iOS compile error `unknown type 'id'` | C++ file included ObjC-only header | Compile that file as ObjC++ (separate subspec with `-x objective-c++`) |
| `RCT_NEW_ARCH_ENABLED` undefined on Android | Old RN gradle plugin | Ensure `newArchEnabled=true` in app's `gradle.properties` |
| iOS: `'to_chars' is unavailable: introduced in iOS 16.3` from `formatter_floating_point.h`, instantiated by `std::format<...>` | `std::format` in code compiled for iOS. libc++ availability-gates the whole `<format>` library to iOS 16.3; the podspec minimum is `ios_min_version = '14.0'`. The desktop C++ test build and Android NDK have no such gate, so `yarn test:cpp` passes and only the iOS build fails. | Use `std::string` concatenation / `std::to_string` in `common/cpp` and `ios/`. Zero-pad by hand (`insert(0, n, '0')`). Android-only files (`android/src/main/cpp`) may keep `std::format`. |
| iOS test target: `Undefined symbol: audioapi::<something>` for a function that exists in `common/cpp` and compiles in the pod | The XCTest bundle links against the host app, and the host app only pulls the static-lib members its own code references. A common symbol nobody in `ios/` or the HostObjects uses never reaches the host. | Compute it inline in the test, or make the library reference it; do not add a fake use in production code. |
| iOS: new `common/cpp` `.cpp` not compiled at all (`Undefined symbol` with no `CompileC` line for it) | CocoaPods expands `source_files` globs at `pod install`, not at build time | `cd apps/fabric-example/ios && pod install` after adding a source file |
| `check-audio-file-properties-enum-sync.sh` passes while `FileFormat` is out of sync | Its extractor only reads `Name = <number>` lines; `FileFormat` has no explicit values on either side, so it compares two empty lists ("in sync (0 entries)") | Keep the C++ and TS `FileFormat` lists in the same order by hand; `EncoderCapabilities.h`'s `static_assert` only checks the C++ count |
| Android/iOS: after editing a C++/TS mirrored enum (`FileFormat`, `BitDepth`, …) the app silently uses the wrong value (e.g. records FLAC when JS asked for ADTS) | Metro hot-reloaded the TS enum with the new numbering, but the installed binary still carries the old C++ numbering; enum values cross JSI as plain numbers | Reinstall the app (`installDebug` / `run-ios`) after any enum reorder; a JS reload is never enough |
| Android incremental build only: `Unresolved reference` to a top-level Kotlin function, or `Cannot access '…': it is internal in file`, while `--rerun-tasks` passes | Incremental Kotlin rounds treat the module's own earlier classes as a foreign module (library is built through the `node_modules/react-native-audio-api` symlink) | Use only public classes/objects across files in `android/src/main/java`; no top-level functions, no `internal` |
| clangd only: `'React/RCTBridgeModule.h' file not found` in `.mm` files | `compile_commands.json` has no framework search path | See *clangd compile database* below — regenerate with `yarn setup:clangd` |

Expand Down
3 changes: 2 additions & 1 deletion apps/common-app/src/demos/Record/Record.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import RecordingVisualization from './RecordingVisualization';
import Status from './Status';
import { RecordingState } from './types';

// concatAudioFiles supports WAV, M4A, and FLAC — the formats recordable on
// concatAudioFiles supports WAV, M4A, FLAC, and ADTS — the formats recordable on
// both iOS and Android.
const RECORDING_EXTENSION = FileFormat.M4A;
const ROTATING_SIZE = 250_000;
Expand All @@ -31,6 +31,7 @@ const RECORDING_EXTENSION_NAME_MAP = {
[FileFormat.Wav]: 'wav',
[FileFormat.M4A]: 'm4a',
[FileFormat.Flac]: 'flac',
[FileFormat.Adts]: 'aac',
};

const Record: FC = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
#import <audioapi/ios/core/NativeAudioRecorder.h>
#import <audioapi/ios/system/AudioEngine.h>
#import <audioapi/ios/system/AudioSessionManager.h>
#import <audioapi/encoding/AdtsHeader.h>
#import <audioapi/encoding/EncoderCapabilities.h>
#import <audioapi/ios/core/utils/IOSEncoder.h>
#import <audioapi/utils/AudioFileProperties.h>

#include <cstring>
#include <memory>
#include <string>
#include <tuple>
#include <vector>

using namespace audioapi;

Expand Down Expand Up @@ -300,7 +305,7 @@ - (void)tearDown {
.subDirectory = "fabric-example-tests",
.fileName = "ios-recorder-test",
},
AudioFileProperties::StreamConfig{.sampleRate = 44100, .channelCount = 2},
AudioLayout{.sampleRate = 44100, .channelCount = 2},
AudioFileProperties::EncodingConfig{
.format = AudioFileProperties::FileFormat::WAV,
.bitRate = 128000,
Expand Down Expand Up @@ -627,3 +632,71 @@ - (void)testConnectWhileActiveInitializesAdapterAndDisconnectClearsIt {
}

@end

/// The file type AVAudioFile writes follows the path extension, so the ADTS format is
/// proven at the byte level: every frame must start with a sync word and its length must
/// land exactly on the next one.
@interface IOSEncoderAdtsTests : XCTestCase
@end

@implementation IOSEncoderAdtsTests

- (void)testAacExtensionWritesSelfDelimitingAdtsFrames {
NSString *path = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"ios-encoder-adts-test.aac"];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];

const AudioLayout layout{.sampleRate = 44100, .channelCount = 1};
ios::encoder::IOSEncoder encoder(EncoderSettings{
.fileLayout = layout,
.encoding =
AudioFileProperties::EncodingConfig{
.format = AudioFileProperties::FileFormat::ADTS,
.bitRate = 64000,
.bitDepth = AudioFileProperties::BitDepth::Bit16,
.flacCompressionLevel = 0,
.iosAudioQuality = AudioFileProperties::IOSAudioQuality::High,
},
});
const EncoderOutputSpec spec =
encoder_capabilities::specForFormat(AudioFileProperties::FileFormat::ADTS);
XCTAssertEqual(spec.container, AudioContainer::ADTS);

auto openResult = encoder.open(
StreamFormat{.layout = layout, .maxFramesPerBuffer = 1024}, spec, path.UTF8String);
XCTAssertTrue(openResult.is_ok(), @"%s",
openResult.is_err() ? openResult.unwrap_err().c_str() : "");

std::vector<float> samples(1024, 0.25f);
const float *channels[1] = {samples.data()};
for (int block = 0; block < 40; ++block) {
XCTAssertTrue(encoder.encode(channels, 1024).is_ok());
}
XCTAssertTrue(encoder.close().is_ok());

NSData *data = [NSData dataWithContentsOfFile:path];
XCTAssertGreaterThan(data.length, adts::HEADER_SIZE);
const auto *bytes = static_cast<const uint8_t *>(data.bytes);

size_t offset = 0;
size_t frames = 0;
while (offset + adts::HEADER_SIZE <= data.length) {
adts::HeaderBytes header{};
std::memcpy(header.data(), bytes + offset, adts::HEADER_SIZE);
XCTAssertEqual(header[0], 0xFF, @"frame %zu", frames);
XCTAssertEqual(header[1] & 0xF6, 0xF0, @"frame %zu", frames);
// 13-bit frame length straddles bytes 3..5; computed here because nothing in the iOS
// library references the ADTS helpers, so they are not linked into the test host.
const size_t frameLength = ((static_cast<size_t>(header[3]) & 0x3U) << 11) |
(static_cast<size_t>(header[4]) << 3) | (static_cast<size_t>(header[5]) >> 5);
XCTAssertGreaterThan(frameLength, adts::HEADER_SIZE, @"frame %zu", frames);
offset += frameLength;
frames += 1;
}
XCTAssertEqual(offset, data.length);
XCTAssertGreaterThan(frames, 0u);

[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}

@end
12 changes: 10 additions & 2 deletions packages/audiodocs/docs/inputs/audio-recorder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,7 @@ enum FileFormat {
Wav,
Caf,
M4A,
Adts,
Flac,
Aiff,
Alac,
Expand All @@ -1017,6 +1018,7 @@ The table below lists which formats each platform can encode with its system API
| `Wav` | WAV / PCM | ✅ | ✅ |
| `M4A` | M4A / AAC-LC | ✅ | ✅ |
| `Flac` | FLAC / FLAC | ✅ | ✅ |
| `Adts` | ADTS / AAC-LC | ✅ | ✅ |
| `Caf` | CAF / PCM | ✅ | ❌ |
| `Aiff` | AIFF / PCM | ✅ | ❌ |
| `Alac` | M4A / Apple Lossless | ✅ | ❌ |
Expand All @@ -1026,8 +1028,14 @@ The table below lists which formats each platform can encode with its system API
| `OpusWebm` | WebM / Opus | ❌ | ✅ |
| `VorbisWebm` | WebM / Vorbis | ❌ | ✅ |

:::tip
Both `Adts` and `Flac` files stay decodable after an app crash, but only the former is lossy.
`Adts` accepts only the sample rates an ADTS header can express: 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200 and 96000 Hz.
Those numbers come straight from MPEG-2 specification.
:::

:::caution Platform support
Selecting a format the current platform cannot encode (for example `Caf` on Android or `OpusOgg` on iOS) fails when file output is enabled, with a descriptive error. Some Android formats also depend on the device OS version (Opus/OGG muxing requires newer releases); if a device lacks a system encoder for the requested format, recording start returns an error. Use `Wav`, `M4A`, or `Flac` for the widest cross-platform support.
Selecting a format the current platform cannot encode (for example `Caf` on Android or `OpusOgg` on iOS) fails when file output is enabled, with a descriptive error. Some Android formats also depend on the device OS version (Opus/OGG muxing requires newer releases); if a device lacks a system encoder for the requested format, recording start returns an error. Use `Wav`, `M4A`, `Flac` or `Adts` for the widest cross-platform support.
:::
Comment thread
mdydek marked this conversation as resolved.

#### FileInfo
Expand Down Expand Up @@ -1186,7 +1194,7 @@ export interface FilePresetType {
</div></td>
<td>
<div class="text-content">
Specifies the sampling frequency used during recording. Higher sample rates capture a wider frequency range but increase processing and storage requirements.
Specifies the sampling frequency used during recording. Higher sample rates capture a wider frequency range but increase processing and storage requirements. AAC output in the `Adts` format is limited to the standard AAC rates listed under [`FileFormat`](#fileformat).
</div>
</td>
</tr>
Expand Down
16 changes: 11 additions & 5 deletions packages/audiodocs/docs/utils/file-concatenation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ recording is finished.
:::

:::caution
**Only WAV, M4A, and FLAC concatenation is supported** — the recording formats
**Only WAV, M4A, FLAC, and ADTS concatenation is supported** — the recording formats
available on both iOS and Android.

WAV and FLAC decode the segments and write them out again through the system
encoder — WAV as 32-bit float PCM, FLAC losslessly. M4A remuxes AAC packets
with system APIs — iOS AVFoundation and Android MediaExtractor/MediaMuxer.

ADTS (`.aac`) appends complete encoded frames without decoding or re-encoding.
Inputs must use the same sample rate and channel configuration. An incomplete
trailing frame, such as one left by an app crash, is discarded before the next
file's frames are appended.
:::

:::tip Segment size and quality
Expand All @@ -45,9 +50,9 @@ If playback quality is poor after joining rotated M4A files, increase `rotateInt

Concatenates compatible local audio files into a single output file.

| Parameter | Type | Description |
| :---: | :---: | :---- |
| `inputPaths` | `string[]` | Local filesystem paths or `file://` URLs to concatenate, in output order. |
| Parameter | Type | Description|
| :----------: | :--------: | :--- |
| `inputPaths` | `string[]` | Local filesystem paths or `file://` URLs to concatenate, in output order. |
| `outputPath` | `string` | Local filesystem path or `file://` URL where the joined audio file should be written. |

#### Returns `Promise<string>`.
Expand All @@ -68,7 +73,8 @@ come from the same recorder configuration:

Use an output path with an extension that matches the input files, for example
joining `.m4a` segments into an `.m4a` output file, `.wav` segments into a
`.wav` output file, or `.flac` segments into a `.flac` output file.
`.wav` output file, `.flac` segments into a `.flac` output file, or ADTS `.aac`
segments into an `.aac` output file.

<details>
<summary>Example usage with rotated recordings</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <audioapi/core/utils/Constants.h>
#include <audioapi/dsp/r8brain/Resampler.hpp>
#include <audioapi/encoding/AdtsHeader.h>
#include <audioapi/utils/AudioBuffer.hpp>
#include <audioapi/utils/AudioFileProperties.h>
#include <audioapi/utils/FileSystem.hpp>
Expand Down Expand Up @@ -663,6 +664,101 @@ class FlacBackend : public MediaCodecBackend {
size_t bytesWritten_{0};
};

// ---------------------------------------------------------------------------
// ADTS backend — MediaCodec AAC-LC encoder, each access unit written behind its own
// ADTS header. No muxer: the stream is complete after any frame, so a killed
// recording keeps everything up to its last complete frame.
// ---------------------------------------------------------------------------

class AdtsBackend : public MediaCodecBackend {
public:
std::string open(
Comment thread
mdydek marked this conversation as resolved.
const AudioLayout &desired,
const std::string &filePath,
const AudioFileProperties::EncodingConfig &encoding,
const EncoderOutputSpec &outputSpec,
AudioLayout &effective) override {
(void)outputSpec;
effective = desired;

auto layoutResult = adts::validateLayout(desired);
if (layoutResult.is_err()) {
return "AdtsBackend: " + layoutResult.unwrap_err();
}
samplingFrequencyIndex_ = *adts::samplingFrequencyIndex(static_cast<int>(desired.sampleRate));

file_.reset(std::fopen(filePath.c_str(), "wb"));
if (file_ == nullptr) {
return "AdtsBackend: failed to open file for writing";
}

std::string err = startCodec(
"audio/mp4a-latm",
static_cast<int>(desired.sampleRate),
desired.channelCount,
static_cast<int>(encoding.bitRate),
true);
if (!err.empty()) {
file_.reset();
return err;
}
return "";
}

[[nodiscard]] size_t getFileSizeBytes() const override {
return bytesWritten_;
}

protected:
std::string onFormatChanged(AMediaFormat *outputFormat) override {
(void)outputFormat;
return "";
}

std::string onEncodedSample(const uint8_t *data, const AMediaCodecBufferInfo &info) override {
// The AudioSpecificConfig the codec emits first belongs in a container; ADTS headers
// carry the same information per frame.
if ((info.flags & AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG) != 0) {
return "";
}
if (file_ == nullptr) {
return "AdtsBackend: file not open";
}

const auto payloadSize = static_cast<size_t>(info.size);
auto header = adts::makeHeader(samplingFrequencyIndex_, channelCount_, payloadSize);
if (!header.has_value()) {
return "AdtsBackend: encoded frame too long for an ADTS header";
}

if (std::fwrite(header->data(), 1, header->size(), file_.get()) != header->size() ||
std::fwrite(data + info.offset, 1, payloadSize, file_.get()) != payloadSize) {
return "AdtsBackend: short write";
}
// Flushed per frame, so the crash-recoverability the format exists for holds at frame
// granularity rather than at stdio's buffer size.
if (std::fflush(file_.get()) != 0) {
return "AdtsBackend: failed to flush file";
}
bytesWritten_ += header->size() + payloadSize;
return "";
}

std::string finalize() override {
codec_.reset();
if (file_ != nullptr && std::fclose(file_.release()) != 0) {
return "AdtsBackend: failed to close file";
}
return "";
}

private:
// Also closes the file when codec draining fails before finalize() is reached.
std::unique_ptr<std::FILE, decltype(&std::fclose)> file_{nullptr, &std::fclose};
uint8_t samplingFrequencyIndex_{0};
size_t bytesWritten_{0};
};

// ---------------------------------------------------------------------------
// Conversion state — planar scratch for channel mapping and rate conversion, built at
// open() only when the input differs from the backend's effective format.
Expand Down Expand Up @@ -732,6 +828,12 @@ OpenEncoderResult AndroidEncoder::open(
backend_ = std::make_unique<FlacBackend>();
break;
case AudioCodec::AAC:
if (outputSpec.container == AudioContainer::ADTS) {
backend_ = std::make_unique<AdtsBackend>();
} else {
backend_ = std::make_unique<MuxedBackend>();
}
break;
case AudioCodec::OPUS:
case AudioCodec::VORBIS:
backend_ = std::make_unique<MuxedBackend>();
Expand Down
Loading
Loading