Skip to content

Commit

Permalink
Merge pull request #1005 from b-editor/avfoundation
Browse files Browse the repository at this point in the history
Implement MediaReader/Writer Classes Using AVFoundation
  • Loading branch information
yuto-trd authored Jun 24, 2024
2 parents 6e5a65b + b9ea235 commit b865a3b
Show file tree
Hide file tree
Showing 20 changed files with 1,701 additions and 3 deletions.
10 changes: 10 additions & 0 deletions Beutl.sln
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Beutl.Extensions.MediaFound
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Beutl.PackageTools.UI", "src\Beutl.PackageTools.UI\Beutl.PackageTools.UI.csproj", "{D8A8061C-CE79-4DF7-B9E8-2002BAD47DD8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beutl.Extensions.AVFoundation", "src\Beutl.Extensions.AVFoundation\Beutl.Extensions.AVFoundation.csproj", "{8B040DCA-6C9C-4009-8FE5-8F764D80907B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -371,6 +373,14 @@ Global
{D8A8061C-CE79-4DF7-B9E8-2002BAD47DD8}.Release|Any CPU.Build.0 = Release|Any CPU
{D8A8061C-CE79-4DF7-B9E8-2002BAD47DD8}.Release|x64.ActiveCfg = Release|Any CPU
{D8A8061C-CE79-4DF7-B9E8-2002BAD47DD8}.Release|x64.Build.0 = Release|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Debug|x64.ActiveCfg = Debug|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Debug|x64.Build.0 = Debug|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Release|Any CPU.Build.0 = Release|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Release|x64.ActiveCfg = Release|Any CPU
{8B040DCA-6C9C-4009-8FE5-8F764D80907B}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageVersion Include="FluentTextTable" Version="1.0.0" />
<PackageVersion Include="ILGPU.Algorithms" Version="1.5.1" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.106" />
<PackageVersion Include="MonoMac.NetStandard" Version="0.0.4" />
<PackageVersion Include="NAudio.Wasapi" Version="2.2.1" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.139" />
<PackageVersion Include="HarfBuzzSharp" Version="7.3.0.2" />
Expand Down Expand Up @@ -80,4 +81,4 @@
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="Vortice.XAudio2" Version="3.5.0" />
</ItemGroup>
</Project>
</Project>
98 changes: 98 additions & 0 deletions src/Beutl.Extensions.AVFoundation/AVFSampleUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Diagnostics;
using Beutl.Media;
using Beutl.Media.Pixel;
using MonoMac.CoreGraphics;
using MonoMac.CoreImage;
using MonoMac.CoreMedia;
using MonoMac.CoreVideo;
using MonoMac.Foundation;

namespace Beutl.Extensions.AVFoundation;

public class AVFSampleUtilities
{
public static unsafe CVPixelBuffer? ConvertToCVPixelBuffer(Bitmap<Bgra8888> bitmap)
{
int width = bitmap.Width;
int height = bitmap.Height;
var pixelBuffer = new CVPixelBuffer(width, height, CVPixelFormatType.CV32BGRA, new CVPixelBufferAttributes
{
PixelFormatType = CVPixelFormatType.CV32BGRA,
Width = width,
Height = height
});

var r = pixelBuffer.Lock(CVOptionFlags.None);
if (r != CVReturn.Success) return null;

Buffer.MemoryCopy(
(void*)bitmap.Data, (void*)pixelBuffer.GetBaseAddress(0),
bitmap.ByteCount, bitmap.ByteCount);

pixelBuffer.Unlock(CVOptionFlags.None);

return pixelBuffer;
}

public static unsafe Bitmap<Bgra8888>? ConvertToBgra(CMSampleBuffer buffer)
{
using var imageBuffer = buffer.GetImageBuffer();
if (imageBuffer is not CVPixelBuffer pixelBuffer) return null;

var r = pixelBuffer.Lock(CVOptionFlags.None);
if (r != CVReturn.Success) return null;

int width = pixelBuffer.Width;
int height = pixelBuffer.Height;
var bitmap = new Bitmap<Bgra8888>(width, height);
if (pixelBuffer.ColorSpace.Model == CGColorSpaceModel.RGB && pixelBuffer.BytesPerRow == width * 4)
{
Buffer.MemoryCopy(
(void*)pixelBuffer.GetBaseAddress(0), (void*)bitmap.Data,
bitmap.ByteCount, bitmap.ByteCount);
pixelBuffer.Unlock(CVOptionFlags.None);
Parallel.For(0, width * height, i =>
{
// argb
// bgra
var o = bitmap.DataSpan[i];
bitmap.DataSpan[i] = new Bgra8888(o.G, o.R, o.A, o.B);
});
return bitmap;
}

int bytesPerRow = width * height * 4;
using (CGColorSpace colorSpace = CGColorSpace.CreateDeviceRGB())
using (var cgContext = new CGBitmapContext(
bitmap.Data, width, height,
8, bytesPerRow, colorSpace,
CGBitmapFlags.ByteOrderDefault | CGBitmapFlags.PremultipliedFirst))
using (var ciImage = CIImage.FromImageBuffer(imageBuffer))
using (var ciContext = new CIContext(NSObjectFlag.Empty))
// CreateCGImageで落ちる、例外なしに
using (var cgImage = ciContext.CreateCGImage(
ciImage, new CGRect(0, 0, width, height), (long)CIFormat.ARGB8, colorSpace))
{
cgContext.DrawImage(new CGRect(0, 0, width, height), cgImage);
}

pixelBuffer.Unlock(CVOptionFlags.None);

return bitmap;
}

public static int SampleCopyToBuffer(CMSampleBuffer buffer, nint buf, int copyBufferPos,
int copyBufferSize)
{
using var dataBuffer = buffer.GetDataBuffer();
Debug.Assert((copyBufferPos + copyBufferSize) <= dataBuffer.DataLength);
dataBuffer.CopyDataBytes((uint)copyBufferPos, (uint)copyBufferSize, buf);

return copyBufferSize;
}

public static int SampleCopyToBuffer(CMSampleBuffer buffer, nint buf, int copyBufferSize)
{
return SampleCopyToBuffer(buffer, buf, 0, copyBufferSize);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Optimize>false</Optimize>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MonoMac.NetStandard" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Beutl.Extensibility\Beutl.Extensibility.csproj" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions src/Beutl.Extensions.AVFoundation/CMTimeUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using MonoMac.CoreMedia;

namespace Beutl.Extensions.AVFoundation.Decoding;

internal static class CMTimeUtilities
{
public static int ConvertFrameFromTimeStamp(CMTime timestamp, double rate)
{
return (int)Math.Round(timestamp.Seconds * rate, MidpointRounding.AwayFromZero);
}

public static CMTime ConvertTimeStampFromFrame(int frame, double rate)
{
return CMTime.FromSeconds(frame / rate, 1);
}
}
127 changes: 127 additions & 0 deletions src/Beutl.Extensions.AVFoundation/Decoding/AVFAudioSampleCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Beutl.Collections;
using Beutl.Logging;
using Microsoft.Extensions.Logging;
using MonoMac.CoreMedia;

namespace Beutl.Extensions.AVFoundation.Decoding;

public class AVFAudioSampleCache(AVFSampleCacheOptions options)
{
private readonly ILogger _logger = Log.CreateLogger<AVFAudioSampleCache>();

public const int AudioSampleWaringGapCount = 1000;

private CircularBuffer<AudioCache> _audioCircularBuffer = new(options.MaxAudioBufferSize);
private short _nBlockAlign;

private readonly record struct AudioCache(int StartSampleNum, CMSampleBuffer Sample, int AudioSampleCount)
{
public bool CopyBuffer(ref int startSample, ref int copySampleLength, ref nint buffer, short nBlockAlign)
{
int querySampleEndPos = startSample + copySampleLength;
int cacheSampleEndPos = StartSampleNum + AudioSampleCount;
// キャッシュ内に startSample位置があるかどうか
if (StartSampleNum <= startSample && startSample < cacheSampleEndPos)
{
// 要求サイズがキャッシュを超えるかどうか
if (querySampleEndPos <= cacheSampleEndPos)
{
// キャッシュ内に収まる
int actualBufferPos = (startSample - StartSampleNum) * nBlockAlign;
int actualBufferSize = copySampleLength * nBlockAlign;
AVFSampleUtilities.SampleCopyToBuffer(Sample, buffer, actualBufferPos, actualBufferSize);

startSample += copySampleLength;
copySampleLength = 0;
buffer += actualBufferSize;

return true;
}
else
{
// 現在のキャッシュ内のデータをコピーする
int actualBufferPos = (startSample - StartSampleNum) * nBlockAlign;
int leftSampleCount = cacheSampleEndPos - startSample;
int actualleftBufferSize = leftSampleCount * nBlockAlign;
AVFSampleUtilities.SampleCopyToBuffer(Sample, buffer, actualBufferPos, actualleftBufferSize);

startSample += leftSampleCount;
copySampleLength -= leftSampleCount;
buffer += actualleftBufferSize;

return true;
}
}

return false;
}
}

public void Reset(short nBlockAlign)
{
_nBlockAlign = nBlockAlign;
CircularBuffer<AudioCache> old = _audioCircularBuffer;
_audioCircularBuffer = new CircularBuffer<AudioCache>(options.MaxAudioBufferSize);
foreach (AudioCache item in old)
{
item.Sample.Dispose();
}
}

public void Add(int startSample, CMSampleBuffer buffer)
{
int lastAudioSampleNum = LastAudioSampleNumber();
if (lastAudioSampleNum != -1)
{
int actualAudioSampleNum = lastAudioSampleNum + _audioCircularBuffer.Back().AudioSampleCount;
if (Math.Abs(startSample - actualAudioSampleNum) > AudioSampleWaringGapCount)
{
_logger.LogWarning(
"sample laggin - lag: {lag} startSample: {startSample} lastAudioSampleNum: {lastAudioSampleNum}",
startSample - actualAudioSampleNum,
startSample,
actualAudioSampleNum);
}

startSample = lastAudioSampleNum + _audioCircularBuffer.Back().AudioSampleCount;
}

int audioSampleCount = buffer.NumSamples;

if (_audioCircularBuffer.IsFull)
{
_audioCircularBuffer.Front().Sample.Dispose();
_audioCircularBuffer.PopFront();
}

var audioCache = new AudioCache(startSample, buffer, audioSampleCount);
_audioCircularBuffer.PushBack(audioCache);
}

public int LastAudioSampleNumber()
{
if (_audioCircularBuffer.Size > 0)
{
AudioCache prevAudioCache = _audioCircularBuffer.Back();
return prevAudioCache.StartSampleNum;
}

return -1;
}

public bool SearchAudioSampleAndCopyBuffer(int startSample, int copySampleLength, nint buffer)
{
foreach (AudioCache audioCache in _audioCircularBuffer)
{
if (audioCache.CopyBuffer(ref startSample, ref copySampleLength, ref buffer, _nBlockAlign))
{
if (copySampleLength == 0)
{
return true;
}
}
}

return false;
}
}
Loading

0 comments on commit b865a3b

Please sign in to comment.