TASK 9 audio milestone: real stream audio + voice filters + auto-duck + free TRAX music — the mixer now feeds the encoder real audio (ffmpeg reads a Windows named pipe, -f f32le -ar 48000 -ac 2 -i \.\pipe\ytllive_audio, replacing anullsrc silence; explicit -map 0:v -map 1:a via EncoderOptions.AudioPipeName), with honest gains reaching the stream (MicVolume scales mic, GameAudioVolume + mute scale loopback, Func<double> seams on AudioMixer), an always-on pure-C# voice chain on the mic BEFORE the meter and mix (LowShelfFilter 120Hz +4dB → HighShelfFilter 8kHz +3dB → NoiseGate 0.005/hysteresis 0.5 → Compressor 0.5 4:1, all TDF2), AutoDucker (mic RMS>0.02 → loopback ×0.25, attack 0.05/release 0.005), and TRAX free background music (MusicPlayer = MediaFoundationReader → VolumeWaveProvider16 at fixed 0.20 → WaveOutEvent via the new sibling NAudio.WinMM 2.2.1 package; plays to the default device so the existing loopback carries it, ducked with game; footer TRAX button with red/yellow/green status dot, left-click toggles/picks, right-click opens the OpenFileDialog picker, tooltip shows the track name; persisted via schema v9 single-row Music). Build-time deviations from the plan (recorded in ai.md + TASKS.md): WasapiCapture in NAudio 2.2.1 exposes no overridable GetDefaultMixFormat so sources run device mix format and the mixer's TinyResampler normalizes any rate to 48kHz (resampler IS the design); FfmpegEncoder.cs untouched — MainViewModel.StopStream calls _audioMixer.StopLive() (pipe EOF) before the frame pump stops; sound-bar relabelled 'Desktop Audio', IsGameAudioBarVisible = game sound OR music playing. Tests: new AudioPipelineTests (DSP/ring-buffer/ducker/resampler units + the ONE integration test Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe reading real pipe bytes; ring-buffer overwrite bug found + fixed), FfmpegEncoderTests/LayoutStorePersistenceTests updated — 196 tests passing, 0 warnings
This commit is contained in:
+45
-2
@@ -18,6 +18,10 @@ public class LayoutStore : IDisposable
|
||||
/// <summary>The app-wide social bar config loaded with the last Load() (null = never defined).</summary>
|
||||
public SocialsConfig? Socials { get; private set; }
|
||||
|
||||
/// <summary>The app-wide background music (TASK 9 TRAX) loaded with the last
|
||||
/// Load() (null = no track chosen).</summary>
|
||||
public Music? Music { get; private set; }
|
||||
|
||||
public LayoutStore(string path)
|
||||
{
|
||||
ActivePath = path;
|
||||
@@ -124,6 +128,13 @@ public class LayoutStore : IDisposable
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS Music (
|
||||
Id TEXT PRIMARY KEY,
|
||||
TrackPath TEXT NOT NULL,
|
||||
IsEnabled INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""",
|
||||
};
|
||||
foreach (var sql in statements)
|
||||
{
|
||||
@@ -141,7 +152,7 @@ public class LayoutStore : IDisposable
|
||||
MigrateToV3();
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "PRAGMA user_version = 8;";
|
||||
cmd.CommandText = "PRAGMA user_version = 9;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -406,6 +417,7 @@ public class LayoutStore : IDisposable
|
||||
{
|
||||
Webcam = null;
|
||||
Socials = null;
|
||||
Music = null;
|
||||
var scenes = new List<Scene>();
|
||||
var sourcesByScene = new Dictionary<string, List<Source>>();
|
||||
var configsByScene = new Dictionary<string, List<WebcamSceneConfig>>();
|
||||
@@ -473,6 +485,20 @@ public class LayoutStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "SELECT TrackPath, IsEnabled FROM Music LIMIT 1;";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
if (reader.Read())
|
||||
{
|
||||
Music = new Music
|
||||
{
|
||||
TrackPath = reader.GetString(0),
|
||||
IsEnabled = reader.GetInt32(1) != 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = """
|
||||
@@ -559,7 +585,7 @@ public class LayoutStore : IDisposable
|
||||
return scenes;
|
||||
}
|
||||
|
||||
public void Save(IEnumerable<Scene> scenes, Webcam? webcam, SocialsConfig? socials)
|
||||
public void Save(IEnumerable<Scene> scenes, Webcam? webcam, SocialsConfig? socials, Music? music = null)
|
||||
{
|
||||
using var tx = _connection.BeginTransaction();
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
@@ -593,6 +619,12 @@ public class LayoutStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM Music;";
|
||||
cmd.Transaction = tx;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM Scene;";
|
||||
cmd.Transaction = tx;
|
||||
@@ -796,6 +828,17 @@ public class LayoutStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
if (music != null && !string.IsNullOrWhiteSpace(music.TrackPath))
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "INSERT INTO Music (Id, TrackPath, IsEnabled) VALUES ($id, $path, $enabled);";
|
||||
cmd.Transaction = tx;
|
||||
cmd.Parameters.AddWithValue("$id", Guid.NewGuid().ToString());
|
||||
cmd.Parameters.AddWithValue("$path", music.TrackPath);
|
||||
cmd.Parameters.AddWithValue("$enabled", music.IsEnabled ? 1 : 0);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM Asset WHERE Id NOT IN (SELECT AssetId FROM Source WHERE AssetId IS NOT NULL);";
|
||||
|
||||
Reference in New Issue
Block a user