diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc3c1dc..5769039 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,27 @@ phase plan these entries follow.
## [Unreleased]
+### Fixed
+
+- **One notch of the wheel jumped three tracks in the session list.** The list of what has been
+ recorded scrolled by whole items, and its rows are two lines tall, so a single wheel notch moved
+ about 150 device-independent pixels — three finished tracks at once. There was no way to move
+ through it slowly. It scrolls by pixels now, like the list on the Metadata page, which had been
+ built that way from the start and did not have the problem.
+
+- **The sample rate appeared on the transport display only once recording started.** The line that
+ says what the file will be — `MP3 320K 48K` — showed its first two parts while idle and grew the
+ third on Start, which read as the display filling in rather than as the deliberate omission it
+ was. The reason recorded for it was wrong: the rate was thought to be unknowable until the
+ capture device was open, when it is a property of the device and readable at any time. It is the
+ same device the recording will open, so the figure shown while idle is the one the recording
+ will use.
+
+- **The transport display ignored settings changed while it was idle.** It re-read the format,
+ bitrate and device only when a session started or stopped, so changing any of them on Settings
+ or Advanced left the Record page describing the file the previous settings would have produced,
+ until something was recorded. It now follows a saved setting straight away.
+
## [0.2.0] - 2026-08-31
**Upgrading from 0.1.0 resets one setting.** The three advertisement switches
diff --git a/src/Offstream.App/Services/RecordingController.cs b/src/Offstream.App/Services/RecordingController.cs
index 12b2422..00ba375 100644
--- a/src/Offstream.App/Services/RecordingController.cs
+++ b/src/Offstream.App/Services/RecordingController.cs
@@ -27,12 +27,23 @@ namespace Offstream.App.Services;
/// dialog.
///
///
-public sealed class RecordingController(IRecordingSessionFactory factory, SettingsDocument settings)
- : IAsyncDisposable
+public sealed class RecordingController : IAsyncDisposable
{
- private readonly IRecordingSessionFactory _factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ private readonly IRecordingSessionFactory _factory;
- private readonly SettingsDocument _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+ private readonly SettingsDocument _settings;
+
+ public RecordingController(IRecordingSessionFactory factory, SettingsDocument settings)
+ {
+ _factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+
+ // Everything on the format line is read from settings, so everything on it goes stale the
+ // moment a setting changes. Nothing else was telling the page: it re-read on start and on
+ // stop, which meant changing the format or the capture device while idle left the line
+ // describing the file the previous settings would have produced.
+ _settings.Changed += OnSettingsChanged;
+ }
///
/// Serialises start against stop.
@@ -53,6 +64,13 @@ public sealed class RecordingController(IRecordingSessionFactory factory, Settin
/// Raised whenever changes.
public event EventHandler? StateChanged;
+ /// Raised when may have changed under the page.
+ ///
+ /// Separate from , which promises to mean "IsRunning changed" and
+ /// would stop meaning it if settings borrowed it.
+ ///
+ public event EventHandler? OutputChanged;
+
/// A finished file landed in the library.
public event EventHandler? TrackSaved;
@@ -73,12 +91,26 @@ public sealed class RecordingController(IRecordingSessionFactory factory, Settin
/// What the output is, as the display prints it — MP3 320K 48K.
///
///
+ ///
/// Read from settings rather than remembered, so an idle page shows what pressing Start would
- /// produce and a running one shows what it is producing. The sample rate only appears once a
- /// session exists: it is the capture endpoint's rate, which is not knowable until the endpoint
- /// is open, and printing a guess on the one line of the page that claims to describe the file
- /// would be worse than printing nothing. Lossless formats omit the bitrate for the same
- /// reason — the setting exists but does not apply.
+ /// produce and a running one shows what it is producing. Lossless formats omit the bitrate
+ /// — the setting exists but does not apply.
+ ///
+ ///
+ /// The sample rate is shown while idle too (2026-09-02). It used to appear only once a
+ /// session existed, on the grounds that the capture endpoint's rate is not knowable until the
+ /// endpoint is open. That is wrong: the rate is a property of the endpoint, and
+ /// reports its mix format at any time. Withholding
+ /// it made one third of this line behave unlike the other two, which read from settings and
+ /// are always there — so the line grew a word on Start for no reason the user could see.
+ ///
+ ///
+ /// It is the same endpoint capture will open —
+ /// resolves the identical id through and takes its format
+ /// from the device — so the idle figure is the one the recording will use, not a guess. A
+ /// device that cannot be read prints nothing rather than a placeholder: this line describes
+ /// the file, and a wrong number on it is worse than a short one.
+ ///
///
public string FormatSummary
{
@@ -94,7 +126,7 @@ public string FormatSummary
parts.Add(string.Create(CultureInfo.CurrentCulture, $"{recording.BitrateKbps}K"));
}
- if (_session?.Level.Format.SampleRate is { } hertz)
+ if (SampleRateHertz() is { } hertz)
{
parts.Add(string.Create(CultureInfo.CurrentCulture, $"{hertz / 1000d:0.#}K"));
}
@@ -103,6 +135,32 @@ public string FormatSummary
}
}
+ ///
+ /// The rate audio is being captured at, or would be captured at from a standing start.
+ ///
+ ///
+ /// A running session already knows, and is asked first: its format came from the device when
+ /// the capture opened, and re-reading the endpoint could disagree with the file being written
+ /// if the default endpoint moved underneath us. Idle, the endpoint is asked directly.
+ ///
+ private int? SampleRateHertz()
+ {
+ if (_session?.Level.Format.SampleRate is { } running) return running;
+
+ try
+ {
+ using var device = AudioEndpoints.Resolve(_settings.Current.Recording.AudioEndpointDeviceId);
+ return device.AudioClient.MixFormat.SampleRate;
+ }
+ catch (Exception ex)
+ {
+ // Nothing here is worth interrupting anyone over: the line simply comes up one word
+ // short, and Start reports a missing endpoint properly when it matters.
+ Log.Debug(ex, "Could not read the capture endpoint's sample rate for the format line");
+ return null;
+ }
+ }
+
/// The library root, so paths can be shown relative to it rather than in full.
public string? OutputPath => _settings.Current.Output.Path;
@@ -197,6 +255,8 @@ public async ValueTask DisposeAsync()
if (_disposed) return;
_disposed = true;
+ _settings.Changed -= OnSettingsChanged;
+
await StopAsync();
_gate.Dispose();
@@ -259,6 +319,10 @@ private async ValueTask Release(RecordingSession session)
await session.DisposeAsync();
}
+ /// A setting was saved, so may no longer be current.
+ private void OnSettingsChanged(object? sender, EventArgs e) =>
+ OutputChanged?.Invoke(this, EventArgs.Empty);
+
///
/// Releases a session that stopped by itself — the recording timer elapsed, or the audio
/// endpoint went away mid-recording.
diff --git a/src/Offstream.App/ViewModels/RecordViewModel.cs b/src/Offstream.App/ViewModels/RecordViewModel.cs
index 07f061c..215a969 100644
--- a/src/Offstream.App/ViewModels/RecordViewModel.cs
+++ b/src/Offstream.App/ViewModels/RecordViewModel.cs
@@ -218,6 +218,7 @@ public RecordViewModel(InMemoryLogSink logSink, RecordingController controller)
controller.StateChanged += OnStateChanged;
controller.TrackSaved += OnTrackSaved;
controller.TrackEnriched += OnTrackEnriched;
+ controller.OutputChanged += OnOutputChanged;
// Seeds the format line, which describes what pressing Start would produce and so has
// something to say before anything is running.
@@ -546,6 +547,9 @@ private void OnProgress(object? sender, RecordingProgress progress) => Dispatch(
private void OnStateChanged(object? sender, EventArgs e) => Dispatch(Sync);
+ /// A setting changed, so the format line may no longer describe what Start would do.
+ private void OnOutputChanged(object? sender, EventArgs e) => Dispatch(Sync);
+
/// Adds a finished file to the session list and the totals.
private void OnTrackSaved(object? sender, TrackSavedEventArgs e) => Dispatch(() =>
{
diff --git a/src/Offstream.App/Views/Pages/RecordPage.xaml b/src/Offstream.App/Views/Pages/RecordPage.xaml
index 936dee0..03806fd 100644
--- a/src/Offstream.App/Views/Pages/RecordPage.xaml
+++ b/src/Offstream.App/Views/Pages/RecordPage.xaml
@@ -497,6 +497,7 @@
ItemsSource="{Binding Saved, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.ScrollUnit="Pixel"
VirtualizingPanel.VirtualizationMode="Recycling">