Skip to content

Commit

Permalink
Set up test coverage for AST audio.
Browse files Browse the repository at this point in the history
  • Loading branch information
MeltyPlayer committed May 6, 2024
1 parent ff46f73 commit 1c73f60
Show file tree
Hide file tree
Showing 28 changed files with 568 additions and 168 deletions.
23 changes: 23 additions & 0 deletions FinModelUtility/Benchmarks/CastingValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using BenchmarkDotNet.Attributes;

namespace benchmarks {
public class CastingValues {
private const int n = 100000000;

[Benchmark]
public unsafe void ViaPointer() {
for (var i = 0; i < n; i++) {
ulong value = 123456;
double castedValue = *(double*) (&value);
}
}

[Benchmark]
public void ViaBitConverter() {
for (var i = 0; i < n; i++) {
ulong value = 123456;
double castedValue = BitConverter.UInt64BitsToDouble(value);
}
}
}
}
2 changes: 1 addition & 1 deletion FinModelUtility/Benchmarks/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace benchmarks {
public class Program {
public static void Main(string[] args) {
var summary = BenchmarkRunner.Run<Loops>(
var summary = BenchmarkRunner.Run<CastingValues>(
ManualConfig.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions
.DisableOptimizationsValidator));
Expand Down
1 change: 1 addition & 0 deletions FinModelUtility/Fin/Fin/Fin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NVorbis" Version="0.10.5" />
<PackageReference Include="OggVorbisEncoder" Version="1.2.2" />
<PackageReference Include="OpenAL.Soft" Version="1.19.1" />
<PackageReference Include="Pfim" Version="0.11.2" />
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0027" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using fin.io;

namespace fin.audio.io.exporters {
public interface IAudioExporter {
void ExportAudio(IAudioBuffer<short> audioBuffer, ISystemFile outputFile);
}
}
108 changes: 108 additions & 0 deletions FinModelUtility/Fin/Fin/src/audio/io/exporters/ogg/OggAudioExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.IO;

using fin.io;
using fin.util.asserts;

using OggVorbisEncoder;

namespace fin.audio.io.exporters.ogg {
/// <summary>
/// Based on example at:
/// https://github.com/SteveLillis/.NET-Ogg-Vorbis-Encoder/blob/master/OggVorbisEncoder.Example/Encoder.cs
/// </summary>
public class OggAudioExporter : IAudioExporter {
private const int WRITE_BUFFER_SIZE = 512;

public void ExportAudio(IAudioBuffer<short> audioBuffer,
ISystemFile outputFile) {
Asserts.SequenceEqual(".ogg", outputFile.FileType.ToLower());

using var outputData = outputFile.OpenWrite();

var channelCount = audioBuffer.AudioChannelsType switch {
AudioChannelsType.MONO => 1,
AudioChannelsType.STEREO => 2,
};

var oggStream = new OggStream(new Random().Next());

// =========================================================
// 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 info = VorbisInfo.InitVariableBitRate(channelCount,
audioBuffer.Frequency,
1f);
var infoPacket = HeaderPacketBuilder.BuildInfoPacket(info);
var commentsPacket
= HeaderPacketBuilder.BuildCommentsPacket(new Comments());
var booksPacket = HeaderPacketBuilder.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);

var lengthInSamples = audioBuffer.LengthInSamples;

var floatSamples = new float[channelCount][];
for (var c = 0; c < channelCount; ++c) {
var channelSamples = floatSamples[c] = new float[lengthInSamples];

var channel = channelCount switch {
1 => AudioChannelType.MONO,
2 => c switch {
0 => AudioChannelType.STEREO_LEFT,
1 => AudioChannelType.STEREO_RIGHT
}
};

for (var i = 0; i < lengthInSamples; ++i) {
var shortSample = audioBuffer.GetPcm(channel, i);
channelSamples[i] = (shortSample / (1f * short.MaxValue));
}
}

for (int readIndex = 0;
readIndex <= lengthInSamples;
readIndex += WRITE_BUFFER_SIZE) {
if (readIndex == lengthInSamples) {
processingState.WriteEndOfStream();
} else {
var writeLength
= Math.Min(lengthInSamples - readIndex, WRITE_BUFFER_SIZE);
processingState.WriteData(floatSamples, writeLength, readIndex);
}

while (!oggStream.Finished &&
processingState.PacketOut(out OggPacket packet)) {
oggStream.PacketIn(packet);

FlushPages_(oggStream, outputData, false);
}
}

FlushPages_(oggStream, outputData, true);
}

private static void FlushPages_(OggStream oggStream,
Stream output,
bool force) {
while (oggStream.PageOut(out OggPage page, force)) {
output.Write(page.Header, 0, page.Header.Length);
output.Write(page.Body, 0, page.Body.Length);
}
}
}
}
10 changes: 10 additions & 0 deletions FinModelUtility/Fin/Fin/src/testing/BGoldenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using fin.io;
using fin.io.bundles;

namespace fin.testing {
public abstract class BGoldenTests<TFileBundle>
where TFileBundle : IFileBundle {
public abstract TFileBundle GetFileBundleFromDirectory(
IFileHierarchyDirectory directory);
}
}
116 changes: 116 additions & 0 deletions FinModelUtility/Fin/Fin/src/testing/GoldenAssert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using CommunityToolkit.HighPerformance;

using fin.io;
using fin.util.asserts;
using fin.util.strings;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace fin.testing {
public static class GoldenAssert {
private const string TMP_NAME = "tmp";

public static ISystemDirectory GetRootGoldensDirectory(
Assembly executingAssembly) {
var assemblyName =
executingAssembly.ManifestModule.Name.SubstringUpTo(".dll");

var executingAssemblyDll = new FinFile(executingAssembly.Location);
var executingAssemblyDir = executingAssemblyDll.AssertGetParent();

var currentDir = executingAssemblyDir;
while (currentDir.Name != assemblyName) {
currentDir = currentDir.AssertGetParent();
}

Assert.IsNotNull(currentDir);

var gloTestsDir = currentDir;
var goldensDirectory = gloTestsDir.AssertGetExistingSubdir("goldens");

return goldensDirectory;
}

public static IEnumerable<IFileHierarchyDirectory> GetGoldenDirectories(
ISystemDirectory rootGoldenDirectory) {
var hierarchy = FileHierarchy.From(rootGoldenDirectory);
return hierarchy.Root.GetExistingSubdirs()
.Where(
subdir => subdir.Name != TMP_NAME);
}

public static IEnumerable<IFileHierarchyDirectory>
GetGoldenInputDirectories(ISystemDirectory rootGoldenDirectory)
=> GetGoldenDirectories(rootGoldenDirectory)
.Select(subdir => subdir.AssertGetExistingSubdir("input"));

public static void RunInTestDirectory(
IFileHierarchyDirectory goldenSubdir,
Action<ISystemDirectory> handler) {
var tmpDirectory = goldenSubdir.Impl.GetOrCreateSubdir(TMP_NAME);
tmpDirectory.DeleteContents();

try {
handler(tmpDirectory);
} finally {
tmpDirectory.DeleteContents();
tmpDirectory.Delete();
}
}

public static void AssertFilesInDirectoriesAreIdentical(
IReadOnlyTreeDirectory lhs,
IReadOnlyTreeDirectory rhs) {
var lhsFiles = lhs.GetExistingFiles()
.ToDictionary(file => file.Name);
var rhsFiles = rhs.GetExistingFiles()
.ToDictionary(file => file.Name);

Assert.IsTrue(lhsFiles.Keys.ToHashSet()
.SetEquals(rhsFiles.Keys.ToHashSet()));

foreach (var (name, lhsFile) in lhsFiles) {
var rhsFile = rhsFiles[name];
try {
AssertFilesAreIdentical_(lhsFile, rhsFile);
} catch (Exception ex) {
throw new Exception($"Found a change in file {name}: ", ex);
}
}
}

private static void AssertFilesAreIdentical_(
IReadOnlyTreeFile lhs,
IReadOnlyTreeFile rhs) {
using var lhsStream = lhs.OpenRead();
using var rhsStream = rhs.OpenRead();

Assert.AreEqual(lhsStream.Length, rhsStream.Length);

var bytesToRead = sizeof(long);
int iterations =
(int) Math.Ceiling((double) lhsStream.Length / bytesToRead);

long lhsLong = 0;
long rhsLong = 0;

var lhsSpan = new Span<long>(ref lhsLong).AsBytes();
var rhsSpan = new Span<long>(ref rhsLong).AsBytes();

for (int i = 0; i < iterations; i++) {
lhsStream.Read(lhsSpan);
rhsStream.Read(rhsSpan);

if (lhsLong != rhsLong) {
Asserts.Fail(
$"Files with name \"{lhs.Name}\" are different around byte #: {i * bytesToRead}");
}
}
}
}
}
96 changes: 96 additions & 0 deletions FinModelUtility/Fin/Fin/src/testing/audio/AudioGoldenAssert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using fin.audio.io;
using fin.audio.io.exporters.ogg;
using fin.audio.io.importers;
using fin.model.io.exporters;
using fin.model.io.exporters.assimp.indirect;
using fin.io;
using fin.testing.audio.stubbed;

namespace fin.testing.audio {
public static class AudioGoldenAssert {
public static IEnumerable<TAudioBundle> GetGoldenAudioBundles<TAudioBundle>(
ISystemDirectory rootGoldenDirectory,
Func<IFileHierarchyDirectory, TAudioBundle>
gatherAudioBundleFromInputDirectory)
where TAudioBundle : IAudioFileBundle {
foreach (var goldenSubdir in
GoldenAssert.GetGoldenDirectories(rootGoldenDirectory)) {
var inputDirectory = goldenSubdir.AssertGetExistingSubdir("input");
var audioBundle = gatherAudioBundleFromInputDirectory(inputDirectory);

yield return audioBundle;
}
}

/// <summary>
/// Asserts model goldens. Assumes that directories will be stored as the following:
///
/// - {goldenDirectory}
/// - {goldenName1}
/// - input
/// - {raw golden files}
/// - output
/// - {exported files}
/// - {goldenName2}
/// ...
/// </summary>
public static void AssertExportGoldens<TAudioBundle>(
ISystemDirectory rootGoldenDirectory,
IAudioImporter<TAudioBundle> audioImporter,
Func<IFileHierarchyDirectory, TAudioBundle>
gatherAudioBundleFromInputDirectory)
where TAudioBundle : IAudioFileBundle {
foreach (var goldenSubdir in
GoldenAssert.GetGoldenDirectories(rootGoldenDirectory)) {
AudioGoldenAssert.AssertGolden(goldenSubdir,
audioImporter,
gatherAudioBundleFromInputDirectory);
}
}

private static string EXTENSION = ".ogg";

public static void AssertGolden<TAudioBundle>(
IFileHierarchyDirectory goldenSubdir,
IAudioImporter<TAudioBundle> audioImporter,
Func<IFileHierarchyDirectory, TAudioBundle>
gatherAudioBundleFromInputDirectory)
where TAudioBundle : IAudioFileBundle {
using var audioManager = new StubbedAudioManager();

var inputDirectory = goldenSubdir.AssertGetExistingSubdir("input");
var audioBundle = gatherAudioBundleFromInputDirectory(inputDirectory);

var outputDirectory = goldenSubdir.AssertGetExistingSubdir("output");
var hasGoldenExport =
outputDirectory.GetFilesWithFileType(EXTENSION).Any();

GoldenAssert.RunInTestDirectory(
goldenSubdir,
tmpDirectory => {
var targetDirectory =
hasGoldenExport ? tmpDirectory : outputDirectory.Impl;

var audioBuffer
= audioImporter.ImportAudio(audioManager, audioBundle);
new OggAudioExporter()
.ExportAudio(
audioBuffer,
new FinFile(
Path.Combine(targetDirectory.FullPath,
$"{audioBundle.MainFile.NameWithoutExtension}{EXTENSION}")));

if (hasGoldenExport) {
GoldenAssert.AssertFilesInDirectoriesAreIdentical(
tmpDirectory,
outputDirectory.Impl);
}
});
}
}
}
Loading

0 comments on commit 1c73f60

Please sign in to comment.