diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe3e80f..e3a47cf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,9 +11,5 @@ permissions:
contents: read
jobs:
- build-native:
- uses: ./.github/workflows/reusable-build-native.yml
-
test:
- needs: build-native
uses: ./.github/workflows/reusable-test.yml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9d58d06..ca8e98f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -35,12 +35,8 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"
- build-native:
- needs: version
- uses: ./.github/workflows/reusable-build-native.yml
-
test:
- needs: [version, build-native]
+ needs: version
uses: ./.github/workflows/reusable-test.yml
pack:
diff --git a/.github/workflows/reusable-build-native.yml b/.github/workflows/reusable-build-native.yml
deleted file mode 100644
index b7bf487..0000000
--- a/.github/workflows/reusable-build-native.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Reusable Build Native
-
-on:
- workflow_call:
-
-env:
- DOTNET_NOLOGO: 'true'
- DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'
- VCPKG_BINARY_SOURCES: 'clear;x-gha,readwrite'
-
-permissions:
- contents: read
-
-jobs:
- build-native:
- name: Build native (${{ matrix.rid }})
- strategy:
- fail-fast: false
- matrix:
- include:
- - rid: osx-arm64
- os: macos-14
- - rid: linux-x64
- os: ubuntu-24.04
- - rid: linux-arm64
- os: ubuntu-24.04-arm
- - rid: win-x64
- os: windows-2022
- runs-on: ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v6
- with:
- submodules: recursive
- fetch-depth: 0
-
- - name: Install hidapi (macOS)
- if: runner.os == 'macOS'
- run: brew install hidapi
-
- - name: Install hidapi + toolchain (Linux)
- if: runner.os == 'Linux'
- run: |
- sudo apt-get update
- sudo apt-get install -y --no-install-recommends \
- cmake build-essential pkg-config libhidapi-dev
-
- - name: Expose GHA cache tokens to vcpkg (Windows)
- if: runner.os == 'Windows'
- uses: actions/github-script@v9
- with:
- script: |
- core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
- core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
-
- - name: Set vcpkg toolchain (Windows)
- if: runner.os == 'Windows'
- shell: bash
- run: |
- echo "CMAKE_EXTRA_ARGS=-DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT//\\//}/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" >> "$GITHUB_ENV"
-
- - name: Cache CMake build dir
- uses: actions/cache@v5
- with:
- path: build/cmake/${{ matrix.rid }}
- key: cmake-${{ matrix.rid }}-${{ hashFiles('headsetcontrollib/CMakeLists.txt', 'headsetcontrollib/lib/**/*.cpp', 'headsetcontrollib/lib/**/*.hpp', 'headsetcontrollib/lib/**/*.h', 'headsetcontrollib/vcpkg.json') }}
- restore-keys: |
- cmake-${{ matrix.rid }}-
-
- - name: Build native library
- shell: bash
- run: build/build-native.sh --rid ${{ matrix.rid }}
-
- - uses: actions/upload-artifact@v7
- with:
- name: native-${{ matrix.rid }}
- path: build/native/${{ matrix.rid }}/
- if-no-files-found: error
- retention-days: 14
diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml
index feaacf4..1fba664 100644
--- a/.github/workflows/reusable-test.yml
+++ b/.github/workflows/reusable-test.yml
@@ -7,6 +7,7 @@ env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'
+ VCPKG_BINARY_SOURCES: 'clear;x-gha,readwrite'
permissions:
contents: read
@@ -29,26 +30,46 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
+ with:
+ submodules: recursive
+ fetch-depth: 0
- uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- - name: Install hidapi runtime (macOS)
+ - name: Install hidapi + toolchain (macOS)
if: runner.os == 'macOS'
run: brew install hidapi
- - name: Install hidapi runtime (Linux)
+ - name: Install hidapi + toolchain (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
- libhidapi-hidraw0 libhidapi-libusb0
+ cmake build-essential pkg-config libhidapi-dev
- - uses: actions/download-artifact@v8
+ - name: Expose GHA cache tokens to vcpkg (Windows)
+ if: runner.os == 'Windows'
+ uses: actions/github-script@v9
with:
- name: native-${{ matrix.rid }}
- path: build/native/${{ matrix.rid }}/
+ script: |
+ core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
+ core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
+
+ - name: Set vcpkg toolchain (Windows)
+ if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ echo "CMAKE_EXTRA_ARGS=-DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT//\\//}/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" >> "$GITHUB_ENV"
+
+ - name: Cache CMake build dir
+ uses: actions/cache@v5
+ with:
+ path: build/cmake/${{ matrix.rid }}
+ key: cmake-${{ matrix.rid }}-${{ hashFiles('headsetcontrollib/CMakeLists.txt', 'headsetcontrollib/lib/**/*.cpp', 'headsetcontrollib/lib/**/*.hpp', 'headsetcontrollib/lib/**/*.h', 'headsetcontrollib/vcpkg.json') }}
+ restore-keys: |
+ cmake-${{ matrix.rid }}-
- name: Cache NuGet packages
uses: actions/cache@v5
@@ -60,6 +81,8 @@ jobs:
- run: dotnet restore
+ # The HscPrepareNativeArtifacts target in HeadsetControl.NET.Native.csproj
+ # invokes build/build-native.sh for the host RID if no artefact is present.
- run: dotnet build -c Release --no-restore
- run: >-
@@ -68,6 +91,14 @@ jobs:
--logger "trx;LogFileName=test-results.trx"
--results-directory TestResults
+ - name: Upload native artefact
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-${{ matrix.rid }}
+ path: build/native/${{ matrix.rid }}/
+ if-no-files-found: error
+ retention-days: 14
+
- if: always()
uses: actions/upload-artifact@v7
with:
diff --git a/Directory.Build.props b/Directory.Build.props
index 4f9f46d..6682628 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- net10.0
+ net8.0;net9.0;net10.0
latest
enable
@@ -35,7 +35,8 @@
git
GPL-3.0-only
false
- 0.2.0
+ README.md
+ 0.3.0
@@ -48,6 +49,11 @@
PackagePath="\"
Visible="false"
Condition="Exists('$(MSBuildThisFileDirectory)LICENSE')" />
+
diff --git a/README.md b/README.md
index d31ebd6..80c1242 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,6 @@ headsetcontrollib/ git submodule with the native source
```bash
git submodule update --init --recursive
-build/build-native.sh # host RID — or: --rid linux-x64
dotnet test -c Release
```
diff --git a/build/build-native.sh b/build/build-native.sh
index 8bac803..95b5704 100755
--- a/build/build-native.sh
+++ b/build/build-native.sh
@@ -44,6 +44,37 @@ BUILD_DIR="${BUILD_ROOT}/${RID}"
OUT_DIR="${OUT_ROOT}/${RID}"
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
+# Serialise concurrent invocations for the same RID — when this script is
+# wired into a multi-targeted .NET build, MSBuild may run it in parallel for
+# each TFM, and the inner CMake build directory is not concurrency-safe.
+# `mkdir` is atomic across processes, so we use a sentinel directory as the
+# lock. The first process builds; later ones wait, then exit early once the
+# artefact is in place.
+LOCK_DIR="${BUILD_DIR}.lock"
+trap 'rmdir "${LOCK_DIR}" 2>/dev/null || true' EXIT
+
+WAITED=0
+while ! mkdir "${LOCK_DIR}" 2>/dev/null; do
+ if [[ ${WAITED} -ge 600 ]]; then
+ echo "error: timed out waiting for ${LOCK_DIR}" >&2
+ exit 1
+ fi
+ sleep 1
+ WAITED=$((WAITED + 1))
+done
+
+case "${RID}" in
+ osx-*) EXPECTED="${OUT_DIR}/libheadsetcontrol.dylib" ;;
+ linux-*) EXPECTED="${OUT_DIR}/libheadsetcontrol.so" ;;
+ win-*) EXPECTED="${OUT_DIR}/headsetcontrol.dll" ;;
+ *) EXPECTED="" ;;
+esac
+
+if [[ -n "${EXPECTED}" && -f "${EXPECTED}" ]]; then
+ echo ">> ${EXPECTED} already present, skipping build"
+ exit 0
+fi
+
echo ">> Configuring native HeadsetControl for ${RID}"
# shellcheck disable=SC2086
cmake -S "${NATIVE_SRC}" -B "${BUILD_DIR}" \
diff --git a/samples/HeadsetControl.NET.Sample/Program.cs b/samples/HeadsetControl.NET.Sample/Program.cs
index aa1c3a3..27fff39 100644
--- a/samples/HeadsetControl.NET.Sample/Program.cs
+++ b/samples/HeadsetControl.NET.Sample/Program.cs
@@ -1,6 +1,7 @@
using HeadsetControl.NET;
+using HeadsetControl.NET.Exceptions;
-bool useTestDevice = args.Contains("--test", StringComparer.OrdinalIgnoreCase);
+var useTestDevice = args.Contains("--test", StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"HeadsetControl native version: {HeadsetControlLibrary.Version}");
Console.WriteLine($"Supported device models: {HeadsetControlLibrary.SupportedDeviceCount}");
@@ -13,7 +14,7 @@
Console.WriteLine();
}
-using HeadsetCollection headsets = HeadsetControlLibrary.Discover();
+using var headsets = HeadsetControlLibrary.Discover();
if (headsets.Count == 0)
{
@@ -21,9 +22,9 @@
return 0;
}
-for (int i = 0; i < headsets.Count; i++)
+for (var i = 0; i < headsets.Count; i++)
{
- Headset headset = headsets[i];
+ var headset = headsets[i];
Console.WriteLine($"[{i}] {headset}");
Console.WriteLine($" Vendor : {headset.VendorName ?? "(unknown)"} (0x{headset.VendorId:X4})");
@@ -47,8 +48,8 @@ static void TryReadBattery(Headset headset)
try
{
- BatteryInfo battery = headset.GetBattery();
- string level = battery.LevelPercent is int pct ? $"{pct}%" : "n/a";
+ var battery = headset.GetBattery();
+ var level = battery.LevelPercent is int pct ? $"{pct}%" : "n/a";
Console.WriteLine($" Battery: {battery.Status} ({level})");
}
catch (HeadsetControlException ex)
@@ -66,7 +67,7 @@ static void TryReadChatMix(Headset headset)
try
{
- ChatMixInfo mix = headset.GetChatMix();
+ var mix = headset.GetChatMix();
Console.WriteLine($" ChatMix: level={mix.Level} game={mix.GameVolumePercent}% chat={mix.ChatVolumePercent}%");
}
catch (HeadsetControlException ex)
diff --git a/src/HeadsetControl.NET.Native/HeadsetControl.NET.Native.csproj b/src/HeadsetControl.NET.Native/HeadsetControl.NET.Native.csproj
index ce2fbf9..88d874a 100644
--- a/src/HeadsetControl.NET.Native/HeadsetControl.NET.Native.csproj
+++ b/src/HeadsetControl.NET.Native/HeadsetControl.NET.Native.csproj
@@ -11,19 +11,15 @@
Native P/Invoke bindings for the HeadsetControl C library. Internal infrastructure for HeadsetControl.NET — application code should consume HeadsetControl.NET instead.
HeadsetControl.NET.Native
-
-
- <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)../../build/native/**/*.dylib" />
- <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)../../build/native/**/*.so" />
- <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)../../build/native/**/*.dll" />
-
-
-
+
+ true
+
@@ -34,4 +30,47 @@
+
+
+
+ <_HscOs Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx
+ <_HscOs Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux
+ <_HscOs Condition="$([MSBuild]::IsOSPlatform('Windows'))">win
+
+ <_HscArch Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">x64
+ <_HscArch Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64
+
+ <_HscHostRid>$(_HscOs)-$(_HscArch)
+
+ <_HscNativeName Condition="'$(_HscOs)' == 'win'">headsetcontrol.dll
+ <_HscNativeName Condition="'$(_HscOs)' == 'osx'">libheadsetcontrol.dylib
+ <_HscNativeName Condition="'$(_HscOs)' == 'linux'">libheadsetcontrol.so
+
+ <_HscNativeHostPath>$(MSBuildThisFileDirectory)..\..\build\native\$(_HscHostRid)\$(_HscNativeName)
+ <_HscBuildScript>$(MSBuildThisFileDirectory)..\..\build\build-native.sh
+
+
+
+
+
+
+
+ <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)..\..\build\native\**\*.dylib" />
+ <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)..\..\build\native\**\*.so" />
+ <_HscNativeArtifact Include="$(MSBuildThisFileDirectory)..\..\build\native\**\*.dll" />
+
+
+
+
+
diff --git a/src/HeadsetControl.NET.Native/NativeConstants.cs b/src/HeadsetControl.NET.Native/NativeConstants.cs
index 0357b92..3ddef9e 100644
--- a/src/HeadsetControl.NET.Native/NativeConstants.cs
+++ b/src/HeadsetControl.NET.Native/NativeConstants.cs
@@ -1,6 +1,6 @@
namespace HeadsetControl.NET.Native;
-internal enum HscResult
+enum HscResult
{
Ok = 0,
Error = -1,
@@ -11,7 +11,7 @@ internal enum HscResult
InvalidParam = -6,
}
-internal enum HscCapability
+enum HscCapability
{
Sidetone = 0,
BatteryStatus = 1,
@@ -34,7 +34,7 @@ internal enum HscCapability
// The native C wrapper currently forwards the underlying C++ battery_status
// enum (0..4) through a static cast, so callers may receive either these
// constants or the C++ values. ResultMapping handles both.
-internal enum HscBatteryStatus
+enum HscBatteryStatus
{
Unavailable = -1,
Charging = -2,
diff --git a/src/HeadsetControl.NET.Native/NativeLibraryLoader.cs b/src/HeadsetControl.NET.Native/NativeLibraryLoader.cs
index 529e36c..0d51e35 100644
--- a/src/HeadsetControl.NET.Native/NativeLibraryLoader.cs
+++ b/src/HeadsetControl.NET.Native/NativeLibraryLoader.cs
@@ -3,33 +3,22 @@
namespace HeadsetControl.NET.Native;
-internal static class NativeLibraryLoader
+static class NativeLibraryLoader
{
public const string LibraryName = "headsetcontrol";
- private static readonly Lock InitLock = new();
- private static bool _initialized;
-
- public static void EnsureInitialized()
+ private static readonly Lazy Initializer = new(() =>
{
- if (_initialized)
- {
- return;
- }
-
- lock (InitLock)
- {
- if (_initialized)
- {
- return;
- }
+ NativeLibrary.SetDllImportResolver(
+ typeof(NativeLibraryLoader).Assembly,
+ Resolve);
- NativeLibrary.SetDllImportResolver(
- typeof(NativeLibraryLoader).Assembly,
- Resolve);
+ return true;
+ });
- _initialized = true;
- }
+ public static void EnsureInitialized()
+ {
+ _ = Initializer.Value;
}
private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
@@ -39,34 +28,34 @@ private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSe
return IntPtr.Zero;
}
- bool trace = string.Equals(
+ var trace = string.Equals(
Environment.GetEnvironmentVariable("HEADSETCONTROL_NATIVE_TRACE"),
"1",
StringComparison.Ordinal);
- foreach (string candidate in EnumerateCandidatePaths())
+ foreach (var candidate in EnumerateCandidatePaths())
{
if (trace)
{
Console.Error.WriteLine($"[HeadsetControl.NET] probe: {candidate}");
}
- if (NativeLibrary.TryLoad(candidate, out IntPtr handle))
+ if (NativeLibrary.TryLoad(candidate, out var handle))
{
return handle;
}
}
- return NativeLibrary.TryLoad(libraryName, assembly, searchPath, out IntPtr fallback)
+ return NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var fallback)
? fallback
: IntPtr.Zero;
}
private static IEnumerable EnumerateCandidatePaths()
{
- string rid = GetRuntimeIdentifier();
- string fileName = GetNativeFileName();
+ var rid = GetRuntimeIdentifier();
+ var fileName = GetNativeFileName();
- string? baseDir = AppContext.BaseDirectory;
+ var baseDir = AppContext.BaseDirectory;
if (!string.IsNullOrEmpty(baseDir))
{
yield return Path.Combine(baseDir, "runtimes", rid, "native", fileName);
@@ -75,7 +64,7 @@ private static IEnumerable EnumerateCandidatePaths()
// Under `dotnet test` BaseDirectory points at the testhost, so also
// probe the directory of this assembly itself.
- string? assemblyDir = GetAssemblyDirectory();
+ var assemblyDir = GetAssemblyDirectory();
if (!string.IsNullOrEmpty(assemblyDir) &&
!string.Equals(assemblyDir, baseDir, StringComparison.Ordinal))
{
@@ -90,7 +79,7 @@ private static IEnumerable EnumerateCandidatePaths()
Justification = "Empty Location is checked; the BaseDirectory probe covers single-file.")]
private static string? GetAssemblyDirectory()
{
- string location = typeof(NativeLibraryLoader).Assembly.Location;
+ var location = typeof(NativeLibraryLoader).Assembly.Location;
return string.IsNullOrEmpty(location) ? null : Path.GetDirectoryName(location);
}
@@ -114,7 +103,7 @@ private static string GetRuntimeIdentifier()
osPart = "unknown";
}
- string archPart = RuntimeInformation.ProcessArchitecture switch
+ var archPart = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
@@ -126,18 +115,8 @@ private static string GetRuntimeIdentifier()
return $"{osPart}-{archPart}";
}
- private static string GetNativeFileName()
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return "headsetcontrol.dll";
- }
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- return "libheadsetcontrol.dylib";
- }
-
- return "libheadsetcontrol.so";
- }
+ private static string GetNativeFileName() =>
+ OperatingSystem.IsWindows() ? "headsetcontrol.dll" :
+ OperatingSystem.IsMacOS() ? "libheadsetcontrol.dylib" :
+ "libheadsetcontrol.so";
}
diff --git a/src/HeadsetControl.NET.Native/NativeMethods.cs b/src/HeadsetControl.NET.Native/NativeMethods.cs
index 989ca18..610fed9 100644
--- a/src/HeadsetControl.NET.Native/NativeMethods.cs
+++ b/src/HeadsetControl.NET.Native/NativeMethods.cs
@@ -4,7 +4,7 @@ namespace HeadsetControl.NET.Native;
// Const-char* returns are marshalled as IntPtr and decoded by NativeStringMarshaller
// so the source generator doesn't free memory the native library still owns.
-internal static partial class NativeMethods
+static partial class NativeMethods
{
private const string LibName = NativeLibraryLoader.LibraryName;
diff --git a/src/HeadsetControl.NET.Native/NativeStringMarshaller.cs b/src/HeadsetControl.NET.Native/NativeStringMarshaller.cs
index e109503..4ba23a4 100644
--- a/src/HeadsetControl.NET.Native/NativeStringMarshaller.cs
+++ b/src/HeadsetControl.NET.Native/NativeStringMarshaller.cs
@@ -2,7 +2,7 @@
namespace HeadsetControl.NET.Native;
-internal static class NativeStringMarshaller
+static class NativeStringMarshaller
{
public static string? PtrToString(IntPtr ptr)
=> ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr);
diff --git a/src/HeadsetControl.NET.Native/NativeStructs.cs b/src/HeadsetControl.NET.Native/NativeStructs.cs
index bbfb2ea..295833e 100644
--- a/src/HeadsetControl.NET.Native/NativeStructs.cs
+++ b/src/HeadsetControl.NET.Native/NativeStructs.cs
@@ -3,7 +3,7 @@
namespace HeadsetControl.NET.Native;
[StructLayout(LayoutKind.Sequential)]
-internal struct HscBattery
+struct HscBattery
{
public int LevelPercent;
public HscBatteryStatus Status;
@@ -13,7 +13,7 @@ internal struct HscBattery
}
[StructLayout(LayoutKind.Sequential)]
-internal struct HscSidetone
+struct HscSidetone
{
public byte CurrentLevel;
public byte MinLevel;
@@ -21,7 +21,7 @@ internal struct HscSidetone
}
[StructLayout(LayoutKind.Sequential)]
-internal struct HscChatMix
+struct HscChatMix
{
public int Level;
public int GameVolumePercent;
@@ -29,7 +29,7 @@ internal struct HscChatMix
}
[StructLayout(LayoutKind.Sequential)]
-internal struct HscInactiveTime
+struct HscInactiveTime
{
public byte Minutes;
public byte MinMinutes;
diff --git a/src/HeadsetControl.NET/Exceptions/HeadsetControlException.cs b/src/HeadsetControl.NET/Exceptions/HeadsetControlException.cs
index bbb71ef..cebe726 100644
--- a/src/HeadsetControl.NET/Exceptions/HeadsetControlException.cs
+++ b/src/HeadsetControl.NET/Exceptions/HeadsetControlException.cs
@@ -1,4 +1,4 @@
-namespace HeadsetControl.NET;
+namespace HeadsetControl.NET.Exceptions;
public class HeadsetControlException : Exception
{
diff --git a/src/HeadsetControl.NET/Headset.cs b/src/HeadsetControl.NET/Headset.cs
index 9a19ce9..b3ad4e6 100644
--- a/src/HeadsetControl.NET/Headset.cs
+++ b/src/HeadsetControl.NET/Headset.cs
@@ -31,19 +31,8 @@ internal Headset(HeadsetCollection owner, IntPtr handle)
CapabilitiesBitmask = NativeMethods.GetCapabilities(handle);
}
- public IEnumerable SupportedCapabilities
- {
- get
- {
- foreach (HeadsetCapability cap in Enum.GetValues())
- {
- if ((CapabilitiesBitmask & (1 << (int)cap)) != 0)
- {
- yield return cap;
- }
- }
- }
- }
+ public IEnumerable SupportedCapabilities => Enum.GetValues()
+ .Where(cap => (CapabilitiesBitmask & 1 << (int)cap) != 0);
public bool Supports(HeadsetCapability capability)
{
@@ -54,10 +43,10 @@ public bool Supports(HeadsetCapability capability)
public BatteryInfo GetBattery()
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.GetBattery(_handle, out HscBattery raw);
+ var result = NativeMethods.GetBattery(_handle, out var raw);
ResultMapping.ThrowIfError(result, "GetBattery", HeadsetCapability.BatteryStatus);
- BatteryStatus status = ResultMapping.MapBatteryStatus(raw.Status);
+ var status = ResultMapping.MapBatteryStatus(raw.Status);
int? level = status == BatteryStatus.Available ? raw.LevelPercent : null;
Voltage? voltage = raw.VoltageMv >= 0 ? new Voltage(raw.VoltageMv) : null;
TimeSpan? timeToFull = raw.TimeToFullMin >= 0 ? TimeSpan.FromMinutes(raw.TimeToFullMin) : null;
@@ -69,7 +58,7 @@ public BatteryInfo GetBattery()
public ChatMixInfo GetChatMix()
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.GetChatMix(_handle, out HscChatMix raw);
+ var result = NativeMethods.GetChatMix(_handle, out var raw);
ResultMapping.ThrowIfError(result, "GetChatMix", HeadsetCapability.ChatMixStatus);
return new ChatMixInfo(raw.Level, raw.GameVolumePercent, raw.ChatVolumePercent);
}
@@ -83,7 +72,7 @@ public SidetoneResult SetSidetone(byte level)
throw new ArgumentOutOfRangeException(nameof(level), level, "Sidetone level must be in 0..128.");
}
- HscResult result = NativeMethods.SetSidetone(_handle, level, out HscSidetone raw);
+ var result = NativeMethods.SetSidetone(_handle, level, out var raw);
ResultMapping.ThrowIfError(result, "SetSidetone", HeadsetCapability.Sidetone);
return new SidetoneResult(raw.CurrentLevel, raw.MinLevel, raw.MaxLevel);
}
@@ -91,14 +80,14 @@ public SidetoneResult SetSidetone(byte level)
public void SetVolumeLimiter(bool enabled)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetVolumeLimiter(_handle, enabled);
+ var result = NativeMethods.SetVolumeLimiter(_handle, enabled);
ResultMapping.ThrowIfError(result, "SetVolumeLimiter", HeadsetCapability.VolumeLimiter);
}
public void SetEqualizerPreset(byte preset)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetEqualizerPreset(_handle, preset);
+ var result = NativeMethods.SetEqualizerPreset(_handle, preset);
ResultMapping.ThrowIfError(result, "SetEqualizerPreset", HeadsetCapability.EqualizerPreset);
}
@@ -114,7 +103,7 @@ public void SetEqualizer(ReadOnlySpan bands)
{
fixed (float* p = bands)
{
- HscResult result = NativeMethods.SetEqualizer(_handle, p, bands.Length);
+ var result = NativeMethods.SetEqualizer(_handle, p, bands.Length);
ResultMapping.ThrowIfError(result, "SetEqualizer", HeadsetCapability.Equalizer);
}
}
@@ -123,19 +112,19 @@ public void SetEqualizer(ReadOnlySpan bands)
public IReadOnlyList GetEqualizerPresets()
{
ThrowIfHandleInvalid();
- int count = NativeMethods.GetEqualizerPresetsCount(_handle);
+ var count = NativeMethods.GetEqualizerPresetsCount(_handle);
if (count <= 0)
{
return Array.Empty();
}
var presets = new EqualizerPreset[count];
- for (int i = 0; i < count; i++)
+ for (var i = 0; i < count; i++)
{
- string name = NativeStringMarshaller.PtrToStringOrEmpty(
+ var name = NativeStringMarshaller.PtrToStringOrEmpty(
NativeMethods.GetEqualizerPresetName(_handle, i));
- int bandCount = NativeMethods.GetEqualizerPresetBandCount(_handle, i);
- float[] bands = bandCount > 0 ? new float[bandCount] : Array.Empty();
+ var bandCount = NativeMethods.GetEqualizerPresetBandCount(_handle, i);
+ var bands = bandCount > 0 ? new float[bandCount] : Array.Empty();
if (bandCount > 0)
{
@@ -143,7 +132,7 @@ public IReadOnlyList GetEqualizerPresets()
{
fixed (float* p = bands)
{
- HscResult result = NativeMethods.GetEqualizerPresetBands(_handle, i, p, bandCount);
+ var result = NativeMethods.GetEqualizerPresetBands(_handle, i, p, bandCount);
ResultMapping.ThrowIfError(result, "GetEqualizerPresets");
}
}
@@ -164,7 +153,7 @@ public void SetMicrophoneVolume(byte volume)
throw new ArgumentOutOfRangeException(nameof(volume), volume, "Microphone volume must be in 0..128.");
}
- HscResult result = NativeMethods.SetMicVolume(_handle, volume);
+ var result = NativeMethods.SetMicVolume(_handle, volume);
ResultMapping.ThrowIfError(result, "SetMicrophoneVolume", HeadsetCapability.MicrophoneVolume);
}
@@ -177,35 +166,35 @@ public void SetMicrophoneMuteLedBrightness(byte brightness)
throw new ArgumentOutOfRangeException(nameof(brightness), brightness, "Brightness must be in 0..3.");
}
- HscResult result = NativeMethods.SetMicMuteLedBrightness(_handle, brightness);
+ var result = NativeMethods.SetMicMuteLedBrightness(_handle, brightness);
ResultMapping.ThrowIfError(result, "SetMicrophoneMuteLedBrightness", HeadsetCapability.MicrophoneMuteLedBrightness);
}
public void SetRotateToMute(bool enabled)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetRotateToMute(_handle, enabled);
+ var result = NativeMethods.SetRotateToMute(_handle, enabled);
ResultMapping.ThrowIfError(result, "SetRotateToMute", HeadsetCapability.RotateToMute);
}
public void SetLights(bool enabled)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetLights(_handle, enabled);
+ var result = NativeMethods.SetLights(_handle, enabled);
ResultMapping.ThrowIfError(result, "SetLights", HeadsetCapability.Lights);
}
public void SetVoicePrompts(bool enabled)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetVoicePrompts(_handle, enabled);
+ var result = NativeMethods.SetVoicePrompts(_handle, enabled);
ResultMapping.ThrowIfError(result, "SetVoicePrompts", HeadsetCapability.VoicePrompts);
}
public void PlayNotificationSound(byte soundId)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.PlayNotificationSound(_handle, soundId);
+ var result = NativeMethods.PlayNotificationSound(_handle, soundId);
ResultMapping.ThrowIfError(result, "PlayNotificationSound", HeadsetCapability.NotificationSound);
}
@@ -213,7 +202,7 @@ public void PlayNotificationSound(byte soundId)
public InactiveTimeResult SetInactiveTime(byte minutes)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetInactiveTime(_handle, minutes, out HscInactiveTime raw);
+ var result = NativeMethods.SetInactiveTime(_handle, minutes, out var raw);
ResultMapping.ThrowIfError(result, "SetInactiveTime", HeadsetCapability.InactiveTime);
return new InactiveTimeResult(raw.Minutes, raw.MinMinutes, raw.MaxMinutes);
}
@@ -221,14 +210,14 @@ public InactiveTimeResult SetInactiveTime(byte minutes)
public void SetBluetoothWhenPoweredOn(bool enabled)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetBluetoothWhenPoweredOn(_handle, enabled);
+ var result = NativeMethods.SetBluetoothWhenPoweredOn(_handle, enabled);
ResultMapping.ThrowIfError(result, "SetBluetoothWhenPoweredOn", HeadsetCapability.BluetoothWhenPoweredOn);
}
public void SetBluetoothCallVolume(byte volume)
{
ThrowIfHandleInvalid();
- HscResult result = NativeMethods.SetBluetoothCallVolume(_handle, volume);
+ var result = NativeMethods.SetBluetoothCallVolume(_handle, volume);
ResultMapping.ThrowIfError(result, "SetBluetoothCallVolume", HeadsetCapability.BluetoothCallVolume);
}
diff --git a/src/HeadsetControl.NET/HeadsetCollection.cs b/src/HeadsetControl.NET/HeadsetCollection.cs
index 430c215..1dbfec6 100644
--- a/src/HeadsetControl.NET/HeadsetCollection.cs
+++ b/src/HeadsetControl.NET/HeadsetCollection.cs
@@ -20,9 +20,9 @@ internal HeadsetCollection(IntPtr nativeArray, int count)
_count = count;
_headsets = new Headset[count];
- for (int i = 0; i < count; i++)
+ for (var i = 0; i < count; i++)
{
- IntPtr handle = Marshal.ReadIntPtr(nativeArray, i * IntPtr.Size);
+ var handle = Marshal.ReadIntPtr(nativeArray, i * IntPtr.Size);
_headsets[i] = new Headset(this, handle);
}
}
@@ -46,11 +46,13 @@ public void Dispose()
IsDisposed = true;
- if (_nativeArray != IntPtr.Zero)
+ if (_nativeArray == IntPtr.Zero)
{
- NativeMethods.FreeHeadsets(_nativeArray, _count);
- _nativeArray = IntPtr.Zero;
- _count = 0;
+ return;
}
+
+ NativeMethods.FreeHeadsets(_nativeArray, _count);
+ _nativeArray = IntPtr.Zero;
+ _count = 0;
}
}
diff --git a/src/HeadsetControl.NET/HeadsetControlLibrary.cs b/src/HeadsetControl.NET/HeadsetControlLibrary.cs
index 45d94e2..d390e4d 100644
--- a/src/HeadsetControl.NET/HeadsetControlLibrary.cs
+++ b/src/HeadsetControl.NET/HeadsetControlLibrary.cs
@@ -13,8 +13,7 @@ static HeadsetControlLibrary()
NativeLibraryLoader.EnsureInitialized();
}
- public static string Version
- => NativeStringMarshaller.PtrToString(NativeMethods.Version()) ?? "0.0.0";
+ public static string Version => NativeStringMarshaller.PtrToString(NativeMethods.Version()) ?? "0.0.0";
public static TimeSpan DeviceTimeout
{
@@ -26,8 +25,8 @@ public static TimeSpan DeviceTimeout
throw new ArgumentOutOfRangeException(nameof(value), value, "Timeout must be non-negative.");
}
- double ms = value.TotalMilliseconds;
- int clamped = ms >= int.MaxValue ? int.MaxValue : (int)ms;
+ var ms = value.TotalMilliseconds;
+ var clamped = ms >= int.MaxValue ? int.MaxValue : (int)ms;
NativeMethods.SetDeviceTimeout(clamped);
}
}
@@ -54,14 +53,14 @@ public static IReadOnlyList SupportedDeviceNames
{
get
{
- int count = NativeMethods.SupportedDeviceCount();
+ var count = NativeMethods.SupportedDeviceCount();
if (count <= 0)
{
return Array.Empty();
}
var names = new string[count];
- for (int i = 0; i < count; i++)
+ for (var i = 0; i < count; i++)
{
names[i] = NativeStringMarshaller.PtrToStringOrEmpty(NativeMethods.SupportedDeviceName(i));
}
@@ -72,7 +71,7 @@ public static IReadOnlyList SupportedDeviceNames
public static HeadsetCollection Discover()
{
- int count = NativeMethods.Discover(out IntPtr nativeArray);
+ var count = NativeMethods.Discover(out var nativeArray);
if (count < 0)
{
diff --git a/src/HeadsetControl.NET/Internal/ResultMapping.cs b/src/HeadsetControl.NET/Internal/ResultMapping.cs
index 97cdc93..3ee787b 100644
--- a/src/HeadsetControl.NET/Internal/ResultMapping.cs
+++ b/src/HeadsetControl.NET/Internal/ResultMapping.cs
@@ -1,8 +1,9 @@
+using HeadsetControl.NET.Exceptions;
using HeadsetControl.NET.Native;
namespace HeadsetControl.NET.Internal;
-internal static class ResultMapping
+static class ResultMapping
{
public static void ThrowIfError(HscResult result, string operation, HeadsetCapability? capability = null)
{
@@ -44,7 +45,7 @@ public static void ThrowIfError(HscResult result, string operation, HeadsetCapab
// current pass-through behaviour.
public static BatteryStatus MapBatteryStatus(HscBatteryStatus raw)
{
- int value = (int)raw;
+ var value = (int)raw;
return value switch
{
-1 => BatteryStatus.Unavailable,
diff --git a/tests/HeadsetControl.NET.Tests/DomainTypeTests.cs b/tests/HeadsetControl.NET.Tests/DomainTypeTests.cs
index e4f36e8..2c6d5b4 100644
--- a/tests/HeadsetControl.NET.Tests/DomainTypeTests.cs
+++ b/tests/HeadsetControl.NET.Tests/DomainTypeTests.cs
@@ -1,3 +1,5 @@
+using HeadsetControl.NET.Exceptions;
+
namespace HeadsetControl.NET.Tests;
public sealed class DomainTypeTests
diff --git a/tests/HeadsetControl.NET.Tests/HeadsetControlLibraryTests.cs b/tests/HeadsetControl.NET.Tests/HeadsetControlLibraryTests.cs
index f81139e..ff7bfe3 100644
--- a/tests/HeadsetControl.NET.Tests/HeadsetControlLibraryTests.cs
+++ b/tests/HeadsetControl.NET.Tests/HeadsetControlLibraryTests.cs
@@ -17,7 +17,7 @@ public void Version_IsNonEmpty()
{
Skip.IfNot(_fixture.IsNativeLibraryAvailable, _fixture.LoadError?.Message);
- string version = HeadsetControlLibrary.Version;
+ var version = HeadsetControlLibrary.Version;
Assert.False(string.IsNullOrWhiteSpace(version));
}
@@ -26,7 +26,7 @@ public void DeviceTimeout_RoundTrips()
{
Skip.IfNot(_fixture.IsNativeLibraryAvailable, _fixture.LoadError?.Message);
- TimeSpan original = HeadsetControlLibrary.DeviceTimeout;
+ var original = HeadsetControlLibrary.DeviceTimeout;
try
{
HeadsetControlLibrary.DeviceTimeout = TimeSpan.FromMilliseconds(1234);
@@ -52,7 +52,7 @@ public void SupportedDeviceNames_AreReadable()
{
Skip.IfNot(_fixture.IsNativeLibraryAvailable, _fixture.LoadError?.Message);
- IReadOnlyList names = HeadsetControlLibrary.SupportedDeviceNames;
+ var names = HeadsetControlLibrary.SupportedDeviceNames;
Assert.Equal(HeadsetControlLibrary.SupportedDeviceCount, names.Count);
Assert.All(names, name => Assert.False(string.IsNullOrWhiteSpace(name)));
}
@@ -63,10 +63,10 @@ public void Discover_WithTestDevice_FindsTheTestHeadset()
Skip.IfNot(_fixture.IsNativeLibraryAvailable, _fixture.LoadError?.Message);
using var scope = new TestDeviceScope();
- using HeadsetCollection headsets = HeadsetControlLibrary.Discover();
+ using var headsets = HeadsetControlLibrary.Discover();
Assert.NotEmpty(headsets);
- Headset test = headsets.Single(h => h.VendorId == 0xF00B && h.ProductId == 0xA00C);
+ var test = headsets.Single(h => h.VendorId == 0xF00B && h.ProductId == 0xA00C);
Assert.False(string.IsNullOrWhiteSpace(test.Name));
}
@@ -76,8 +76,8 @@ public void Discover_DisposedCollection_DisablesHeadsets()
Skip.IfNot(_fixture.IsNativeLibraryAvailable, _fixture.LoadError?.Message);
using var scope = new TestDeviceScope();
- HeadsetCollection headsets = HeadsetControlLibrary.Discover();
- Headset test = headsets.First(h => h.VendorId == 0xF00B);
+ var headsets = HeadsetControlLibrary.Discover();
+ var test = headsets.First(h => h.VendorId == 0xF00B);
headsets.Dispose();
diff --git a/tests/HeadsetControl.NET.Tests/HeadsetTests.cs b/tests/HeadsetControl.NET.Tests/HeadsetTests.cs
index 55d0f18..ccb9f9b 100644
--- a/tests/HeadsetControl.NET.Tests/HeadsetTests.cs
+++ b/tests/HeadsetControl.NET.Tests/HeadsetTests.cs
@@ -1,3 +1,4 @@
+using HeadsetControl.NET.Exceptions;
using HeadsetControl.NET.Tests.Support;
namespace HeadsetControl.NET.Tests;
@@ -15,7 +16,7 @@ public HeadsetTests(NativeLibraryFixture fixture)
private (HeadsetCollection collection, Headset test) OpenTestDevice()
{
var collection = HeadsetControlLibrary.Discover();
- Headset test = collection.First(h => h.VendorId == 0xF00B);
+ var test = collection.First(h => h.VendorId == 0xF00B);
return (collection, test);
}
@@ -45,9 +46,9 @@ public void Headset_Supports_IsConsistentWithBitmask()
var (collection, test) = OpenTestDevice();
using (collection)
{
- foreach (HeadsetCapability cap in Enum.GetValues())
+ foreach (var cap in Enum.GetValues())
{
- bool bit = (test.CapabilitiesBitmask & (1 << (int)cap)) != 0;
+ var bit = (test.CapabilitiesBitmask & (1 << (int)cap)) != 0;
Assert.Equal(bit, test.Supports(cap));
}
}
@@ -104,7 +105,7 @@ public void GetBattery_ReturnsKnownStatus()
Skip.IfNot(test.Supports(HeadsetCapability.BatteryStatus),
"Test device build does not expose battery status.");
- BatteryInfo battery = test.GetBattery();
+ var battery = test.GetBattery();
Assert.True(Enum.IsDefined(battery.Status));
}
}
diff --git a/tests/HeadsetControl.NET.Tests/ResultMappingTests.cs b/tests/HeadsetControl.NET.Tests/ResultMappingTests.cs
index f854734..10e1bb0 100644
--- a/tests/HeadsetControl.NET.Tests/ResultMappingTests.cs
+++ b/tests/HeadsetControl.NET.Tests/ResultMappingTests.cs
@@ -1,3 +1,4 @@
+using HeadsetControl.NET.Exceptions;
using HeadsetControl.NET.Internal;
using HeadsetControl.NET.Native;
@@ -14,7 +15,7 @@ public void ThrowIfError_Ok_DoesNotThrow()
[Fact]
public void ThrowIfError_NotSupported_ThrowsFeatureNotSupported()
{
- FeatureNotSupportedException ex = Assert.Throws(
+ var ex = Assert.Throws(
() => ResultMapping.ThrowIfError(HscResult.NotSupported, "op", HeadsetCapability.Sidetone));
Assert.Equal(HeadsetCapability.Sidetone, ex.Capability);
@@ -24,7 +25,7 @@ public void ThrowIfError_NotSupported_ThrowsFeatureNotSupported()
[Fact]
public void ThrowIfError_NotSupported_WithoutCapability_StillThrows()
{
- FeatureNotSupportedException ex = Assert.Throws(
+ var ex = Assert.Throws(
() => ResultMapping.ThrowIfError(HscResult.NotSupported, "op"));
Assert.Null(ex.Capability);
@@ -33,7 +34,7 @@ public void ThrowIfError_NotSupported_WithoutCapability_StillThrows()
[Fact]
public void ThrowIfError_DeviceOffline_ThrowsDeviceOffline()
{
- DeviceOfflineException ex = Assert.Throws(
+ var ex = Assert.Throws(
() => ResultMapping.ThrowIfError(HscResult.DeviceOffline, "op"));
Assert.Equal(HeadsetControlErrorCode.DeviceOffline, ex.ErrorCode);
@@ -63,7 +64,7 @@ public void ThrowIfError_InvalidParam_ThrowsInvalidParameter()
[Fact]
public void ThrowIfError_GenericError_ThrowsBaseException()
{
- HeadsetControlException ex = Assert.Throws(
+ var ex = Assert.Throws(
() => ResultMapping.ThrowIfError(HscResult.Error, "op"));
Assert.Equal(HeadsetControlErrorCode.Error, ex.ErrorCode);
diff --git a/tests/HeadsetControl.NET.Tests/Support/NativeLibraryFixture.cs b/tests/HeadsetControl.NET.Tests/Support/NativeLibraryFixture.cs
index a136962..d6519a3 100644
--- a/tests/HeadsetControl.NET.Tests/Support/NativeLibraryFixture.cs
+++ b/tests/HeadsetControl.NET.Tests/Support/NativeLibraryFixture.cs
@@ -8,7 +8,7 @@ public NativeLibraryFixture()
{
try
{
- string version = HeadsetControlLibrary.Version;
+ var version = HeadsetControlLibrary.Version;
IsNativeLibraryAvailable = !string.IsNullOrEmpty(version);
Version = version;
}
diff --git a/tests/HeadsetControl.NET.Tests/Support/TestDeviceScope.cs b/tests/HeadsetControl.NET.Tests/Support/TestDeviceScope.cs
index f9ac1de..b05d8fb 100644
--- a/tests/HeadsetControl.NET.Tests/Support/TestDeviceScope.cs
+++ b/tests/HeadsetControl.NET.Tests/Support/TestDeviceScope.cs
@@ -1,6 +1,11 @@
namespace HeadsetControl.NET.Tests.Support;
-internal sealed class TestDeviceScope : IDisposable
+///
+/// Enables the synthetic HeadsetControl Test device for the lifetime of the
+/// scope and restores the previous state on disposal. Keeps tests from
+/// leaking global library state into each other.
+///
+sealed class TestDeviceScope : IDisposable
{
private readonly bool _previousEnabled;
private readonly int _previousProfile;