mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-16 03:00:57 +00:00
[sceAudio] added basic audio output support (#33)
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
@@ -12,7 +14,70 @@ public static class AudioOutExports
|
|||||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||||
private static int _nextPortHandle;
|
private static int _nextPortHandle;
|
||||||
|
|
||||||
private sealed record PortState(int UserId, int Type, uint BufferLength, uint Frequency, int Format);
|
private sealed class PortState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _paceGate = new();
|
||||||
|
private long _nextSilentOutput;
|
||||||
|
|
||||||
|
public PortState(
|
||||||
|
int userId,
|
||||||
|
int type,
|
||||||
|
uint bufferLength,
|
||||||
|
uint frequency,
|
||||||
|
int format,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat,
|
||||||
|
WinMmAudioPort? backend)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
Type = type;
|
||||||
|
BufferLength = bufferLength;
|
||||||
|
Frequency = frequency;
|
||||||
|
Format = format;
|
||||||
|
Channels = channels;
|
||||||
|
BytesPerSample = bytesPerSample;
|
||||||
|
IsFloat = isFloat;
|
||||||
|
Backend = backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int UserId { get; }
|
||||||
|
public int Type { get; }
|
||||||
|
public uint BufferLength { get; }
|
||||||
|
public uint Frequency { get; }
|
||||||
|
public int Format { get; }
|
||||||
|
public int Channels { get; }
|
||||||
|
public int BytesPerSample { get; }
|
||||||
|
public bool IsFloat { get; }
|
||||||
|
public WinMmAudioPort? Backend { get; }
|
||||||
|
public int BufferByteLength =>
|
||||||
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
|
public void PaceSilence()
|
||||||
|
{
|
||||||
|
long delay;
|
||||||
|
lock (_paceGate)
|
||||||
|
{
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_nextSilentOutput < now)
|
||||||
|
{
|
||||||
|
_nextSilentOutput = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = _nextSilentOutput - now;
|
||||||
|
_nextSilentOutput += checked(
|
||||||
|
(long)Math.Ceiling(
|
||||||
|
Stopwatch.Frequency * (double)BufferLength / Frequency));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Backend?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JfEPXVxhFqA",
|
Nid = "JfEPXVxhFqA",
|
||||||
@@ -33,13 +98,41 @@ public static class AudioOutExports
|
|||||||
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
|
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||||
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
|
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
|
||||||
var format = unchecked((int)ctx[CpuRegister.R9]);
|
var format = unchecked((int)ctx[CpuRegister.R9]);
|
||||||
if (bufferLength == 0 || frequency == 0)
|
if (bufferLength == 0 || frequency == 0 ||
|
||||||
|
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WinMmAudioPort? backend = null;
|
||||||
|
string backendName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
backend = new WinMmAudioPort(frequency);
|
||||||
|
backendName = "winmm";
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
backendName = "silent";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
var handle = Interlocked.Increment(ref _nextPortHandle);
|
var handle = Interlocked.Increment(ref _nextPortHandle);
|
||||||
Ports[handle] = new PortState(userId, type, bufferLength, frequency, format);
|
Ports[handle] = new PortState(
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
bufferLength,
|
||||||
|
frequency,
|
||||||
|
format,
|
||||||
|
channels,
|
||||||
|
bytesPerSample,
|
||||||
|
isFloat,
|
||||||
|
backend);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||||
|
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||||
|
$"{bufferLength} frames, backend={backendName}");
|
||||||
return SetReturn(ctx, handle);
|
return SetReturn(ctx, handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +144,13 @@ public static class AudioOutExports
|
|||||||
public static int AudioOutClose(CpuContext ctx)
|
public static int AudioOutClose(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return SetReturn(
|
if (!Ports.TryRemove(handle, out var port))
|
||||||
ctx,
|
{
|
||||||
Ports.TryRemove(handle, out _)
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
? 0
|
}
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
|
port.Dispose();
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -65,8 +160,44 @@ public static class AudioOutExports
|
|||||||
LibraryName = "libSceAudioOut")]
|
LibraryName = "libSceAudioOut")]
|
||||||
public static int AudioOutOutput(CpuContext ctx)
|
public static int AudioOutOutput(CpuContext ctx)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = 0;
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (!Ports.TryGetValue(handle, out var port))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||||
|
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port.Backend is null ||
|
||||||
|
!port.Backend.Submit(
|
||||||
|
source,
|
||||||
|
port.BufferLength,
|
||||||
|
port.Channels,
|
||||||
|
port.BytesPerSample,
|
||||||
|
port.IsFloat))
|
||||||
|
{
|
||||||
|
port.PaceSilence();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -89,4 +220,23 @@ public static class AudioOutExports
|
|||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetFormat(
|
||||||
|
int rawFormat,
|
||||||
|
out int channels,
|
||||||
|
out int bytesPerSample,
|
||||||
|
out bool isFloat)
|
||||||
|
{
|
||||||
|
var format = rawFormat & 0xFF;
|
||||||
|
channels = format switch
|
||||||
|
{
|
||||||
|
0 or 3 => 1,
|
||||||
|
1 or 4 => 2,
|
||||||
|
2 or 5 or 6 or 7 => 8,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
|
||||||
|
isFloat = bytesPerSample == 4;
|
||||||
|
return channels != 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
302
src/SharpEmu.Libs/Audio/WinMmAudioPort.cs
Normal file
302
src/SharpEmu.Libs/Audio/WinMmAudioPort.cs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
internal sealed class WinMmAudioPort : IDisposable
|
||||||
|
{
|
||||||
|
private const uint WaveMapper = uint.MaxValue;
|
||||||
|
private const uint CallbackEvent = 0x0005_0000;
|
||||||
|
private const ushort WaveFormatPcm = 1;
|
||||||
|
private const uint WaveHeaderDone = 0x0000_0001;
|
||||||
|
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly AutoResetEvent _completion = new(false);
|
||||||
|
private readonly Queue<NativeBuffer> _buffers = new();
|
||||||
|
private IntPtr _device;
|
||||||
|
private int _queuedPcmBytes;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public WinMmAudioPort(uint sampleRate)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = new WaveFormat
|
||||||
|
{
|
||||||
|
FormatTag = WaveFormatPcm,
|
||||||
|
Channels = 2,
|
||||||
|
SamplesPerSecond = sampleRate,
|
||||||
|
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||||
|
BlockAlign = 4,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
ExtraSize = 0,
|
||||||
|
};
|
||||||
|
var result = WaveOutOpen(
|
||||||
|
out _device,
|
||||||
|
WaveMapper,
|
||||||
|
ref format,
|
||||||
|
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||||
|
IntPtr.Zero,
|
||||||
|
CallbackEvent);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Submit(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
uint frames,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputLength = checked((int)frames * 4);
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
while (_queuedPcmBytes != 0 &&
|
||||||
|
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
||||||
|
{
|
||||||
|
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReapCompletedBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ConvertToStereoPcm16(
|
||||||
|
source,
|
||||||
|
output.AsSpan(0, outputLength),
|
||||||
|
checked((int)frames),
|
||||||
|
channels,
|
||||||
|
bytesPerSample,
|
||||||
|
isFloat);
|
||||||
|
return QueueBuffer(output.AsSpan(0, outputLength));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
if (_device != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
WaveOutReset(_device);
|
||||||
|
while (_buffers.TryDequeue(out var buffer))
|
||||||
|
{
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
WaveOutClose(_device);
|
||||||
|
_device = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
_completion.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||||
|
var headerAddress = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = new WaveHeader
|
||||||
|
{
|
||||||
|
Data = dataAddress,
|
||||||
|
BufferLength = checked((uint)data.Length),
|
||||||
|
};
|
||||||
|
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||||
|
Marshal.StructureToPtr(header, headerAddress, false);
|
||||||
|
|
||||||
|
var result = WaveOutPrepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WaveOutWrite(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
headerAddress,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||||
|
_queuedPcmBytes += data.Length;
|
||||||
|
dataAddress = IntPtr.Zero;
|
||||||
|
headerAddress = IntPtr.Zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (headerAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(headerAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(dataAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReapCompletedBuffers()
|
||||||
|
{
|
||||||
|
while (_buffers.TryPeek(out var buffer))
|
||||||
|
{
|
||||||
|
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||||
|
if ((header.Flags & WaveHeaderDone) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffers.Dequeue();
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseBuffer(NativeBuffer buffer)
|
||||||
|
{
|
||||||
|
WaveOutUnprepareHeader(
|
||||||
|
_device,
|
||||||
|
buffer.Header,
|
||||||
|
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||||
|
_queuedPcmBytes -= buffer.Length;
|
||||||
|
Marshal.FreeHGlobal(buffer.Header);
|
||||||
|
Marshal.FreeHGlobal(buffer.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConvertToStereoPcm16(
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
Span<byte> destination,
|
||||||
|
int frames,
|
||||||
|
int channels,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||||
|
for (var frame = 0; frame < frames; frame++)
|
||||||
|
{
|
||||||
|
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||||
|
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||||
|
var right = channels == 1
|
||||||
|
? left
|
||||||
|
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static short ReadSample(
|
||||||
|
ReadOnlySpan<byte> frame,
|
||||||
|
int channel,
|
||||||
|
int bytesPerSample,
|
||||||
|
bool isFloat)
|
||||||
|
{
|
||||||
|
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||||
|
if (!isFloat)
|
||||||
|
{
|
||||||
|
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||||
|
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||||
|
return checked((short)MathF.Round(value * short.MaxValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||||
|
private struct WaveFormat
|
||||||
|
{
|
||||||
|
public ushort FormatTag;
|
||||||
|
public ushort Channels;
|
||||||
|
public uint SamplesPerSecond;
|
||||||
|
public uint AverageBytesPerSecond;
|
||||||
|
public ushort BlockAlign;
|
||||||
|
public ushort BitsPerSample;
|
||||||
|
public ushort ExtraSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WaveHeader
|
||||||
|
{
|
||||||
|
public IntPtr Data;
|
||||||
|
public uint BufferLength;
|
||||||
|
public uint BytesRecorded;
|
||||||
|
public nuint User;
|
||||||
|
public uint Flags;
|
||||||
|
public uint Loops;
|
||||||
|
public IntPtr Next;
|
||||||
|
public nuint Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||||
|
private static extern uint WaveOutOpen(
|
||||||
|
out IntPtr device,
|
||||||
|
uint deviceId,
|
||||||
|
ref WaveFormat format,
|
||||||
|
IntPtr callback,
|
||||||
|
IntPtr instance,
|
||||||
|
uint flags);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||||
|
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||||
|
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||||
|
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||||
|
private static extern uint WaveOutReset(IntPtr device);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||||
|
private static extern uint WaveOutClose(IntPtr device);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user