From 1a025a590eab83921deca30494797d5aa3ed78a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Sg=C3=A1netz?= Date: Mon, 17 Apr 2023 23:42:32 +0200 Subject: [PATCH] Conversion tracks --- Cavern.Format/Common/RenderTrack.cs | 97 +++++++++++++++++++ Cavern.Format/Common/Track.cs | 24 ++--- .../Container/AudioWriterIntoContainer.cs | 76 +++++++++++++++ Cavern/Filters/DualConvolver.cs | 12 +-- .../VirtualizerFilter.VirtualChannel.cs | 1 - 5 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 Cavern.Format/Common/RenderTrack.cs create mode 100644 Cavern.Format/Output/Container/AudioWriterIntoContainer.cs diff --git a/Cavern.Format/Common/RenderTrack.cs b/Cavern.Format/Common/RenderTrack.cs new file mode 100644 index 00000000..308a0780 --- /dev/null +++ b/Cavern.Format/Common/RenderTrack.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; + +using Cavern.Format.Container; + +namespace Cavern.Format.Common { + /// + /// Encodes audio content as a to be used in a . + /// + public class RenderTrack : Track, IDisposable { + /// + /// Seconds that pass with each block. + /// + internal readonly double timeStep; + + /// + /// Encodes the audio into . + /// + readonly AudioWriter encoder; + + /// + /// Encoded bytes are stored in this stream. + /// + readonly MemoryStream output = new MemoryStream(); + + /// + /// The reused result of if the block length doesn't change. + /// + byte[] blockCache = new byte[0]; + + /// + /// Number of blocks requested by . + /// + int blocksWritten = 0; + + /// + /// Encodes audio content as a to be used in a . + /// + /// Codec used for encoding + /// The fixed number of samples that will be encoded + /// Number of output channels + /// Rendering environment sample rate + /// Bit depth of the if applicable + public RenderTrack(Codec format, int blockSize, int channelCount, int sampleRate, BitDepth bits) { + timeStep = blockSize / (double)sampleRate; + Format = format; + encoder = format switch { + Codec.PCM_LE => new RIFFWaveWriter(output, channelCount, blockSize, sampleRate, bits), + Codec.PCM_Float => new RIFFWaveWriter(output, channelCount, blockSize, sampleRate, bits), + _ => throw new UnsupportedCodecException(true, format), + }; + Extra = new TrackExtraAudio { + SampleRate = encoder.SampleRate, + ChannelCount = encoder.ChannelCount, + Bits = encoder.Bits + }; + } + + /// + /// Process a block of samples and put the encoded block in so could read it. + /// To use this class, do the following steps:
+ /// - create the fixed-size block of samples
+ /// -
+ /// - , it will call + ///
+ public void EncodeNextBlock(float[] samples) => encoder.WriteBlock(samples, 0, samples.Length); + + /// + /// Continue reading the track. + /// + public override byte[] ReadNextBlock() { + if (blockCache.LongLength != output.Position) { + blockCache = new byte[output.Position]; + } + output.Position = 0; // For reading + output.Read(blockCache); + output.Position = 0; // For overwriting + blocksWritten++; + return blockCache; + } + + /// + /// Returns if the next block can be completely decoded by itself. + /// + public override bool IsNextBlockKeyframe() => true; + + /// + /// Get the block's offset in seconds. + /// + public override double GetNextBlockOffset() => blocksWritten * timeStep; + + /// + /// Free up the resources used by this object. + /// + public void Dispose() => encoder.Dispose(); + } +} \ No newline at end of file diff --git a/Cavern.Format/Common/Track.cs b/Cavern.Format/Common/Track.cs index c8400abc..d261ce68 100644 --- a/Cavern.Format/Common/Track.cs +++ b/Cavern.Format/Common/Track.cs @@ -38,7 +38,7 @@ public class Track { /// /// The position of the track in the container's list of tracks. /// - int trackNumber; + internal int trackNumber; /// /// Create a track to be placed in the list of a container's tracks after it's set up with @@ -57,27 +57,27 @@ public class Track { this.trackNumber = trackNumber; } - /// - /// Late-init the and . - /// - internal void Override(ContainerReader source, int trackNumber) { - Source = source; - this.trackNumber = trackNumber; - } - /// /// Continue reading the track. /// - public byte[] ReadNextBlock() => Source.ReadNextBlock(trackNumber); + public virtual byte[] ReadNextBlock() => Source.ReadNextBlock(trackNumber); /// /// Returns if the next block can be completely decoded by itself. /// - public bool IsNextBlockKeyframe() => Source.IsNextBlockKeyframe(trackNumber); + public virtual bool IsNextBlockKeyframe() => Source.IsNextBlockKeyframe(trackNumber); /// /// Get the block's offset in seconds. /// - public double GetNextBlockOffset() => Source.GetNextBlockOffset(trackNumber); + public virtual double GetNextBlockOffset() => Source.GetNextBlockOffset(trackNumber); + + /// + /// Late-init the and . + /// + internal void Override(ContainerReader source, int trackNumber) { + Source = source; + this.trackNumber = trackNumber; + } } } \ No newline at end of file diff --git a/Cavern.Format/Output/Container/AudioWriterIntoContainer.cs b/Cavern.Format/Output/Container/AudioWriterIntoContainer.cs new file mode 100644 index 00000000..ea066c92 --- /dev/null +++ b/Cavern.Format/Output/Container/AudioWriterIntoContainer.cs @@ -0,0 +1,76 @@ +using System; + +using Cavern.Format.Common; + +namespace Cavern.Format.Container { + /// + /// Writes the audio data as a new into a , amongst other tracks. + /// + public class AudioWriterIntoContainer : AudioWriter { + /// + /// Handles the container itself. + /// + readonly ContainerWriter container; + + /// + /// The new track to encode. + /// + readonly RenderTrack track; + + /// + /// Writes the audio data as a new into a , amongst other tracks. + /// + /// Output file name + /// Tracks to bring from other containers, can be empty + /// The codec of the new audio track + /// Total number of samples for all channels that will be encoded in each frame + /// Number of output channels + /// Content length in samples for a single channel + /// Sample rate of the new audio track + /// Bit rate of the new audio track if applicable + /// The container format is either unknown by file extension or + /// there was no file extension + public AudioWriterIntoContainer(string path, Track[] tracks, Codec newTrack, int blockSize, + int channelCount, long length, int sampleRate, BitDepth bits) : base(path, channelCount, length, sampleRate, bits) { + int index = path.LastIndexOf('.') + 1; + if (index == 0) { + throw new UnsupportedFormatException(); + } + + Array.Resize(ref tracks, tracks.Length + 1); + tracks[^1] = track = new RenderTrack(newTrack, blockSize, channelCount, sampleRate, bits); + + switch (path[index..]) { + case "mkv": + case "mka": + case "webm": + case "weba": + container = new MatroskaWriter(writer, tracks, length / (double)sampleRate); + break; + default: + throw new UnsupportedFormatException(); + } + } + + /// + /// Create the file header. + /// + public override void WriteHeader() => container.WriteHeader(); + + /// + /// Write a block of mono or interlaced samples. + /// + /// Samples to write + /// Start position in the input array (inclusive) + /// End position in the input array (exclusive) + public override void WriteBlock(float[] samples, long from, long to) { + track.EncodeNextBlock(samples); + container.WriteBlock(track.timeStep); + } + + /// + /// Dispose the stream writer through the as it might want to write a footer. + /// + public override void Dispose() => container.Dispose(); + } +} \ No newline at end of file diff --git a/Cavern/Filters/DualConvolver.cs b/Cavern/Filters/DualConvolver.cs index 9d4ecf02..45d749a8 100644 --- a/Cavern/Filters/DualConvolver.cs +++ b/Cavern/Filters/DualConvolver.cs @@ -51,10 +51,10 @@ public class DualConvolver { int fftSize = 2 << QMath.Log2Ceil(impulseLength); // Zero padding for the falloff to have space cache = new ThreadSafeFFTCache(fftSize); filter = new Complex[fftSize]; - for (int sample = 0; sample < impulse1.Length; ++sample) { + for (int sample = 0; sample < impulse1.Length; sample++) { filter[sample].Real = impulse1[sample]; } - for (int sample = 0; sample < impulse2.Length; ++sample) { + for (int sample = 0; sample < impulse2.Length; sample++) { filter[sample].Imaginary = impulse2[sample]; } filter.InPlaceFFT(cache); @@ -82,10 +82,10 @@ public class DualConvolver { /// void ProcessTimeslot(float[] samplesInOut, float[] samplesOut, int from, int to) { // Move samples and pad present - for (int i = from; i < to; ++i) { + for (int i = from; i < to; i++) { present[i - from] = new Complex(samplesInOut[i], 0); } - for (int i = to - from; i < present.Length; ++i) { + for (int i = to - from; i < present.Length; i++) { present[i].Clear(); } @@ -95,14 +95,14 @@ public class DualConvolver { present.InPlaceIFFT(cache); // Append the result to the future - for (int i = 0; i < present.Length; ++i) { + for (int i = 0; i < present.Length; i++) { future[i + delay1].Real += present[i].Real; future[i + delay2].Imaginary += present[i].Imaginary; } // Write the output and remove those samples from the future to -= from; - for (int i = 0; i < to; ++i) { + for (int i = 0; i < to; i++) { samplesInOut[from + i] = future[i].Real; samplesOut[from + i] = future[i].Imaginary; } diff --git a/Cavern/Virtualizer/VirtualizerFilter.VirtualChannel.cs b/Cavern/Virtualizer/VirtualizerFilter.VirtualChannel.cs index 86ea3745..7285f23b 100644 --- a/Cavern/Virtualizer/VirtualizerFilter.VirtualChannel.cs +++ b/Cavern/Virtualizer/VirtualizerFilter.VirtualChannel.cs @@ -2,7 +2,6 @@ using Cavern.Channels; using Cavern.Filters; -using Cavern.Utilities; namespace Cavern.Virtualizer { ///