Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,50 @@ jobs:
shell: pwsh
working-directory: LiveSPICEVst
run: dotnet publish -c Release --framework net8.0-windows /p:DebugType=None /p:UseSharedCompilation=false /p:UseRazorBuildServer=false
# --check asserts results against Tests/Stats/*.csv; --sampleRate 44100 because
# that is the configuration the committed baselines were generated with.
- name: Run circuit tests
shell: pwsh
working-directory: Tests
run: dotnet run -c Release --framework net6.0-windows test "Circuits\*.schx"
run: dotnet run -c Release --framework net6.0-windows test "Circuits\*.schx" --check --sampleRate 44100
- name: Run examples
shell: pwsh
working-directory: Tests
run: dotnet run -c Release --framework net6.0-windows test "Examples\*.schx"
run: dotnet run -c Release --framework net6.0-windows test "Examples\*.schx" --check --sampleRate 44100

test-macos:
name: Test (macOS)
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: true
- uses: actions/setup-dotnet@v2
with:
# https://dotnet.microsoft.com/en-us/download/dotnet
dotnet-version: 8.0.x
# LiveSPICE/LiveSPICEVst are WPF and can't build here; LiveSPICE.Core.sln is the
# portable subset (Util, Audio, Circuit, ComputerAlgebra, Tests).
- name: Build portable projects
run: dotnet build LiveSPICE.Core.sln -c Release
# Same assertion as the Windows job: macOS reproduces the Windows-generated
# baselines to ~1e-12 (scale-normalized) at this configuration.
- name: Run circuit tests
working-directory: Tests
run: dotnet run -c Release --framework net8.0 --no-build test "Circuits/*.schx" --check --sampleRate 44100
- name: Run examples
working-directory: Tests
run: dotnet run -c Release --framework net8.0 --no-build test "Examples/*.schx" --check --sampleRate 44100
# The live audio path needs a device, which CI runners don't have. The offline render path
# exercises schematic load, solve, codegen and the buffer loop without one.
- name: List audio devices
working-directory: LiveSPICE.CLI
run: dotnet run -c Release --no-build -- list
- name: Render a circuit offline
working-directory: LiveSPICE.CLI
run: |
dotnet run -c Release --no-build -- tone --output /tmp/tone.wav --frequency 82 --seconds 1
dotnet run -c Release --no-build -- render \
--schematic "../Tests/Examples/Passive 1stOrder Lowpass RC.schx" \
--input /tmp/tone.wav --output /tmp/out.wav
2 changes: 1 addition & 1 deletion Circuit/Circuit.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<OutputType>Library</OutputType>
<AssemblyName>Circuit</AssemblyName>
<Product>Circuit</Product>
Expand Down
10 changes: 9 additions & 1 deletion Circuit/Schematic/Schematic.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using ComputerAlgebra;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
Expand Down Expand Up @@ -346,13 +347,15 @@ private void LogComponents()
Log.WriteLine(MessageType.Verbose, " (" + Wires.Count() + " wires)");
}

// The .NET wrapper for this doesn't support allowing overwriting until .NET 8 :(
#if !NET8_0_OR_GREATER
// File.Move can't overwrite when targeting netstandard2.0, so fall back to the Win32 call there.
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool MoveFileEx(string existingFileName, string newFileName, int flags);

static int MOVEFILE_REPLACE_EXISTING = 1;
static int MOVEFILE_COPY_ALLOWED = 2;
#endif

public void Save(string FileName)
{
Expand All @@ -362,6 +365,10 @@ public void Save(string FileName)
// VST plugins are watching for changes.
string temp = FileName + ".temp";
doc.Save(temp);
#if NET8_0_OR_GREATER
// Overwriting File.Move is atomic where the platform supports it, and is portable.
File.Move(temp, FileName, overwrite: true);
#else
if (!MoveFileEx(temp, FileName, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING))
{
// If the MoveFileEx call failed, just save it the regular way. This should never
Expand All @@ -371,6 +378,7 @@ public void Save(string FileName)
// be written for some reason).
doc.Save(FileName);
}
#endif
Log.WriteLine(MessageType.Info, "Schematic saved to '" + FileName + "'");
LogComponents();
}
Expand Down
217 changes: 217 additions & 0 deletions CoreAudio/AudioUnitApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using System;
using System.Runtime.InteropServices;

namespace CoreAudio
{
[StructLayout(LayoutKind.Sequential)]
internal struct AudioComponentDescription
{
public uint componentType;
public uint componentSubType;
public uint componentManufacturer;
public uint componentFlags;
public uint componentFlagsMask;
}

[StructLayout(LayoutKind.Sequential)]
internal struct AudioStreamBasicDescription
{
public double mSampleRate;
public uint mFormatID;
public uint mFormatFlags;
public uint mBytesPerPacket;
public uint mFramesPerPacket;
public uint mBytesPerFrame;
public uint mChannelsPerFrame;
public uint mBitsPerChannel;
public uint mReserved;
}

[StructLayout(LayoutKind.Sequential)]
internal struct AudioTimeStamp
{
public double mSampleTime;
public ulong mHostTime;
public double mRateScalar;
public ulong mWordClockTime;
public long mSMPTETime1;
public long mSMPTETime2;
public long mSMPTETime3;
public uint mFlags;
public uint mReserved;
}

[StructLayout(LayoutKind.Sequential)]
internal struct AURenderCallbackStruct
{
public AudioUnitApi.AURenderCallback inputProc;
public IntPtr inputProcRefCon;
}

/// <summary>
/// Helper for building and reading the variable-length AudioBufferList structure by hand.
/// The layout is { uint mNumberBuffers; AudioBuffer mBuffers[]; } where AudioBuffer is
/// { uint mNumberChannels; uint mDataByteSize; void* mData; }. The list is pointer-aligned,
/// so the first buffer starts at IntPtr.Size, not at 4.
/// </summary>
internal static class AudioBufferList
{
// uint + uint + pointer, rounded up to pointer alignment.
public static readonly int AudioBufferSize = IntPtr.Size == 8 ? 16 : 12;

public static int SizeOf(int Buffers)
{
return IntPtr.Size + Buffers * AudioBufferSize;
}

/// <summary>Allocate a zeroed AudioBufferList for the given number of non-interleaved buffers.</summary>
public static IntPtr Allocate(int Buffers)
{
int size = SizeOf(Buffers);
IntPtr list = Marshal.AllocHGlobal(size);
for (int i = 0; i < size; ++i)
Marshal.WriteByte(list, i, 0);
Marshal.WriteInt32(list, 0, Buffers);
return list;
}

public static int GetCount(IntPtr List)
{
return Marshal.ReadInt32(List, 0);
}

public static void SetCount(IntPtr List, int Count)
{
Marshal.WriteInt32(List, 0, Count);
}

private static int Offset(int Index)
{
return IntPtr.Size + Index * AudioBufferSize;
}

public static void SetBuffer(IntPtr List, int Index, int Channels, int ByteSize, IntPtr Data)
{
int offset = Offset(Index);
Marshal.WriteInt32(List, offset, Channels);
Marshal.WriteInt32(List, offset + 4, ByteSize);
Marshal.WriteIntPtr(List, offset + 8, Data);
}

public static IntPtr GetData(IntPtr List, int Index)
{
return Marshal.ReadIntPtr(List, Offset(Index) + 8);
}

public static int GetDataByteSize(IntPtr List, int Index)
{
return Marshal.ReadInt32(List, Offset(Index) + 4);
}

public static int GetChannels(IntPtr List, int Index)
{
return Marshal.ReadInt32(List, Offset(Index));
}

public static void SetDataByteSize(IntPtr List, int Index, int ByteSize)
{
Marshal.WriteInt32(List, Offset(Index) + 4, ByteSize);
}
}

internal static class AudioUnitApi
{
private const string AudioToolbox = "/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox";

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int AURenderCallback(
IntPtr inRefCon, ref uint ioActionFlags, ref AudioTimeStamp inTimeStamp,
uint inBusNumber, uint inNumberFrames, IntPtr ioData);

public static uint FourCC(string s) { return CoreAudioApi.FourCC(s); }

public static readonly uint kAudioUnitType_Output = FourCC("auou");
public static readonly uint kAudioUnitSubType_HALOutput = FourCC("ahal");
public static readonly uint kAudioUnitManufacturer_Apple = FourCC("appl");
public static readonly uint kAudioFormatLinearPCM = FourCC("lpcm");

public const uint kAudioFormatFlagIsFloat = 1 << 0;
public const uint kAudioFormatFlagIsPacked = 1 << 3;
public const uint kAudioFormatFlagIsNonInterleaved = 1 << 5;

public const uint kAudioUnitProperty_StreamFormat = 8;
public const uint kAudioUnitProperty_SetRenderCallback = 23;
public const uint kAudioUnitProperty_MaximumFramesPerSlice = 14;
public const uint kAudioOutputUnitProperty_CurrentDevice = 2000;
public const uint kAudioOutputUnitProperty_EnableIO = 2003;
public const uint kAudioOutputUnitProperty_SetInputCallback = 2005;

public const uint kAudioUnitScope_Global = 0;
public const uint kAudioUnitScope_Input = 1;
public const uint kAudioUnitScope_Output = 2;

// AUHAL bus numbering: bus 0 is output to the device, bus 1 is input from it.
public const uint OutputBus = 0;
public const uint InputBus = 1;

[DllImport(AudioToolbox)]
public static extern IntPtr AudioComponentFindNext(IntPtr inComponent, ref AudioComponentDescription inDesc);

[DllImport(AudioToolbox)]
public static extern int AudioComponentInstanceNew(IntPtr inComponent, out IntPtr outInstance);

[DllImport(AudioToolbox)]
public static extern int AudioComponentInstanceDispose(IntPtr inInstance);

[DllImport(AudioToolbox)]
public static extern int AudioUnitInitialize(IntPtr inUnit);

[DllImport(AudioToolbox)]
public static extern int AudioUnitUninitialize(IntPtr inUnit);

[DllImport(AudioToolbox)]
public static extern int AudioOutputUnitStart(IntPtr ci);

[DllImport(AudioToolbox)]
public static extern int AudioOutputUnitStop(IntPtr ci);

[DllImport(AudioToolbox)]
public static extern int AudioUnitRender(
IntPtr inUnit, ref uint ioActionFlags, ref AudioTimeStamp inTimeStamp,
uint inOutputBusNumber, uint inNumberFrames, IntPtr ioData);

[DllImport(AudioToolbox)]
public static extern int AudioUnitSetProperty(
IntPtr inUnit, uint inID, uint inScope, uint inElement, ref uint inData, uint inDataSize);

[DllImport(AudioToolbox)]
public static extern int AudioUnitSetProperty(
IntPtr inUnit, uint inID, uint inScope, uint inElement, ref AudioStreamBasicDescription inData, uint inDataSize);

[DllImport(AudioToolbox)]
public static extern int AudioUnitSetProperty(
IntPtr inUnit, uint inID, uint inScope, uint inElement, ref AURenderCallbackStruct inData, uint inDataSize);

[DllImport(AudioToolbox)]
public static extern int AudioUnitGetProperty(
IntPtr inUnit, uint inID, uint inScope, uint inElement, ref AudioStreamBasicDescription outData, ref uint ioDataSize);

/// <summary>The canonical non-interleaved 32 bit float format, one buffer per channel.</summary>
public static AudioStreamBasicDescription NonInterleavedFloat32(double SampleRate, int Channels)
{
return new AudioStreamBasicDescription()
{
mSampleRate = SampleRate,
mFormatID = kAudioFormatLinearPCM,
mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved,
// For non-interleaved formats these are per-channel, so they describe one buffer.
mBytesPerPacket = sizeof(float),
mFramesPerPacket = 1,
mBytesPerFrame = sizeof(float),
mChannelsPerFrame = (uint)Channels,
mBitsPerChannel = 8 * sizeof(float),
mReserved = 0,
};
}
}
}
18 changes: 18 additions & 0 deletions CoreAudio/CoreAudio.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<AssemblyName>CoreAudio</AssemblyName>
<Product>CoreAudio</Product>
<Copyright>Copyright © 2020</Copyright>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Audio\Audio.csproj" />
<ProjectReference Include="..\Util\Util.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
</ItemGroup>
</Project>
Loading
Loading