Skip to content

Automation: interpolate dynamics at audio engine level - #207

Open
RomanPudashkin wants to merge 2 commits into
musescore:mainfrom
RomanPudashkin:dynamics_interpolation
Open

Automation: interpolate dynamics at audio engine level#207
RomanPudashkin wants to merge 2 commits into
musescore:mainfrom
RomanPudashkin:dynamics_interpolation

Conversation

@RomanPudashkin

Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces discrete dynamic-level maps with automation curves. It adds curve evaluation, resampling, ease-based interpolation, and normalized dynamic-level conversion. Envelope evaluation and RPC serialization use the new types. Main-stream playback receives dynamic automation layers, while off-stream playback no longer receives them. FluidSynth, MuseSampler, and VST sequencers resample curves every 30 ms and emit deduplicated dynamic events.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided, so the required issue reference, motivation, checklist, and testing details are missing. Add the required description sections, including the issue reference, change motivation, completed checklist, testing evidence, and build configuration.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: audio-level interpolation of dynamics automation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/audio/engine/internal/abstracteventsequencer.h`:
- Line 174: Update the automation evaluation used by the dynamic-level path
around evaluateCurveAt so positions before the first automation point return
curve.begin()->second directly, rather than interpolating with a negative
factor. Preserve normal interpolation for positions at or after the first point
and retain the existing dynamicLevelFromNormalized conversion.

In `@framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp`:
- Around line 121-134: Update the expression-event generation in the layer
resampling loop to map each layerIdx to its target MIDI channel and set that
channel on every generated ControlChange event before inserting it into
destination. Also set channelIdx on the initial expression event in the
corresponding setup path before the note-on event, preserving the existing
note-channel mapping.

In `@framework/mpe/automationpoint.h`:
- Around line 181-187: Update the steps calculation in the resampling loop
around intervalDuration and stepInterval to use ceiling division rather than
truncating integer division, while retaining the minimum of one step. Preserve
the existing interpolation and onSample behavior so each interpolated segment
does not exceed stepInterval.
- Around line 146-157: Update the evaluation logic around findLessOrEqual so
that when it returns curve.end() for a position before the first key,
immediately return curve.begin()->second.outValue before interpolation. Preserve
interpolation for positions at or after an existing key, and add a test covering
evaluation before the first key.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 248196af-e2e7-449b-9406-42fab230e1ac

📥 Commits

Reviewing files that changed from the base of the PR and between 20f30a5 and c5b6326.

📒 Files selected for processing (14)
  • framework/audio/common/automatablevalue.h
  • framework/audio/common/rpc/rpcpacker.h
  • framework/audio/engine/internal/abstracteventsequencer.h
  • framework/audio/engine/internal/nodes/eventaudionode.cpp
  • framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp
  • framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.h
  • framework/audio/tests/rpcpacker_tests.cpp
  • framework/mpe/automationpoint.h
  • framework/mpe/events.h
  • framework/mpe/mpetypes.h
  • framework/musesampler/internal/musesamplersequencer.cpp
  • framework/musesampler/internal/musesamplersequencer.h
  • framework/vst/internal/synth/vstsequencer.cpp
  • framework/vst/internal/synth/vstsequencer.h
💤 Files with no reviewable changes (1)
  • framework/audio/engine/internal/nodes/eventaudionode.cpp

Comment thread framework/audio/engine/internal/abstracteventsequencer.h
Comment on lines +121 to +134
for (const auto& [layerIdx, curve] : layers) {
std::optional<int> lastLevel;

mpe::resampleCurve(curve, STEP_INTERVAL_US, [&](mpe::timestamp_t t, muse::real_t normalized) {
const int level = expressionLevel(mpe::dynamicLevelFromNormalized(normalized));
if (lastLevel == level) {
return;
}
lastLevel = level;

midi::Event event(Event::Opcode::ControlChange, Event::MessageType::ChannelVoice10);
event.setIndex(midi::EXPRESSION_CONTROLLER);
event.setData(level);
destination[t].emplace_back(std::move(event));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Route expression controls to the note channel.

These controller events do not set a channel. The note-on event at Line 161 sets channelIdx, so expression controls do not affect notes on other channels.

Map each layerIdx to its target MIDI channel when resampling the curve. Set that channel on each generated controller event. Set channelIdx on the initial expression event before the note-on event.

Also applies to: 153-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/audio/engine/internal/synthesizers/fluidsynth/fluidsequencer.cpp`
around lines 121 - 134, Update the expression-event generation in the layer
resampling loop to map each layerIdx to its target MIDI channel and set that
channel on every generated ControlChange event before inserting it into
destination. Also set channelIdx on the initial expression event in the
corresponding setup path before the note-on event, preserving the existing
note-channel mapping.

Comment thread framework/mpe/automationpoint.h
Comment on lines +181 to +187
const Key intervalDuration = next->first - it->first;
const size_t steps = std::max(size_t(intervalDuration / stepInterval), size_t(1));

for (size_t j = 1; j < steps; ++j) {
const Key t = it->first + intervalDuration * j / steps;
const real_t normalized = evaluateAt(next->second, it->second.outValue, real_t(j) / real_t(steps));
onSample(t, normalized);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use ceiling division for resampling steps.

Line 182 truncates the interval ratio. For a 59 ms segment with a 30 ms interval, steps is 1, so the loop emits no intermediate sample. The output gap becomes 59 ms.

Calculate steps with ceiling division. This ensures that no interpolated segment exceeds stepInterval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/mpe/automationpoint.h` around lines 181 - 187, Update the steps
calculation in the resampling loop around intervalDuration and stepInterval to
use ceiling division rather than truncating integer division, while retaining
the minimum of one step. Preserve the existing interpolation and onSample
behavior so each interpolated segment does not exceed stepInterval.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant