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
10 changes: 8 additions & 2 deletions apps/common-app/src/demos/Record/Record.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
import {
AudioBuffer,
AudioManager,
concatAudioFiles,
FileFormat,
RecordingNotificationManager,
} from 'react-native-audio-api';

Expand Down Expand Up @@ -123,8 +125,12 @@ const Record: FC = () => {
return;
}

const audioBuffer = await audioContext.decodeAudioData(info.paths[0]);
const outputPath = info.paths[0].replace(/[^/]+$/, 'recording.m4a');

const finalPath = await concatAudioFiles(info.paths, outputPath);
const audioBuffer = await audioContext.decodeAudioData(finalPath);
setRecordedBuffer(audioBuffer);

setState(RecordingState.ReadyToPlay);
currentPositionSV.value = 0;
}, []);
Expand Down Expand Up @@ -240,7 +246,7 @@ const Record: FC = () => {
}, [onPauseRecording, onResumeRecording]);

useEffect(() => {
Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000 });
Recorder.enableFileOutput({ rotateIntervalBytes: 1_000_000, format: FileFormat.M4A });

return () => {
Recorder.disableFileOutput();
Expand Down
8 changes: 2 additions & 6 deletions packages/audiodocs/docs/inputs/audio-recorder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,7 @@ export default MyRecorder;
```tsx
import React, { useState } from 'react';
import { View, Pressable, Text } from 'react-native';
import {
AudioRecorder,
AudioContext,
AudioManager,
} from 'react-native-audio-api';
import { AudioRecorder, AudioContext, AudioManager } from 'react-native-audio-api';

AudioManager.setAudioSessionOptions({
iosCategory: 'playAndRecord',
Expand Down Expand Up @@ -731,7 +727,7 @@ interface AudioRecorderFileOptions {
```

- `channelCount` - The desired channel count in the resulting file. not all file formats supports all possible channel counts.
- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to 0 (default), file output rotation is disabled. When active, new files are named with the original prefix appended with a timestamp.
- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to 0 (default), file output rotation is disabled. When active, new files are named with the original prefix appended with a timestamp. You can join the rotated files after recording with [`concatAudioFiles`](/docs/utils/file-concatenation#concataudiofiles).
- `format` - The desired extension and file format of the recorder file. Check: [FileFormat](#fileformat) below.
- `preset` - The desired recorder file properties, you can use either one of built-in properties or tweak low-level parameters yourself. Check [FilePresetType](#filepresettype) for more details.
- `directory` - Either `FileDirectory.Cache` or `FileDirectory.Document` (default: `FileDirectory.Cache`). Determines the system directory that the file will be saved to.
Expand Down
98 changes: 98 additions & 0 deletions packages/audiodocs/docs/utils/file-concatenation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
sidebar_position: 3
sidebar_label: File concatenation
---
Comment thread
mdydek marked this conversation as resolved.

import { MobileOnly } from '@site/src/components/Badges';

# File concatenation <MobileOnly />

You can concatenate existing audio files without creating an `AudioContext` using
the exported [`concatAudioFiles`](#concataudiofiles) function.

This is useful when recording with the
[`rotateIntervalBytes`](/docs/inputs/audio-recorder#audiorecorderfileoptions)
option. Rotation keeps long recordings split into smaller files while recording,
and `concatAudioFiles` can join those files back into one file after the
recording is finished.

:::warning
`concatAudioFiles` is not supported on web.
:::

:::caution
FFmpeg-backed formats require FFmpeg to be available in the native build. WAV
concatenation uses miniaudio and does not require FFmpeg.
:::

### `concatAudioFiles`

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. |
| `outputPath` | `string` | Local filesystem path or `file://` URL where the joined audio file should be written. |

#### Returns `Promise<string>`.

The promise resolves with the provided `outputPath` when concatenation
completes.

#### Compatibility

All input files must use compatible audio parameters. In practice, files should
come from the same recorder configuration:

- the same container format,
- the same audio codec,
- the same sample rate,
- the same channel layout,
- compatible codec parameters.

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

<details>
<summary>Example usage with rotated recordings</summary>

```tsx
import { AudioRecorder, FileDirectory, FileFormat, concatAudioFiles } from 'react-native-audio-api';

const audioRecorder = new AudioRecorder();

audioRecorder.enableFileOutput({
directory: FileDirectory.Cache,
fileNamePrefix: 'meeting',
format: FileFormat.M4A,
rotateIntervalBytes: 8 * 1024 * 1024,
});

// Start and stop the recorder as usual.
const result = audioRecorder.stop();

if (result.status === 'success') {
const outputPath = result.paths[0].replace(/[^/]+$/, 'meeting-complete.m4a');

await concatAudioFiles(result.paths, outputPath);
}
```

</details>

<details>
<summary>Example usage with explicit paths</summary>

```tsx
import { concatAudioFiles } from 'react-native-audio-api';

const outputPath = '/path/to/session.m4a';

await concatAudioFiles(
['/path/to/session-001.m4a', '/path/to/session-002.m4a', '/path/to/session-003.m4a'],
outputPath
);
```

</details>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ set_source_files_properties(
COMPILE_FLAGS "-O3"
)

if(CMAKE_C_COMPILER_ID MATCHES "Clang")
set_source_files_properties(
"${COMMON_CPP_DIR}/audioapi/dsp/r8brain/fft/pffft_double.c"
PROPERTIES
COMPILE_FLAGS "-Wno-#pragma-messages"
)
endif()

set(INCLUDE_DIR ${COMMON_CPP_DIR}/audioapi/external/include)
set(FFMPEG_INCLUDE_DIR ${COMMON_CPP_DIR}/audioapi/external/include_ffmpeg)
set(EXTERNAL_DIR ${COMMON_CPP_DIR}/audioapi/external/android)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ void AndroidAudioRecorder::onErrorAfterClose(oboe::AudioStream *stream, oboe::Re
audioEventHandlerRegistry_->dispatchEvent(
AudioEvent::RECORDER_ERROR,
callbackId,
RecorderErrorPayload{.message = std::move(message)});
StringPayload{.name = "message", .reason = std::move(message)});
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <audioapi/HostObjects/inputs/AudioRecorderHostObject.h>
#include <audioapi/HostObjects/sources/AudioBufferHostObject.h>
#include <audioapi/HostObjects/utils/AudioDecoderHostObject.h>
#include <audioapi/HostObjects/utils/AudioFileUtilsHostObject.h>
#include <audioapi/HostObjects/utils/AudioStretcherHostObject.h>
#include <audioapi/core/AudioContext.h>
#include <audioapi/core/OfflineAudioContext.h>
Expand Down Expand Up @@ -37,6 +38,7 @@ class AudioAPIModuleInstaller {
jsiRuntime, jsCallInvoker, audioEventHandlerRegistry, uiRuntime);
auto createAudioBuffer = getCreateAudioBufferFunction(jsiRuntime);
auto createAudioDecoder = getCreateAudioDecoderFunction(jsiRuntime, jsCallInvoker);
auto createAudioFileUtils = getCreateAudioFileUtilsFunction(jsiRuntime, jsCallInvoker);
auto createAudioStretcher = getCreateAudioStretcherFunction(jsiRuntime, jsCallInvoker);

jsiRuntime->global().setProperty(*jsiRuntime, "createAudioContext", createAudioContext);
Expand All @@ -45,6 +47,7 @@ class AudioAPIModuleInstaller {
*jsiRuntime, "createOfflineAudioContext", createOfflineAudioContext);
jsiRuntime->global().setProperty(*jsiRuntime, "createAudioBuffer", createAudioBuffer);
jsiRuntime->global().setProperty(*jsiRuntime, "createAudioDecoder", createAudioDecoder);
jsiRuntime->global().setProperty(*jsiRuntime, "createAudioFileUtils", createAudioFileUtils);
jsiRuntime->global().setProperty(*jsiRuntime, "createAudioStretcher", createAudioStretcher);

auto audioEventHandlerRegistryHostObject =
Expand Down Expand Up @@ -185,6 +188,24 @@ class AudioAPIModuleInstaller {
});
}

static jsi::Function getCreateAudioFileUtilsFunction(
jsi::Runtime *jsiRuntime,
const std::shared_ptr<react::CallInvoker> &jsCallInvoker) {
return jsi::Function::createFromHostFunction(
*jsiRuntime,
jsi::PropNameID::forAscii(*jsiRuntime, "createAudioFileUtils"),
0,
[jsCallInvoker](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
auto audioFileUtilsHostObject =
std::make_shared<AudioFileUtilsHostObject>(&runtime, jsCallInvoker);
return jsi::Object::createFromHostObject(runtime, audioFileUtilsHostObject);
});
}

static jsi::Function getCreateAudioBufferFunction(jsi::Runtime *jsiRuntime) {
return jsi::Function::createFromHostFunction(
*jsiRuntime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ AudioFileSourceNodeHostObject::AudioFileSourceNodeHostObject(
const AudioFileSourceOptions &options)
: AudioScheduledSourceNodeHostObject(context->createFileSource(options), options),
loop_(options.loop),
volume_(options.volume),
duration_(std::static_pointer_cast<AudioFileSourceNode>(node_)->getDuration()) {
duration_(std::static_pointer_cast<AudioFileSourceNode>(node_)->getDuration()),
volume_(options.volume) {
addGetters(
JSI_EXPORT_PROPERTY_GETTER(AudioFileSourceNodeHostObject, volume),
JSI_EXPORT_PROPERTY_GETTER(AudioFileSourceNodeHostObject, loop),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <audioapi/HostObjects/utils/AudioFileUtilsHostObject.h>
#include <audioapi/core/utils/AudioFileConcatenator.h>
#include <audioapi/jsi/JsiPromise.h>

#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace audioapi {

AudioFileUtilsHostObject::AudioFileUtilsHostObject(
jsi::Runtime *runtime,
const std::shared_ptr<react::CallInvoker> &callInvoker) {
promiseVendor_ = std::make_shared<PromiseVendor>(runtime, callInvoker);
addFunctions(JSI_EXPORT_FUNCTION(AudioFileUtilsHostObject, concatAudioFiles));
}

JSI_HOST_FUNCTION_IMPL(AudioFileUtilsHostObject, concatAudioFiles) {
if (count < 2 || !args[0].isObject() || !args[1].isString()) {
throw jsi::JSError(runtime, "concatAudioFiles expects input paths and an output path.");
}

auto inputPathArray = args[0].asObject(runtime).asArray(runtime);
const auto inputPathCount = inputPathArray.size(runtime);
std::vector<std::string> inputPaths;
inputPaths.reserve(inputPathCount);

for (size_t i = 0; i < inputPathCount; ++i) {
auto value = inputPathArray.getValueAtIndex(runtime, i);
if (!value.isString()) {
throw jsi::JSError(runtime, "concatAudioFiles input paths must be strings.");
}
inputPaths.push_back(value.asString(runtime).utf8(runtime));
}

auto outputPath = args[1].asString(runtime).utf8(runtime);

auto promise = promiseVendor_->createAsyncPromise(
[inputPaths = std::move(inputPaths), outputPath]() -> PromiseResolver {
auto result = audioapi::concatAudioFiles(inputPaths, outputPath);

if (result.is_err()) {
return [result = std::move(result)](
jsi::Runtime &runtime) -> std::variant<jsi::Value, std::string> {
return result.unwrap_err();
};
}

return [outputPath = std::move(outputPath)](
jsi::Runtime &runtime) -> std::variant<jsi::Value, std::string> {
return jsi::String::createFromUtf8(runtime, outputPath);
};
});

return promise;
}

} // namespace audioapi
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <audioapi/jsi/JsiHostObject.h>
#include <audioapi/jsi/JsiPromise.h>

#include <jsi/jsi.h>
#include <memory>

namespace audioapi {
using namespace facebook;

class AudioFileUtilsHostObject : public JsiHostObject {
public:
explicit AudioFileUtilsHostObject(
jsi::Runtime *runtime,
const std::shared_ptr<react::CallInvoker> &callInvoker);

JSI_HOST_FUNCTION_DECL(concatAudioFiles);

private:
std::shared_ptr<PromiseVendor> promiseVendor_;
};

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ OfflineAudioContext::OfflineAudioContext(
const RuntimeRegistry &runtimeRegistry)
: BaseAudioContext(sampleRate, audioEventHandlerRegistry, runtimeRegistry),
length_(length),
numberOfChannels_(numberOfChannels),
currentSampleFrame_(0),
audioBuffer_(
std::make_shared<DSPAudioBuffer>(RENDER_QUANTUM_SIZE, numberOfChannels, sampleRate)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class OfflineAudioContext : public BaseAudioContext {
OfflineAudioContextResultCallback resultCallback_;

const size_t length_;
const int numberOfChannels_;
size_t currentSampleFrame_;

std::shared_ptr<DSPAudioBuffer> audioBuffer_;
Expand Down
Loading
Loading