Support independent VST instruments for melody and chords - #27
Merged
Merged
Conversation
The VST2 playback engine sent both melody and chord notes to one loaded plugin instance, unlike the bundled SoundFont path which already lets melody and chords use independent General MIDI instruments (they're multi-timbral across MIDI channels; most VST2 instrument plugins are not, so a single shared plugin couldn't actually give melody and chords distinct sounds). - AudioEngineDialog now has separate Melody/Chords VST plugin path fields; leaving Chords blank reuses the melody plugin for both parts (preserves today's single-plugin behavior as the default). - BassVstSynthesizer optionally loads a second BASSVST channel for the chord plugin, routing NoteOn/NoteOff/ProgramChange by ScoreMidiSchedule.MelodyChannel/ChordChannel; falls back to one shared handle when the chord path is empty or the same file, so the common case doesn't pay for a second plugin instance. - AppTheme persists VstMelodyPluginPath/VstChordPluginPath instead of one VstPluginPath, migrating an existing settings.json's old field to VstMelodyPluginPath on load so a previously configured plugin isn't silently dropped. Verified by compiling the real, unmodified source via `dotnet build` against the true NuGet/WinForms graph, then executing that same built assembly's IL under Mono/Xvfb for the full existing multi-tab/playback test suite -- no regressions. The BASSVST native audio path itself isn't exercised (no VST plugin files or audio hardware in this sandbox); please confirm on a real Windows machine with an actual VST2 instrument plugin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The bundled SoundFont engine (
BassMidiSynthesizer) is multi-timbral: it already plays melodyand chords with independently selectable GM instruments, since one BASSMIDI stream can host all
16 MIDI channels with different programs at once. The VST engine (
BassVstSynthesizer) never hadthis — it hosted a single BASSVST plugin instance shared by both the melody and chord MIDI
channels, so a VST instrument choice applied to the whole score instead of per-part, unlike the
InstrumentDialog(SoundFont) experience. This PR brings the VST path to parity.What changed
AudioEngineDialog: now exposes independent Melody/Chords VST plugin path fields (mirroringInstrumentDialog's existing two-instrument pattern), instead of one shared path. LeavingChords blank reuses the melody plugin for both parts.
BassVstSynthesizer: takes(melodyPluginPath, chordPluginPath). Loads two independentBASSVST plugin handles and routes
NoteOn/NoteOff/ProgramChangeby MIDI channel(
ScoreMidiSchedule.ChordChannel→ chord handle, everything else → melody handle — confirmed viagrep that these are the only two channels used anywhere in the codebase). When the chord path is
blank or identical to the melody path, it falls back to sharing a single handle (matching the
previous single-plugin behavior exactly, and avoiding a redundant load / double-free on
dispose).
AppTheme:VstPluginPathsetting replaced withVstMelodyPluginPath+VstChordPluginPath. Existing settings migrate automatically on load (oldVstPluginPathvaluebecomes the new melody path if the new field isn't present yet); nothing is silently lost on
upgrade.
AppBootstrapper/MainForm: updated to construct/persist both paths through thedialog and into
BassVstSynthesizer's new constructor.README.md: updated the VST feature bullet to describe independent melody/chord plugins.Test plan
This sandbox has no .NET SDK WindowsDesktop workload, real Windows, or audio hardware, so BASSVST
itself can't be exercised directly here. Verified via the same pipeline used for every change this
session:
dotnet buildagainst the real project/package graph succeeds.dotnet format --verify-no-changespasses.Harness.csandMainFormHarness.cs, coveringMainForm, DI wiring, and non-UI services) under Mono + Xvfbagainst the built assembly — all pass unmodified, confirming no regressions to tab/session/
playback-coordinator behavior from the earlier phases.
double-free when melody and chord paths are the same (the common case, and the exact behavior
every existing user's settings will have after migration).
Not verified in this sandbox (same caveat as all prior BASS-touching work this session): actually
loading two distinct real VST2 plugin DLLs and hearing independent melody/chord instrument audio
requires a real Windows machine.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Pguj4XSScE141p1ScWoqEr
Generated by Claude Code