diff --git a/RMuseum/RMuseum.csproj b/RMuseum/RMuseum.csproj index afa80d93..42d58c1b 100644 --- a/RMuseum/RMuseum.csproj +++ b/RMuseum/RMuseum.csproj @@ -19,6 +19,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/RMuseum/Services/Implementation/AudioNarrationService.cs b/RMuseum/Services/Implementation/AudioNarrationService.cs index 3b9c0454..9eda60d0 100644 --- a/RMuseum/Services/Implementation/AudioNarrationService.cs +++ b/RMuseum/Services/Implementation/AudioNarrationService.cs @@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using MySql.Data.MySqlClient; +using NAudio.Wave; +using OggVorbisEncoder; using RMuseum.DbContext; using RMuseum.Models.GanjoorAudio; using RMuseum.Models.GanjoorAudio.ViewModels; @@ -282,6 +284,14 @@ namespace RMuseum.Services.Implementation { //here we should produce and save ogg file + using(Mp3FileReader mp3FileReader = new Mp3FileReader(mp3file.FilePath)) + { + byte[] samples = new byte[mp3FileReader.Length]; + await mp3FileReader.ReadAsync(samples, 0, (int)mp3FileReader.Length); + var oggBytes = ConvertRawPCMFile(mp3FileReader.Mp3WaveFormat.SampleRate, mp3FileReader.Mp3WaveFormat.Channels, samples, PCMSample.EightBit, mp3FileReader.Mp3WaveFormat.SampleRate, mp3FileReader.Mp3WaveFormat.Channels); + File.WriteAllBytes(Path.Combine(Path.GetDirectoryName(mp3file.FilePath), $"{Path.GetFileNameWithoutExtension(mp3file.FilePath)}.ogg") , oggBytes); + } + session.UploadedFiles.Where(f => f.Id == file.Id).SingleOrDefault().ProcessResultMsg = ""; session.UploadedFiles.Where(f => f.Id == file.Id).SingleOrDefault().ProcessResult = true; session.UploadedFiles.Where(f => f.Id == mp3file.Id).SingleOrDefault().ProcessResultMsg = ""; @@ -313,7 +323,137 @@ namespace RMuseum.Services.Implementation } } - + enum PCMSample : int + { + EightBit = 1, + SixteenBit = 2 + } + + private static byte[] ConvertRawPCMFile(int OutputSampleRate, int OutputChannels, byte[] PCMSamples, PCMSample PCMSampleSize, int PCMSampleRate, int PCMChannels) + { + int NumPCMSamples = (PCMSamples.Length / (int)PCMSampleSize / PCMChannels); + float PCMDuraton = NumPCMSamples / (float)PCMSampleRate; + + int NumOutputSamples = (int)(PCMDuraton * OutputSampleRate); + //Ensure that samble buffer is aligned to write chunk size + NumOutputSamples = (NumOutputSamples / WriteBufferSize) * WriteBufferSize; + + float[][] OutSamples = new float[OutputChannels][]; + + for (int ch = 0; ch < OutputChannels; ch++) + OutSamples[ch] = new float[NumOutputSamples]; + + for (int sampleNumber = 0; sampleNumber < NumOutputSamples; sampleNumber++) + { + float rawSample = 0.0f; + + for (int ch = 0; ch < OutputChannels; ch++) + { + int sampleIndex = (sampleNumber * PCMChannels) * (int)PCMSampleSize; + + if (ch < PCMChannels) sampleIndex += (ch * (int)PCMSampleSize); + + switch (PCMSampleSize) + { + case PCMSample.EightBit: + rawSample = ByteToSample(PCMSamples[sampleIndex]); + break; + case PCMSample.SixteenBit: + rawSample = ShortToSample((short)(PCMSamples[sampleIndex + 1] << 8 | PCMSamples[sampleIndex])); + break; + } + + OutSamples[ch][sampleNumber] = rawSample; + } + } + + return GenerateFile(OutSamples, OutputSampleRate, OutputChannels); + } + + private static float ByteToSample(short pcmValue) + { + return pcmValue / 128f; + } + + private static float ShortToSample(short pcmValue) + { + return pcmValue / 32768f; + } + + private static byte[] GenerateFile(float[][] FloatSamples, int SampleRate, int Channels) + { + using (MemoryStream outputData = new MemoryStream()) + { + // Stores all the static vorbis bitstream settings + var info = VorbisInfo.InitVariableBitRate(Channels, SampleRate, 0.5f); + + // set up our packet->stream encoder + var serial = new Random().Next(); + var oggStream = new OggStream(serial); + + // ========================================================= + // HEADER + // ========================================================= + // Vorbis streams begin with three headers; the initial header (with + // most of the codec setup parameters) which is mandated by the Ogg + // bitstream spec. The second header holds any comment fields. The + // third header holds the bitstream codebook. + var headerBuilder = new HeaderPacketBuilder(); + + var comments = new Comments(); + comments.AddTag("ARTIST", "TEST"); + + var infoPacket = headerBuilder.BuildInfoPacket(info); + var commentsPacket = headerBuilder.BuildCommentsPacket(comments); + var booksPacket = headerBuilder.BuildBooksPacket(info); + + oggStream.PacketIn(infoPacket); + oggStream.PacketIn(commentsPacket); + oggStream.PacketIn(booksPacket); + + // Flush to force audio data onto its own page per the spec + FlushPages(oggStream, outputData, true); + + // ========================================================= + // BODY (Audio Data) + // ========================================================= + var processingState = ProcessingState.Create(info); + + processingState.WriteData(FloatSamples, FloatSamples[0].Length); + for (int readIndex = 0; readIndex <= FloatSamples[0].Length; readIndex += WriteBufferSize) + { + + OggPacket packet; + while (!oggStream.Finished + && processingState.PacketOut(out packet)) + { + oggStream.PacketIn(packet); + + FlushPages(oggStream, outputData, false); + } + } + + FlushPages(oggStream, outputData, true); + + return outputData.ToArray(); + } + } + + private static void FlushPages(OggStream oggStream, Stream Output, bool Force) + { + OggPage page; + while (oggStream.PageOut(out page, Force)) + { + Output.Write(page.Header, 0, page.Header.Length); + Output.Write(page.Body, 0, page.Body.Length); + } + } + + private static int WriteBufferSize = 512; + private static readonly int[] SampleRates = { 8000, 11025, 16000, 22050, 32000, 44100 }; + + + /// /// Get Upload Session (including files) ///