Skip to content

Commit

Permalink
Conversion tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Apr 17, 2023
1 parent 69597b4 commit 1a025a5
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 19 deletions.
97 changes: 97 additions & 0 deletions Cavern.Format/Common/RenderTrack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.IO;

using Cavern.Format.Container;

namespace Cavern.Format.Common {
/// <summary>
/// Encodes audio content as a <see cref="Track"/> to be used in a <see cref="ContainerWriter"/>.
/// </summary>
public class RenderTrack : Track, IDisposable {
/// <summary>
/// Seconds that pass with each block.
/// </summary>
internal readonly double timeStep;

/// <summary>
/// Encodes the audio into <see cref="output"/>.
/// </summary>
readonly AudioWriter encoder;

/// <summary>
/// Encoded bytes are stored in this stream.
/// </summary>
readonly MemoryStream output = new MemoryStream();

/// <summary>
/// The reused result of <see cref="ReadNextBlock"/> if the block length doesn't change.
/// </summary>
byte[] blockCache = new byte[0];

/// <summary>
/// Number of blocks requested by <see cref="ReadNextBlock"/>.
/// </summary>
int blocksWritten = 0;

/// <summary>
/// Encodes audio content as a <see cref="Track"/> to be used in a <see cref="ContainerWriter"/>.
/// </summary>
/// <param name="format">Codec used for encoding</param>
/// <param name="blockSize">The fixed number of samples that will be encoded</param>
/// <param name="channelCount">Number of output channels</param>
/// <param name="sampleRate">Rendering environment sample rate</param>
/// <param name="bits">Bit depth of the <paramref name="format"/> if applicable</param>
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
};
}

/// <summary>
/// Process a block of samples and put the encoded block in <see cref="output"/> so <see cref="ReadNextBlock"/> could read it.
/// To use this class, do the following steps:<br />
/// - create the fixed-size block of samples<br />
/// - <see cref="EncodeNextBlock(float[])"/><br />
/// - <see cref="ContainerWriter.WriteBlock(double)"/>, it will call <see cref="ReadNextBlock"/>
/// </summary>
public void EncodeNextBlock(float[] samples) => encoder.WriteBlock(samples, 0, samples.Length);

/// <summary>
/// Continue reading the track.
/// </summary>
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;
}

/// <summary>
/// Returns if the next block can be completely decoded by itself.
/// </summary>
public override bool IsNextBlockKeyframe() => true;

/// <summary>
/// Get the block's offset in seconds.
/// </summary>
public override double GetNextBlockOffset() => blocksWritten * timeStep;

/// <summary>
/// Free up the resources used by this object.
/// </summary>
public void Dispose() => encoder.Dispose();
}
}
24 changes: 12 additions & 12 deletions Cavern.Format/Common/Track.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class Track {
/// <summary>
/// The position of the track in the container's list of tracks.
/// </summary>
int trackNumber;
internal int trackNumber;

/// <summary>
/// Create a track to be placed in the list of a container's tracks after it's set up with
Expand All @@ -57,27 +57,27 @@ public class Track {
this.trackNumber = trackNumber;
}

/// <summary>
/// Late-init the <see cref="Source"/> and <see cref="trackNumber"/>.
/// </summary>
internal void Override(ContainerReader source, int trackNumber) {
Source = source;
this.trackNumber = trackNumber;
}

/// <summary>
/// Continue reading the track.
/// </summary>
public byte[] ReadNextBlock() => Source.ReadNextBlock(trackNumber);
public virtual byte[] ReadNextBlock() => Source.ReadNextBlock(trackNumber);

/// <summary>
/// Returns if the next block can be completely decoded by itself.
/// </summary>
public bool IsNextBlockKeyframe() => Source.IsNextBlockKeyframe(trackNumber);
public virtual bool IsNextBlockKeyframe() => Source.IsNextBlockKeyframe(trackNumber);

/// <summary>
/// Get the block's offset in seconds.
/// </summary>
public double GetNextBlockOffset() => Source.GetNextBlockOffset(trackNumber);
public virtual double GetNextBlockOffset() => Source.GetNextBlockOffset(trackNumber);

/// <summary>
/// Late-init the <see cref="Source"/> and <see cref="trackNumber"/>.
/// </summary>
internal void Override(ContainerReader source, int trackNumber) {
Source = source;
this.trackNumber = trackNumber;
}
}
}
76 changes: 76 additions & 0 deletions Cavern.Format/Output/Container/AudioWriterIntoContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;

using Cavern.Format.Common;

namespace Cavern.Format.Container {
/// <summary>
/// Writes the audio data as a new <see cref="track"/> into a <see cref="container"/>, amongst other tracks.
/// </summary>
public class AudioWriterIntoContainer : AudioWriter {
/// <summary>
/// Handles the container itself.
/// </summary>
readonly ContainerWriter container;

/// <summary>
/// The new track to encode.
/// </summary>
readonly RenderTrack track;

/// <summary>
/// Writes the audio data as a new <see cref="track"/> into a <see cref="container"/>, amongst other tracks.
/// </summary>
/// <param name="path">Output file name</param>
/// <param name="tracks">Tracks to bring from other containers, can be empty</param>
/// <param name="newTrack">The codec of the new audio track</param>
/// <param name="blockSize">Total number of samples for all channels that will be encoded in each frame</param>
/// <param name="channelCount">Number of output channels</param>
/// <param name="length">Content length in samples for a single channel</param>
/// <param name="sampleRate">Sample rate of the new audio track</param>
/// <param name="bits">Bit rate of the new audio track if applicable</param>
/// <exception cref="UnsupportedFormatException">The container format is either unknown by file extension or
/// there was no file extension</exception>
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();
}
}

/// <summary>
/// Create the file header.
/// </summary>
public override void WriteHeader() => container.WriteHeader();

/// <summary>
/// Write a block of mono or interlaced samples.
/// </summary>
/// <param name="samples">Samples to write</param>
/// <param name="from">Start position in the input array (inclusive)</param>
/// <param name="to">End position in the input array (exclusive)</param>
public override void WriteBlock(float[] samples, long from, long to) {
track.EncodeNextBlock(samples);
container.WriteBlock(track.timeStep);
}

/// <summary>
/// Dispose the stream writer through the <see cref="container"/> as it might want to write a footer.
/// </summary>
public override void Dispose() => container.Dispose();
}
}
12 changes: 6 additions & 6 deletions Cavern/Filters/DualConvolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -82,10 +82,10 @@ public class DualConvolver {
/// </summary>
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();
}

Expand All @@ -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;
}
Expand Down
1 change: 0 additions & 1 deletion Cavern/Virtualizer/VirtualizerFilter.VirtualChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using Cavern.Channels;
using Cavern.Filters;
using Cavern.Utilities;

namespace Cavern.Virtualizer {
/// <summary>
Expand Down

0 comments on commit 1a025a5

Please sign in to comment.