diff --git a/CUETools.Codecs.ffmpeg/AudioDecoder.cs b/CUETools.Codecs.ffmpeg/AudioDecoder.cs new file mode 100644 index 0000000..4f83e27 --- /dev/null +++ b/CUETools.Codecs.ffmpeg/AudioDecoder.cs @@ -0,0 +1,320 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using FFmpeg.AutoGen; + +namespace CUETools.Codecs.ffmpegdll +{ + public unsafe class AudioDecoder : IAudioSource, IDisposable + { + private static void RegisterLibrariesSearchPath(string path) + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + case PlatformID.Win32S: + case PlatformID.Win32Windows: + SetDllDirectory(path); + break; + //case PlatformID.Unix: + //case PlatformID.MacOSX: + // string currentValue = Environment.GetEnvironmentVariable(LD_LIBRARY_PATH); + // if (string.IsNullOrWhiteSpace(currentValue) == false && currentValue.Contains(path) == false) + // { + // string newValue = currentValue + Path.PathSeparator + path; + // Environment.SetEnvironmentVariable(LD_LIBRARY_PATH, newValue); + // } + // break; + } + } + + [DllImport("kernel32", SetLastError = true)] + private static extern bool SetDllDirectory(string lpPathName); + + public AudioDecoder(DecoderSettings settings, string path, Stream IO) + { + m_settings = settings; + + _path = path; + + m_stream = (IO != null) ? IO : new FileStream (path, FileMode.Open, FileAccess.Read, FileShare.Read); + + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + case PlatformID.Win32S: + case PlatformID.Win32Windows: + var myPath = new Uri(typeof(AudioDecoder).Assembly.CodeBase).LocalPath; + var current = System.IO.Path.GetDirectoryName(myPath); + var probe = Environment.Is64BitProcess ? "x64" : "win32"; + while (current != null) + { + var ffmpegDirectory = System.IO.Path.Combine(current, probe); + if (Directory.Exists(ffmpegDirectory)) + { + Console.WriteLine($"FFmpeg binaries found in: {ffmpegDirectory}"); + RegisterLibrariesSearchPath(ffmpegDirectory); + break; + } + current = Directory.GetParent(current)?.FullName; + } + break; + //case PlatformID.Unix: + //case PlatformID.MacOSX: + // var libraryPath = Environment.GetEnvironmentVariable(LD_LIBRARY_PATH); + // RegisterLibrariesSearchPath(libraryPath); + // break; + } + + pkt = ffmpeg.av_packet_alloc(); + if (pkt == null) + throw new Exception("Unable to initialize the decoder"); + + decoded_frame = ffmpeg.av_frame_alloc(); + if (decoded_frame == null) + throw new Exception("Could not allocate audio frame"); + + ffmpeg.avcodec_register_all(); + + codec = ffmpeg.avcodec_find_decoder(m_settings.Codec); + if (codec == null) + throw new Exception("Codec not found"); + + parser = ffmpeg.av_parser_init((int)codec->id); + if (parser == null) + throw new Exception("Parser not found\n"); + + c = ffmpeg.avcodec_alloc_context3(codec); + if (c == null) + throw new Exception("Could not allocate audio codec context"); + //c->refcounted_frames == 0; + + /* open it */ + if (ffmpeg.avcodec_open2(c, codec, null) < 0) + throw new Exception("Could not open codec"); + + data_buf = new byte[AUDIO_INBUF_SIZE]; + data_size = 0; + data_offs = 0; + m_decoded_frame_offset = 0; + m_decoded_frame_size = 0; + + fill(); + + //switch(c->sample_fmt) + //{ + // case AVSampleFormat.AV_SAMPLE_FMT_S16: + // break; + // case AVSampleFormat.AV_SAMPLE_FMT_S32: + // break; + // default: + // throw new Exception("Bad sample format"); + //} + + bytes_per_sample = ffmpeg.av_get_bytes_per_sample(c->sample_fmt); + /* This should not occur, checking just for paranoia */ + if (bytes_per_sample < 0) throw new Exception("Failed to calculate data size"); + + pcm = new AudioPCMConfig( + c->bits_per_raw_sample, c->channels, c->sample_rate, + (AudioPCMConfig.SpeakerConfig)0); // c->channel_layout; + //_sampleCount = MACLibDll.c_APEDecompress_GetInfo(pAPEDecompress, APE_DECOMPRESS_FIELDS.APE_DECOMPRESS_TOTAL_BLOCKS, 0, 0).ToInt64(); + _sampleCount = -1; + _sampleOffset = 0; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + //if (m_StreamIO != null) + //{ + // m_StreamIO.Dispose(); + // m_StreamIO = null; + //} + if (m_stream != null) + { + m_stream.Dispose(); + m_stream = null; + } + } + + AVCodecContext* c1 = c; + ffmpeg.avcodec_free_context(&c1); + c = c1; + //c = null; + + ffmpeg.av_parser_close(parser); + parser = null; + + AVFrame* decoded_frame1 = decoded_frame; + ffmpeg.av_frame_free(&decoded_frame1); + decoded_frame = decoded_frame1; + //decoded_frame = null; + + AVPacket* pkt1 = pkt; + ffmpeg.av_packet_free(&pkt1); + pkt = pkt1; + //pkt = null; + } + + ~AudioDecoder() + { + Dispose(false); + } + + private DecoderSettings m_settings; + + public IAudioDecoderSettings Settings => m_settings; + + public AudioPCMConfig PCM => pcm; + + public string Path => _path; + + public long Length => _sampleCount; + + public long Position + { + get => _sampleOffset; + + set + { + throw new NotSupportedException(); + //_bufferOffset = 0; + //_bufferLength = 0; + //_sampleOffset = value; + //int res = MACLibDll.c_APEDecompress_Seek(pAPEDecompress, (int)value); + //if (0 != res) + // throw new Exception("unable to seek:" + res.ToString()); + } + } + + public long Remaining => _sampleCount < 0 ? -1 : _sampleCount - _sampleOffset; + + public void Close() + { + Dispose(true); + } + + const int AUDIO_INBUF_SIZE = 65536; + const int AUDIO_REFILL_THRESH = 4096; + + private void fill() + { + while (true) + { + if (m_decoded_frame_size > 0) + return; + int ret = ffmpeg.avcodec_receive_frame(c, decoded_frame); + if (ret == ffmpeg.AVERROR_EOF) + return; + if (ret != ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + if (ret < 0) throw new Exception("Error during decoding"); + m_decoded_frame_offset = 0; + m_decoded_frame_size = decoded_frame->nb_samples; + return; + } + if (pkt->size != 0) + { + /* send the packet with the compressed data to the decoder */ + ret = ffmpeg.avcodec_send_packet(c, pkt); + if (ret < 0) throw new Exception("Error submitting the packet to the decoder"); + pkt->size = 0; + continue; + } + if (data_size < AUDIO_REFILL_THRESH) + { + Array.Copy(data_buf, data_offs, data_buf, 0, data_size); + data_offs = 0; + int len = m_stream.Read(data_buf, data_size, data_buf.Length - data_size); + data_size += len; + if (data_size <= 0) return; + } + fixed (byte* data = &data_buf[data_offs]) + ret = ffmpeg.av_parser_parse2(parser, c, &pkt->data, &pkt->size, + data, data_size, ffmpeg.AV_NOPTS_VALUE, ffmpeg.AV_NOPTS_VALUE, 0); + if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + return; + if (ret < 0) + throw new Exception("Error while parsing"); + data_offs += ret; + data_size -= ret; + } + } + + public int Read(AudioBuffer buff, int maxLength) + { + buff.Prepare(this, maxLength); + + long buffOffset = 0; + long samplesNeeded = buff.Length; + long _channelCount = pcm.ChannelCount; + + while (samplesNeeded != 0) + { + if (m_decoded_frame_size == 0) + { + fill(); + if (m_decoded_frame_size == 0) + break; + } + long copyCount = Math.Min(samplesNeeded, m_decoded_frame_size); + + switch (c->sample_fmt) + { + case AVSampleFormat.AV_SAMPLE_FMT_S32: + { + byte* ptr = decoded_frame->data[0u] + c->channels * bytes_per_sample * m_decoded_frame_offset; + int rshift = 32 - pcm.BitsPerSample; + int* smp = (int*)ptr; + fixed (int* dst_start = &buff.Samples[buffOffset, 0]) + { + int* dst = dst_start; + int* dst_end = dst_start + copyCount * c->channels; + while (dst < dst_end) + *(dst++) = *(smp++) >> rshift; + } + } + break; + default: + throw new NotSupportedException(); + } + + samplesNeeded -= copyCount; + buffOffset += copyCount; + m_decoded_frame_offset += copyCount; + m_decoded_frame_size -= copyCount; + _sampleOffset += copyCount; + } + + buff.Length = (int)buffOffset; + return buff.Length; + } + + AVPacket* pkt; + AVFrame* decoded_frame; + AVCodec* codec; + AVCodecParserContext* parser; + AVCodecContext* c; + + long _sampleCount, _sampleOffset; + AudioPCMConfig pcm; + string _path; + Stream m_stream; + long m_decoded_frame_offset; + long m_decoded_frame_size; + + byte[] data_buf; + int data_size; + int data_offs; + + int bytes_per_sample; + } +} diff --git a/CUETools.Codecs.ffmpeg/CUETools.Codecs.ffmpeg.csproj b/CUETools.Codecs.ffmpeg/CUETools.Codecs.ffmpeg.csproj new file mode 100644 index 0000000..b9f9f5a --- /dev/null +++ b/CUETools.Codecs.ffmpeg/CUETools.Codecs.ffmpeg.csproj @@ -0,0 +1,30 @@ + + + + net45;netstandard2.0 + 2.1.7.0 + CUETools.Codecs.ffmpegdll + CUETools.Codecs.ffmpegdll + CUETools + A library for encoding various files using official encoder. + Copyright (c) 2018 Grigory Chudov + Grigory Chudov + true + ..\bin\$(Configuration)\plugins + https://github.com/gchudov/cuetools.net + git + + + + + + False + + + + + + + + + diff --git a/CUETools.Codecs.ffmpeg/DecoderSettings.cs b/CUETools.Codecs.ffmpeg/DecoderSettings.cs new file mode 100644 index 0000000..317439b --- /dev/null +++ b/CUETools.Codecs.ffmpeg/DecoderSettings.cs @@ -0,0 +1,68 @@ +using FFmpeg.AutoGen; +using Newtonsoft.Json; +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace CUETools.Codecs.ffmpegdll +{ + + [JsonObject(MemberSerialization.OptIn)] + public abstract class DecoderSettings: IAudioDecoderSettings + { + #region IAudioDecoderSettings implementation + + [Browsable(false)] + public string Name => "ffmpeg"; + + [Browsable(false)] + public Type DecoderType => typeof(AudioDecoder); + + [Browsable(false)] + public int Priority => 1; + + [Browsable(false)] + public abstract string Extension { get; } + + public IAudioDecoderSettings Clone() + { + return MemberwiseClone() as IAudioDecoderSettings; + } + #endregion + + public abstract AVCodecID Codec { get; } + + // [DisplayName("Version")] + // [Description("Library version")] + // public string Version => Marshal.PtrToStringAnsi(MACLibDll.GetVersionString()); + } + + public class MLPDecoderSettings : DecoderSettings, IAudioDecoderSettings + { + public override string Extension => "mlp"; + public override AVCodecID Codec => AVCodecID.AV_CODEC_ID_MLP; + public MLPDecoderSettings() + { + this.Init(); + } + } + public class FLACDecoderSettings : DecoderSettings, IAudioDecoderSettings + { + public override string Extension => "flac"; + public override AVCodecID Codec => AVCodecID.AV_CODEC_ID_FLAC; + public FLACDecoderSettings() + { + this.Init(); + } + } + + public class WavPackDecoderSettings : DecoderSettings, IAudioDecoderSettings + { + public override string Extension => "wv"; + public override AVCodecID Codec => AVCodecID.AV_CODEC_ID_WAVPACK; + public WavPackDecoderSettings() + { + this.Init(); + } + } +} \ No newline at end of file diff --git a/CUETools.Codecs/CUEToolsCodecsConfig.cs b/CUETools.Codecs/CUEToolsCodecsConfig.cs index aeef7b9..40f4d32 100644 --- a/CUETools.Codecs/CUEToolsCodecsConfig.cs +++ b/CUETools.Codecs/CUEToolsCodecsConfig.cs @@ -87,6 +87,7 @@ namespace CUETools.Codecs formats.Add("mp3", new CUEToolsFormat("mp3", CUEToolsTagger.TagLibSharp, false, true, false, true, null, encodersViewModel.GetDefault("mp3", false), null)); formats.Add("ogg", new CUEToolsFormat("ogg", CUEToolsTagger.TagLibSharp, false, true, false, true, null, encodersViewModel.GetDefault("ogg", false), null)); formats.Add("opus", new CUEToolsFormat("opus", CUEToolsTagger.TagLibSharp, false, true, false, true, null, encodersViewModel.GetDefault("opus", false), null)); + formats.Add("mlp", new CUEToolsFormat("mlp", CUEToolsTagger.APEv2, true, false, false, false, null, null, decodersViewModel.GetDefault("mlp"))); } } } diff --git a/CUETools/CUETools.sln b/CUETools/CUETools.sln index dd66dc4..3f648ef 100644 --- a/CUETools/CUETools.sln +++ b/CUETools/CUETools.sln @@ -195,6 +195,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MACLibDll", "..\ThirdParty\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.MACLib", "..\CUETools.Codecs.MACLib\CUETools.Codecs.MACLib.csproj", "{8FE405A0-E837-4088-88AD-B63652E34790}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.ffmpeg", "..\CUETools.Codecs.ffmpeg\CUETools.Codecs.ffmpeg.csproj", "{A2C09014-C430-4E58-A323-306CCDF313C5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFmpeg.AutoGen", "..\..\FFmpeg.AutoGen\FFmpeg.AutoGen\FFmpeg.AutoGen.csproj", "{B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -745,6 +749,30 @@ Global {8FE405A0-E837-4088-88AD-B63652E34790}.Release|Any CPU.Build.0 = Release|Any CPU {8FE405A0-E837-4088-88AD-B63652E34790}.Release|Win32.ActiveCfg = Release|Any CPU {8FE405A0-E837-4088-88AD-B63652E34790}.Release|x64.ActiveCfg = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|Win32.ActiveCfg = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|Win32.Build.0 = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Debug|x64.Build.0 = Debug|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|Any CPU.Build.0 = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|Win32.ActiveCfg = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|Win32.Build.0 = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|x64.ActiveCfg = Release|Any CPU + {A2C09014-C430-4E58-A323-306CCDF313C5}.Release|x64.Build.0 = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|Win32.Build.0 = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Debug|x64.Build.0 = Debug|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|Any CPU.Build.0 = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|Win32.ActiveCfg = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|Win32.Build.0 = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|x64.ActiveCfg = Release|Any CPU + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -805,6 +833,8 @@ Global {1A87F412-BA74-4DBB-9F77-FD55C042FB63} = {8B179853-B7D6-479C-B8B2-6CBCE835D040} {CEE4A179-49FC-4D51-B045-F717D3A6C13A} = {8B179853-B7D6-479C-B8B2-6CBCE835D040} {8FE405A0-E837-4088-88AD-B63652E34790} = {93B7AE1D-DEF6-4A04-A222-5CDE09DF262D} + {A2C09014-C430-4E58-A323-306CCDF313C5} = {93B7AE1D-DEF6-4A04-A222-5CDE09DF262D} + {B0A0C17A-C1BC-4CB1-BE1E-F545F54A7923} = {8B179853-B7D6-479C-B8B2-6CBCE835D040} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C634D169-5814-4203-94B6-6A11371DDA95} diff --git a/ThirdParty/flac b/ThirdParty/flac index 8f4f296..900bf23 160000 --- a/ThirdParty/flac +++ b/ThirdParty/flac @@ -1 +1 @@ -Subproject commit 8f4f296372384d3622d230247c370329662ca1a5 +Subproject commit 900bf231417431c9d7487cb4ad2cc4c09e7446f9 diff --git a/ThirdParty/openclnet b/ThirdParty/openclnet index 0d5dba4..e4619a0 160000 --- a/ThirdParty/openclnet +++ b/ThirdParty/openclnet @@ -1 +1 @@ -Subproject commit 0d5dba44e2c810c6c60d19044c2a8ed3a6d65659 +Subproject commit e4619a09e5afe4bc958f6777b70f8b6b6111e778