Recording-settings dialog rework + menu mnemonic clean-up

Recording settings dialog
=========================

Promote what were row attributes inside the format-attributes table
into their own dedicated listboxes. Pre-rework each format's
attribute list contained a row per (quality knob × channel mode)
combination — doubling the row count for no real benefit. WAV had
6 rows, MP3 / OGG-Opus had 8, FLAC had 4. Channel mode is now its
own listbox; per-format attribute lists shrink to 3 (WAV), 4
(MP3 / OGG-Opus), 2 (FLAC).

FLAC compression level was previously a hidden RecordingSettings
field locked at the libFLAC default of 5. Now exposed as its own
conditional listbox with all 9 levels (0..8). Visible only when the
selected file format is FLAC; the column collapses to 0% width
otherwise so the dialog reads cleanly as 4 columns for non-FLAC
formats and 5 for FLAC.

Mnemonics:
  * Source         Alt+S    (unchanged)
  * File format    Alt+F    (unchanged)
  * Attributes     Alt+A    (unchanged)
  * Compression    Alt+L    (new, FLAC-only visible)
  * Channels       Alt+C    (new)
  * OK             Alt+O    (unchanged)
  * Cancel         Alt+N    (was Alt+C — moved to free C for Channels)

Record menu mnemonics
=====================

Item mnemonics inside the Record menu reorganised so the keystroke
inside the menu matches the same letter used for Ctrl+R outside it,
and Settings reads naturally:

  * Start recording / Stop recording   Alt+R  (was Alt+S)
  * Recording settings                 Alt+S  (was Alt+T)
  * Open recordings folder             Alt+O  (unchanged)
  * Change recordings folder           Alt+C  (unchanged)

UpdateStartStopRecordingMenuLabel keeps the underline on a literal
R in both states ("Sta&rt recording" → "Stop &recording") so the
keystroke does the same job regardless of which label is active.

No version bump — the running version stays at 1.3.0. These changes
will be bundled with the next public release alongside whatever else
is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-15 13:40:42 +01:00
co-authored by Claude Opus 4.7
parent d927b34c9c
commit 45b709d49d
3 changed files with 249 additions and 106 deletions
+10 -8
View File
@@ -196,8 +196,8 @@ ul, ol { padding-left: 1.4em; }
<table>
<tr><th>Item</th><th>Shortcut</th><th>What it does</th></tr>
<tr><td><strong>Start recording / Stop recording</strong></td><td>Ctrl+R, or Alt+O, S</td><td>Toggle. The label flips between the two states so NVDA announces whichever action the next press would perform. Each press of Start plays a short cue sound (if enabled in Preferences), then creates a new file in your recordings folder named <code>RemSound-yyyy-MM-dd_HH-mm-ss.&lt;ext&gt;</code>. Stop closes the file then plays the stop cue. Ctrl+R works from anywhere in the main window.</td></tr>
<tr><td><strong>Recording settings&hellip;</strong></td><td>Alt+O, T</td><td>Opens the Recording settings dialog. Three listboxes side-by-side: <em>Recording source</em> (Alt+S), <em>File format</em> (Alt+F), <em>Audio format attributes</em> (Alt+A). The attributes list repopulates based on the selected format. OK saves to the current profile; Cancel discards.</td></tr>
<tr><td><strong>Start recording / Stop recording</strong></td><td>Ctrl+R, or Alt+O, R</td><td>Toggle. The label flips between &ldquo;Start recording&rdquo; and &ldquo;Stop recording&rdquo; so NVDA announces whichever action the next press would perform. Either label is activated by Alt+R (the underline stays on a letter R in both). Each press of Start plays a short cue sound (if enabled in Preferences), then creates a new file in your recordings folder named <code>RemSound-yyyy-MM-dd_HH-mm-ss.&lt;ext&gt;</code>. Stop closes the file then plays the stop cue. Ctrl+R works from anywhere in the main window.</td></tr>
<tr><td><strong>Recording settings&hellip;</strong></td><td>Alt+O, S</td><td>Opens the Recording settings dialog. Up to five listboxes: <em>Recording source</em> (Alt+S), <em>File format</em> (Alt+F), <em>Audio format attributes</em> (Alt+A), <em>FLAC compression level</em> (Alt+L &mdash; visible only when FLAC is selected), <em>Channels</em> (Alt+C). The attributes list repopulates based on the selected format. OK saves to the current profile; Cancel discards.</td></tr>
<tr><td><strong>Open current recordings folder</strong></td><td>Alt+O, O</td><td>Opens your current recordings folder in Windows Explorer. Creates the folder if it doesn&rsquo;t exist yet (first launch after a fresh install).</td></tr>
<tr><td><strong>Change recordings folder&hellip;</strong></td><td>Alt+O, C</td><td>Folder picker. Pick a different folder for future recordings. The choice is saved on the current profile, so different profiles can record to different places.</td></tr>
</table>
@@ -612,8 +612,8 @@ Audient USB Audio ASIO Driver &mdash; Pair 3 (channels 5/6): Loop-back 1 (L) / L
<tr><td>Ctrl+K</td><td>Open Keyboard shortcuts dialog</td></tr>
<tr><td>Ctrl+P</td><td>Open Preferences dialog</td></tr>
<tr><td>Ctrl+R</td><td>Start or stop recording (toggles)</td></tr>
<tr><td>Alt+O, S</td><td>Start or stop recording (via menu &mdash; Record menu is Alt+O, item is S)</td></tr>
<tr><td>Alt+O, T</td><td>Recording settings dialog</td></tr>
<tr><td>Alt+O, R</td><td>Start or stop recording (via menu &mdash; Record menu is Alt+O, item is R for &ldquo;recording&rdquo;)</td></tr>
<tr><td>Alt+O, S</td><td>Recording settings dialog</td></tr>
<tr><td>Alt+O, O</td><td>Open current recordings folder</td></tr>
<tr><td>Alt+O, C</td><td>Change recordings folder</td></tr>
<tr><td>Alt+F, O</td><td>Open profile</td></tr>
@@ -829,16 +829,18 @@ Bind whatever key combinations you prefer (e.g. Ctrl+Shift+Up / Ctrl+Shift+Down
<h3>Recording settings dialog</h3>
<p>Reached via <strong>Record &rarr; Recording settings</strong>. Three keyboard-navigable listboxes:</p>
<p>Reached via <strong>Record &rarr; Recording settings</strong>. Up to five keyboard-navigable listboxes laid out left-to-right. Channel mode used to be folded into every attribute row, which doubled their length for no real gain; it&rsquo;s now its own dedicated list. FLAC&rsquo;s compression level has its own list too, but it only shows when the file format is FLAC.</p>
<table>
<tr><th>List</th><th>Shortcut</th><th>What goes in it</th></tr>
<tr><td><strong>Recording source</strong></td><td>Alt+S</td><td>Receive only / Send only / Both. See the source explainer above.</td></tr>
<tr><td><strong>File format</strong></td><td>Alt+F</td><td>WAV / MP3 / Ogg-Opus / FLAC. The attributes list to the right repopulates whenever you change this.</td></tr>
<tr><td><strong>Audio format attributes</strong></td><td>Alt+A</td><td>Repopulates per format, with the 48 kHz sample rate stated on every row so there&rsquo;s no ambiguity. WAV: six rows pairing bit-depth (16 / 24 / 32-bit float) with channel mode (stereo / mono). MP3: eight rows pairing CBR bitrate (128 / 192 / 256 / 320 kbps) with channel mode. OGG-Opus: eight rows pairing VBR bitrate (96 / 128 / 192 / 256 kbps) with channel mode. FLAC: four rows pairing bit-depth (16 / 24-bit) with channel mode &mdash; FLAC is integer-PCM only, so no 32-bit-float row. Compression level for FLAC is fixed at 5 (the libFLAC reference default).</td></tr>
<tr><td><strong>File format</strong></td><td>Alt+F</td><td>WAV / MP3 / Ogg-Opus / FLAC. The attributes (and FLAC compression) lists to the right repopulate whenever you change this.</td></tr>
<tr><td><strong>Audio format attributes</strong></td><td>Alt+A</td><td>Repopulates per format, with the 48 kHz sample rate stated on every row so there&rsquo;s no ambiguity. WAV: three rows (16-bit PCM / 24-bit PCM / 32-bit float). MP3: four rows (128 / 192 / 256 / 320 kbps CBR). OGG-Opus: four rows (96 / 128 / 192 / 256 kbps VBR). FLAC: two rows (16-bit / 24-bit) &mdash; FLAC is integer-PCM only, so no 32-bit-float row.</td></tr>
<tr><td><strong>FLAC compression level</strong></td><td>Alt+L</td><td>Visible <strong>only when FLAC is the selected file format</strong>. Nine rows for levels 0..8, with friendly labels on the endpoints (&ldquo;0 &mdash; fastest encode, biggest file&rdquo;, &ldquo;5 &mdash; default (libFLAC reference)&rdquo;, &ldquo;8 &mdash; slowest encode, smallest file&rdquo;). All levels produce bit-identical lossless audio &mdash; it&rsquo;s a pure encode-time vs file-size trade-off.</td></tr>
<tr><td><strong>Channels</strong></td><td>Alt+C</td><td>Stereo or Mono. Applies to every format.</td></tr>
</table>
<p>OK saves the choices to the current profile. Cancel (or Esc) discards. Settings persist via the normal profile-save mechanism &mdash; changes here flag the profile as having unsaved changes and you&rsquo;ll be prompted on exit if you haven&rsquo;t saved.</p>
<p>OK (Alt+O) saves the choices to the current profile. Cancel (Alt+N) or Esc discards. Settings persist via the normal profile-save mechanism &mdash; changes here flag the profile as having unsaved changes and you&rsquo;ll be prompted on exit if you haven&rsquo;t saved.</p>
<h2 id="logs">19. Logs and diagnostics</h2>
+13 -3
View File
@@ -1139,14 +1139,20 @@ public sealed class MainForm : Form
// / Record) and reads as "Recording" naturally enough for the mnemonic to stick.
var recordMenu = new ToolStripMenuItem("Rec&ord") { AccessibleName = "Record menu" };
startStopRecordingMenuItem = new ToolStripMenuItem("&Start recording")
// Start/Stop uses Alt+R — matches the Ctrl+R global toggle so the same letter does
// the same job from either entry point. The "&" position shifts when the label flips
// (Sta&rt → Stop &recording) so the underline stays on an R in both states. See
// UpdateStartStopRecordingMenuLabel for the runtime label flip.
startStopRecordingMenuItem = new ToolStripMenuItem("Sta&rt recording")
{
ShortcutKeys = Keys.Control | Keys.R,
AccessibleName = "Start recording",
};
startStopRecordingMenuItem.Click += (_, _) => ToggleRecording();
var settingsItem = new ToolStripMenuItem("Recording se&ttings...")
// Recording settings → Alt+S (was Alt+T). S reads more naturally than T for
// "settings", and the slot freed up when Start/Stop moved off Alt+S.
var settingsItem = new ToolStripMenuItem("Recording &settings...")
{
AccessibleName = "Recording settings",
};
@@ -1216,7 +1222,11 @@ public sealed class MainForm : Form
void Apply()
{
if (startStopRecordingMenuItem is null) return;
startStopRecordingMenuItem.Text = nowRecording ? "&Stop recording" : "&Start recording";
// Mnemonic stays on an "R" in both states: "Sta&rt recording" (Alt+R activates
// the R in Start) when not recording, "Stop &recording" (Alt+R activates the R
// in recording) when recording. Same keystroke does the same job in both states
// — matches the Ctrl+R global toggle.
startStopRecordingMenuItem.Text = nowRecording ? "Stop &recording" : "Sta&rt recording";
startStopRecordingMenuItem.AccessibleName = nowRecording ? "Stop recording" : "Start recording";
}
if (InvokeRequired) BeginInvoke(Apply);
+226 -95
View File
@@ -3,15 +3,26 @@ using RemSound.Core;
namespace RemSound.App;
/// <summary>
/// Recording settings dialog. Three listboxes laid out left-to-right:
/// * Recording &source (Alt+S) — what audio gets captured
/// * File &format (Alt+F) — WAV / MP3 / Ogg / FLAC
/// * Audio &attributes (Alt+A) — bit depth or bitrate, plus channel mode
/// 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
///
/// The attributes list repopulates whenever the file-format selection changes, so the user
/// always sees only the choices that make sense for the format. Selecting a WAV-only
/// attribute then switching the format to MP3 doesn't carry forward — the format-attributes
/// list resets to a sensible default for the new format.
/// 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.
///
/// 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
@@ -68,6 +79,52 @@ internal sealed class RecordingSettingsDialog : Form
Height = 200,
};
// 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;
private readonly Button okButton = new()
{
Text = "&OK",
@@ -77,7 +134,10 @@ internal sealed class RecordingSettingsDialog : Form
private readonly Button cancelButton = new()
{
Text = "&Cancel",
// 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",
AutoSize = true,
DialogResult = DialogResult.Cancel,
};
@@ -102,15 +162,19 @@ internal sealed class RecordingSettingsDialog : Form
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
KeyPreview = true;
ClientSize = new Size(700, 360);
ClientSize = new Size(900, 380);
PopulateSourceList();
PopulateFormatList();
PopulateAttributesList();
PopulateFlacCompressionList();
PopulateChannelList();
SelectFromSource(working.Source);
SelectFromFormat(working.FileFormat);
SelectFromAttributes(working);
SelectFromFlacCompression(working);
SelectFromChannelMode(working);
sourceList.SelectedIndexChanged += (_, _) =>
{
@@ -126,6 +190,7 @@ internal sealed class RecordingSettingsDialog : Form
working.FileFormat = newFormat;
PopulateAttributesList();
SelectFromAttributes(working);
UpdateFlacCompressionVisibility();
};
attributesList.SelectedIndexChanged += (_, _) =>
@@ -134,32 +199,59 @@ internal sealed class RecordingSettingsDialog : Form
ApplyAttributesSelection();
};
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;
};
okButton.Click += (_, _) =>
{
ChangedAnything = !SettingsEqual(initialSnapshot, working);
};
// Three columns side by side, OK/Cancel row beneath.
var grid = new TableLayoutPanel
// 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
{
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 3,
ColumnCount = 5,
RowCount = 2,
};
for (var i = 0; i < 3; i++) grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.3f));
for (var i = 0; i < 5; i++) grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20f));
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);
compressionColumn = MakeColumn(flacCompressionLabel, flacCompressionList);
var channelColumn = MakeColumn(channelLabel, channelList);
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);
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();
var buttonRow = new FlowLayoutPanel
{
@@ -178,12 +270,38 @@ internal sealed class RecordingSettingsDialog : Form
AcceptButton = okButton;
CancelButton = cancelButton;
// Tab order top-to-bottom of the visible flow: source, format, attributes, OK, Cancel.
// 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.
sourceList.TabIndex = 0;
formatList.TabIndex = 1;
attributesList.TabIndex = 2;
okButton.TabIndex = 3;
cancelButton.TabIndex = 4;
flacCompressionList.TabIndex = 3;
channelList.TabIndex = 4;
okButton.TabIndex = 5;
cancelButton.TabIndex = 6;
}
/// <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;
}
}
private static Control MakeColumn(Label label, ListBox list)
@@ -227,58 +345,45 @@ internal sealed class RecordingSettingsDialog : Form
}
// === Per-format attribute tables ===
// All four formats currently 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 part of every row because it determines file shape
// alongside the format-specific quality knob.
// 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).
private static readonly (int Bits, RecordingChannelMode Mode, string Label)[] WavAttributes =
private static readonly (int Bits, string Label)[] WavAttributes =
{
(16, RecordingChannelMode.Stereo, "16-bit PCM, 48 kHz, stereo"),
(16, RecordingChannelMode.Mono, "16-bit PCM, 48 kHz, mono"),
(24, RecordingChannelMode.Stereo, "24-bit PCM, 48 kHz, stereo"),
(24, RecordingChannelMode.Mono, "24-bit PCM, 48 kHz, mono"),
(32, RecordingChannelMode.Stereo, "32-bit float, 48 kHz, stereo"),
(32, RecordingChannelMode.Mono, "32-bit float, 48 kHz, mono"),
(16, "16-bit PCM, 48 kHz"),
(24, "24-bit PCM, 48 kHz"),
(32, "32-bit float, 48 kHz"),
};
private static readonly (int Kbps, RecordingChannelMode Mode, string Label)[] Mp3Attributes =
private static readonly (int Kbps, string Label)[] Mp3Attributes =
{
(128, RecordingChannelMode.Stereo, "128 kbps, 48 kHz, stereo"),
(128, RecordingChannelMode.Mono, "128 kbps, 48 kHz, mono"),
(192, RecordingChannelMode.Stereo, "192 kbps, 48 kHz, stereo"),
(192, RecordingChannelMode.Mono, "192 kbps, 48 kHz, mono"),
(256, RecordingChannelMode.Stereo, "256 kbps, 48 kHz, stereo"),
(256, RecordingChannelMode.Mono, "256 kbps, 48 kHz, mono"),
(320, RecordingChannelMode.Stereo, "320 kbps, 48 kHz, stereo"),
(320, RecordingChannelMode.Mono, "320 kbps, 48 kHz, mono"),
(128, "128 kbps, 48 kHz"),
(192, "192 kbps, 48 kHz"),
(256, "256 kbps, 48 kHz"),
(320, "320 kbps, 48 kHz"),
};
// 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.
private static readonly (int Kbps, RecordingChannelMode Mode, string Label)[] OggOpusAttributes =
private static readonly (int Kbps, string Label)[] OggOpusAttributes =
{
(96, RecordingChannelMode.Stereo, "96 kbps, 48 kHz, stereo"),
(96, RecordingChannelMode.Mono, "96 kbps, 48 kHz, mono"),
(128, RecordingChannelMode.Stereo, "128 kbps, 48 kHz, stereo"),
(128, RecordingChannelMode.Mono, "128 kbps, 48 kHz, mono"),
(192, RecordingChannelMode.Stereo, "192 kbps, 48 kHz, stereo"),
(192, RecordingChannelMode.Mono, "192 kbps, 48 kHz, mono"),
(256, RecordingChannelMode.Stereo, "256 kbps, 48 kHz, stereo"),
(256, RecordingChannelMode.Mono, "256 kbps, 48 kHz, mono"),
(96, "96 kbps, 48 kHz"),
(128, "128 kbps, 48 kHz"),
(192, "192 kbps, 48 kHz"),
(256, "256 kbps, 48 kHz"),
};
// FLAC is lossless — quality knob is just bit depth (and silently, compression level,
// which we hard-fix at the reference encoder's default 5). 32-bit float isn't a FLAC
// option (FLAC stores integer PCM), so it's deliberately absent.
private static readonly (int Bits, RecordingChannelMode Mode, string Label)[] FlacAttributes =
// 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 =
{
(16, RecordingChannelMode.Stereo, "16-bit, 48 kHz, stereo"),
(16, RecordingChannelMode.Mono, "16-bit, 48 kHz, mono"),
(24, RecordingChannelMode.Stereo, "24-bit, 48 kHz, stereo"),
(24, RecordingChannelMode.Mono, "24-bit, 48 kHz, mono"),
(16, "16-bit, 48 kHz"),
(24, "24-bit, 48 kHz"),
};
private void PopulateAttributesList()
@@ -288,16 +393,16 @@ internal sealed class RecordingSettingsDialog : Form
switch (working.FileFormat)
{
case RecordingFileFormat.Wav:
foreach (var (_, _, label) in WavAttributes) attributesList.Items.Add(label);
foreach (var (_, label) in WavAttributes) attributesList.Items.Add(label);
break;
case RecordingFileFormat.Mp3:
foreach (var (_, _, label) in Mp3Attributes) attributesList.Items.Add(label);
foreach (var (_, label) in Mp3Attributes) attributesList.Items.Add(label);
break;
case RecordingFileFormat.Ogg:
foreach (var (_, _, label) in OggOpusAttributes) attributesList.Items.Add(label);
foreach (var (_, label) in OggOpusAttributes) attributesList.Items.Add(label);
break;
case RecordingFileFormat.Flac:
foreach (var (_, _, label) in FlacAttributes) attributesList.Items.Add(label);
foreach (var (_, label) in FlacAttributes) attributesList.Items.Add(label);
break;
default:
attributesList.Items.Add("Default settings");
@@ -306,6 +411,37 @@ internal sealed class RecordingSettingsDialog : Form
attributesList.EndUpdate();
}
/// <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();
}
private void SelectFromSource(RecordingSource src)
{
var idx = (int)src;
@@ -325,50 +461,46 @@ internal sealed class RecordingSettingsDialog : Form
case RecordingFileFormat.Wav:
for (var i = 0; i < WavAttributes.Length; i++)
{
var (bits, mode, _) = WavAttributes[i];
if (bits == s.WavBitsPerSample && mode == s.ChannelMode)
if (WavAttributes[i].Bits == s.WavBitsPerSample)
{
attributesList.SelectedIndex = i;
return;
}
}
attributesList.SelectedIndex = 2; // 24-bit stereo default
attributesList.SelectedIndex = 1; // 24-bit default
break;
case RecordingFileFormat.Mp3:
for (var i = 0; i < Mp3Attributes.Length; i++)
{
var (kbps, mode, _) = Mp3Attributes[i];
if (kbps == s.Mp3BitrateKbps && mode == s.ChannelMode)
if (Mp3Attributes[i].Kbps == s.Mp3BitrateKbps)
{
attributesList.SelectedIndex = i;
return;
}
}
attributesList.SelectedIndex = 6; // 320 kbps stereo default
attributesList.SelectedIndex = 3; // 320 kbps default
break;
case RecordingFileFormat.Ogg:
for (var i = 0; i < OggOpusAttributes.Length; i++)
{
var (kbps, mode, _) = OggOpusAttributes[i];
if (kbps == s.OggOpusBitrateKbps && mode == s.ChannelMode)
if (OggOpusAttributes[i].Kbps == s.OggOpusBitrateKbps)
{
attributesList.SelectedIndex = i;
return;
}
}
attributesList.SelectedIndex = 4; // 192 kbps stereo default
attributesList.SelectedIndex = 2; // 192 kbps default
break;
case RecordingFileFormat.Flac:
for (var i = 0; i < FlacAttributes.Length; i++)
{
var (bits, mode, _) = FlacAttributes[i];
if (bits == s.FlacBitsPerSample && mode == s.ChannelMode)
if (FlacAttributes[i].Bits == s.FlacBitsPerSample)
{
attributesList.SelectedIndex = i;
return;
}
}
attributesList.SelectedIndex = 2; // 24-bit stereo default
attributesList.SelectedIndex = 1; // 24-bit default
break;
default:
if (attributesList.Items.Count > 0) attributesList.SelectedIndex = 0;
@@ -383,42 +515,41 @@ internal sealed class RecordingSettingsDialog : Form
switch (working.FileFormat)
{
case RecordingFileFormat.Wav:
if (idx < WavAttributes.Length)
{
var (bits, mode, _) = WavAttributes[idx];
working.WavBitsPerSample = bits;
working.ChannelMode = mode;
}
if (idx < WavAttributes.Length) working.WavBitsPerSample = WavAttributes[idx].Bits;
break;
case RecordingFileFormat.Mp3:
if (idx < Mp3Attributes.Length)
{
var (kbps, mode, _) = Mp3Attributes[idx];
working.Mp3BitrateKbps = kbps;
working.ChannelMode = mode;
}
if (idx < Mp3Attributes.Length) working.Mp3BitrateKbps = Mp3Attributes[idx].Kbps;
break;
case RecordingFileFormat.Ogg:
if (idx < OggOpusAttributes.Length)
{
var (kbps, mode, _) = OggOpusAttributes[idx];
working.OggOpusBitrateKbps = kbps;
working.ChannelMode = mode;
}
if (idx < OggOpusAttributes.Length) working.OggOpusBitrateKbps = OggOpusAttributes[idx].Kbps;
break;
case RecordingFileFormat.Flac:
if (idx < FlacAttributes.Length)
{
var (bits, mode, _) = FlacAttributes[idx];
working.FlacBitsPerSample = bits;
working.ChannelMode = mode;
}
if (idx < FlacAttributes.Length) working.FlacBitsPerSample = FlacAttributes[idx].Bits;
break;
default:
break;
}
}
/// <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;
}
private static bool SettingsEqual(RecordingSettings a, RecordingSettings b) =>
a.Source == b.Source
&& a.FileFormat == b.FileFormat