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">