using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Threading; using SlamCross.Controls; namespace SlamCross.Views; public partial class TrimWindow : Window { // Trim byte positions are offsets into s16le @ 22050 Hz mono (matches AudioService) private const int TrimSampleRate = 22050; private const int TrimBytesPerSample = 2; private double _totalDuration; private Process? _previewFf; private Process? _previewPa; private bool _isPreviewing; private bool _suppressChange; private string _filePath = ""; public bool Confirmed { get; private set; } public int StartPos { get; private set; } public int EndPos { get; private set; } public TrimWindow() { InitializeComponent(); } public TrimWindow(string filePath, int startPos, int endPos) { InitializeComponent(); _filePath = filePath; StartPos = startPos; EndPos = endPos; _ = LoadAsync(filePath, startPos, endPos); } private static double BytesToSec(int b) => b / (TrimSampleRate * (double)TrimBytesPerSample); private static int SecToBytes(double sec) => (int)(sec * TrimSampleRate * TrimBytesPerSample); private async Task LoadAsync(string filePath, int startPos, int endPos) { _totalDuration = await GetDurationAsync(filePath); double startSec = BytesToSec(startPos); double endSec = endPos > 0 ? BytesToSec(endPos) : _totalDuration; if (this.FindControl("DurationLabel") is TextBlock lbl) lbl.Text = $"Duration: {_totalDuration:F2}s | Drag to select trim region"; _suppressChange = true; if (this.FindControl("StartSec") is NumericUpDown sb) { sb.Maximum = (decimal)_totalDuration; sb.Value = (decimal)startSec; } if (this.FindControl("EndSec") is NumericUpDown eb) { eb.Maximum = (decimal)_totalDuration; eb.Value = (decimal)endSec; } _suppressChange = false; var peaks = await Task.Run(() => ComputePeaks(filePath, 4096)); var wf = this.FindControl("Waveform"); if (wf == null) return; wf.SetPeaks(peaks); wf.SetSelection( _totalDuration > 0 ? startSec / _totalDuration : 0, _totalDuration > 0 ? endSec / _totalDuration : 1); wf.SelectionChanged += (startNorm, endNorm) => { double s = startNorm * _totalDuration; double e = endNorm * _totalDuration; _suppressChange = true; if (this.FindControl("StartSec") is NumericUpDown nsb) nsb.Value = (decimal)s; if (this.FindControl("EndSec") is NumericUpDown neb) neb.Value = (decimal)e; _suppressChange = false; }; } private static async Task GetDurationAsync(string filePath) { try { var psi = new ProcessStartInfo("ffprobe") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; psi.ArgumentList.Add("-v"); psi.ArgumentList.Add("quiet"); psi.ArgumentList.Add("-show_entries"); psi.ArgumentList.Add("format=duration"); psi.ArgumentList.Add("-of"); psi.ArgumentList.Add("csv=p=0"); psi.ArgumentList.Add(filePath); using var proc = Process.Start(psi)!; var s = await proc.StandardOutput.ReadToEndAsync(); await proc.WaitForExitAsync(); return double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; } catch { return 0; } } private static (float Low, float High)[] ComputePeaks(string filePath, int binCount) { try { var psi = new ProcessStartInfo("ffmpeg") { UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; psi.ArgumentList.Add("-i"); psi.ArgumentList.Add(filePath); psi.ArgumentList.Add("-f"); psi.ArgumentList.Add("f32le"); psi.ArgumentList.Add("-ar"); psi.ArgumentList.Add("8000"); psi.ArgumentList.Add("-ac"); psi.ArgumentList.Add("1"); psi.ArgumentList.Add("-loglevel"); psi.ArgumentList.Add("quiet"); psi.ArgumentList.Add("pipe:1"); using var proc = Process.Start(psi)!; using var ms = new MemoryStream(); proc.StandardOutput.BaseStream.CopyTo(ms); proc.WaitForExit(); var bytes = ms.ToArray(); int totalSamples = bytes.Length / 4; if (totalSamples == 0) return Array.Empty<(float, float)>(); var samples = MemoryMarshal.Cast(bytes.AsSpan()); int actualBins = Math.Min(binCount, totalSamples); var peaks = new (float Low, float High)[actualBins]; for (int i = 0; i < actualBins; i++) { int binStart = (int)((long)i * totalSamples / actualBins); int binEnd = (int)((long)(i + 1) * totalSamples / actualBins); float lo = 0f, hi = 0f; for (int j = binStart; j < binEnd; j++) { float v = samples[j]; if (v < lo) lo = v; if (v > hi) hi = v; } peaks[i] = (lo, hi); } return peaks; } catch { return Array.Empty<(float, float)>(); } } private void Time_Changed(object? sender, NumericUpDownValueChangedEventArgs e) { if (_suppressChange || _totalDuration <= 0) return; var wf = this.FindControl("Waveform"); if (wf == null) return; double s = (double)(this.FindControl("StartSec")?.Value ?? 0m); double en = (double)(this.FindControl("EndSec")?.Value ?? (decimal)_totalDuration); wf.SetSelection(s / _totalDuration, en / _totalDuration); } private void StopPreview() { _isPreviewing = false; try { _previewFf?.Kill(); } catch { } try { _previewPa?.Kill(); } catch { } _previewFf = null; _previewPa = null; } private void Play_Click(object? sender, RoutedEventArgs e) { if (_isPreviewing) { StopPreview(); if (this.FindControl