Skip to content
Merged
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
8 changes: 4 additions & 4 deletions JianpuEditor/AppBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,16 @@ public static ServiceProvider ConfigureServices()

private static IMidiOutput CreateMidiOutput()
{
var vstPluginPath = AppTheme.VstPluginPath;
if (!string.IsNullOrWhiteSpace(vstPluginPath))
var melodyVstPluginPath = AppTheme.VstMelodyPluginPath;
if (!string.IsNullOrWhiteSpace(melodyVstPluginPath))
{
try
{
return new BassVstSynthesizer(vstPluginPath);
return new BassVstSynthesizer(melodyVstPluginPath, AppTheme.VstChordPluginPath);
}
catch (Exception ex)
{
AppLog.Exception("Failed to initialize BASSVST plugin '" + vstPluginPath + "', falling back to bundled SoundFont", ex);
AppLog.Exception("Failed to initialize BASSVST plugin(s) (melody='" + melodyVstPluginPath + "', chords='" + AppTheme.VstChordPluginPath + "'), falling back to bundled SoundFont", ex);
}
}

Expand Down
19 changes: 13 additions & 6 deletions JianpuEditor/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1954,26 +1954,33 @@ private void ShowInstrumentDialog()

private void ShowAudioEngineDialog()
{
using (var dialog = new AudioEngineDialog(AppTheme.VstPluginPath, _midiOutput.EngineName))
using (var dialog = new AudioEngineDialog(AppTheme.VstMelodyPluginPath, AppTheme.VstChordPluginPath, _midiOutput.EngineName))
{
if (dialog.ShowDialog(this) != DialogResult.OK)
{
return;
}

var selectedPath = dialog.SelectedVstPluginPath;
if (!string.IsNullOrWhiteSpace(selectedPath) && !File.Exists(selectedPath))
var selectedMelodyPath = dialog.SelectedMelodyVstPluginPath;
var selectedChordPath = dialog.SelectedChordVstPluginPath;
if (!string.IsNullOrWhiteSpace(selectedMelodyPath) && !File.Exists(selectedMelodyPath))
{
MessageBox.Show("VST plugin file not found: " + selectedPath, "Audio Engine", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("VST plugin file not found: " + selectedMelodyPath, "Audio Engine", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}

if (AppTheme.VstPluginPath == selectedPath)
if (!string.IsNullOrWhiteSpace(selectedChordPath) && !File.Exists(selectedChordPath))
{
MessageBox.Show("VST plugin file not found: " + selectedChordPath, "Audio Engine", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}

AppTheme.SetVstPluginPath(selectedPath);
if (AppTheme.VstMelodyPluginPath == selectedMelodyPath && AppTheme.VstChordPluginPath == selectedChordPath)
{
return;
}

AppTheme.SetVstPluginPaths(selectedMelodyPath, selectedChordPath);
MessageBox.Show(
"Audio engine setting saved. Restart Jianpu Editor for this to take effect.",
"Audio Engine",
Expand Down
37 changes: 28 additions & 9 deletions JianpuEditor/Rendering/AppTheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ public static bool IsDarkMode
/// to the Chinese/Western convention (lines below) this renderer defaults to.</summary>
public static bool UnderlinesAbove { get; private set; }

/// <summary>Path to a VST2 instrument plugin DLL to use for playback instead of the bundled SoundFont, or empty to use the SoundFont.</summary>
public static string VstPluginPath { get; private set; } = string.Empty;
/// <summary>Path to a VST2 instrument plugin DLL to use for the melody part instead of the
/// bundled SoundFont, or empty to use the SoundFont. Required for VST playback; the chord
/// part falls back to this same plugin when <see cref="VstChordPluginPath"/> is empty.</summary>
public static string VstMelodyPluginPath { get; private set; } = string.Empty;

/// <summary>Path to a separate VST2 instrument plugin DLL for the chord part, or empty to
/// reuse <see cref="VstMelodyPluginPath"/> for chords too.</summary>
public static string VstChordPluginPath { get; private set; } = string.Empty;

public static event Action ThemeChanged;

Expand All @@ -69,14 +75,18 @@ public static void Load()
var settings = JsonConvert.DeserializeObject<ThemeSettings>(json);
Current = (settings?.DarkMode ?? false) ? Theme.ManuscriptDark : Theme.ManuscriptLight;
FillMeasurePlaceholdersOnAdd = settings?.FillMeasurePlaceholdersOnAdd ?? true;
VstPluginPath = settings?.VstPluginPath ?? string.Empty;
// VstMelodyPluginPath falls back to the pre-dual-instrument VstPluginPath field so
// a setting saved by an older build isn't silently dropped.
VstMelodyPluginPath = settings?.VstMelodyPluginPath ?? settings?.VstPluginPath ?? string.Empty;
VstChordPluginPath = settings?.VstChordPluginPath ?? string.Empty;
UnderlinesAbove = settings?.UnderlinesAbove ?? false;
}
catch
{
Current = Theme.ManuscriptLight;
FillMeasurePlaceholdersOnAdd = true;
VstPluginPath = string.Empty;
VstMelodyPluginPath = string.Empty;
VstChordPluginPath = string.Empty;
UnderlinesAbove = false;
}
}
Expand Down Expand Up @@ -128,15 +138,17 @@ public static void SetUnderlinesAbove(bool enabled, bool persist = true)
ThemeChanged?.Invoke();
}

public static void SetVstPluginPath(string path, bool persist = true)
public static void SetVstPluginPaths(string melodyPath, string chordPath, bool persist = true)
{
path = path ?? string.Empty;
if (VstPluginPath == path)
melodyPath = melodyPath ?? string.Empty;
chordPath = chordPath ?? string.Empty;
if (VstMelodyPluginPath == melodyPath && VstChordPluginPath == chordPath)
{
return;
}

VstPluginPath = path;
VstMelodyPluginPath = melodyPath;
VstChordPluginPath = chordPath;
if (persist)
{
Save();
Expand Down Expand Up @@ -248,7 +260,8 @@ private static void Save()
{
DarkMode = Current.IsDark,
FillMeasurePlaceholdersOnAdd = FillMeasurePlaceholdersOnAdd,
VstPluginPath = VstPluginPath,
VstMelodyPluginPath = VstMelodyPluginPath,
VstChordPluginPath = VstChordPluginPath,
UnderlinesAbove = UnderlinesAbove
},
Formatting.Indented);
Expand All @@ -266,8 +279,14 @@ private sealed class ThemeSettings

public bool FillMeasurePlaceholdersOnAdd { get; set; } = true;

/// <summary>Legacy field from before melody/chord VST plugins were separate; read as a
/// fallback for VstMelodyPluginPath, never written.</summary>
public string VstPluginPath { get; set; } = string.Empty;

public string VstMelodyPluginPath { get; set; } = string.Empty;

public string VstChordPluginPath { get; set; } = string.Empty;

public bool UnderlinesAbove { get; set; }
}
}
Expand Down
85 changes: 65 additions & 20 deletions JianpuEditor/Services/BassVstSynthesizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,69 +8,109 @@
namespace JianpuEditor.Services
{
/// <summary>
/// Hosts a VST2 instrument plugin (via BASSVST) as the playback engine, as an alternative to
/// the bundled SoundFont. Both the melody and chord parts are sent to this one plugin instance
/// on separate MIDI channels (0 and 1), the same way BassMidiSynthesizer uses one SoundFont for both.
/// Hosts one or two VST2 instrument plugins (via BASSVST) as the playback engine, as an
/// alternative to the bundled SoundFont. Most VST2 instruments present a single sound
/// regardless of MIDI channel (unlike a GM SoundFont, which is multi-timbral across channels),
/// so genuinely independent melody/chord instruments need two separately loaded plugin
/// instances -- one per part, routed by <see cref="ScoreMidiSchedule.MelodyChannel"/> /
/// <see cref="ScoreMidiSchedule.ChordChannel"/>. When only a melody plugin is configured (or
/// the chord plugin path is the same file), both parts share that one loaded instance instead,
/// matching the single-plugin behavior this class originally had.
/// </summary>
internal sealed class BassVstSynthesizer : IMidiOutput
{
private const int OutputChannels = 2;
private const int SampleRate = 44100;
private const int MidiChannelCount = 16;

private readonly int _vstHandle;
private readonly int _melodyHandle;
private readonly int _chordHandle;
private readonly bool _sharedHandle;
private bool _disposed;

public string EngineName { get; }

public BassVstSynthesizer(string vstPluginPath)
public BassVstSynthesizer(string melodyPluginPath, string chordPluginPath)
{
if (string.IsNullOrWhiteSpace(vstPluginPath) || !File.Exists(vstPluginPath))
if (string.IsNullOrWhiteSpace(melodyPluginPath) || !File.Exists(melodyPluginPath))
{
throw new FileNotFoundException("VST plugin file not found.", vstPluginPath);
throw new FileNotFoundException("VST plugin file not found.", melodyPluginPath);
}

_sharedHandle = string.IsNullOrWhiteSpace(chordPluginPath)
|| string.Equals(Path.GetFullPath(chordPluginPath), Path.GetFullPath(melodyPluginPath), StringComparison.OrdinalIgnoreCase);

if (!_sharedHandle && !File.Exists(chordPluginPath))
{
throw new FileNotFoundException("VST plugin file not found.", chordPluginPath);
}

if (!Bass.Init())
{
throw new InvalidOperationException("Failed to initialize BASS audio output. Error: " + Bass.LastError);
}

_vstHandle = BassVst.ChannelCreate(SampleRate, OutputChannels, vstPluginPath, BassFlags.Default);
if (_vstHandle == 0)
_melodyHandle = LoadPlugin(melodyPluginPath);
_chordHandle = _sharedHandle ? _melodyHandle : LoadPlugin(chordPluginPath);

EngineName = _sharedHandle
? "VST2: " + Path.GetFileName(melodyPluginPath)
: "VST2: " + Path.GetFileName(melodyPluginPath) + " / " + Path.GetFileName(chordPluginPath);
AppLog.Info("BassVstSynthesizer initialized: melody=" + melodyPluginPath + ", chords=" + (_sharedHandle ? "(same)" : chordPluginPath));
}

private static int LoadPlugin(string pluginPath)
{
var handle = BassVst.ChannelCreate(SampleRate, OutputChannels, pluginPath, BassFlags.Default);
if (handle == 0)
{
var error = Bass.LastError;
Bass.Free();
throw new InvalidOperationException("Failed to load VST plugin: " + vstPluginPath + ". Error: " + Bass.LastError);
throw new InvalidOperationException("Failed to load VST plugin: " + pluginPath + ". Error: " + error);
}

if (!Bass.ChannelPlay(_vstHandle))
if (!Bass.ChannelPlay(handle))
{
AppLog.Error("Failed to start BASSVST channel playback. Error: " + Bass.LastError);
AppLog.Error("Failed to start BASSVST channel playback for '" + pluginPath + "'. Error: " + Bass.LastError);
}

EngineName = "VST2: " + Path.GetFileName(vstPluginPath);
AppLog.Info("BassVstSynthesizer initialized: " + vstPluginPath);
return handle;
}

private int HandleFor(int channel)
{
return channel == ScoreMidiSchedule.ChordChannel ? _chordHandle : _melodyHandle;
}

public void NoteOn(int channel, int note, int velocity)
{
BassVst.ProcessEvent(_vstHandle, channel, (int)MidiEventType.Note, note | (velocity << 8));
BassVst.ProcessEvent(HandleFor(channel), channel, (int)MidiEventType.Note, note | (velocity << 8));
}

public void NoteOff(int channel, int note)
{
BassVst.ProcessEvent(_vstHandle, channel, (int)MidiEventType.Note, note);
BassVst.ProcessEvent(HandleFor(channel), channel, (int)MidiEventType.Note, note);
}

public void ProgramChange(int channel, int program)
{
BassVst.ProcessEvent(_vstHandle, channel, (int)MidiEventType.Program, program);
BassVst.ProcessEvent(HandleFor(channel), channel, (int)MidiEventType.Program, program);
}

public void AllNotesOff()
{
AllNotesOff(_melodyHandle);
if (!_sharedHandle)
{
AllNotesOff(_chordHandle);
}
}

private static void AllNotesOff(int handle)
{
for (var channel = 0; channel < MidiChannelCount; channel++)
{
BassVst.ProcessEvent(_vstHandle, channel, (int)MidiEventType.NotesOff, 0);
BassVst.ProcessEvent(handle, channel, (int)MidiEventType.NotesOff, 0);
}
}

Expand All @@ -91,9 +131,14 @@ public void Dispose()
AppLog.Exception("Failed to send AllNotesOff before closing BASSVST", ex);
}

if (_vstHandle != 0)
if (_melodyHandle != 0)
{
BassVst.ChannelFree(_melodyHandle);
}

if (!_sharedHandle && _chordHandle != 0)
{
BassVst.ChannelFree(_vstHandle);
BassVst.ChannelFree(_chordHandle);
}

Bass.Free();
Expand Down
Loading
Loading