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
27 changes: 24 additions & 3 deletions JianpuEditor/AppBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public static ServiceProvider ConfigureServices()

private static IMidiOutput CreateMidiOutput()
{
// VST support is not currently exposed in the Audio Engine dialog (good free VST2
// instruments have become hard to find), but a plugin path left over from an older
// build's settings.json is still honored here.
var melodyVstPluginPath = AppTheme.VstMelodyPluginPath;
if (!string.IsNullOrWhiteSpace(melodyVstPluginPath))
{
Expand All @@ -78,15 +81,33 @@ private static IMidiOutput CreateMidiOutput()
}
}

var soundFontPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Soundfonts", "GeneralUser-GS.sf2");
var bundledSoundFontPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Soundfonts", "GeneralUser-GS.sf2");
var customSoundFontPath = AppTheme.CustomSoundFontPath;
var soundFontPath = !string.IsNullOrWhiteSpace(customSoundFontPath) && File.Exists(customSoundFontPath)
? customSoundFontPath
: bundledSoundFontPath;

try
{
return new BassMidiSynthesizer(soundFontPath);
}
catch (Exception ex)
{
AppLog.Exception("Failed to initialize BASSMIDI SoundFont synthesizer, falling back to system MIDI device", ex);
return new WindowsMidiSynthesizer();
AppLog.Exception("Failed to initialize BASSMIDI SoundFont synthesizer (" + soundFontPath + ")", ex);
if (soundFontPath == bundledSoundFontPath)
{
return new WindowsMidiSynthesizer();
}

try
{
return new BassMidiSynthesizer(bundledSoundFontPath);
}
catch (Exception fallbackEx)
{
AppLog.Exception("Failed to initialize bundled SoundFont synthesizer, falling back to system MIDI device", fallbackEx);
return new WindowsMidiSynthesizer();
}
}
}
}
Expand Down
19 changes: 6 additions & 13 deletions JianpuEditor/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1954,33 +1954,26 @@ private void ShowInstrumentDialog()

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

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

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

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

AppTheme.SetVstPluginPaths(selectedMelodyPath, selectedChordPath);
AppTheme.SetCustomSoundFontPath(selectedSoundFontPath);
MessageBox.Show(
"Audio engine setting saved. Restart Jianpu Editor for this to take effect.",
"Audio Engine",
Expand Down
24 changes: 24 additions & 0 deletions JianpuEditor/Rendering/AppTheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public static bool IsDarkMode
/// reuse <see cref="VstMelodyPluginPath"/> for chords too.</summary>
public static string VstChordPluginPath { get; private set; } = string.Empty;

/// <summary>Path to a custom SoundFont (.sf2) or SFZ instrument file to use instead of the
/// bundled General MIDI SoundFont, or empty to use the bundled one.</summary>
public static string CustomSoundFontPath { get; private set; } = string.Empty;

public static event Action ThemeChanged;

public static void Load()
Expand All @@ -79,6 +83,7 @@ public static void Load()
// a setting saved by an older build isn't silently dropped.
VstMelodyPluginPath = settings?.VstMelodyPluginPath ?? settings?.VstPluginPath ?? string.Empty;
VstChordPluginPath = settings?.VstChordPluginPath ?? string.Empty;
CustomSoundFontPath = settings?.CustomSoundFontPath ?? string.Empty;
UnderlinesAbove = settings?.UnderlinesAbove ?? false;
}
catch
Expand All @@ -87,6 +92,7 @@ public static void Load()
FillMeasurePlaceholdersOnAdd = true;
VstMelodyPluginPath = string.Empty;
VstChordPluginPath = string.Empty;
CustomSoundFontPath = string.Empty;
UnderlinesAbove = false;
}
}
Expand Down Expand Up @@ -155,6 +161,21 @@ public static void SetVstPluginPaths(string melodyPath, string chordPath, bool p
}
}

public static void SetCustomSoundFontPath(string path, bool persist = true)
{
path = path ?? string.Empty;
if (CustomSoundFontPath == path)
{
return;
}

CustomSoundFontPath = path;
if (persist)
{
Save();
}
}

public static Color FormBackground
{
get { return Current.Paper; }
Expand Down Expand Up @@ -262,6 +283,7 @@ private static void Save()
FillMeasurePlaceholdersOnAdd = FillMeasurePlaceholdersOnAdd,
VstMelodyPluginPath = VstMelodyPluginPath,
VstChordPluginPath = VstChordPluginPath,
CustomSoundFontPath = CustomSoundFontPath,
UnderlinesAbove = UnderlinesAbove
},
Formatting.Indented);
Expand All @@ -287,6 +309,8 @@ private sealed class ThemeSettings

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

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

public bool UnderlinesAbove { get; set; }
}
}
Expand Down
137 changes: 40 additions & 97 deletions JianpuEditor/Views/AudioEngineDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ namespace JianpuEditor.Views
{
public sealed class AudioEngineDialog : Form
{
private readonly RadioButton _soundFontOption;
private readonly RadioButton _vstOption;
private readonly TextBox _melodyVstPathBox;
private readonly Button _melodyBrowseButton;
private readonly TextBox _chordVstPathBox;
private readonly Button _chordBrowseButton;
private readonly TextBox _soundFontPathBox;
private readonly Button _browseButton;

public AudioEngineDialog(string currentMelodyVstPluginPath, string currentChordVstPluginPath, string activeEngineName)
public AudioEngineDialog(string currentCustomSoundFontPath, string activeEngineName)
{
Text = "Audio Engine";
FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent;
MinimizeBox = false;
MaximizeBox = false;
ShowInTaskbar = false;
ClientSize = new Size(420, 268);

var hasVstPath = !string.IsNullOrWhiteSpace(currentMelodyVstPluginPath);
ClientSize = new Size(420, 190);

var activeLabel = new Label
{
Expand All @@ -32,87 +26,58 @@ public AudioEngineDialog(string currentMelodyVstPluginPath, string currentChordV
Font = new Font(Font, FontStyle.Bold)
};

_soundFontOption = new RadioButton
var pathLabel = new Label
{
Text = "Bundled SoundFont (default)",
Location = new Point(16, 40),
AutoSize = true,
Checked = !hasVstPath
Text = "Instrument file (SoundFont .sf2 or SFZ .sfz):",
Location = new Point(16, 44),
AutoSize = true
};

_vstOption = new RadioButton
_soundFontPathBox = new TextBox
{
Text = "VST2 instrument plugin(s):",
Location = new Point(16, 68),
AutoSize = true,
Checked = hasVstPath
Location = new Point(16, 64),
Width = 324,
Text = currentCustomSoundFontPath ?? string.Empty
};

var melodyLabel = new Label { Text = "Melody:", Location = new Point(36, 98), AutoSize = true };
_melodyVstPathBox = new TextBox
{
Location = new Point(100, 94),
Width = 236,
Text = currentMelodyVstPluginPath ?? string.Empty,
Enabled = hasVstPath
};
_melodyBrowseButton = new Button
_browseButton = new Button
{
Text = "Browse...",
Location = new Point(340, 92),
Width = 64,
Enabled = hasVstPath
Location = new Point(340, 62),
Width = 64
};
_melodyBrowseButton.Click += (s, e) => BrowseInto(_melodyVstPathBox);

var chordLabel = new Label { Text = "Chords:", Location = new Point(36, 126), AutoSize = true };
_chordVstPathBox = new TextBox
{
Location = new Point(100, 122),
Width = 236,
Text = currentChordVstPluginPath ?? string.Empty,
Enabled = hasVstPath
};
_chordBrowseButton = new Button
{
Text = "Browse...",
Location = new Point(340, 120),
Width = 64,
Enabled = hasVstPath
};
_chordBrowseButton.Click += (s, e) => BrowseInto(_chordVstPathBox);

_vstOption.CheckedChanged += (s, e) =>
_browseButton.Click += (s, e) =>
{
_melodyVstPathBox.Enabled = _vstOption.Checked;
_melodyBrowseButton.Enabled = _vstOption.Checked;
_chordVstPathBox.Enabled = _vstOption.Checked;
_chordBrowseButton.Enabled = _vstOption.Checked;
using (var browseDialog = new OpenFileDialog
{
Filter = "SoundFont/SFZ Instrument (*.sf2;*.sfz)|*.sf2;*.sfz|All Files (*.*)|*.*",
Title = "Select Instrument File"
})
{
if (browseDialog.ShowDialog(this) == DialogResult.OK)
{
_soundFontPathBox.Text = browseDialog.FileName;
}
}
};

var hintLabel = new Label
{
Text = "Hosts VST2 instrument DLLs (not VST3) via BASSVST -- one for melody, one for\n" +
"chords. Leave Chords blank to use the melody plugin for both. Takes effect\n" +
"after restarting Jianpu Editor. If a configured plugin fails to load, playback\n" +
"silently falls back to the bundled SoundFont.",
Location = new Point(16, 152),
Size = new Size(388, 66),
Text = "Leave blank to use the bundled General MIDI SoundFont. Point to a full-quality\n" +
"SF2 or SFZ instrument library instead for better piano/guitar/cello sound.\n" +
"Takes effect after restarting Jianpu Editor. If the file fails to load,\n" +
"playback silently falls back to the bundled SoundFont.",
Location = new Point(16, 94),
Size = new Size(388, 60),
ForeColor = Color.DimGray
};

var okButton = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(236, 230), Width = 76 };
var cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(320, 230), Width = 76 };
var okButton = new Button { Text = "OK", DialogResult = DialogResult.OK, Location = new Point(236, 152), Width = 76 };
var cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, Location = new Point(320, 152), Width = 76 };

Controls.Add(activeLabel);
Controls.Add(_soundFontOption);
Controls.Add(_vstOption);
Controls.Add(melodyLabel);
Controls.Add(_melodyVstPathBox);
Controls.Add(_melodyBrowseButton);
Controls.Add(chordLabel);
Controls.Add(_chordVstPathBox);
Controls.Add(_chordBrowseButton);
Controls.Add(pathLabel);
Controls.Add(_soundFontPathBox);
Controls.Add(_browseButton);
Controls.Add(hintLabel);
Controls.Add(okButton);
Controls.Add(cancelButton);
Expand All @@ -121,31 +86,9 @@ public AudioEngineDialog(string currentMelodyVstPluginPath, string currentChordV
}

/// <summary>Empty string means "use the bundled SoundFont".</summary>
public string SelectedMelodyVstPluginPath
public string SelectedSoundFontPath
{
get { return _vstOption.Checked ? (_melodyVstPathBox.Text ?? string.Empty).Trim() : string.Empty; }
}

/// <summary>Empty string means "reuse the melody plugin for chords too" (or "use the
/// bundled SoundFont", if <see cref="SelectedMelodyVstPluginPath"/> is also empty).</summary>
public string SelectedChordVstPluginPath
{
get { return _vstOption.Checked ? (_chordVstPathBox.Text ?? string.Empty).Trim() : string.Empty; }
}

private void BrowseInto(TextBox targetBox)
{
using (var dialog = new OpenFileDialog
{
Filter = "VST2 Plugin (*.dll)|*.dll|All Files (*.*)|*.*",
Title = "Select VST2 Instrument Plugin"
})
{
if (dialog.ShowDialog(this) == DialogResult.OK)
{
targetBox.Text = dialog.FileName;
}
}
get { return (_soundFontPathBox.Text ?? string.Empty).Trim(); }
}
}
}
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ A Jianpu (numbered musical notation) editing tool built on C# WinForms, supporti
- **Instrument selection**
- `Edit → Instruments...` (or the toolbar "Instruments..." button) picks a General MIDI instrument for the melody and for chords independently
- Applies to both live playback and MIDI export, so the exported file sounds the same as in-app playback; saved with the score (`MelodyInstrument`/`ChordInstrument`, default Acoustic Grand Piano)
- **VST instrument plugin (optional)**
- `Edit → Audio Engine...` can point playback at VST2 instrument plugin DLLs instead of the bundled SoundFont, hosted via BASSVST, with independent melody and chord plugins (leave Chords blank to reuse the melody plugin for both) -- most VST2 instruments aren't multi-timbral across MIDI channels the way the bundled SoundFont is, so a single shared plugin can't otherwise give melody and chords distinct sounds
- This is a machine-local app preference (not saved in the score file, since a plugin path isn't portable between machines) and takes effect after restarting the app
- VST2 only, not VST3; MIDI export is unaffected (always raw MIDI regardless of playback engine)
- The toolbar shows an **"Engine: ..."** indicator (next to "Instruments...") naming whichever engine is actually active — click it to open `Edit → Audio Engine...`, which also lists the active engine at the top. If a configured VST plugin fails to load, playback silently falls back to the bundled SoundFont; the indicator and dialog reflect that fallback rather than the (non-functional) configured path
- **Custom instrument file (optional)**
- `Edit → Audio Engine...` can point playback at a custom SoundFont (`.sf2`) or SFZ (`.sfz`) instrument file instead of the bundled SoundFont, for higher-quality piano/guitar/cello sounds than the default General MIDI set — both formats load through the same BASSMIDI engine, so instrument selection, multi-timbral melody/chord playback, and MIDI export all keep working unchanged
- This is a machine-local app preference (not saved in the score file, since a file path isn't portable between machines) and takes effect after restarting the app
- The toolbar shows an **"Engine: ..."** indicator (next to "Instruments...") naming whichever engine is actually active — click it to open `Edit → Audio Engine...`, which also lists the active engine at the top. If a configured instrument file fails to load, playback silently falls back to the bundled SoundFont; the indicator and dialog reflect that fallback rather than the (non-functional) configured path

## Requirements

Expand Down
Loading