Skip to content

Add audio data exchange and waveform visualization (AudioDataExchange + JS API + VST3 DataExchange) - #8

Merged
erikgrahn13 merged 4 commits into
mainfrom
codex/implement-unified-idataexchangehandler-for-vst3
Jul 13, 2026
Merged

Add audio data exchange and waveform visualization (AudioDataExchange + JS API + VST3 DataExchange)#8
erikgrahn13 merged 4 commits into
mainfrom
codex/implement-unified-idataexchangehandler-for-vst3

Conversation

@erikgrahn13

Copy link
Copy Markdown
Owner

Motivation

  • Provide a lightweight, RT-safe path to export interleaved float audio from the audio thread to the UI/JS layer for real-time visualization.
  • Expose audio samples to the embedded QuickJS runtime so widgets can render waveforms without blocking audio.
  • Integrate the data path across standalone audio backends and VST3 host DataExchange so the feature works in both standalone and plugin builds.

Description

  • Introduce AudioDataExchange.h implementing AudioDataBlock, IDataSink, AudioDataQueue, appendInterleavedFloatData, ScopedSendContext and sendAudioDataToUI helpers for producing and queuing interleaved float audio.
  • Thread the AudioDataQueue through the stack by adding queue accessors and plumbing: ISingularityAudio, standalone backends (PipeWire, ASIO), SingularityController, IJSEngine/QuickJSEngine, and VST3 VST3Processor/VST3Controller with DataExchangeHandler integration and IDataSink implementations to receive blocks.
  • Add QuickJS binding getAudioData in QuickJSEngine to return the latest AudioDataBlock to JS, and include the Waveform widget (widgets/waveform.js) plus example usage in examples/ExampleEffect/App.js and plugin example ExampleEffect.h calling sendAudioDataToUI.
  • Update APIs and constructors to accept an optional AudioDataQueue* (e.g. IJSEngine::createJSEngine, SingularityController) and wire VST3 queue configuration and lifecycle (connect, disconnect, queueOpened, onDataExchangeBlocksReceived).

Testing

  • Built the project for standalone and plugin targets to verify compilation after API and header additions; compilation succeeded.
  • Performed a smoke test of the standalone ExampleEffect demonstrating the Waveform widget and getAudioData JS call; the widget rendered without blocking the audio thread.
  • Built the VST3 plugin including DataExchange integration and validated that the host-target build completes and the exchange handler compiles; build checks succeeded.

Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c838c6fbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vst3/vst3processor.h
{
auto configCallback = [] (Vst::DataExchangeHandler::Config& config, const Vst::ProcessSetup&) {
config.blockSize = sizeof(Singularity::AudioDataExchange::AudioDataBlock);
config.numBlocks = 8;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid dropping VST waveform slices after 128 frames

In the VST path the plugin is invoked by ProcessDataSlicer slicer(16), and the new example calls sendAudioDataToUI once per plugin process call. With this exchange queue limited to 8 blocks, any host buffer larger than 128 frames (for example 256 or 512 samples) fills the DataExchange queue during a single audio callback; subsequent getCurrentOrNewBlock() calls return invalid and the later slices are never delivered, so the UI waveform only reflects the beginning of each block. Accumulate into one block per host buffer or size/throttle the exchange around the slicer.

Useful? React with 👍 / 👎.

@erikgrahn13
erikgrahn13 force-pushed the codex/implement-unified-idataexchangehandler-for-vst3 branch from 7dfd73b to f4a10d4 Compare July 13, 2026 16:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4a10d4fc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

break;
}

template<typename SampleT>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Close resetOutputParameters before declaring helpers

Standalone APP targets that include this header will fail to compile because the new member template declaration is still inside resetOutputParameters(); the function's closing brace was not restored after the loop, so C++ sees a template declaration at block scope. Add the missing } before these helpers so the new audio wrappers are class members.

Useful? React with 👍 / 👎.

Comment thread QuickJSEngine.cpp
Comment on lines +872 to +874
if (audioDataQueue_)
while (audioDataQueue_->popAudioDataBlock(latestAudioData_))
receivedNewBlock = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return every drained audio block to JavaScript

When the audio thread produces more than one block between UI draws, this loop pops all queued blocks into latestAudioData_ but the API returns only the last one and increments the revision once. The new Waveform calls getAudioData() once per draw, so at normal audio/UI rates the intermediate blocks are discarded and the waveform history becomes gapped/too slow; either return all drained blocks or pop only one block per call.

Useful? React with 👍 / 👎.

@erikgrahn13
erikgrahn13 merged commit 51dcff4 into main Jul 13, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e69408593

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread widgets/waveform.js
if (Math.abs(candidate) > Math.abs(sample))
sample = candidate;
}
history.push(Math.max(-1, Math.min(1, sample)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Append only new audio frames

When getAudioData() advances its revision, it returns QuickJSEngine::latestAudioData_ as a rolling snapshot, not just the newly drained block. This history.push(...) therefore re-appends the previous snapshot on every revision; with consecutive 128-frame updates the widget records 128 frames, then appends all 256 frames, then all 384, and once the native buffer is full each UI tick injects about 2048 old samples again. That makes the waveform repeat old audio and scroll at the wrong time scale during normal continuous playback, so the widget needs to track which frames were already consumed or the native API should return deltas.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant