Speed change on the fly is almost working. Still hangs sometime.

This commit is contained in:
Alexander Shabarshov 2026-08-20 08:53:35 +01:00
parent a181acb706
commit 5e654f62f6
11 changed files with 247 additions and 184 deletions

View File

@ -4,7 +4,7 @@ namespace ABStemPlayer.ViewModels;
public sealed class MixerViewModel public sealed class MixerViewModel
{ {
public ObservableCollection<StemChannelViewModel> Stems { get; } = new(); public ObservableCollection<StemChannelViewModel> Stems { get; } = [];
public MixerViewModel() public MixerViewModel()
{ {

View File

@ -54,8 +54,8 @@ public sealed partial class PlaybackViewModel : ObservableObject
[ObservableProperty] private TimeSpan _totalTime; [ObservableProperty] private TimeSpan _totalTime;
[ObservableProperty] private MixerViewModel _mixer = null!; [ObservableProperty] private MixerViewModel _mixer = null!;
public ObservableCollection<SegmentViewModel> Segments { get; } = new(); public ObservableCollection<SegmentViewModel> Segments { get; } = [];
public ObservableCollection<WaveformBandViewModel> Bands { get; } = new(); public ObservableCollection<WaveformBandViewModel> Bands { get; } = [];
private TimeSpan? _loopA; private TimeSpan? _loopA;

View File

@ -25,11 +25,11 @@ public partial class WaveformBandViewModel : ObservableObject
[ObservableProperty] private double _canvasHeight; [ObservableProperty] private double _canvasHeight;
[ObservableProperty] private Geometry? _waveformGeometry; [ObservableProperty] private Geometry? _waveformGeometry;
public ObservableCollection<WaveformBar> WaveformBars { get; } = new(); public ObservableCollection<WaveformBar> WaveformBars { get; } = [];
[ObservableProperty] private double _playbackX; [ObservableProperty] private double _playbackX;
public ObservableCollection<SegmentViewModel> Segments { get; } = new(); public ObservableCollection<SegmentViewModel> Segments { get; } = [];
public TimeSpan Duration { get; set; } public TimeSpan Duration { get; set; }
public string DurationFormatted => Duration.ToString("mm\\:ss"); public string DurationFormatted => Duration.ToString("mm\\:ss");

View File

@ -1,8 +1,11 @@
using System.Runtime.InteropServices;
namespace AudioCore.Impl; namespace AudioCore.Impl;
public class AudioBuffer<T> : IDisposable public class AudioBuffer<T> : IDisposable where T : unmanaged
{ {
private readonly GenericBufferPool<T> _owner; private readonly GenericBufferPool<T> _owner;
private static GenericBufferPool<byte> _bytePool = new GenericBufferPool<byte>();
private bool _disposed; private bool _disposed;
public T[] Samples { get; } public T[] Samples { get; }
@ -24,4 +27,19 @@ public class AudioBuffer<T> : IDisposable
_owner.Return(Samples); _owner.Return(Samples);
_disposed = true; _disposed = true;
} }
public async Task WriteAsync(Stream stream, CancellationToken token)
{
using var outBuf = _bytePool.Rent(Samples.Length * sizeof(T));
var src = Samples.AsSpan(0, Length);
var dst = outBuf.Span;
MemoryMarshal.Cast<T, byte>(src).CopyTo(dst);
await stream.WriteAsync(outBuf.Samples, 0, dst.Length, token)
.ConfigureAwait(false);
}
} }

View File

@ -1,6 +1,7 @@
using System.Buffers; using System.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using static AudioCore.Models.Tracer;
namespace AudioCore.Impl; namespace AudioCore.Impl;
@ -19,15 +20,15 @@ public sealed class FfmpegProcess : IDisposable
public FfmpegProcess(string name, string commandLine, bool redirectOutput = true, bool redirectInput = true) public FfmpegProcess(string name, string commandLine, bool redirectOutput = true, bool redirectInput = true)
{ {
_name = name; _name = name;
_commandLine = commandLine; _commandLine = commandLine;
_redirectOutput = redirectOutput; _redirectOutput = redirectOutput;
_redirectInput = redirectInput; _redirectInput = redirectInput;
} }
public void StartProcess() public void StartProcess()
{ {
Debug.WriteLine($"{_name}: Starting ffmpeg process: {_commandLine.Replace("\r", "").Replace("\n", " ").Replace("\t", " ")}"); Msg($"{_name}: Starting ffmpeg process: {_commandLine.Replace("\r", "").Replace("\n", " ").Replace("\t", " ")}");
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@ -119,7 +120,7 @@ public sealed class FfmpegProcess : IDisposable
private void DisposeProcessOnly() private void DisposeProcessOnly()
{ {
if (Proc != null) if (Proc != null)
Debug.WriteLine($"{_name}: Disposing ffmpeg process"); Msg($"{_name}: Disposing ffmpeg process");
try { Stdout?.Dispose(); Stdout = null; } catch { } try { Stdout?.Dispose(); Stdout = null; } catch { }
try { Stdin?.Dispose(); Stdin = null; } catch { } try { Stdin?.Dispose(); Stdin = null; } catch { }

View File

@ -2,7 +2,7 @@ using System.Buffers;
namespace AudioCore.Impl; namespace AudioCore.Impl;
public class GenericBufferPool<T> public class GenericBufferPool<T> where T : unmanaged
{ {
private readonly ArrayPool<T> _pool = ArrayPool<T>.Shared; private readonly ArrayPool<T> _pool = ArrayPool<T>.Shared;

View File

@ -1,5 +1,4 @@
using System.Diagnostics; using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using static AudioCore.Models.Tracer; using static AudioCore.Models.Tracer;
namespace AudioCore.Impl; namespace AudioCore.Impl;
@ -8,7 +7,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
{ {
private readonly AudioBufferPool _pool; private readonly AudioBufferPool _pool;
private readonly int _sampleRate; private readonly int _sampleRate;
private long[] _sourcePositions; private readonly long[] _sourcePositions;
private sealed class SourceChunkInfo private sealed class SourceChunkInfo
{ {
@ -17,15 +16,22 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
} }
private readonly Queue<SourceChunkInfo>[] _pendingInput; private readonly Queue<SourceChunkInfo>[] _pendingInput;
private double[] _fractionalInput; private readonly double[] _fractionalInput;
private readonly List<StemProcess> _stemProcesses = [];
private readonly int _stemCount;
private int _activeIo; // Interlocked counter
private float _speed = 1.0f;
private CancellationTokenSource? _cts;
private CancellationToken _token;
private Task? _readerTask;
// One RubberBand/ffmpeg process per stem (each is stereo: 2 channels) // One RubberBand/ffmpeg process per stem (each is stereo: 2 channels)
private sealed class StemProcess : IDisposable private sealed class StemProcess : IDisposable
{ {
public readonly int StemIndex; public readonly int StemIndex;
public FfmpegProcess? Ff; public FfmpegProcess? Ff;
public Stream? Stdin; public Stream? Stdin;
public Stream? Stdout; public Stream? Stdout;
public BlockingRingBuffer Ring; public BlockingRingBuffer Ring;
public StemProcess(int stemIndex, int sampleRate) public StemProcess(int stemIndex, int sampleRate)
@ -45,13 +51,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
} }
} }
private readonly List<StemProcess> _stemProcesses = new();
private readonly int _stemCount;
private float _speed = 1.0f;
private CancellationTokenSource? _cts;
private CancellationToken _token;
private Task? _readerTask;
public RubberBandTimeStretchEngine(AudioBufferPool pool, int sampleRate = 44100, int stemCount = 6) public RubberBandTimeStretchEngine(AudioBufferPool pool, int sampleRate = 44100, int stemCount = 6)
{ {
@ -59,25 +59,27 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
_sampleRate = sampleRate; _sampleRate = sampleRate;
_stemProcesses.Capacity = stemCount; _stemProcesses.Capacity = stemCount;
_stemCount = stemCount; _stemCount = stemCount;
_sourcePositions = new long[_stemCount]; _sourcePositions = new long[_stemCount];
_fractionalInput = new double[_stemCount]; _fractionalInput = new double[_stemCount];
_pendingInput = Enumerable.Range(0, _stemCount)
.Select(_ => new Queue<SourceChunkInfo>())
.ToArray();
_pendingInput = [.. Enumerable.Range(0, _stemCount).Select(_ => new Queue<SourceChunkInfo>())];
} }
public async Task Configure(PlaybackSpeedSettings settings, CancellationToken token) public async Task Configure(PlaybackSpeedSettings settings, CancellationToken globalToken)
{ {
Trace(settings); Trace(settings);
var speedChanged = Math.Abs(_speed - settings.Speed) > 0.01f;
_speed = settings.Speed; _speed = settings.Speed;
if (_cts != null) if (globalToken != CancellationToken.None)
await DisposeProcesses().ConfigureAwait(false); _token = globalToken;
if ( token != CancellationToken.None ) if (_cts != null && speedChanged)
_token = token; {
await DisposeProcesses().ConfigureAwait(false);
}
} }
@ -88,160 +90,179 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SubmitStems(IReadOnlyList<AudioBlock> stemBlocks, CancellationToken token) public async Task SubmitStems(IReadOnlyList<AudioBlock> stemBlocks, CancellationToken token)
{ {
if (stemBlocks.Count != _stemCount) Interlocked.Increment(ref _activeIo);
try
{
if (stemBlocks.Count != _stemCount)
throw new ArgumentException($"Expected {_stemCount} stems, but got {stemBlocks.Count}."); throw new ArgumentException($"Expected {_stemCount} stems, but got {stemBlocks.Count}.");
EnsureStemProcesses(stemBlocks.Count); EnsureStemProcesses(stemBlocks.Count);
// No-stretch path: just enqueue into per-stem rings
if (Math.Abs(_speed - 1.0f) < 0.01f)
{
for (int i = 0; i < stemBlocks.Count; i++)
{
if (token.IsCancellationRequested)
return;
var block = stemBlocks[i];
// Track source position for passthrough mode
_pendingInput[i].Enqueue(new SourceChunkInfo
{
SourcePosition = block.Position,
SourceFrames = block.Frames
});
var bytes = MemoryMarshal.AsBytes(block.Buffer.Span);
_stemProcesses[i].Ring.Write(bytes, bytes.Length, token);
}
return;
}
// Stretch path: one ffmpeg+rubberband per stem
StartProcessesIfNeeded(stemBlocks.Count);
// No-stretch path: just enqueue into per-stem rings
if (Math.Abs(_speed - 1.0f) < 0.01f)
{
for (int i = 0; i < stemBlocks.Count; i++) for (int i = 0; i < stemBlocks.Count; i++)
{ {
var block = stemBlocks[i]; var block = stemBlocks[i];
// Track source position for passthrough mode // Track source position for stretched mode
_pendingInput[i].Enqueue(new SourceChunkInfo _pendingInput[i].Enqueue(new SourceChunkInfo
{ {
SourcePosition = block.Position, SourcePosition = block.Position,
SourceFrames = block.Frames SourceFrames = block.Frames
}); });
var bytes = MemoryMarshal.AsBytes(block.Buffer.Span); var proc = _stemProcesses[i];
_stemProcesses[i].Ring.Write(bytes, bytes.Length, token);
}
return Task.CompletedTask; try
}
// Stretch path: one ffmpeg+rubberband per stem
StartProcessesIfNeeded(stemBlocks.Count);
for (int i = 0; i < stemBlocks.Count; i++)
{
var block = stemBlocks[i];
// Track source position for stretched mode
_pendingInput[i].Enqueue(new SourceChunkInfo
{
SourcePosition = block.Position,
SourceFrames = block.Frames
});
var proc = _stemProcesses[i];
var span = block.Buffer.Span;
var bytes = MemoryMarshal.AsBytes(span);
try
{
if (token.IsCancellationRequested)
return Task.CompletedTask;
// Only write if ffmpeg is alive
if (proc.Stdin != null && !(proc.Ff?.Proc?.HasExited ?? true))
{ {
proc.Stdin.Write(bytes); if (token.IsCancellationRequested)
return;
try // Only write if ffmpeg is alive
if (proc.Stdin != null && !(proc.Ff?.Proc?.HasExited ?? true))
{ {
proc.Stdin.Flush(); await block.Buffer.WriteAsync(proc.Stdin, token).ConfigureAwait(false);
}
catch (ObjectDisposedException) try
{ {
// ffmpeg exited early await proc.Stdin.FlushAsync(token).ConfigureAwait(false);
}
catch (ObjectDisposedException)
{
// ffmpeg exited early
}
} }
} }
} catch (TaskCanceledException)
catch {
{ break;
// Ignore write errors (ffmpeg may exit early) }
catch
{
// Ignore write errors (ffmpeg may exit early)
}
} }
} }
finally
return Task.CompletedTask; {
Interlocked.Decrement(ref _activeIo);
}
} }
public async Task<TimeStretchedAudioBlock[]> ReceiveStems(CancellationToken token) public async Task<TimeStretchedAudioBlock[]> ReceiveStems(CancellationToken token)
{ {
EnsureStemProcesses(_stemCount); Interlocked.Increment(ref _activeIo);
try
int framesPerBlock = (int)(_sampleRate / 2); // 0.5 seconds
int samplesPerBlock = framesPerBlock * 2; // stereo
int bytesPerBlock = samplesPerBlock * sizeof(float);
var result = new TimeStretchedAudioBlock[_stemCount];
for (int i = 0; i < _stemCount; i++)
{ {
var proc = _stemProcesses[i]; EnsureStemProcesses(_stemCount);
// Wait until *some* data is available int framesPerBlock = (int)(_sampleRate / 2); // 0.5 seconds
int available = 0; int samplesPerBlock = framesPerBlock * 2; // stereo
while (!token.IsCancellationRequested) int bytesPerBlock = samplesPerBlock * sizeof(float);
var result = new TimeStretchedAudioBlock[_stemCount];
for (int i = 0; i < _stemCount; i++)
{ {
available = await proc.Ring.WaitForDataToRead(token).ConfigureAwait(false); var proc = _stemProcesses[i];
if (available > 0)
break;
await Task.Delay(1, token).ConfigureAwait(false); // Wait until *some* data is available
} int available = 0;
while (!token.IsCancellationRequested)
if (token.IsCancellationRequested)
return Array.Empty<TimeStretchedAudioBlock>();
// Determine block size (final block may be smaller)
int bytesToRead = Math.Min(bytesPerBlock, available);
int samplesToRead = bytesToRead / sizeof(float);
int framesToRead = samplesToRead / 2;
// Allocate a temporary byte[] buffer (safe across await)
byte[] temp = new byte[bytesToRead];
int totalRead = 0;
// Read exactly bytesToRead into temp[]
while (totalRead < bytesToRead && !token.IsCancellationRequested)
{
int toRead = bytesToRead - totalRead;
int read = proc.Ring.Read(temp.AsSpan(totalRead, toRead), toRead);
if (read > 0)
{ {
totalRead += read; available = await proc.Ring.WaitForDataToRead(token).ConfigureAwait(false);
continue; if (available > 0)
break;
await Task.Delay(1, token).ConfigureAwait(false);
} }
await Task.Delay(1, token).ConfigureAwait(false); if (token.IsCancellationRequested)
return [];
// Determine block size (final block may be smaller)
int bytesToRead = Math.Min(bytesPerBlock, available);
int samplesToRead = bytesToRead / sizeof(float);
int framesToRead = samplesToRead / 2;
// Allocate a temporary byte[] buffer (safe across await)
byte[] temp = new byte[bytesToRead];
int totalRead = 0;
// Read exactly bytesToRead into temp[]
while (totalRead < bytesToRead && !token.IsCancellationRequested)
{
int toRead = bytesToRead - totalRead;
int read = proc.Ring.Read(temp.AsSpan(totalRead, toRead), toRead);
if (read > 0)
{
totalRead += read;
continue;
}
await Task.Delay(1, token).ConfigureAwait(false);
}
if (token.IsCancellationRequested)
return [];
// Now allocate the float buffer
var outBuf = _pool.Rent(samplesToRead);
outBuf.Length = samplesToRead;
// Copy temp[] → float buffer (safe, no await)
var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
temp.AsSpan().CopyTo(outBytes);
//
// *** Correct source-position mapping ***
//
long sourcePos = ComputeSourcePosition(i, framesToRead);
result[i] = new TimeStretchedAudioBlock(
outBuf,
framesToRead,
2,
_sampleRate,
sourcePos);
} }
if (token.IsCancellationRequested) return result;
return Array.Empty<TimeStretchedAudioBlock>(); }
finally
// Now allocate the float buffer {
var outBuf = _pool.Rent(samplesToRead); Interlocked.Decrement(ref _activeIo);
outBuf.Length = samplesToRead;
// Copy temp[] → float buffer (safe, no await)
var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
temp.AsSpan().CopyTo(outBytes);
//
// *** Correct source-position mapping ***
//
long sourcePos = ComputeSourcePosition(i, framesToRead);
result[i] = new TimeStretchedAudioBlock(
outBuf,
framesToRead,
2,
_sampleRate,
sourcePos);
} }
return result;
} }
private long ComputeSourcePosition(int stemIndex, int outputFrames) private long ComputeSourcePosition(int stemIndex, int outputFrames)
@ -341,37 +362,37 @@ private long ComputeSourcePosition(int stemIndex, int outputFrames)
var buf = new byte[4096]; var buf = new byte[4096];
try while (!token.IsCancellationRequested)
{ {
while (!token.IsCancellationRequested) try
{ {
bool anyActive = false;
foreach (var proc in _stemProcesses) foreach (var proc in _stemProcesses)
{ {
if (proc.Stdout == null) if (proc.Stdout == null)
continue; continue;
anyActive = true; var read = await proc.Stdout.ReadAsync(buf, token).ConfigureAwait(false);
var read = await proc.Stdout.ReadAsync(buf, 0, buf.Length, token).ConfigureAwait(false);
if (read > 0) if (read > 0)
proc.Ring.Write(buf, read, token); proc.Ring.Write(buf, read, token);
} }
if (!anyActive)
break;
} }
catch (OperationCanceledException)
{
// expected on dispose; let outer while exit via token
return;
}
catch { }
} }
catch { }
} }
private async Task DisposeProcesses() private async Task DisposeProcesses()
{ {
Trace();
if (_cts != null) if (_cts != null)
{ {
Msg("Cancelling RubberBand/ffmpeg reader task..."); Msg("Cancelling RubberBand/ffmpeg reader task...");
try { _cts.Cancel(); } catch { } try { await _cts.CancelAsync(); } catch { }
} }
if (_readerTask != null) if (_readerTask != null)
@ -390,16 +411,29 @@ private long ComputeSourcePosition(int stemIndex, int outputFrames)
_stemProcesses.Clear(); _stemProcesses.Clear();
} }
if (_cts != null) _cts?.Dispose();
_cts = null;
// Reset position tracking
for (int i = 0; i < _stemCount; i++)
{ {
_cts.Dispose(); _pendingInput[i].Clear();
_cts = null; _fractionalInput[i] = 0;
_sourcePositions[i] = 0;
} }
_readerTask = null;
_cts = null;
// Wait until no active I/O before tearing down
while (Interlocked.CompareExchange(ref _activeIo, 0, 0) != 0)
await Task.Delay(1, CancellationToken.None).ConfigureAwait(false);
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
Trace();
await DisposeProcesses().ConfigureAwait(false); await DisposeProcesses().ConfigureAwait(false);
} }
} }

View File

@ -48,6 +48,9 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
private PipelineState? _pipeline; private PipelineState? _pipeline;
private long _pendingSeekFrames; private long _pendingSeekFrames;
private PlaybackSpeedSettings _currentSpeed = new(){ Speed = 1.0f };
private PlaybackSpeedSettings _prevSpeed = new(){ Speed = 1.0f };
public StemPlaybackEngine( public StemPlaybackEngine(
IStemDecoderFactory stemDecoderFactory, IStemDecoderFactory stemDecoderFactory,
IAudioOutputDevice outputDevice, IAudioOutputDevice outputDevice,
@ -168,7 +171,10 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
if (pipelineToDispose is not null) if (pipelineToDispose is not null)
{ {
try { pipelineToDispose.Cts?.Cancel(); } catch { } if (pipelineToDispose.Cts != null)
{
try { await pipelineToDispose.Cts.CancelAsync(); } catch { }
}
var task = pipelineToDispose.RenderTask; var task = pipelineToDispose.RenderTask;
if (task is not null && task.Id != Task.CurrentId) if (task is not null && task.Id != Task.CurrentId)
@ -207,7 +213,7 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
{ {
Trace(settings); Trace(settings);
await _timeStretchEngine.Configure(settings, _pipeline?.Cts?.Token ?? CancellationToken.None).ConfigureAwait(false); _currentSpeed = settings;
} }
public Task UpdateMixerAsync(MixerSettings settings) public Task UpdateMixerAsync(MixerSettings settings)
@ -392,6 +398,12 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
var gotFirstBlock = false; var gotFirstBlock = false;
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
if ( _prevSpeed.Speed != _currentSpeed.Speed )
{
await _timeStretchEngine.Configure(_currentSpeed, token).ConfigureAwait(false);
_prevSpeed = _currentSpeed;
}
var stretchedBlocks = await _timeStretchEngine.ReceiveStems(token).ConfigureAwait(false); var stretchedBlocks = await _timeStretchEngine.ReceiveStems(token).ConfigureAwait(false);
if (stretchedBlocks == null || stretchedBlocks.Length == 0 || stretchedBlocks[0].Buffer == null) if (stretchedBlocks == null || stretchedBlocks.Length == 0 || stretchedBlocks[0].Buffer == null)

View File

@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
using NAudio.CoreAudioApi; using NAudio.CoreAudioApi;
using NAudio.Dmo; using NAudio.Dmo;
using NAudio.Wave; using NAudio.Wave;
using static AudioCore.Models.Tracer;
namespace AudioCore.Impl; namespace AudioCore.Impl;

View File

@ -10,7 +10,7 @@ public sealed class PlaybackSpeedSettings
public interface ITimeStretchEngine public interface ITimeStretchEngine
{ {
Task Configure(PlaybackSpeedSettings settings, CancellationToken token); Task Configure(PlaybackSpeedSettings settings, CancellationToken globalToken = default);
// Streaming block processing // Streaming block processing
Task IsReadyToAcceptStems(CancellationToken token); Task IsReadyToAcceptStems(CancellationToken token);

View File

@ -8,15 +8,12 @@ namespace AudioCore.Models;
public static class Tracer public static class Tracer
{ {
public static void Trace([CallerMemberName] string method = null!) => Debug.WriteLine($"TRACE: ____________ {method}() called"); public static void Trace([CallerFilePath] string path = null!, [CallerMemberName] string method = null!)
public static void Trace<T>(T args, [CallerArgumentExpression("args")] string argsExpression = "", [CallerMemberName] string method = "") => Debug.WriteLine($"TRACE: ____________ {Path.GetFileNameWithoutExtension(path)}.{method}() called");
{ public static void Trace<T>(T args, [CallerArgumentExpression("args")] string argsExpression = "", [CallerFilePath] string path = null!, [CallerMemberName] string method = "")
Debug.WriteLine($"TRACE: ____________ {method}({argsExpression}: {args}) called"); => Debug.WriteLine($"TRACE: ____________ {Path.GetFileNameWithoutExtension(path)}.{method}({argsExpression}: {args}) called");
} public static void Trace<T1, T2>(T1 arg1, T2 arg2, [CallerArgumentExpression("arg1")] string arg1Expression = "", [CallerArgumentExpression("arg2")] string arg2Expression = "", [CallerFilePath] string path = null!, [CallerMemberName] string method = "")
public static void Trace<T1, T2>(T1 arg1, T2 arg2, [CallerArgumentExpression("arg1")] string arg1Expression = "", [CallerArgumentExpression("arg2")] string arg2Expression = "", [CallerMemberName] string method = "") => Debug.WriteLine($"TRACE: ____________ {Path.GetFileNameWithoutExtension(path)}.{method}({arg1Expression}: {arg1}, {arg2Expression}: {arg2}) called");
{ public static void Msg(string message, [CallerFilePath] string path = null!, [CallerMemberName] string method = null!)
Debug.WriteLine($"TRACE: ____________ {method}({arg1Expression}: {arg1}, {arg2Expression}: {arg2}) called"); => Debug.WriteLine($"TRACE: ____________ {Path.GetFileNameWithoutExtension(path)}.{method}(): {message}");
}
public static void Msg(string message, [CallerMemberName] string method = null!) => Debug.WriteLine($"TRACE: ____________ {method}(): {message}");
} }