Skip to content

Feat/long file playback#993

Open
mdydek wants to merge 19 commits intomainfrom
feat/long-file-playback
Open

Feat/long file playback#993
mdydek wants to merge 19 commits intomainfrom
feat/long-file-playback

Conversation

@mdydek
Copy link
Contributor

@mdydek mdydek commented Mar 20, 2026

Closes #735

⚠️ Breaking changes ⚠️

Introduced changes

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added/Conducted relevant tests
  • Performed self-review of the code
  • Updated Web Audio API coverage
  • Added support for web
  • Updated old arch android spec file

@mdydek mdydek added the feature New feature label Mar 20, 2026
@mdydek mdydek marked this pull request as ready for review March 26, 2026 09:13
Comment on lines +103 to +113
void AudioFileSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);

auto event = [sourceNode, callbackId](BaseAudioContext &) {
sourceNode->setOnPositionChangedCallbackId(callbackId);
};

sourceNode->unregisterOnPositionChangedCallback(onPositionChangedCallbackId_);
sourceNode->scheduleAudioEvent(std::move(event));
onPositionChangedCallbackId_ = callbackId;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void AudioFileSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);
auto event = [sourceNode, callbackId](BaseAudioContext &) {
sourceNode->setOnPositionChangedCallbackId(callbackId);
};
sourceNode->unregisterOnPositionChangedCallback(onPositionChangedCallbackId_);
sourceNode->scheduleAudioEvent(std::move(event));
onPositionChangedCallbackId_ = callbackId;
}
void AudioFileSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);
auto event = [sourceNode, callbackId, onPositionChangedCallbackId_ = this.onPositionChangedCallbackId_](BaseAudioContext &) {
sourceNode->unregisterOnPositionChangedCallback(onPositionChangedCallbackId_);
sourceNode->setOnPositionChangedCallbackId(callbackId);
};
sourceNode->scheduleAudioEvent(std::move(event));
onPositionChangedCallbackId_ = callbackId;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? unregisterOnPositionChangedCallback(onPositionChangedCallbackId_) calls audioEventHandlerRegistry_->unregisterHandler(AudioEvent::ENDED, callbackId) that calls invokeAsync on JS thread

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmmm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but what if we call a callback in between unregistration and setOnPositionChangeCallback
in this case we try to invoke non existent callback
are we sure this will work fine?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map with callback is accessed only inside invokeAsync block, so indeed it is thread safe

Comment on lines +120 to +130
void AudioFileSourceNodeHostObject::setOnEndedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);

auto event = [sourceNode, callbackId](BaseAudioContext &) {
sourceNode->setOnEndedCallbackId(callbackId);
};

sourceNode->unregisterOnEndedCallback(onEndedCallbackId_);
sourceNode->scheduleAudioEvent(std::move(event));
onEndedCallbackId_ = callbackId;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void AudioFileSourceNodeHostObject::setOnEndedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);
auto event = [sourceNode, callbackId](BaseAudioContext &) {
sourceNode->setOnEndedCallbackId(callbackId);
};
sourceNode->unregisterOnEndedCallback(onEndedCallbackId_);
sourceNode->scheduleAudioEvent(std::move(event));
onEndedCallbackId_ = callbackId;
}
void AudioFileSourceNodeHostObject::setOnEndedCallbackId(uint64_t callbackId) {
auto sourceNode = std::static_pointer_cast<AudioFileSourceNode>(node_);
auto event = [sourceNode, callbackId, onEndedCallbackId_ = this.onEndedCallbackId_](BaseAudioContext &) {
sourceNode->unregisterOnEndedCallback(onEndedCallbackId_);
sourceNode->setOnEndedCallbackId(callbackId);
};
sourceNode->scheduleAudioEvent(std::move(event));
onEndedCallbackId_ = callbackId;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment on lines +430 to +438
size_t frames = interleaved.size() / static_cast<size_t>(channels);
auto buf = std::make_shared<AudioBuffer>(frames, channels, static_cast<float>(sample_rate));
for (int c = 0; c < channels; ++c) {
auto span = buf->getChannel(c)->span();
for (size_t i = 0; i < frames; ++i) {
span[i] = interleaved[i * static_cast<size_t>(channels) + static_cast<size_t>(c)];
}
}
return buf;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use AudioBuffer method deinterleaveFrom

Comment on lines +21 to +28
JSI_PROPERTY_GETTER_DECL(volume);
JSI_PROPERTY_SETTER_DECL(volume);
JSI_PROPERTY_GETTER_DECL(loop);
JSI_PROPERTY_SETTER_DECL(loop);
JSI_PROPERTY_GETTER_DECL(currentTime);
JSI_PROPERTY_GETTER_DECL(duration);
JSI_PROPERTY_SETTER_DECL(onPositionChanged);
JSI_PROPERTY_SETTER_DECL(onEnded);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
JSI_PROPERTY_GETTER_DECL(volume);
JSI_PROPERTY_SETTER_DECL(volume);
JSI_PROPERTY_GETTER_DECL(loop);
JSI_PROPERTY_SETTER_DECL(loop);
JSI_PROPERTY_GETTER_DECL(currentTime);
JSI_PROPERTY_GETTER_DECL(duration);
JSI_PROPERTY_SETTER_DECL(onPositionChanged);
JSI_PROPERTY_SETTER_DECL(onEnded);
JSI_PROPERTY_GETTER_DECL(volume);
JSI_PROPERTY_GETTER_DECL(loop);
JSI_PROPERTY_GETTER_DECL(currentTime);
JSI_PROPERTY_GETTER_DECL(duration);
JSI_PROPERTY_SETTER_DECL(volume);
JSI_PROPERTY_SETTER_DECL(loop);
JSI_PROPERTY_SETTER_DECL(onPositionChanged);
JSI_PROPERTY_SETTER_DECL(onEnded);


JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, duration) {
auto node = std::static_pointer_cast<AudioFileSourceNode>(node_);
return {node->getDuration()};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can store duration in shadow state, as well as volume and loop.

std::shared_ptr<AudioFileSourceNode> BaseAudioContext::createFileSource(
const AudioFileSourceOptions &options) {
auto fileSource = std::make_shared<AudioFileSourceNode>(shared_from_this(), options);
// graphManager_->addProcessingNode(fileSource);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

Comment on lines +126 to +155
ma_decoder_config config =
ma_decoder_config_init(ma_format_f32, 0, static_cast<ma_uint32>(context->getSampleRate()));
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays)
ma_decoding_backend_vtable *customBackends[] = {
ma_decoding_backend_libvorbis, ma_decoding_backend_libopus};
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
config.ppCustomBackendVTables = customBackends;
config.customBackendCount = sizeof(customBackends) / sizeof(customBackends[0]);

maDecoder_ = std::make_unique<ma_decoder>();
ma_result result;
if (useFilePath) {
result = ma_decoder_init_file(state->filePath.c_str(), &config, maDecoder_.get());
} else {
result = ma_decoder_init_memory(
state->memoryData.data(), state->memoryData.size(), &config, maDecoder_.get());
}

if (result == MA_SUCCESS) {
state->channels = static_cast<int>(maDecoder_->outputChannels);
state->sampleRate = static_cast<float>(maDecoder_->outputSampleRate);
ma_uint64 length = 0;
if (ma_decoder_get_length_in_pcm_frames(maDecoder_.get(), &length) == MA_SUCCESS) {
duration_ = static_cast<double>(length) / state->sampleRate;
}
} else {
ma_decoder_uninit(maDecoder_.get());
maDecoder_.reset();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way to hide ma decoder initialization impl details inside MiniAudio Decoder class?

Comment on lines +207 to +208
ma_uint64 framesRead = 0;
ma_decoder_read_pcm_frames(maDecoder_.get(), buf, frameCount, &framesRead);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets hide mini audio impl details

Comment on lines +266 to +272
for (size_t i = 0; i < frameCount; i++) {
for (int ch = 0; ch < numOutputChannels; ch++) {
int srcCh = ch < state.channels ? ch : state.channels - 1;
processingBuffer->getChannel(ch)->span()[destSampleOffset + i] =
vol * state.interleavedBuffer[i * state.channels + srcCh];
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use deinterleaveFrom AudioBuffer method

AudioFileSourceNode::AudioFileSourceNode(
const std::shared_ptr<BaseAudioContext> &context,
const AudioFileSourceOptions &options)
: AudioNode(context, options),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should inherit from AudioScheduledSourceNode

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is highly misleading that AudioNode that is not a child of ASSN is a source

ffmpegdecoder::FFmpegDecoder ffmpegDecoder_;
ffmpegdecoder::FFmpegDecoderConfig cfg;
#endif // RN_AUDIO_API_FFMPEG_DISABLED
std::atomic<bool> filePaused_{false};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it will be better to store state of node like in ASSN. why pause is not send via events queue? I do not think that atomic usage here is necessary. is there any reason behind it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Buffering Audio from files

4 participants