The Csound API defines the following CS_AUDIODEVICE struct:
typedef struct {
char device_name[64];
char device_id[64];
char rt_module[64];
int max_nchnls;
int isOutput;
} CS_AUDIODEVICE;
Then there is an API function called csoundGetAudioDevList:
PUBLIC int csoundGetAudioDevList(
CSOUND* csound,
CS_AUDIODEVICE* list,
int isOutput
)
that is usually called twice. The first time, a null pointer is passed in to get the number of devices. Then the second time, a pointer sized to number of devices times the size of CS_AUDIODEVICE is passed in to get the actual array of devices.
I have struggled mightily to define my DLLImport declaration and get this data marshalled correctly.
In F#, the opaque struct Csound is defined as:
[<Struct>]
type CSOUND = struct end
This works fine.
Attempt 1
[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE =
[<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
val device_name: string // char[64]
[<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
val device_id: string // char[64]
[<MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)>]
val rt_module: string // char[64]
val max_nchnls: int
val isOutput: int
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, CS_AUDIODEVICE[] devices, int isOutput)
let numberOfDevices = csoundGetAudioDevList(csound, [||], 1)
let mutable devices = [| for _ in 1..numberOfDevices -> CS_AUDIODEVICE() |]
csoundGetAudioDevList(csound, devices, 1) |> ignore
devices
According to Microsoft's own documentation and https://stackoverflow.com/a/8759368/17800932 this should just work, but it doesn't. It crashes. I don't know what it crashes, as I just get Session termination detected in FSI (F# interactive).
Attempt 2
I tried redefining my struct:
[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE =
[<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
val device_name: char[] // char[64]
[<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
val device_id: char[] // char[64]
[<MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)>]
val rt_module: char[] // char[64]
val max_nchnls: int
val isOutput: int
The rest of the setup is the same. And the result is the same in that this crashes.
Attempt 3
In the past, when I have had difficult marshaling things in a straightforward way, I have usually turned to IntPtr. I tried variations of
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, IntPtr devices, int isOutput)
let numberOfDevices = csoundGetAudioDevList(csound, IntPtr.Zero, 1)
let mutable pointer = IntPtr(numberOfDevices * Marshal.SizeOf(CS_AUDIODEVICE))
let mutable devices = [| for _ in 1..numberOfDevices -> CS_AUDIODEVICE() |]
Marshal.StructureToPtr(devices, pointer, true)
csoundGetAudioDevList(csound, pointer, 1) |> ignore
//for i in 1..numberOfDevices do
// devices[i-1] <- Marshal.PtrToStructure<CS_AUDIODEVICE>(pointer)
// pointer <- IntPtr.Add(pointer, Marshal.SizeOf<CS_AUDIODEVICE>())
devices <- Marshal.PtrToStructure<CS_AUDIODEVICE[]>(pointer)
devices
I was trying to follow https://stackoverflow.com/a/27483917/17800932 here plus things I have done before.
I simply cannot figure this out. Most things just silently crash, some things return stack traces that I can't understand, and some things even explicitly crash the CLR.
One frustration I have is that I cannot find a way to simply reason about this and read documentation on .NET and/or Csound's API. I do not have control over the API, only the documentation I linked above, so I can't just modify the function in the DLL that I am trying to call.
In my experience with these DLL bindings is that things that should work don't always, and I have found that with DLLImport stuff, I just gotta try sensible things and eventually get something working. So any explanations that not only provide something that works but explains why/how would be super appreciated!
Main questions:
- How should the
CS_AUDIODEVICEstruct be defined? - What is the proper
DLLImportdeclaration of thecsoundGetAudioDevListfunction? - How do I marshal the list of audio devices out of the DLL?
- Why/how does this all work?
Thank you!
Here is the rest of the code if you want to try and actually reproduce this. You need the Csound API. On Windows, it is located at Csound-6.18.1-windows-x64-binaries\build\Release\csound64.dll in the Windows binaries download.
open System.Runtime.InteropServices
[<Literal>]
let csoundDLLPath = @"<path to Csound DLL>"
[<Struct>]
type CSOUND = struct end
[<Struct; StructLayout(LayoutKind.Sequential)>]
type CS_AUDIODEVICE = ???
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern CSOUND* csoundCreate()
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundStart(CSOUND* csound)
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundSetOption(CSOUND* csound, string option)
[<DllImport(csoundDLLPath, CallingConvention = CallingConvention.Cdecl)>]
extern int csoundGetAudioDevList(CSOUND* csound, ???, int isOutput)
let c = csoundCreate()
csoundSetOption(c, "-odac") // this enables using the system's audio devices as the Csound output device
csoundStart c
// This is where csoundGetAudioDevList would be called
charis 1-byte, while in dotnetcharis 2-byte. Maybe it's the problem? Try to change constant size from 64 to 32CS_AUDIODEVICEcan be declared in following way: sharplab.io/…[<Struct; StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)>]That will automatically make it 1-byte characters. You don't need to specifyPack, and you don't neeed to changeSizeConst=64to32