Compare commits

...

3 Commits

9 changed files with 199 additions and 88 deletions

View File

@ -47,7 +47,7 @@ public partial class WaveformBandViewModel : ObservableObject
return; return;
} }
double ratio = current.TotalSeconds / total.TotalSeconds; double ratio = current.TotalMicroseconds / total.TotalMicroseconds;
PlaybackX = ratio * CanvasWidth; PlaybackX = ratio * CanvasWidth;
} }

View File

@ -37,8 +37,8 @@
<!-- File Open --> <!-- File Open -->
<Button Command="{Binding OpenFileCommand}" <Button Command="{Binding OpenFileCommand}"
Width="32" Width="43"
Height="32" Height="43"
Background="Black" Background="Black"
Grid.Column="0" Grid.Column="0"
Margin="0,0,6,0" Margin="0,0,6,0"
@ -46,6 +46,9 @@
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M1 4C1 2.34315 2.34315 1 4 1H7.76393C8.90025 1 9.93904 1.64201 10.4472 2.65836L11.3416 4.44721C11.511 4.786 11.8573 5 12.2361 5H20C21.6569 5 23 6.34315 23 8V20C23 21.6569 21.6569 23 20 23H4C2.34315 23 1 21.6569 1 20V4ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V8C21 7.44772 20.5523 7 20 7H12.2361C11.0998 7 10.061 6.35799 9.55279 5.34164L8.65836 3.55279C8.48897 3.214 8.1427 3 7.76393 3H4Z M1 4C1 2.34315 2.34315 1 4 1H7.76393C8.90025 1 9.93904 1.64201 10.4472 2.65836L11.3416 4.44721C11.511 4.786 11.8573 5 12.2361 5H20C21.6569 5 23 6.34315 23 8V20C23 21.6569 21.6569 23 20 23H4C2.34315 23 1 21.6569 1 20V4ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V8C21 7.44772 20.5523 7 20 7H12.2361C11.0998 7 10.061 6.35799 9.55279 5.34164L8.65836 3.55279C8.48897 3.214 8.1427 3 7.76393 3H4Z
</Path.Data> </Path.Data>
@ -54,8 +57,8 @@
<!-- Rewind --> <!-- Rewind -->
<Button Command="{Binding RewindCommand}" <Button Command="{Binding RewindCommand}"
Width="32" Width="43"
Height="32" Height="43"
Background="Black" Background="Black"
Grid.Column="1" Grid.Column="1"
Margin="0,0,6,0" Margin="0,0,6,0"
@ -63,6 +66,9 @@
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M18.3956 19.7691C19.0541 20.2687 20 19.799 20 18.9724L20 5.02764C20 4.20106 19.0541 3.73137 18.3956 4.23095L9.20476 11.2033C8.67727 11.6035 8.67727 12.3965 9.20476 12.7967L18.3956 19.7691ZM22 18.9724C22 21.4521 19.1624 22.8612 17.1868 21.3625L7.99598 14.3901C6.41353 13.1896 6.41353 10.8104 7.99599 9.60994L17.1868 2.63757C19.1624 1.13885 22 2.5479 22 5.02764L22 18.9724Z M18.3956 19.7691C19.0541 20.2687 20 19.799 20 18.9724L20 5.02764C20 4.20106 19.0541 3.73137 18.3956 4.23095L9.20476 11.2033C8.67727 11.6035 8.67727 12.3965 9.20476 12.7967L18.3956 19.7691ZM22 18.9724C22 21.4521 19.1624 22.8612 17.1868 21.3625L7.99598 14.3901C6.41353 13.1896 6.41353 10.8104 7.99599 9.60994L17.1868 2.63757C19.1624 1.13885 22 2.5479 22 5.02764L22 18.9724Z
M2 3C2 2.44772 2.44772 2 3 2C3.55228 2 4 2.44772 4 3V21C4 21.5523 3.55228 22 3 22C2.44772 22 2 21.5523 2 21V3Z M2 3C2 2.44772 2.44772 2 3 2C3.55228 2 4 2.44772 4 3V21C4 21.5523 3.55228 22 3 22C2.44772 22 2 21.5523 2 21V3Z
@ -71,9 +77,9 @@
</Button> </Button>
<!-- Play --> <!-- Play -->
<Button Width="32" <Button Background="Black"
Height="32" Width="43"
Background="Black" Height="43"
Grid.Column="2" Grid.Column="2"
Margin="0,0,6,0" Margin="0,0,6,0"
ToolTip.Tip="Play/Pause" ToolTip.Tip="Play/Pause"
@ -83,13 +89,16 @@
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0" RenderTransformOrigin="0,0"
Data="{Binding IsPlaying, Converter={StaticResource PlayPauseGeometry}}"> Data="{Binding IsPlaying, Converter={StaticResource PlayPauseGeometry}}">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
</Path> </Path>
</Button> </Button>
<!-- Stop --> <!-- Stop -->
<Button Command="{Binding StopCommand}" <Button Command="{Binding StopCommand}"
Width="32" Width="43"
Height="32" Height="43"
Background="Black" Background="Black"
Grid.Column="4" Grid.Column="4"
Margin="0,0,6,0" Margin="0,0,6,0"
@ -97,6 +106,9 @@
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M22 5C22 3.34315 20.6569 2 19 2H5C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5ZM20 5C20 4.44772 19.5523 4 19 4H5C4.44772 4 4 4.44772 4 5V19C4 19.5523 4.44772 20 5 20H19C19.5523 20 20 19.5523 20 19V5Z M22 5C22 3.34315 20.6569 2 19 2H5C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5ZM20 5C20 4.44772 19.5523 4 19 4H5C4.44772 4 4 4.44772 4 5V19C4 19.5523 4.44772 20 5 20H19C19.5523 20 20 19.5523 20 19V5Z
</Path.Data> </Path.Data>
@ -105,14 +117,17 @@
<!-- Fast Forward --> <!-- Fast Forward -->
<Button Command="{Binding FastForwardCommand}" <Button Command="{Binding FastForwardCommand}"
Width="32" Width="43"
Height="32" Height="43"
Background="Black" Background="Black"
Grid.Column="5" Grid.Column="5"
ToolTip.Tip="Fast Forward"> ToolTip.Tip="Fast Forward">
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M5.60439 4.23093C4.94586 3.73136 4 4.20105 4 5.02762V18.9724C4 19.799 4.94586 20.2686 5.60439 19.7691L14.7952 12.7967C15.3227 12.3965 15.3227 11.6035 14.7952 11.2033L5.60439 4.23093ZM2 5.02762C2 2.54789 4.83758 1.13883 6.81316 2.63755L16.004 9.60993C17.5865 10.8104 17.5865 13.1896 16.004 14.3901L6.81316 21.3625C4.83758 22.8612 2 21.4521 2 18.9724V5.02762Z M5.60439 4.23093C4.94586 3.73136 4 4.20105 4 5.02762V18.9724C4 19.799 4.94586 20.2686 5.60439 19.7691L14.7952 12.7967C15.3227 12.3965 15.3227 11.6035 14.7952 11.2033L5.60439 4.23093ZM2 5.02762C2 2.54789 4.83758 1.13883 6.81316 2.63755L16.004 9.60993C17.5865 10.8104 17.5865 13.1896 16.004 14.3901L6.81316 21.3625C4.83758 22.8612 2 21.4521 2 18.9724V5.02762Z
M20 3C20 2.44772 20.4477 2 21 2C21.5523 2 22 2.44772 22 3V21C22 21.5523 21.5523 22 21 22C20.4477 22 20 21.5523 20 21V3Z M20 3C20 2.44772 20.4477 2 21 2C21.5523 2 22 2.44772 22 3V21C22 21.5523 21.5523 22 21 22C20.4477 22 20 21.5523 20 21V3Z
@ -169,8 +184,8 @@
Margin="0,0,6,0"/> Margin="0,0,6,0"/>
<Button Command="{Binding SetPointACommand}" <Button Command="{Binding SetPointACommand}"
Width="32" Width="43"
Height="32" Height="43"
Grid.Column="1" Grid.Column="1"
Background="Black" Background="Black"
Margin="0,0,6,0" Margin="0,0,6,0"
@ -179,6 +194,9 @@
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M18.3956 19.7691C19.0541 20.2687 20 19.799 20 18.9724L20 5.02764C20 4.20106 19.0541 3.73137 18.3956 4.23095L9.20476 11.2033C8.67727 11.6035 8.67727 12.3965 9.20476 12.7967L18.3956 19.7691ZM22 18.9724C22 21.4521 19.1624 22.8612 17.1868 21.3625L7.99598 14.3901C6.41353 13.1896 6.41353 10.8104 7.99599 9.60994L17.1868 2.63757C19.1624 1.13885 22 2.5479 22 5.02764L22 18.9724Z M18.3956 19.7691C19.0541 20.2687 20 19.799 20 18.9724L20 5.02764C20 4.20106 19.0541 3.73137 18.3956 4.23095L9.20476 11.2033C8.67727 11.6035 8.67727 12.3965 9.20476 12.7967L18.3956 19.7691ZM22 18.9724C22 21.4521 19.1624 22.8612 17.1868 21.3625L7.99598 14.3901C6.41353 13.1896 6.41353 10.8104 7.99599 9.60994L17.1868 2.63757C19.1624 1.13885 22 2.5479 22 5.02764L22 18.9724Z
M2 3C2 2.44772 2.44772 2 3 2C3.55228 2 4 2.44772 4 3V21C4 21.5523 3.55228 22 3 22C2.44772 22 2 21.5523 2 21V3Z M2 3C2 2.44772 2.44772 2 3 2C3.55228 2 4 2.44772 4 3V21C4 21.5523 3.55228 22 3 22C2.44772 22 2 21.5523 2 21V3Z
@ -188,8 +206,8 @@
</Button> </Button>
<Button Command="{Binding SetPointBCommand}" <Button Command="{Binding SetPointBCommand}"
Width="32" Width="43"
Height="32" Height="43"
Background="Black" Background="Black"
Grid.Column="2" Grid.Column="2"
ToolTip.Tip="Set Loop Point B"> ToolTip.Tip="Set Loop Point B">
@ -197,6 +215,9 @@
<Path Fill="#e0e0e0" <Path Fill="#e0e0e0"
Stretch="None" Stretch="None"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Path.RenderTransform>
<TranslateTransform X="0" Y="3"/>
</Path.RenderTransform>
<Path.Data> <Path.Data>
M5.60439 4.23093C4.94586 3.73136 4 4.20105 4 5.02762V18.9724C4 19.799 4.94586 20.2686 5.60439 19.7691L14.7952 12.7967C15.3227 12.3965 15.3227 11.6035 14.7952 11.2033L5.60439 4.23093ZM2 5.02762C2 2.54789 4.83758 1.13883 6.81316 2.63755L16.004 9.60993C17.5865 10.8104 17.5865 13.1896 16.004 14.3901L6.81316 21.3625C4.83758 22.8612 2 21.4521 2 18.9724V5.02762Z M5.60439 4.23093C4.94586 3.73136 4 4.20105 4 5.02762V18.9724C4 19.799 4.94586 20.2686 5.60439 19.7691L14.7952 12.7967C15.3227 12.3965 15.3227 11.6035 14.7952 11.2033L5.60439 4.23093ZM2 5.02762C2 2.54789 4.83758 1.13883 6.81316 2.63755L16.004 9.60993C17.5865 10.8104 17.5865 13.1896 16.004 14.3901L6.81316 21.3625C4.83758 22.8612 2 21.4521 2 18.9724V5.02762Z
M20 3C20 2.44772 20.4477 2 21 2C21.5523 2 22 2.44772 22 3V21C22 21.5523 21.5523 22 21 22C20.4477 22 20 21.5523 20 21V3Z M20 3C20 2.44772 20.4477 2 21 2C21.5523 2 22 2.44772 22 3V21C22 21.5523 21.5523 22 21 22C20.4477 22 20 21.5523 20 21V3Z

View File

@ -3,9 +3,7 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<Authors>Alexander Shabarshov</Authors> <Authors>Alexander Shabarshov</Authors>
<Description> <Description>Audio engine core for ABStemPlayer. Audio pipeline. </Description>
Audio engine core for ABStemPlayer. Audio pipeline.
</Description>
<PackageProjectUrl>https://github.com/unclshura/ABStemPlayer</PackageProjectUrl> <PackageProjectUrl>https://github.com/unclshura/ABStemPlayer</PackageProjectUrl>
<PackageTags>audio, player, ab, guitar, drums, piano, stem, mixer, c#</PackageTags> <PackageTags>audio, player, ab, guitar, drums, piano, stem, mixer, c#</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
@ -33,7 +31,9 @@
<PropertyGroup> <PropertyGroup>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<InformationalVersion>$(Version).$(BuildNumber)+$(SourceRevisionId)</InformationalVersion> <BuildNumber>0</BuildNumber>
<SourceRevisionId></SourceRevisionId>
<InformationalVersion>$(Version).$(BuildNumber)$(SourceRevisionId)</InformationalVersion>
<AssemblyVersion>$(Version)</AssemblyVersion> <AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version).$(BuildNumber)</FileVersion> <FileVersion>$(Version).$(BuildNumber)</FileVersion>
</PropertyGroup> </PropertyGroup>

View File

@ -10,6 +10,15 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
private readonly int _sampleRate; private readonly int _sampleRate;
private long[] _sourcePositions; private long[] _sourcePositions;
private sealed class SourceChunkInfo
{
public long SourcePosition; // starting frame index in source stream
public int SourceFrames; // number of frames in this chunk
}
private readonly Queue<SourceChunkInfo>[] _pendingInput;
private double[] _fractionalInput;
// 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
{ {
@ -51,6 +60,12 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
_stemProcesses.Capacity = stemCount; _stemProcesses.Capacity = stemCount;
_stemCount = stemCount; _stemCount = stemCount;
_sourcePositions = new long[_stemCount]; _sourcePositions = new long[_stemCount];
_fractionalInput = new double[_stemCount];
_pendingInput = Enumerable.Range(0, _stemCount)
.Select(_ => new Queue<SourceChunkInfo>())
.ToArray();
} }
public async Task Configure(PlaybackSpeedSettings settings, CancellationToken token) public async Task Configure(PlaybackSpeedSettings settings, CancellationToken token)
@ -75,32 +90,48 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
public Task SubmitStems(IReadOnlyList<AudioBlock> stemBlocks, CancellationToken token) public Task SubmitStems(IReadOnlyList<AudioBlock> stemBlocks, CancellationToken token)
{ {
if ( stemBlocks.Count != _stemCount) 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);
// No-stretch path: just enqueue into per-stem rings // No-stretch path: just enqueue into per-stem rings
if (Math.Abs(_speed - 1.0f) < 0.01f) if (Math.Abs(_speed - 1.0f) < 0.01f)
{ {
EnsureStemProcesses(stemBlocks.Count);
for (int i = 0; i < stemBlocks.Count; i++) for (int i = 0; i < stemBlocks.Count; i++)
{ {
var proc = _stemProcesses[i]; var block = stemBlocks[i];
var bytes = MemoryMarshal.AsBytes(stemBlocks[i].Buffer.Span);
proc.Ring.Write(bytes, bytes.Length, token); // 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 Task.CompletedTask; return Task.CompletedTask;
} }
// Stretch path: one ffmpeg+rubberband per stem // Stretch path: one ffmpeg+rubberband per stem
EnsureStemProcesses(stemBlocks.Count);
StartProcessesIfNeeded(stemBlocks.Count); StartProcessesIfNeeded(stemBlocks.Count);
for (int i = 0; i < stemBlocks.Count; i++) 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 proc = _stemProcesses[i];
var span = stemBlocks[i].Buffer.Span; var span = block.Buffer.Span;
var bytes = MemoryMarshal.AsBytes(span); var bytes = MemoryMarshal.AsBytes(span);
try try
@ -108,29 +139,31 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
return Task.CompletedTask; return Task.CompletedTask;
// Only write if ffmpeg is alive
if (proc.Stdin != null && !(proc.Ff?.Proc?.HasExited ?? true)) if (proc.Stdin != null && !(proc.Ff?.Proc?.HasExited ?? true))
{
proc.Stdin.Write(bytes); proc.Stdin.Write(bytes);
if (token.IsCancellationRequested)
return Task.CompletedTask;
try try
{ {
proc.Stdin.Flush(); proc.Stdin.Flush();
} }
catch (System.ObjectDisposedException) catch (ObjectDisposedException)
{ {
// process has exited // ffmpeg exited early
}
} }
} }
catch catch
{ {
// ignore // Ignore write errors (ffmpeg may exit early)
} }
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task<TimeStretchedAudioBlock[]> ReceiveStems(CancellationToken token) public async Task<TimeStretchedAudioBlock[]> ReceiveStems(CancellationToken token)
{ {
EnsureStemProcesses(_stemCount); EnsureStemProcesses(_stemCount);
@ -195,10 +228,10 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
var outBytes = MemoryMarshal.AsBytes(outBuf.Span); var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
temp.AsSpan().CopyTo(outBytes); temp.AsSpan().CopyTo(outBytes);
// Compute source position //
long sourceFrames = (long)(framesToRead * _speed); // *** Correct source-position mapping ***
long sourcePos = _sourcePositions[i]; //
_sourcePositions[i] += sourceFrames; long sourcePos = ComputeSourcePosition(i, framesToRead);
result[i] = new TimeStretchedAudioBlock( result[i] = new TimeStretchedAudioBlock(
outBuf, outBuf,
@ -211,6 +244,54 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IAsyncDisp
return result; return result;
} }
private long ComputeSourcePosition(int stemIndex, int outputFrames)
{
// inputFrames = outputFrames / speed
double neededInputFrames = outputFrames / _speed;
// Add fractional requirement
_fractionalInput[stemIndex] += neededInputFrames;
long sourcePos = 0;
bool first = true;
var queue = _pendingInput[stemIndex];
// If no input chunks exist (warm-up), return last known position
if (queue.Count == 0)
return _sourcePositions[stemIndex];
while (_fractionalInput[stemIndex] >= 1 && queue.Count > 0)
{
var chunk = queue.Peek();
int take = (int)Math.Min(chunk.SourceFrames, Math.Floor(_fractionalInput[stemIndex]));
if (take <= 0)
break;
if (first)
{
sourcePos = chunk.SourcePosition;
first = false;
}
chunk.SourceFrames -= take;
_fractionalInput[stemIndex] -= take;
if (chunk.SourceFrames == 0)
queue.Dequeue();
}
// If we consumed nothing (fraction < 1), use last known position
if (first)
sourcePos = _sourcePositions[stemIndex];
_sourcePositions[stemIndex] = sourcePos;
return sourcePos;
}
private void EnsureStemProcesses(int stemCount) private void EnsureStemProcesses(int stemCount)
{ {

View File

@ -39,7 +39,6 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
private LoopRegion _loopRegion = new(); private LoopRegion _loopRegion = new();
private long _decodedFramePosition;
private long _loopStartFrames; private long _loopStartFrames;
private long _loopEndFrames; private long _loopEndFrames;
@ -96,7 +95,6 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
} }
_pendingSeekFrames = 0; _pendingSeekFrames = 0;
_decodedFramePosition = 0;
} }
} }
@ -126,7 +124,6 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
d.Seek(_pendingSeekFrames); d.Seek(_pendingSeekFrames);
} }
_decodedFramePosition = _pendingSeekFrames;
} }
_pipeline.RenderTask = Task.Run(() => RenderLoopAsync(_pipeline, _pipeline.Cts.Token)); _pipeline.RenderTask = Task.Run(() => RenderLoopAsync(_pipeline, _pipeline.Cts.Token));
@ -163,7 +160,6 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
if (!IsPlaying && _pipeline is null) if (!IsPlaying && _pipeline is null)
return; return;
_decodedFramePosition = 0;
_pendingSeekFrames = 0; _pendingSeekFrames = 0;
pipelineToDispose = _pipeline; pipelineToDispose = _pipeline;
@ -201,12 +197,6 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
{ {
foreach (var d in _pipeline.Decoders) foreach (var d in _pipeline.Decoders)
d.Seek(frameIndex); d.Seek(frameIndex);
_decodedFramePosition = frameIndex;
}
else
{
_decodedFramePosition = frameIndex;
} }
} }
@ -349,15 +339,9 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
try { d.Seek(loopStart); } catch { } try { d.Seek(loopStart); } catch { }
} }
lock (_stateLock)
_decodedFramePosition = loopStart;
// continue decoding from the loop start // continue decoding from the loop start
continue; continue;
} }
lock (_stateLock)
_decodedFramePosition = nextPosition;
} }
} }
catch { } catch { }
@ -430,19 +414,7 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
_outputDevice.Write(mixed.Buffer.Span); _outputDevice.Write(mixed.Buffer.Span);
gotFirstBlock = true; gotFirstBlock = true;
try await ReportProgress(mixed).ConfigureAwait(false);
{
double progress;
lock (_stateLock)
{
var total = _session?.StemSet.TotalFrames ?? 1L;
progress = (double)mixed.Position / Math.Max(total, 1L);
}
if (_progressReporter != null)
await _progressReporter.ReportProgress(progress).ConfigureAwait(false);
}
catch { }
try { mixed.Dispose(); } catch { } try { mixed.Dispose(); } catch { }
foreach (var b in stretchedBlocks) foreach (var b in stretchedBlocks)
@ -459,6 +431,25 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
} }
} }
private async Task ReportProgress(MixedAudioBlock mixed)
{
if (_progressReporter == null)
return;
try
{
double progress;
lock (_stateLock)
{
var total = _session?.StemSet.TotalFrames ?? 1L;
progress = (double)mixed.Position / Math.Max(total, 1L);
}
await _progressReporter.ReportProgress(progress).ConfigureAwait(false);
}
catch { }
}
private long TimeToFrames(TimeSpan time) private long TimeToFrames(TimeSpan time)
{ {
return (long)(time.TotalSeconds * _outputDevice.SampleRate); return (long)(time.TotalSeconds * _outputDevice.SampleRate);

View File

@ -2,7 +2,7 @@ using AudioCore.Impl;
namespace AudioCore.Models; namespace AudioCore.Models;
public readonly struct AudioBlock : IDisposable public readonly struct AudioBlock : IAudioBlock, IDisposable
{ {
public AudioBuffer<float> Buffer { get; } public AudioBuffer<float> Buffer { get; }
public int SampleRate { get; } public int SampleRate { get; }

View File

@ -0,0 +1,14 @@
using AudioCore.Impl;
namespace AudioCore.Models;
public interface IAudioBlock : IDisposable
{
AudioBuffer<float> Buffer { get; }
int SampleRate { get; }
int Channels { get; }
long Position { get; }
int Frames { get; }
int Length { get; }
Span<float> Span { get; }
}

View File

@ -2,7 +2,7 @@ using AudioCore.Impl;
namespace AudioCore.Models; namespace AudioCore.Models;
public readonly struct MixedAudioBlock : IDisposable public readonly struct MixedAudioBlock : IAudioBlock, IDisposable
{ {
public AudioBuffer<float> Buffer { get; } public AudioBuffer<float> Buffer { get; }
public int Frames { get; } public int Frames { get; }
@ -10,6 +10,8 @@ public readonly struct MixedAudioBlock : IDisposable
public int SampleRate { get; } public int SampleRate { get; }
public long Position { get; } public long Position { get; }
public Span<float> Span => Buffer.Span;
public int Length => Buffer.Length;
public MixedAudioBlock(AudioBuffer<float> buffer, int frames, int channels, int sampleRate, long samplePosition) public MixedAudioBlock(AudioBuffer<float> buffer, int frames, int channels, int sampleRate, long samplePosition)
{ {
Buffer = buffer; Buffer = buffer;

View File

@ -2,7 +2,7 @@ using AudioCore.Impl;
namespace AudioCore.Models; namespace AudioCore.Models;
public readonly struct TimeStretchedAudioBlock : IDisposable public readonly struct TimeStretchedAudioBlock : IAudioBlock, IDisposable
{ {
public AudioBuffer<float> Buffer { get; } public AudioBuffer<float> Buffer { get; }
public int Frames { get; } public int Frames { get; }
@ -10,6 +10,8 @@ public readonly struct TimeStretchedAudioBlock : IDisposable
public int SampleRate { get; } public int SampleRate { get; }
public long Position { get; } public long Position { get; }
public Span<float> Span => Buffer.Span;
public int Length => Buffer.Length;
public TimeStretchedAudioBlock(AudioBuffer<float> buffer, int frames, int channels, int sampleRate, long position) public TimeStretchedAudioBlock(AudioBuffer<float> buffer, int frames, int channels, int sampleRate, long position)
{ {
Buffer = buffer; Buffer = buffer;