2026-05-15 12:40:01 +01:00
|
|
|
using RemSound.Core;
|
|
|
|
|
|
|
|
|
|
namespace RemSound.App;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-15 13:40:42 +01:00
|
|
|
/// Recording settings dialog. Up to five listboxes laid out left-to-right:
|
|
|
|
|
/// * Recording &source (Alt+S) — what audio gets captured
|
|
|
|
|
/// * File &format (Alt+F) — WAV / MP3 / OGG-Opus / FLAC
|
|
|
|
|
/// * Audio format &attributes (Alt+A) — bit depth (WAV/FLAC) or bitrate (MP3/Ogg)
|
|
|
|
|
/// * FLAC compression &level (Alt+L) — 0..8; visible only when FLAC is selected
|
|
|
|
|
/// * &Channels (Alt+C) — Stereo / Mono
|
2026-05-15 12:40:01 +01:00
|
|
|
///
|
2026-05-15 13:40:42 +01:00
|
|
|
/// Channel mode was originally folded into every attribute row (16-bit stereo / 16-bit
|
|
|
|
|
/// mono / 24-bit stereo / 24-bit mono / …) which doubled the attribute count for no real
|
|
|
|
|
/// benefit. Pulling it out into its own listbox keeps each list focused on one decision.
|
|
|
|
|
///
|
|
|
|
|
/// FLAC has TWO independent quality knobs (bit depth + compression level); the bit depth
|
|
|
|
|
/// stays in the attributes column, the compression level gets its own conditional column
|
|
|
|
|
/// that only appears when FLAC is selected. Compression level 0 = fastest encode and
|
|
|
|
|
/// biggest file; 8 = slowest and smallest. The libFLAC reference default is 5, which is
|
|
|
|
|
/// where the slider defaults if nothing's been set. All levels produce bit-identical
|
|
|
|
|
/// audio — it's a pure encode-time-vs-file-size tradeoff.
|
|
|
|
|
///
|
|
|
|
|
/// The attribute and compression lists repopulate whenever the file-format selection
|
|
|
|
|
/// changes, so the user always sees only the choices that make sense for the format.
|
2026-05-15 12:40:01 +01:00
|
|
|
///
|
|
|
|
|
/// Settings are written back to the profile only when the user presses OK. Cancel / Esc
|
|
|
|
|
/// discards. The dialog also exposes <see cref="ChangedAnything"/> so the caller can
|
|
|
|
|
/// MarkProfileDirty after a successful OK.
|
|
|
|
|
///
|
|
|
|
|
/// Reachable from the Record menu → "Recording settings...".
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal sealed class RecordingSettingsDialog : Form
|
|
|
|
|
{
|
|
|
|
|
private readonly RecordingSettings working; // mutated as the user interacts
|
|
|
|
|
|
|
|
|
|
private readonly Label sourceLabel = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "Recording &source (Alt+S):",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 0, 4),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly ListBox sourceList = new()
|
|
|
|
|
{
|
|
|
|
|
AccessibleName = "Recording source",
|
|
|
|
|
SelectionMode = SelectionMode.One,
|
|
|
|
|
IntegralHeight = false,
|
|
|
|
|
Height = 120,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly Label formatLabel = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "File &format (Alt+F):",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 0, 4),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly ListBox formatList = new()
|
|
|
|
|
{
|
|
|
|
|
AccessibleName = "File format",
|
|
|
|
|
SelectionMode = SelectionMode.One,
|
|
|
|
|
IntegralHeight = false,
|
|
|
|
|
Height = 120,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly Label attributesLabel = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "Audio format &attributes (Alt+A):",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 0, 4),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly ListBox attributesList = new()
|
|
|
|
|
{
|
|
|
|
|
AccessibleName = "Audio format attributes",
|
|
|
|
|
SelectionMode = SelectionMode.One,
|
|
|
|
|
IntegralHeight = false,
|
|
|
|
|
Height = 200,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
// FLAC compression level. Wrapped in its own column that toggles visibility on the
|
|
|
|
|
// current file-format selection — visible for FLAC, hidden otherwise. Held as fields
|
|
|
|
|
// so the column-visibility refresh in <see cref="UpdateFlacCompressionVisibility"/>
|
|
|
|
|
// can flip them together.
|
|
|
|
|
private readonly Label flacCompressionLabel = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "FLAC compression &level (Alt+L):",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 0, 4),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly ListBox flacCompressionList = new()
|
|
|
|
|
{
|
|
|
|
|
AccessibleName = "FLAC compression level",
|
|
|
|
|
SelectionMode = SelectionMode.One,
|
|
|
|
|
IntegralHeight = false,
|
|
|
|
|
Height = 200,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Channel mode (Stereo / Mono) extracted out of the per-format attributes so the
|
|
|
|
|
// attributes list only carries the bit-depth-or-bitrate decision. Always visible —
|
|
|
|
|
// every format has a channel-count choice.
|
|
|
|
|
private readonly Label channelLabel = new()
|
|
|
|
|
{
|
|
|
|
|
// Alt+C — the natural letter for Channels. The Cancel button gives this letter
|
|
|
|
|
// up (it's on Alt+N) so the mnemonic is unambiguous.
|
|
|
|
|
Text = "&Channels (Alt+C):",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 0, 4),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly ListBox channelList = new()
|
|
|
|
|
{
|
|
|
|
|
AccessibleName = "Channels",
|
|
|
|
|
SelectionMode = SelectionMode.One,
|
|
|
|
|
IntegralHeight = false,
|
|
|
|
|
Height = 80,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The compression column is wrapped in a Panel held here so its Visible can be
|
|
|
|
|
// toggled atomically with the column's ColumnStyle width — visible+20% width when
|
|
|
|
|
// FLAC, hidden+0% width otherwise. Kept as a field so UpdateFlacCompressionVisibility
|
|
|
|
|
// and the layout-building code share the same instance.
|
|
|
|
|
private Control? compressionColumn;
|
|
|
|
|
private TableLayoutPanel? grid;
|
|
|
|
|
|
2026-07-06 10:05:00 +01:00
|
|
|
private readonly AccessibleCheckBox splitTracksBox = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "Split recording into separate &tracks — one file per peer, plus your own (Alt+T)",
|
|
|
|
|
AccessibleName = "Split recording into separate tracks, one file per peer plus your own send",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
};
|
|
|
|
|
private readonly AccessibleCheckBox bypassShapingBox = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "Bypass pan and EQ when recording — record the &raw audio (Alt+R)",
|
|
|
|
|
AccessibleName = "Bypass pan and EQ when recording, record the raw audio",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 12:40:01 +01:00
|
|
|
private readonly Button okButton = new()
|
|
|
|
|
{
|
|
|
|
|
Text = "&OK",
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
DialogResult = DialogResult.OK,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private readonly Button cancelButton = new()
|
|
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
// Alt+N rather than the conventional Alt+C — Channels takes Alt+C as its natural
|
|
|
|
|
// mnemonic. Esc still dismisses the dialog (CancelButton wiring below), so the
|
|
|
|
|
// less-conventional letter has no real cost.
|
|
|
|
|
Text = "Ca&ncel",
|
2026-05-15 12:40:01 +01:00
|
|
|
AutoSize = true,
|
|
|
|
|
DialogResult = DialogResult.Cancel,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>True if the user pressed OK and any setting actually changed. The caller
|
|
|
|
|
/// uses this to mark the profile dirty.</summary>
|
|
|
|
|
public bool ChangedAnything { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>The final settings (after OK). Equals the input settings if Cancel was
|
|
|
|
|
/// pressed — caller should ignore this on a non-OK DialogResult.</summary>
|
|
|
|
|
public RecordingSettings Result => working;
|
|
|
|
|
|
|
|
|
|
public RecordingSettingsDialog(RecordingSettings current)
|
|
|
|
|
{
|
|
|
|
|
working = current?.Clone() ?? new RecordingSettings();
|
2026-07-06 10:05:00 +01:00
|
|
|
|
|
|
|
|
splitTracksBox.Checked = working.SplitTracks;
|
|
|
|
|
splitTracksBox.CheckedChanged += (_, _) => working.SplitTracks = splitTracksBox.Checked;
|
|
|
|
|
bypassShapingBox.Checked = working.BypassShaping;
|
|
|
|
|
bypassShapingBox.CheckedChanged += (_, _) => working.BypassShaping = bypassShapingBox.Checked;
|
2026-05-15 12:40:01 +01:00
|
|
|
var initialSnapshot = working.Clone();
|
|
|
|
|
|
|
|
|
|
Text = "Recording settings";
|
|
|
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
|
|
|
MinimizeBox = false;
|
|
|
|
|
MaximizeBox = false;
|
|
|
|
|
ShowInTaskbar = false;
|
|
|
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
|
|
|
KeyPreview = true;
|
2026-05-15 13:40:42 +01:00
|
|
|
ClientSize = new Size(900, 380);
|
2026-05-15 12:40:01 +01:00
|
|
|
|
|
|
|
|
PopulateSourceList();
|
|
|
|
|
PopulateFormatList();
|
|
|
|
|
PopulateAttributesList();
|
2026-05-15 13:40:42 +01:00
|
|
|
PopulateFlacCompressionList();
|
|
|
|
|
PopulateChannelList();
|
2026-05-15 12:40:01 +01:00
|
|
|
|
|
|
|
|
SelectFromSource(working.Source);
|
|
|
|
|
SelectFromFormat(working.FileFormat);
|
|
|
|
|
SelectFromAttributes(working);
|
2026-05-15 13:40:42 +01:00
|
|
|
SelectFromFlacCompression(working);
|
|
|
|
|
SelectFromChannelMode(working);
|
2026-05-15 12:40:01 +01:00
|
|
|
|
|
|
|
|
sourceList.SelectedIndexChanged += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (sourceList.SelectedIndex < 0) return;
|
2026-07-08 11:36:02 +01:00
|
|
|
working.Source = SourceOrder[sourceList.SelectedIndex];
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
formatList.SelectedIndexChanged += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (formatList.SelectedIndex < 0) return;
|
|
|
|
|
var newFormat = (RecordingFileFormat)formatList.SelectedIndex;
|
|
|
|
|
if (newFormat == working.FileFormat) return;
|
|
|
|
|
working.FileFormat = newFormat;
|
|
|
|
|
PopulateAttributesList();
|
|
|
|
|
SelectFromAttributes(working);
|
2026-05-15 13:40:42 +01:00
|
|
|
UpdateFlacCompressionVisibility();
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
attributesList.SelectedIndexChanged += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (attributesList.SelectedIndex < 0) return;
|
|
|
|
|
ApplyAttributesSelection();
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
flacCompressionList.SelectedIndexChanged += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (flacCompressionList.SelectedIndex < 0) return;
|
|
|
|
|
// The compression list always carries levels 0..8 at indices 0..8 — pure
|
|
|
|
|
// 1:1 mapping. Saved even when FLAC isn't the active format so flipping
|
|
|
|
|
// formats back-and-forth preserves the user's compression choice.
|
|
|
|
|
working.FlacCompressionLevel = flacCompressionList.SelectedIndex;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
channelList.SelectedIndexChanged += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (channelList.SelectedIndex < 0) return;
|
|
|
|
|
working.ChannelMode = (RecordingChannelMode)channelList.SelectedIndex;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 12:40:01 +01:00
|
|
|
okButton.Click += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
ChangedAnything = !SettingsEqual(initialSnapshot, working);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
// Five columns side by side, OK/Cancel row beneath. The FLAC-compression column
|
|
|
|
|
// (index 3) is collapsed to zero width when the selected file format isn't FLAC,
|
|
|
|
|
// so non-FLAC formats see a clean 4-column layout without a blank gap.
|
|
|
|
|
grid = new TableLayoutPanel
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
|
|
|
|
Dock = DockStyle.Fill,
|
|
|
|
|
Padding = new Padding(12),
|
2026-05-15 13:40:42 +01:00
|
|
|
ColumnCount = 5,
|
2026-05-15 12:40:01 +01:00
|
|
|
RowCount = 2,
|
|
|
|
|
};
|
2026-05-15 13:40:42 +01:00
|
|
|
for (var i = 0; i < 5; i++) grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20f));
|
2026-05-15 12:40:01 +01:00
|
|
|
grid.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
|
|
|
|
grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
|
|
|
|
|
|
|
|
var sourceColumn = MakeColumn(sourceLabel, sourceList);
|
|
|
|
|
var formatColumn = MakeColumn(formatLabel, formatList);
|
|
|
|
|
var attributesColumn = MakeColumn(attributesLabel, attributesList);
|
2026-05-15 13:40:42 +01:00
|
|
|
compressionColumn = MakeColumn(flacCompressionLabel, flacCompressionList);
|
|
|
|
|
var channelColumn = MakeColumn(channelLabel, channelList);
|
2026-05-15 12:40:01 +01:00
|
|
|
grid.Controls.Add(sourceColumn, 0, 0);
|
|
|
|
|
grid.SetRowSpan(sourceColumn, 2);
|
|
|
|
|
grid.Controls.Add(formatColumn, 1, 0);
|
|
|
|
|
grid.SetRowSpan(formatColumn, 2);
|
|
|
|
|
grid.Controls.Add(attributesColumn, 2, 0);
|
|
|
|
|
grid.SetRowSpan(attributesColumn, 2);
|
2026-05-15 13:40:42 +01:00
|
|
|
grid.Controls.Add(compressionColumn, 3, 0);
|
|
|
|
|
grid.SetRowSpan(compressionColumn, 2);
|
|
|
|
|
grid.Controls.Add(channelColumn, 4, 0);
|
|
|
|
|
grid.SetRowSpan(channelColumn, 2);
|
|
|
|
|
|
|
|
|
|
// Initial column-visibility refresh, AFTER the grid has all five children so we
|
|
|
|
|
// can flip the compression column's ColumnStyle width together with its Visible.
|
|
|
|
|
UpdateFlacCompressionVisibility();
|
2026-05-15 12:40:01 +01:00
|
|
|
|
|
|
|
|
var buttonRow = new FlowLayoutPanel
|
|
|
|
|
{
|
|
|
|
|
Dock = DockStyle.Bottom,
|
|
|
|
|
FlowDirection = FlowDirection.RightToLeft,
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
Padding = new Padding(0, 0, 12, 12),
|
|
|
|
|
};
|
|
|
|
|
buttonRow.Controls.Add(cancelButton);
|
|
|
|
|
buttonRow.Controls.Add(okButton);
|
|
|
|
|
|
|
|
|
|
// Order: dialog body first (grid), then buttons docked beneath.
|
|
|
|
|
Controls.Add(grid);
|
|
|
|
|
Controls.Add(buttonRow);
|
|
|
|
|
|
2026-07-06 10:05:00 +01:00
|
|
|
// The two per-recording toggles sit across the top of the dialog. Added last so docking
|
|
|
|
|
// resolves them to the top strip, above the option columns.
|
|
|
|
|
var optionsRow = new FlowLayoutPanel
|
|
|
|
|
{
|
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
FlowDirection = FlowDirection.TopDown,
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
WrapContents = false,
|
|
|
|
|
Padding = new Padding(12, 12, 12, 0),
|
|
|
|
|
};
|
|
|
|
|
optionsRow.Controls.Add(splitTracksBox);
|
|
|
|
|
optionsRow.Controls.Add(bypassShapingBox);
|
|
|
|
|
Controls.Add(optionsRow);
|
|
|
|
|
|
2026-05-15 12:40:01 +01:00
|
|
|
AcceptButton = okButton;
|
|
|
|
|
CancelButton = cancelButton;
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
// Tab order left-to-right across the visible columns. The FLAC compression list
|
|
|
|
|
// sits between attributes and channels in tab order so the Alt+L mnemonic and Tab
|
|
|
|
|
// both land there when it's visible; when hidden it's still in tab order but
|
|
|
|
|
// skipped because Visible=false controls aren't focusable.
|
2026-07-07 20:20:31 +01:00
|
|
|
// The two toggles (top of the dialog) come first in tab order, then the option columns, then
|
|
|
|
|
// the buttons. The CONTAINERS are ordered too — without this the grid's controls (sourceList)
|
|
|
|
|
// sat first and grabbed initial focus, landing the user partway down, past the checkboxes.
|
|
|
|
|
optionsRow.TabIndex = 0;
|
|
|
|
|
grid.TabIndex = 1;
|
|
|
|
|
buttonRow.TabIndex = 2;
|
|
|
|
|
splitTracksBox.TabIndex = 0;
|
|
|
|
|
bypassShapingBox.TabIndex = 1;
|
2026-05-15 12:40:01 +01:00
|
|
|
sourceList.TabIndex = 0;
|
|
|
|
|
formatList.TabIndex = 1;
|
|
|
|
|
attributesList.TabIndex = 2;
|
2026-05-15 13:40:42 +01:00
|
|
|
flacCompressionList.TabIndex = 3;
|
|
|
|
|
channelList.TabIndex = 4;
|
|
|
|
|
okButton.TabIndex = 5;
|
|
|
|
|
cancelButton.TabIndex = 6;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 20:20:31 +01:00
|
|
|
protected override void OnShown(EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
base.OnShown(e);
|
|
|
|
|
// Land on the first checkbox at the top, not partway down on the option columns.
|
|
|
|
|
splitTracksBox.Focus();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
/// <summary>Show or hide the FLAC compression column based on the current file-format
|
|
|
|
|
/// selection. Driven from the formatList change handler and once at dialog open.
|
|
|
|
|
/// When hidden, the column's ColumnStyle width is set to 0% and the other four columns
|
|
|
|
|
/// each take 25% — visually the dialog reads as a clean 4-column layout. When visible,
|
|
|
|
|
/// all five columns share 20% each.</summary>
|
|
|
|
|
private void UpdateFlacCompressionVisibility()
|
|
|
|
|
{
|
|
|
|
|
if (grid is null || compressionColumn is null) return;
|
|
|
|
|
var isFlac = working.FileFormat == RecordingFileFormat.Flac;
|
|
|
|
|
compressionColumn.Visible = isFlac;
|
|
|
|
|
if (isFlac)
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < 5; i++) grid.ColumnStyles[i].Width = 20f;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Distribute the hidden column's space evenly across the four remaining ones.
|
|
|
|
|
for (var i = 0; i < 5; i++) grid.ColumnStyles[i].Width = (i == 3) ? 0f : 25f;
|
|
|
|
|
}
|
2026-05-15 12:40:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Control MakeColumn(Label label, ListBox list)
|
|
|
|
|
{
|
|
|
|
|
var panel = new TableLayoutPanel
|
|
|
|
|
{
|
|
|
|
|
Dock = DockStyle.Fill,
|
|
|
|
|
ColumnCount = 1,
|
|
|
|
|
RowCount = 2,
|
|
|
|
|
};
|
|
|
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
|
|
|
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
|
|
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
|
|
|
list.Dock = DockStyle.Fill;
|
|
|
|
|
panel.Controls.Add(label, 0, 0);
|
|
|
|
|
panel.Controls.Add(list, 0, 1);
|
|
|
|
|
return panel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PopulateSourceList()
|
|
|
|
|
{
|
|
|
|
|
sourceList.BeginUpdate();
|
|
|
|
|
sourceList.Items.Clear();
|
2026-07-08 11:36:02 +01:00
|
|
|
// Display order is decoupled from the enum via SourceOrder (both first as the default, then
|
|
|
|
|
// received, then sent). Add the labels in that same order.
|
|
|
|
|
sourceList.Items.Add("Record both sent and received audio");
|
2026-05-15 12:40:01 +01:00
|
|
|
sourceList.Items.Add("Record all received audio");
|
|
|
|
|
sourceList.Items.Add("Record all sent audio");
|
|
|
|
|
sourceList.EndUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PopulateFormatList()
|
|
|
|
|
{
|
|
|
|
|
formatList.BeginUpdate();
|
|
|
|
|
formatList.Items.Clear();
|
|
|
|
|
// Order MUST match RecordingFileFormat enum values 0..3.
|
|
|
|
|
formatList.Items.Add("WAV (uncompressed)");
|
|
|
|
|
formatList.Items.Add("MP3");
|
|
|
|
|
formatList.Items.Add("Ogg-Opus");
|
|
|
|
|
formatList.Items.Add("FLAC (lossless)");
|
|
|
|
|
formatList.EndUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === Per-format attribute tables ===
|
2026-05-15 13:40:42 +01:00
|
|
|
// All four formats record at the engine's 48 kHz mix rate, so labels include "48 kHz"
|
|
|
|
|
// to make the sample rate explicit (it's not a choice — it's a statement of fact about
|
|
|
|
|
// what gets written, which removes a common surprise for users who expected to see a
|
|
|
|
|
// rate picker). Channel mode is NOT part of any row — it has its own dedicated listbox
|
|
|
|
|
// since 2026-05-15 (was previously folded into every row, doubling the count for no
|
|
|
|
|
// real benefit).
|
2026-05-15 12:40:01 +01:00
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
private static readonly (int Bits, string Label)[] WavAttributes =
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
(16, "16-bit PCM, 48 kHz"),
|
|
|
|
|
(24, "24-bit PCM, 48 kHz"),
|
|
|
|
|
(32, "32-bit float, 48 kHz"),
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
private static readonly (int Kbps, string Label)[] Mp3Attributes =
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
(128, "128 kbps, 48 kHz"),
|
|
|
|
|
(192, "192 kbps, 48 kHz"),
|
|
|
|
|
(256, "256 kbps, 48 kHz"),
|
|
|
|
|
(320, "320 kbps, 48 kHz"),
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// OGG-Opus is VBR — kbps numbers are the encoder's target average. Opus' music-quality
|
|
|
|
|
// sweet spot starts around 96 kbps; we expose 96 / 128 / 192 / 256 so users have a
|
|
|
|
|
// smaller-file option without it sounding obviously lossy on dense material.
|
2026-05-15 13:40:42 +01:00
|
|
|
private static readonly (int Kbps, string Label)[] OggOpusAttributes =
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
(96, "96 kbps, 48 kHz"),
|
|
|
|
|
(128, "128 kbps, 48 kHz"),
|
|
|
|
|
(192, "192 kbps, 48 kHz"),
|
|
|
|
|
(256, "256 kbps, 48 kHz"),
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
// FLAC is integer-PCM only — quality knob in this column is just bit depth. The
|
|
|
|
|
// compression level is its own listbox (visible only when FLAC is selected).
|
|
|
|
|
private static readonly (int Bits, string Label)[] FlacAttributes =
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
(16, "16-bit, 48 kHz"),
|
|
|
|
|
(24, "24-bit, 48 kHz"),
|
2026-05-15 12:40:01 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private void PopulateAttributesList()
|
|
|
|
|
{
|
|
|
|
|
attributesList.BeginUpdate();
|
|
|
|
|
attributesList.Items.Clear();
|
|
|
|
|
switch (working.FileFormat)
|
|
|
|
|
{
|
|
|
|
|
case RecordingFileFormat.Wav:
|
2026-05-15 13:40:42 +01:00
|
|
|
foreach (var (_, label) in WavAttributes) attributesList.Items.Add(label);
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Mp3:
|
2026-05-15 13:40:42 +01:00
|
|
|
foreach (var (_, label) in Mp3Attributes) attributesList.Items.Add(label);
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Ogg:
|
2026-05-15 13:40:42 +01:00
|
|
|
foreach (var (_, label) in OggOpusAttributes) attributesList.Items.Add(label);
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Flac:
|
2026-05-15 13:40:42 +01:00
|
|
|
foreach (var (_, label) in FlacAttributes) attributesList.Items.Add(label);
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
attributesList.Items.Add("Default settings");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
attributesList.EndUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
/// <summary>Populate the FLAC compression listbox with all 9 levels (0..8). Levels are
|
|
|
|
|
/// identified verbatim — the user picks by index, which maps 1:1 to the level number.
|
|
|
|
|
/// Headers on the "fastest" and "smallest" ends are tagged so the trade-off is obvious
|
|
|
|
|
/// without needing a separate explanatory label.</summary>
|
|
|
|
|
private void PopulateFlacCompressionList()
|
|
|
|
|
{
|
|
|
|
|
flacCompressionList.BeginUpdate();
|
|
|
|
|
flacCompressionList.Items.Clear();
|
|
|
|
|
flacCompressionList.Items.Add("0 — fastest encode, biggest file");
|
|
|
|
|
flacCompressionList.Items.Add("1");
|
|
|
|
|
flacCompressionList.Items.Add("2");
|
|
|
|
|
flacCompressionList.Items.Add("3");
|
|
|
|
|
flacCompressionList.Items.Add("4");
|
|
|
|
|
flacCompressionList.Items.Add("5 — default (libFLAC reference)");
|
|
|
|
|
flacCompressionList.Items.Add("6");
|
|
|
|
|
flacCompressionList.Items.Add("7");
|
|
|
|
|
flacCompressionList.Items.Add("8 — slowest encode, smallest file");
|
|
|
|
|
flacCompressionList.EndUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Populate the channel listbox. Order MUST match RecordingChannelMode enum
|
|
|
|
|
/// values 0 / 1 so the SelectedIndex → enum mapping is direct.</summary>
|
|
|
|
|
private void PopulateChannelList()
|
|
|
|
|
{
|
|
|
|
|
channelList.BeginUpdate();
|
|
|
|
|
channelList.Items.Clear();
|
|
|
|
|
channelList.Items.Add("Stereo");
|
|
|
|
|
channelList.Items.Add("Mono");
|
|
|
|
|
channelList.EndUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 11:36:02 +01:00
|
|
|
// Display order for the recording-source list (decoupled from the RecordingSource enum values): both
|
|
|
|
|
// at the top as the default, then received, then sent.
|
|
|
|
|
private static readonly RecordingSource[] SourceOrder =
|
|
|
|
|
[RecordingSource.Both, RecordingSource.ReceivedOnly, RecordingSource.SentOnly];
|
|
|
|
|
|
2026-05-15 12:40:01 +01:00
|
|
|
private void SelectFromSource(RecordingSource src)
|
|
|
|
|
{
|
2026-07-08 11:36:02 +01:00
|
|
|
var idx = Array.IndexOf(SourceOrder, src);
|
2026-05-15 12:40:01 +01:00
|
|
|
if (idx >= 0 && idx < sourceList.Items.Count) sourceList.SelectedIndex = idx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SelectFromFormat(RecordingFileFormat fmt)
|
|
|
|
|
{
|
|
|
|
|
var idx = (int)fmt;
|
|
|
|
|
if (idx >= 0 && idx < formatList.Items.Count) formatList.SelectedIndex = idx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SelectFromAttributes(RecordingSettings s)
|
|
|
|
|
{
|
|
|
|
|
switch (s.FileFormat)
|
|
|
|
|
{
|
|
|
|
|
case RecordingFileFormat.Wav:
|
|
|
|
|
for (var i = 0; i < WavAttributes.Length; i++)
|
|
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
if (WavAttributes[i].Bits == s.WavBitsPerSample)
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
|
|
|
|
attributesList.SelectedIndex = i;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 13:40:42 +01:00
|
|
|
attributesList.SelectedIndex = 1; // 24-bit default
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Mp3:
|
|
|
|
|
for (var i = 0; i < Mp3Attributes.Length; i++)
|
|
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
if (Mp3Attributes[i].Kbps == s.Mp3BitrateKbps)
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
|
|
|
|
attributesList.SelectedIndex = i;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 13:40:42 +01:00
|
|
|
attributesList.SelectedIndex = 3; // 320 kbps default
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Ogg:
|
|
|
|
|
for (var i = 0; i < OggOpusAttributes.Length; i++)
|
|
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
if (OggOpusAttributes[i].Kbps == s.OggOpusBitrateKbps)
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
|
|
|
|
attributesList.SelectedIndex = i;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 13:40:42 +01:00
|
|
|
attributesList.SelectedIndex = 2; // 192 kbps default
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Flac:
|
|
|
|
|
for (var i = 0; i < FlacAttributes.Length; i++)
|
|
|
|
|
{
|
2026-05-15 13:40:42 +01:00
|
|
|
if (FlacAttributes[i].Bits == s.FlacBitsPerSample)
|
2026-05-15 12:40:01 +01:00
|
|
|
{
|
|
|
|
|
attributesList.SelectedIndex = i;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 13:40:42 +01:00
|
|
|
attributesList.SelectedIndex = 1; // 24-bit default
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
if (attributesList.Items.Count > 0) attributesList.SelectedIndex = 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyAttributesSelection()
|
|
|
|
|
{
|
|
|
|
|
var idx = attributesList.SelectedIndex;
|
|
|
|
|
if (idx < 0) return;
|
|
|
|
|
switch (working.FileFormat)
|
|
|
|
|
{
|
|
|
|
|
case RecordingFileFormat.Wav:
|
2026-05-15 13:40:42 +01:00
|
|
|
if (idx < WavAttributes.Length) working.WavBitsPerSample = WavAttributes[idx].Bits;
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Mp3:
|
2026-05-15 13:40:42 +01:00
|
|
|
if (idx < Mp3Attributes.Length) working.Mp3BitrateKbps = Mp3Attributes[idx].Kbps;
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Ogg:
|
2026-05-15 13:40:42 +01:00
|
|
|
if (idx < OggOpusAttributes.Length) working.OggOpusBitrateKbps = OggOpusAttributes[idx].Kbps;
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
case RecordingFileFormat.Flac:
|
2026-05-15 13:40:42 +01:00
|
|
|
if (idx < FlacAttributes.Length) working.FlacBitsPerSample = FlacAttributes[idx].Bits;
|
2026-05-15 12:40:01 +01:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 13:40:42 +01:00
|
|
|
/// <summary>Select the FLAC compression list row that matches the current settings.
|
|
|
|
|
/// Levels 0..8 map directly to list indices 0..8. Out-of-range or unset values fall
|
|
|
|
|
/// back to level 5 (the libFLAC reference default).</summary>
|
|
|
|
|
private void SelectFromFlacCompression(RecordingSettings s)
|
|
|
|
|
{
|
|
|
|
|
var level = s.FlacCompressionLevel;
|
|
|
|
|
if (level < 0 || level > 8) level = 5;
|
|
|
|
|
flacCompressionList.SelectedIndex = level;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Select the channel-mode row matching the current setting. Falls back to
|
|
|
|
|
/// Stereo (index 0) for any unrecognised value.</summary>
|
|
|
|
|
private void SelectFromChannelMode(RecordingSettings s)
|
|
|
|
|
{
|
|
|
|
|
var idx = (int)s.ChannelMode;
|
|
|
|
|
if (idx < 0 || idx >= channelList.Items.Count) idx = 0;
|
|
|
|
|
channelList.SelectedIndex = idx;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 12:40:01 +01:00
|
|
|
private static bool SettingsEqual(RecordingSettings a, RecordingSettings b) =>
|
|
|
|
|
a.Source == b.Source
|
|
|
|
|
&& a.FileFormat == b.FileFormat
|
|
|
|
|
&& a.ChannelMode == b.ChannelMode
|
|
|
|
|
&& a.WavBitsPerSample == b.WavBitsPerSample
|
|
|
|
|
&& a.Mp3BitrateKbps == b.Mp3BitrateKbps
|
|
|
|
|
&& a.OggOpusBitrateKbps == b.OggOpusBitrateKbps
|
|
|
|
|
&& a.FlacBitsPerSample == b.FlacBitsPerSample
|
|
|
|
|
&& a.FlacCompressionLevel == b.FlacCompressionLevel
|
2026-07-09 20:54:53 +01:00
|
|
|
// Include the two v5 toggles the dialog can change, so toggling only these still marks the
|
|
|
|
|
// profile dirty (before, changing just split-tracks / bypass-shaping reported "no change").
|
|
|
|
|
&& a.SplitTracks == b.SplitTracks
|
|
|
|
|
&& a.BypassShaping == b.BypassShaping
|
2026-05-15 12:40:01 +01:00
|
|
|
&& string.Equals(a.Folder ?? string.Empty, b.Folder ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|