Files
LlamaCasty/ytLive.Tests/AlertAudioPersistenceTests.cs
T
gramps b00bf48b9d feat(alerts): sound byte under Built-in video, and alert audio that reaches the device
Creator request: a sound-byte browse + label under Built-in video, with an option to loop the
sound while the clip plays. The video itself is always one pass to completion.

- Source gains AlertAudioPath and AlertAudioLoop, persisted (column + migration + INSERT +
  SELECT + reader, the whole chain — that chain is where the blur crashed once).
- The sound byte plays through MediaPlayer on the default output device; MediaEnded re-seeks to
  zero while looping, and StopClip kills it so a tail cannot bleed into the next alert.
- The clip's OWN soundtrack moved off the mixer and onto the device as well
  (Services/Audio/AlertAudioOutput: one WaveOutEvent + buffered float provider).

Why the move: the creator asked that alert audio — the clip's track or the added sound byte —
appear in the desktop sound bar. Only audio that reaches the default device is picked up by the
WASAPI loopback that feeds both the meter and the stream. The old EnqueueAlertAudio route put the
sound in the stream but left the meter flat and the room silent.

Tests: the sound byte and its loop flag round-trip through SQLite, and a fresh alert layer starts
with no sound byte and no loop. Release 0 warnings.
2026-11-01 14:13:57 -08:00

62 lines
2.1 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Xunit;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.Tests;
/// <summary>
/// The alert sound byte (creator ruling 2026-11-01): a file that plays with the alert clip, with an
/// optional loop. It is configured on the alert layer, so it has to survive a save/reload — the
/// same startup path that a missing SELECT column broke once already.
/// </summary>
public class AlertAudioPersistenceTests
{
[Fact]
public void The_sound_byte_and_its_loop_flag_survive_a_save_and_reload()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"llamacasty-alertaudio-{Guid.NewGuid():N}.db");
try
{
var scenes = new ObservableCollection<Scene> { new() { Name = SceneCatalog.Live } };
var alert = new Source
{
Name = "Stream Alerts",
Type = SourceType.AlertBox,
X = 620, Y = 430, Width = 680, Height = 200,
AlertAudioPath = @"C:\sounds\ding.wav",
AlertAudioLoop = true,
};
scenes[0].Elements.Add(alert);
using (var store = new LayoutStore(dbPath)) store.Save(scenes, null, null);
using (var reopened = new LayoutStore(dbPath))
{
var loaded = reopened.Load().SelectMany(s => s.Elements).OfType<Source>()
.Single(s => s.Type == SourceType.AlertBox);
Assert.Equal(@"C:\sounds\ding.wav", loaded.AlertAudioPath);
Assert.True(loaded.AlertAudioLoop);
Assert.Equal("ding.wav", loaded.AlertAudioFileDisplay);
Assert.True(loaded.HasAlertAudio);
}
}
finally
{
try { File.Delete(dbPath); } catch (IOException) { }
}
}
[Fact]
public void A_fresh_alert_layer_has_no_sound_byte_and_does_not_loop()
{
var alert = new Source { Type = SourceType.AlertBox };
Assert.False(alert.HasAlertAudio);
Assert.False(alert.AlertAudioLoop);
Assert.Equal("No sound byte", alert.AlertAudioFileDisplay);
}
}