Skip to content

Commit 5deb8d4

Browse files
committed
Update 5.5.0 - See changelog.txt for details
1 parent 72725eb commit 5deb8d4

22 files changed

+4213
-850
lines changed

Installer.iss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
; =====================================================
2-
; StreamTweak v4.4.0 - GitHub Release Installer
2+
; StreamTweak v5.0.0 - GitHub Release Installer
33
; =====================================================
44
#define MyAppName "StreamTweak"
5-
#define MyAppVersion "4.4.0"
5+
#define MyAppVersion "5.0.0"
66
#define MyAppPublisher "FoggyBytes"
77
#define MyAppExeName "StreamTweak.exe"
88
#define MyAppURL "https://github.com/FoggyBytes/StreamTweak"

README.md

Lines changed: 87 additions & 23 deletions
Large diffs are not rendered by default.

StreamTweak/App.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<MenuItem x:Name="StreamingModeMenuItem" Header="Start Streaming Mode" Click="MenuStreamingMode_Click"/>
1717
<MenuItem x:Name="AutoModeMenuItem" Header="Auto Mode: Enabled"
1818
IsCheckable="True" IsChecked="True" Click="MenuAutoMode_Click"/>
19-
<MenuItem x:Name="DolbyModeMenuItem" Header="Dolby Atmos: Disabled"
19+
<MenuItem x:Name="DolbyModeMenuItem" Header="Spatial Audio: Disabled"
2020
IsCheckable="True" IsChecked="False" Click="MenuDolbyMode_Click"/>
2121
<MenuItem x:Name="HdrModeMenuItem" Header="HDR: Off"
2222
IsCheckable="True" IsChecked="False" Click="MenuHdrMode_Click"/>

StreamTweak/App.xaml.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ public partial class App : Application
3333
// Managed apps — paths of apps killed at stream start, relaunched at stream end
3434
private List<string> _appsToRelaunch = new();
3535

36-
// Dolby audio monitoring
36+
// Spatial audio monitoring
3737
private readonly DolbyAudioMonitor _dolbyMonitor = new();
3838
private bool isAudioMonitorEnabled = false;
39+
private string _audioOutputDevice = "Steam Streaming Speakers";
40+
private SpatialAudioFormat _audioSpatialFormat = SpatialAudioFormat.DolbyAtmos;
3941

4042
// HDR tray state
4143
private bool _trayHdrEnabled = false;
@@ -85,7 +87,12 @@ protected override void OnStartup(StartupEventArgs e)
8587
var (mbps, connected) = GetCurrentSpeed();
8688
return connected ? mbps.ToString() : "UNKNOWN";
8789
};
88-
_bridge.StatsProvider = () => _metricsCollector.GetLatestSample().ToJson();
90+
_bridge.StatsProvider = () => _metricsCollector.GetLatestSample().ToJson();
91+
_bridge.AppStoresProvider = () => GameLibraryState.Current.ToAppStoresJson();
92+
93+
// Auto-sync game library in background if enabled
94+
if (GameLibraryState.Current.SyncEnabled)
95+
_ = GameLibraryService.PerformSyncAsync();
8996
}
9097

9198
private void LoadConfig()
@@ -115,8 +122,19 @@ private void LoadConfig()
115122

116123
if (root.TryGetProperty("AudioMonitorEnabled", out var audioEl))
117124
isAudioMonitorEnabled = audioEl.GetBoolean();
125+
126+
if (root.TryGetProperty("AudioOutputDevice", out var devEl))
127+
_audioOutputDevice = devEl.GetString() ?? "Steam Streaming Speakers";
128+
129+
if (root.TryGetProperty("AudioSpatialFormat", out var fmtEl))
130+
_audioSpatialFormat = fmtEl.GetString() == "WindowsSonic"
131+
? SpatialAudioFormat.WindowsSonic
132+
: SpatialAudioFormat.DolbyAtmos;
118133
}
119134
catch { }
135+
136+
_dolbyMonitor.TargetDeviceName = _audioOutputDevice;
137+
_dolbyMonitor.SpatialFormat = _audioSpatialFormat;
120138
}
121139

122140
private bool IsCurrentSpeed1G()
@@ -223,8 +241,8 @@ private void UpdateTrayMenu()
223241
{
224242
DolbyModeMenuItem.IsChecked = isAudioMonitorEnabled;
225243
DolbyModeMenuItem.Header = isAudioMonitorEnabled
226-
? "Dolby Atmos: Enabled"
227-
: "Dolby Atmos: Disabled";
244+
? "Spatial Audio: Enabled"
245+
: "Spatial Audio: Disabled";
228246
}
229247

230248
if (HdrModeMenuItem != null)
@@ -505,6 +523,18 @@ private void OpenSettings()
505523
UpdateTrayMenu();
506524
};
507525

526+
settingsWindow.AudioDeviceChanged += (s, args) =>
527+
{
528+
_audioOutputDevice = settingsWindow.SelectedAudioDevice;
529+
_dolbyMonitor.TargetDeviceName = _audioOutputDevice;
530+
};
531+
532+
settingsWindow.AudioFormatChanged += (s, args) =>
533+
{
534+
_audioSpatialFormat = settingsWindow.SelectedAudioFormat;
535+
_dolbyMonitor.SpatialFormat = _audioSpatialFormat;
536+
};
537+
508538
settingsWindow.Closed += (s, args) =>
509539
{
510540
LoadConfig();
@@ -514,7 +544,7 @@ private void OpenSettings()
514544

515545
settingsWindow.SyncAudioMonitorState(isAudioMonitorEnabled);
516546
settingsWindow.SyncDolbyMonitorStatus(
517-
_dolbyMonitor.IsEnabled ? "Monitoring for Steam Streaming Speakers…" : "Disabled.");
547+
_dolbyMonitor.IsEnabled ? "Ready — waiting for next stream…" : "Disabled.");
518548

519549
settingsWindow.Show();
520550
}
@@ -664,6 +694,7 @@ private async void HandleAutoStreamStart()
664694
_isAutoSessionActive = true;
665695
SessionLogger.StartSession("Auto", capturedOriginalSpeed);
666696
settingsWindow?.RefreshSessionHistory();
697+
settingsWindow?.SetSessionActive(true);
667698
}
668699
catch { }
669700
}
@@ -703,6 +734,7 @@ private async void HandleAutoStreamStop(string endReason = "User")
703734
_appsToRelaunch.Clear();
704735
_isAutoSessionActive = false;
705736
settingsWindow?.RefreshSessionHistory();
737+
settingsWindow?.SetSessionActive(false);
706738
}
707739
}
708740
catch { }
@@ -828,11 +860,13 @@ private void OnBridgeRestoreRequested()
828860

829861
#endregion
830862

831-
#region Dolby Audio Monitoring
863+
#region Spatial Audio Monitoring
832864

833865
private void StartDolbyMonitor()
834866
{
835867
if (!isAudioMonitorEnabled || _dolbyMonitor.IsEnabled) return;
868+
_dolbyMonitor.TargetDeviceName = _audioOutputDevice;
869+
_dolbyMonitor.SpatialFormat = _audioSpatialFormat;
836870
_dolbyMonitor.Enable();
837871
}
838872

StreamTweak/CoverArtFetcher.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using System.Windows.Media.Imaging;
9+
10+
namespace StreamTweak
11+
{
12+
/// <summary>
13+
/// Downloads and caches game cover art images.
14+
/// Currently supports Steam (library_600x900.jpg from Cloudflare CDN).
15+
/// Cover art is cached in %LOCALAPPDATA%\StreamTweak\covers\.
16+
/// </summary>
17+
public static class CoverArtFetcher
18+
{
19+
private static readonly HttpClient _http = new HttpClient
20+
{
21+
Timeout = TimeSpan.FromSeconds(10)
22+
};
23+
24+
// ── Public API ────────────────────────────────────────────────────────
25+
26+
/// <summary>
27+
/// Downloads missing cover art for all games in parallel (up to 5 concurrent).
28+
/// Already-cached images are skipped. Failures are silently ignored.
29+
/// </summary>
30+
public static async Task FetchAllAsync(IEnumerable<DiscoveredGame> games, string cacheDir)
31+
{
32+
Directory.CreateDirectory(cacheDir);
33+
34+
var toFetch = games.Where(g => GetDownloadUrl(g) != null && GetCachedPath(g, cacheDir) == null).ToList();
35+
if (toFetch.Count == 0) return;
36+
37+
using var semaphore = new SemaphoreSlim(5);
38+
var tasks = toFetch.Select(g => FetchOneAsync(g, cacheDir, semaphore));
39+
await Task.WhenAll(tasks);
40+
}
41+
42+
/// <summary>
43+
/// Returns the expected cache file path for a game regardless of whether it exists yet.
44+
/// Returns null for games with no deterministic filename (e.g., empty name).
45+
/// </summary>
46+
public static string? GetCacheFilePath(DiscoveredGame game, string cacheDir)
47+
{
48+
string? fileName = GetCacheFileName(game);
49+
return fileName == null ? null : Path.Combine(cacheDir, fileName);
50+
}
51+
52+
/// <summary>
53+
/// Returns the full path to the cached cover image for a game, or null if not yet cached.
54+
/// </summary>
55+
public static string? GetCachedPath(DiscoveredGame game, string cacheDir)
56+
{
57+
string? path = GetCacheFilePath(game, cacheDir);
58+
return (path != null && File.Exists(path)) ? path : null;
59+
}
60+
61+
// ── Internals ─────────────────────────────────────────────────────────
62+
63+
private static async Task FetchOneAsync(DiscoveredGame game, string cacheDir, SemaphoreSlim semaphore)
64+
{
65+
await semaphore.WaitAsync();
66+
try
67+
{
68+
string? url = GetDownloadUrl(game);
69+
if (url == null) return;
70+
71+
string? fileName = GetCacheFileName(game);
72+
if (fileName == null) return;
73+
74+
string cachePath = Path.Combine(cacheDir, fileName);
75+
if (File.Exists(cachePath)) return; // already cached
76+
77+
byte[] bytes = await _http.GetByteArrayAsync(url);
78+
79+
// Sunshine/Vibeshine requires PNG for image-path.
80+
// Steam CDN delivers JPEG → decode and re-encode as PNG.
81+
using var jpegStream = new MemoryStream(bytes);
82+
var decoder = BitmapDecoder.Create(
83+
jpegStream,
84+
BitmapCreateOptions.None,
85+
BitmapCacheOption.OnLoad);
86+
var frame = decoder.Frames[0];
87+
88+
using var pngStream = new FileStream(cachePath, FileMode.Create, FileAccess.Write);
89+
var encoder = new PngBitmapEncoder();
90+
encoder.Frames.Add(BitmapFrame.Create(frame));
91+
encoder.Save(pngStream);
92+
}
93+
catch { /* silently skip on network/IO errors */ }
94+
finally
95+
{
96+
semaphore.Release();
97+
}
98+
}
99+
100+
private static string? GetDownloadUrl(DiscoveredGame game)
101+
{
102+
// Prefer API-provided URL from IStoreBrowseService (exact, always correct)
103+
if (!string.IsNullOrEmpty(game.CoverUrl))
104+
return game.CoverUrl;
105+
106+
// Legacy CDN fallback for Steam games without an API-provided URL
107+
return game.Store switch
108+
{
109+
"Steam" when game.SteamAppId != null =>
110+
$"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.SteamAppId}/library_600x900.jpg",
111+
_ => null
112+
};
113+
}
114+
115+
private static string? GetCacheFileName(DiscoveredGame game)
116+
{
117+
if (game.SteamAppId != null)
118+
return $"steam_{game.SteamAppId}.png";
119+
120+
string store = game.Store.Replace(" ", "").ToLowerInvariant();
121+
122+
// Non-Steam: prefer StoreId (stable, deterministic) over sanitized name
123+
if (game.StoreId != null)
124+
{
125+
string safeId = new string(game.StoreId
126+
.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.')
127+
.ToArray());
128+
if (!string.IsNullOrEmpty(safeId))
129+
return $"{store}_{safeId}.png";
130+
}
131+
132+
// Fallback: sanitized name
133+
string safe = new string(game.Name
134+
.Where(c => char.IsLetterOrDigit(c) || c == '-')
135+
.ToArray());
136+
return string.IsNullOrEmpty(safe) ? null : $"{store}_{safe}.png";
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)