From e43b1f30cdee810b33ca28e3f0f3698f6a8104c4 Mon Sep 17 00:00:00 2001 From: Mute Date: Sat, 8 Aug 2026 19:52:56 +0000 Subject: [PATCH] Remove music button and MP3 decode, keep live capture only - Remove Music button, OnMusic/RunMusicAsync/StopMusic and all music fields - MusicAnalyzer: remove Analyze, DecodeToMono, MusicPattern record; keep ExtractFeatures/MapPitchToPeriod/BuildWaveFrame/TickFeature (used by LiveCapture) - Remove unused NAudio.Wave and System.Diagnostics imports from MainForm - Fix LiveCapture overlap: sliding window with 50% overlap (was dropping every other frame) - Buttons resized to 80px (Test/Live/Pattern/Stop All) --- LiveCapture.cs | 20 ++++++- MainForm.cs | 141 ++++------------------------------------------- MusicAnalyzer.cs | 101 --------------------------------- 3 files changed, 27 insertions(+), 235 deletions(-) diff --git a/LiveCapture.cs b/LiveCapture.cs index 6fee0c6..27c90ec 100644 --- a/LiveCapture.cs +++ b/LiveCapture.cs @@ -91,6 +91,7 @@ public class LiveCapture : IDisposable var window = new FftSharp.Windows.Hanning(); var buffer = new double[MusicAnalyzer.WindowSize]; var tf = new MusicAnalyzer.TickFeature(); + double[]? overlap = null; // last HopSize samples from previous window while (_running) { @@ -98,10 +99,23 @@ public class LiveCapture : IDisposable lock (_bufferLock) { - if (_sampleBuffer.Count >= MusicAnalyzer.WindowSize) + int needed = overlap != null ? MusicAnalyzer.HopSize : MusicAnalyzer.WindowSize; + if (_sampleBuffer.Count >= needed) { - for (int i = 0; i < MusicAnalyzer.WindowSize; i++) - buffer[i] = _sampleBuffer.Dequeue(); + if (overlap != null) + { + Array.Copy(overlap, 0, buffer, 0, MusicAnalyzer.HopSize); + for (int i = 0; i < MusicAnalyzer.HopSize; i++) + buffer[MusicAnalyzer.HopSize + i] = _sampleBuffer.Dequeue(); + } + else + { + for (int i = 0; i < MusicAnalyzer.WindowSize; i++) + buffer[i] = _sampleBuffer.Dequeue(); + } + // Save last HopSize samples for next window's overlap + overlap = new double[MusicAnalyzer.HopSize]; + Array.Copy(buffer, MusicAnalyzer.HopSize, overlap, 0, MusicAnalyzer.HopSize); windowData = buffer; } } diff --git a/MainForm.cs b/MainForm.cs index b4c2ccf..7c225a1 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.Drawing.Drawing2D; -using System.Diagnostics; using NAudio.CoreAudioApi; -using NAudio.Wave; namespace Substation; @@ -48,17 +46,13 @@ public class MainForm : Form readonly Icon _iconHot; string _trayState = ""; - readonly Button _btnMusic; readonly Button _btnLive; readonly Button _btnPattern; readonly Label _lblTrack; readonly Label _lblAudioDev; readonly ComboBox _cboAudioDev; - bool _isMusicRunning; bool _isLiveRunning; bool _isPatternRunning; - WaveOutEvent? _audioOut; - Stopwatch? _musicStopwatch; LiveCapture? _liveCapture; public MainForm(CoyoteDevice device, State state, Server server) @@ -125,39 +119,31 @@ public class MainForm : Form { Text = "Test", Location = new Point(16, 96), - Size = new Size(68, 32) + Size = new Size(80, 32) }; _btnTest.Click += OnTest; - _btnMusic = new Button - { - Text = "Music", - Location = new Point(92, 96), - Size = new Size(68, 32) - }; - _btnMusic.Click += OnMusic; - _btnLive = new Button { Text = "Live", - Location = new Point(168, 96), - Size = new Size(68, 32) + Location = new Point(104, 96), + Size = new Size(80, 32) }; _btnLive.Click += OnLive; _btnPattern = new Button { Text = "Pattern", - Location = new Point(244, 96), - Size = new Size(68, 32) + Location = new Point(192, 96), + Size = new Size(80, 32) }; _btnPattern.Click += OnPattern; _btnStop = new Button { Text = "Stop All", - Location = new Point(320, 96), - Size = new Size(76, 32) + Location = new Point(280, 96), + Size = new Size(80, 32) }; _btnStop.Click += OnStop; @@ -307,7 +293,7 @@ public class MainForm : Form _chkScaleB.CheckedChanged += OnLimitChanged; OnLimitChanged(null, EventArgs.Empty); - Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnMusic, _btnLive, _btnPattern, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB }); + Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnLive, _btnPattern, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB }); // Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange _iconNeutral = CreateVoltageIcon(Color.Gray); @@ -475,9 +461,8 @@ public class MainForm : Form void RefreshButtonStates() { - bool busy = IsTestRunning || _isMusicRunning || _isLiveRunning || _isPatternRunning; + bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning; _btnTest.Enabled = !_server.HasClient && !busy; - _btnMusic.Enabled = !_server.HasClient && !busy; _btnLive.Enabled = !_server.HasClient && !busy; _btnPattern.Enabled = !_server.HasClient && !busy; _btnStop.Enabled = true; @@ -487,12 +472,7 @@ public class MainForm : Form { if (IsDisposed) return; - if (_isMusicRunning && _musicStopwatch != null) - { - var elapsed = _musicStopwatch.Elapsed; - _lblTrack.Text = $"Playing {elapsed:mm\\:ss} / {_musicTotalTime:mm\\:ss} {Path.GetFileName(_musicFilePath)}"; - } - else if (_isLiveRunning) + if (_isLiveRunning) { _lblTrack.Text = "Live capture"; } @@ -564,7 +544,6 @@ public class MainForm : Form void OnStop(object? sender, EventArgs e) { - StopMusic(); StopLive(); StopPattern(); _state.SetStrength('A', 0); @@ -590,106 +569,6 @@ public class MainForm : Form _state.SwapChannels = _chkSwap.Checked; } - string _musicFilePath = ""; - TimeSpan _musicTotalTime; - - void OnMusic(object? sender, EventArgs e) - { - if (_isMusicRunning || _server.HasClient) return; - - using var dlg = new OpenFileDialog - { - Filter = "MP3 files (*.mp3)|*.mp3|All files (*.*)|*.*", - Title = "Select music to drive e-stim" - }; - if (dlg.ShowDialog() != DialogResult.OK) return; - - _musicFilePath = dlg.FileName; - _musicTotalTime = TimeSpan.Zero; - _isMusicRunning = true; - _btnMusic.Enabled = false; - _btnTest.Enabled = false; - _lblTrack.Text = $"Analyzing... {Path.GetFileName(_musicFilePath)}"; - - var filePath = _musicFilePath; - Task.Run(() => RunMusicAsync(filePath)); - } - - async Task RunMusicAsync(string filePath) - { - MusicPattern? pattern = null; - try - { - var progress = new Progress(pct => - { - if (!IsDisposed) - _lblTrack.Text = $"Analyzing... {pct}% {Path.GetFileName(filePath)}"; - }); - pattern = await Task.Run(() => MusicAnalyzer.Analyze(filePath, (IProgress)progress)); - } - catch (Exception ex) - { - BeginInvoke(() => - { - _lblTrack.Text = $"Analysis failed: {ex.Message}"; - _isMusicRunning = false; - RefreshButtonStates(); - }); - return; - } - - _musicTotalTime = pattern.Duration; - - // Set a moderate strength ceiling for music - _state.SetStrength('A', 60); - _state.SetStrength('B', 60); - - // Clear any existing patterns and enqueue all music frames - _state.Stop('A'); - _state.Stop('B'); - _state.EnqueueStream('A', pattern.ChannelA); - _state.EnqueueStream('B', pattern.ChannelB); - - // Start audio playback + stopwatch simultaneously - try - { - var reader = new AudioFileReader(filePath); - _audioOut = new WaveOutEvent(); - _audioOut.Init(reader); - _audioOut.PlaybackStopped += (_, _) => BeginInvoke(StopMusic); - _musicStopwatch = Stopwatch.StartNew(); - _audioOut.Play(); - } - catch (Exception ex) - { - Console.Error.WriteLine($"[music] playback failed: {ex.Message}"); - _musicStopwatch = Stopwatch.StartNew(); - } - - BeginInvoke(() => _lblTrack.Text = $"Playing 00:00 / {_musicTotalTime:mm\\:ss} {Path.GetFileName(filePath)}"); - } - - void StopMusic() - { - if (_audioOut != null) - { - try { _audioOut.Stop(); } catch { } - try { _audioOut.Dispose(); } catch { } - _audioOut = null; - } - _musicStopwatch = null; - - if (_isMusicRunning) - { - _isMusicRunning = false; - if (!IsDisposed) - { - _lblTrack.Text = ""; - RefreshButtonStates(); - } - } - } - void PopulateAudioDevices() { using var enumerator = new MMDeviceEnumerator(); diff --git a/MusicAnalyzer.cs b/MusicAnalyzer.cs index e90eb7c..9588f1f 100644 --- a/MusicAnalyzer.cs +++ b/MusicAnalyzer.cs @@ -1,11 +1,8 @@ using System.Numerics; using FftSharp; -using NAudio.Wave; namespace Substation; -public record MusicPattern(List ChannelA, List ChannelB, TimeSpan Duration); - public static class MusicAnalyzer { public const int WindowSize = 2048; @@ -15,79 +12,6 @@ public static class MusicAnalyzer const double RhythmLow = 20, RhythmHigh = 250; const double MelodyLow = 300, MelodyHigh = 4000; - public static MusicPattern Analyze(string mp3Path, IProgress? progress = null) - { - const int sampleRate = 44100; - var samples = DecodeToMono(mp3Path, sampleRate); - var duration = TimeSpan.FromSeconds((double)samples.Length / sampleRate); - - var tickCount = (int)Math.Ceiling((double)samples.Length / sampleRate / TickDuration); - var channelA = new List(tickCount); - var channelB = new List(tickCount); - - var window = new FftSharp.Windows.Hanning(); - var buffer = new double[WindowSize]; - int fftsPerTick = (int)Math.Round(TickDuration * sampleRate / HopSize); - - double[]? prevRhythmMag = null; - double maxFlux = 0; - double maxMelodyEnergy = 0; - - // First pass: collect per-tick features - var tickFeatures = new List(tickCount); - int pos = 0; - int fftIndex = 0; - - while (pos + WindowSize <= samples.Length) - { - for (int i = 0; i < WindowSize; i++) - buffer[i] = samples[pos + i]; - - window.ApplyInPlace(buffer); - var spectrum = FFT.Forward(buffer); - var mag = FFT.Magnitude(spectrum); - - var (rhythmEnergy, rhythmFlux, melodyEnergy, melodyFreq) = - ExtractFeatures(mag, prevRhythmMag, sampleRate); - - if (rhythmFlux > maxFlux) maxFlux = rhythmFlux; - if (melodyEnergy > maxMelodyEnergy) maxMelodyEnergy = melodyEnergy; - - prevRhythmMag = mag; - - int tickIdx = fftIndex / Math.Max(1, fftsPerTick); - while (tickFeatures.Count <= tickIdx) - tickFeatures.Add(new TickFeature()); - - var tf = tickFeatures[tickIdx]; - tf.RhythmFlux = Math.Max(tf.RhythmFlux, rhythmFlux); - tf.MelodyEnergy += melodyEnergy; - tf.MelodyFreqSamples.Add(melodyFreq); - tf.MelodyCount++; - - pos += HopSize; - fftIndex++; - - if (progress != null && fftIndex % 50 == 0) - { - var pct = (int)((double)pos / samples.Length * 100); - progress.Report(pct); - } - } - - progress?.Report(100); - - // Second pass: normalize and build WaveFrames - foreach (var tf in tickFeatures) - { - var (frameA, frameB) = BuildWaveFrame(tf, maxFlux, maxMelodyEnergy); - channelA.Add(frameA); - channelB.Add(frameB); - } - - return new MusicPattern(channelA, channelB, duration); - } - public static (double rhythmEnergy, double rhythmFlux, double melodyEnergy, double melodyFreq) ExtractFeatures(double[] magnitude, double[]? prevRhythmMag, int sampleRate) { @@ -147,31 +71,6 @@ public static class MusicAnalyzer return Math.Clamp(ms, 10, 1000); } - static double[] DecodeToMono(string mp3Path, int sampleRate) - { - using var reader = new Mp3FileReader(mp3Path); - var format = new WaveFormat(sampleRate, 16, 1); - using var resampler = new MediaFoundationResampler(reader, format); - resampler.ResamplerQuality = 60; - - var sampleList = new List(); - var buffer = new byte[sampleRate * 2]; // 1s worth of 16-bit mono - int read; - while ((read = resampler.Read(buffer, 0, buffer.Length)) > 0) - { - for (int i = 0; i < read; i += 2) - { - short sample = (short)(buffer[i] | (buffer[i + 1] << 8)); - sampleList.Add(sample / 32768f); - } - } - - var result = new double[sampleList.Count]; - for (int i = 0; i < sampleList.Count; i++) - result[i] = sampleList[i]; - return result; - } - public class TickFeature { public double RhythmFlux;