CsoundNative.kt
src/jvmMain/kotlin/afield/audio/csound/CsoundNative.kt · 264 lines
package afield.audio.csound
Finds and binds libcsound, the optional native library behind CsoundInstance, and explains how
to build it when it is missing.
import java.lang.foreign.Arena
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.MemoryLayout
import java.lang.foreign.MemorySegment
import java.lang.foreign.SymbolLookup
import java.lang.foreign.ValueLayout.ADDRESS
import java.lang.foreign.ValueLayout.JAVA_DOUBLE
import java.lang.foreign.ValueLayout.JAVA_INT
import java.lang.invoke.MethodHandle
import java.nio.file.Path
import kotlin.io.path.exists
object CsoundLibrary
Where libcsound is, and what to do when it is not there.
Csound is an optional native dependency, like libpd and LuaJIT: a clone of Field builds, tests
and runs without it, and only code that reaches for CsoundInstance needs it. Nothing is
committed; python3 tools/build_csound_native.py (or ./gradlew buildCsoundNative) builds one
pinned Csound into build/field-csound/.
Resolution order, first hit wins:
-Dafield.csound.library=<path>AFIELD_CSOUND_LIBRARY=<path>build/field-csound/build/…relative to the working directory (the checkout, forrunJvm,jvmTestand the backdoor)- an installed Csound:
/Library/Frameworks/CsoundLib64.frameworkon a Mac,libcsound64.so.6.0in the usual Linux library directories
It must be a double-precision Csound (MYFLT = double), which every official release is.
object CsoundLibrary {
private val os: String get() = System.getProperty("os.name").lowercase()
private val mac: Boolean get() = os.contains("mac") || os.contains("darwin")
val searchedPaths: List<Path>
Every path resolvedPath tries, in order — quoted in the "not built" error.
val searchedPaths: List<Path>
get() = listOfNotNull(
System.getProperty("afield.csound.library")?.takeIf { it.isNotBlank() },
System.getenv("AFIELD_CSOUND_LIBRARY")?.takeIf { it.isNotBlank() },
).map(Path::of) + when {
mac -> listOf(
Path.of("build", "field-csound", "build", "CsoundLib64.framework", "CsoundLib64").toAbsolutePath(),
Path.of("/Library/Frameworks/CsoundLib64.framework/CsoundLib64"),
)
os.contains("win") -> listOf(Path.of("C:/Program Files/Csound6_x64/bin/csound64.dll"))
else -> listOf(Path.of("build", "field-csound", "build", "libcsound64.so").toAbsolutePath()) +
listOf("/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/lib64", "/usr/lib", "/usr/local/lib")
.map { Path.of(it, "libcsound64.so.6.0") }
}
val resolvedPath: Path? get() = searchedPaths.firstOrNull { it.exists() }
Not cached: building it during a live session is picked up by the next box run.
val isAvailable: Boolean get() = resolvedPath != null
True when a libcsound is where searchedPaths looks.
val HOW_TO_BUILD: String
The one instruction a missing library needs.
val HOW_TO_BUILD: String
get() = "Build it with `python3 tools/build_csound_native.py` or `./gradlew buildCsoundNative` " +
"(needs cmake, flex, bison, libsndfile and the network once), or point -Dafield.csound.library / " +
"AFIELD_CSOUND_LIBRARY at a libcsound you have."
fun describe(): String
One line for a box or a test to print.
fun describe(): String = resolvedPath?.let { "Csound at $it" } ?: "Csound not built. $HOW_TO_BUILD"
}
internal object CsoundNative
The part of csound.h Field uses, bound with Java FFM. Downcalls only: Csound's messages are
collected in its own message buffer and read by CsoundInstance.poll, so no upcall ever runs on
the audio thread.
Nothing here is thread-safe; CsoundInstance holds its lock around every call except the channel
accessors, which Csound itself guards.
declaration — 74 lines, from line 85
internal object CsoundNative {
private val linker: Linker = Linker.nativeLinker()
private val lookup: SymbolLookup by lazy {
val path = CsoundLibrary.resolvedPath
?: throw UnsatisfiedLinkError(
"Csound is not built. ${CsoundLibrary.HOW_TO_BUILD}\nLooked at: " +
CsoundLibrary.searchedPaths.joinToString(", "),
)
SymbolLookup.libraryLookup(path, Arena.global())
}
private fun handle(name: String, descriptor: FunctionDescriptor): MethodHandle {
val symbol = lookup.find(name).orElseThrow {
UnsatisfiedLinkError("libcsound has no symbol $name; is it Csound 6.14 or later?")
}
return linker.downcallHandle(symbol, descriptor)
}
private fun v(vararg args: MemoryLayout) = FunctionDescriptor.ofVoid(*args)
private fun i(vararg args: MemoryLayout) = FunctionDescriptor.of(JAVA_INT, *args)
private fun p(vararg args: MemoryLayout) = FunctionDescriptor.of(ADDRESS, *args)
private fun d(vararg args: MemoryLayout) = FunctionDescriptor.of(JAVA_DOUBLE, *args)
private val initializeH by lazy { handle("csoundInitialize", i(JAVA_INT)) }
private val setGlobalEnvH by lazy { handle("csoundSetGlobalEnv", i(ADDRESS, ADDRESS)) }
private val createH by lazy { handle("csoundCreate", p(ADDRESS)) }
private val destroyH by lazy { handle("csoundDestroy", v(ADDRESS)) }
private val getVersionH by lazy { handle("csoundGetVersion", i()) }
private val setOptionH by lazy { handle("csoundSetOption", i(ADDRESS, ADDRESS)) }
private val compileCsdTextH by lazy { handle("csoundCompileCsdText", i(ADDRESS, ADDRESS)) }
private val compileOrcH by lazy { handle("csoundCompileOrc", i(ADDRESS, ADDRESS)) }
private val readScoreH by lazy { handle("csoundReadScore", i(ADDRESS, ADDRESS)) }
private val inputMessageH by lazy { handle("csoundInputMessage", v(ADDRESS, ADDRESS)) }
private val startH by lazy { handle("csoundStart", i(ADDRESS)) }
private val performKsmpsH by lazy { handle("csoundPerformKsmps", i(ADDRESS)) }
private val cleanupH by lazy { handle("csoundCleanup", i(ADDRESS)) }
private val getSpoutH by lazy { handle("csoundGetSpout", p(ADDRESS)) }
private val getSpinH by lazy { handle("csoundGetSpin", p(ADDRESS)) }
private val getKsmpsH by lazy { handle("csoundGetKsmps", i(ADDRESS)) }
private val getNchnlsH by lazy { handle("csoundGetNchnls", i(ADDRESS)) }
private val getNchnlsInputH by lazy { handle("csoundGetNchnlsInput", i(ADDRESS)) }
private val getSrH by lazy { handle("csoundGetSr", d(ADDRESS)) }
private val get0dBFSH by lazy { handle("csoundGet0dBFS", d(ADDRESS)) }
private val getScoreTimeH by lazy { handle("csoundGetScoreTime", d(ADDRESS)) }
private val setControlChannelH by lazy { handle("csoundSetControlChannel", v(ADDRESS, ADDRESS, JAVA_DOUBLE)) }
private val getControlChannelH by lazy { handle("csoundGetControlChannel", d(ADDRESS, ADDRESS, ADDRESS)) }
private val createMessageBufferH by lazy { handle("csoundCreateMessageBuffer", v(ADDRESS, JAVA_INT)) }
private val getFirstMessageH by lazy { handle("csoundGetFirstMessage", p(ADDRESS)) }
private val getFirstMessageAttrH by lazy { handle("csoundGetFirstMessageAttr", i(ADDRESS)) }
private val popFirstMessageH by lazy { handle("csoundPopFirstMessage", v(ADDRESS)) }
private val getMessageCntH by lazy { handle("csoundGetMessageCnt", i(ADDRESS)) }
private val destroyMessageBufferH by lazy { handle("csoundDestroyMessageBuffer", v(ADDRESS)) }
private val getTableH by lazy { handle("csoundGetTable", i(ADDRESS, ADDRESS, JAVA_INT)) }
private val tableLengthH by lazy { handle("csoundTableLength", i(ADDRESS, JAVA_INT)) }
private val tableCopyInH by lazy { handle("csoundTableCopyIn", v(ADDRESS, JAVA_INT, ADDRESS)) }
private val setStringChannelH by lazy { handle("csoundSetStringChannel", v(ADDRESS, ADDRESS, ADDRESS)) }
private val getStringChannelH by lazy { handle("csoundGetStringChannel", v(ADDRESS, ADDRESS, ADDRESS)) }
private val getChannelDatasizeH by lazy { handle("csoundGetChannelDatasize", i(ADDRESS, ADDRESS)) }
private val setAudioChannelH by lazy { handle("csoundSetAudioChannel", v(ADDRESS, ADDRESS, ADDRESS)) }
private val getAudioChannelH by lazy { handle("csoundGetAudioChannel", v(ADDRESS, ADDRESS, ADDRESS)) }
private val getChannelPtrH by lazy { handle("csoundGetChannelPtr", i(ADDRESS, ADDRESS, ADDRESS, JAVA_INT)) }
private val getControlChannelHintsH by lazy { handle("csoundGetControlChannelHints", i(ADDRESS, ADDRESS, ADDRESS)) }
private val listChannelsH by lazy { handle("csoundListChannels", i(ADDRESS, ADDRESS)) }
private val deleteChannelListH by lazy { handle("csoundDeleteChannelList", v(ADDRESS, ADDRESS)) }
private val setOutputChannelCallbackH by lazy { handle("csoundSetOutputChannelCallback", v(ADDRESS, ADDRESS)) }
private val setInputChannelCallbackH by lazy { handle("csoundSetInputChannelCallback", v(ADDRESS, ADDRESS)) }
private val newOpcodeListH by lazy { handle("csoundNewOpcodeList", i(ADDRESS, ADDRESS)) }
private val disposeOpcodeListH by lazy { handle("csoundDisposeOpcodeList", v(ADDRESS, ADDRESS)) }
private val setHostImplementedMIDIIOH by lazy { handle("csoundSetHostImplementedMIDIIO", v(ADDRESS, JAVA_INT)) }
private val setExternalMidiInOpenCallbackH by lazy { handle("csoundSetExternalMidiInOpenCallback", v(ADDRESS, ADDRESS)) }
private val setExternalMidiReadCallbackH by lazy { handle("csoundSetExternalMidiReadCallback", v(ADDRESS, ADDRESS)) }
private val setExternalMidiInCloseCallbackH by lazy { handle("csoundSetExternalMidiInCloseCallback", v(ADDRESS, ADDRESS)) }
val CHANNEL_CALLBACK: FunctionDescriptor = v(ADDRESS, ADDRESS, ADDRESS, ADDRESS)
channelCallback_t: (CSOUND*, const char* name, void* value, const void* type).
val MIDI_OPEN: FunctionDescriptor = i(ADDRESS, ADDRESS, ADDRESS)
int (*)(CSOUND*, void** userData, const char* devName).
val MIDI_READ: FunctionDescriptor = i(ADDRESS, ADDRESS, ADDRESS, JAVA_INT)
int (*)(CSOUND*, void* userData, unsigned char* buf, int nBytes).
val MIDI_CLOSE: FunctionDescriptor = i(ADDRESS, ADDRESS)
int (*)(CSOUND*, void* userData).
const val CONTROL_CHANNEL = 1
CSOUND_CONTROL_CHANNEL, CSOUND_AUDIO_CHANNEL, CSOUND_STRING_CHANNEL, CSOUND_INPUT_CHANNEL, CSOUND_OUTPUT_CHANNEL.
const val AUDIO_CHANNEL = 2
const val STRING_CHANNEL = 3
const val INPUT_CHANNEL = 16
const val OUTPUT_CHANNEL = 32
fun linker(): Linker = linker
private var initialised = false
fun ensureInitialised()
Once per process: no signal handlers and no atexit hook (the JVM owns both), and an empty plugin directory, so an installed Csound's plugin opcodes are not loaded into this one.
@Synchronized
fun ensureInitialised() {
if (initialised) return
// CSOUNDINIT_NO_SIGNAL_HANDLER | CSOUNDINIT_NO_ATEXIT
initializeH.invokeExact(3) as Int
val empty = java.nio.file.Files.createTempDirectory("field-csound-plugins-").toFile().apply { deleteOnExit() }
Arena.ofConfined().use { a ->
setGlobalEnvH.invokeExact(a.allocateFrom("OPCODE6DIR64"), a.allocateFrom(empty.absolutePath)) as Int
}
initialised = true
}
fun create(): MemorySegment = createH.invokeExact(MemorySegment.NULL) as MemorySegment
fun destroy(cs: MemorySegment) { destroyH.invokeExact(cs) }
fun version(): Int = getVersionH.invokeExact() as Int
fun setOption(cs: MemorySegment, option: MemorySegment): Int
fun setOption(cs: MemorySegment, option: MemorySegment): Int = setOptionH.invokeExact(cs, option) as Int
fun compileCsdText(cs: MemorySegment, csd: MemorySegment): Int
fun compileCsdText(cs: MemorySegment, csd: MemorySegment): Int = compileCsdTextH.invokeExact(cs, csd) as Int
fun compileOrc(cs: MemorySegment, orc: MemorySegment): Int
fun compileOrc(cs: MemorySegment, orc: MemorySegment): Int = compileOrcH.invokeExact(cs, orc) as Int
fun readScore(cs: MemorySegment, sco: MemorySegment): Int
fun readScore(cs: MemorySegment, sco: MemorySegment): Int = readScoreH.invokeExact(cs, sco) as Int
fun inputMessage(cs: MemorySegment, line: MemorySegment) { inputMessageH.invokeExact(cs, line) }
fun start(cs: MemorySegment): Int = startH.invokeExact(cs) as Int
fun performKsmps(cs: MemorySegment): Int = performKsmpsH.invokeExact(cs) as Int
fun cleanup(cs: MemorySegment): Int = cleanupH.invokeExact(cs) as Int
fun spout(cs: MemorySegment): MemorySegment = getSpoutH.invokeExact(cs) as MemorySegment
fun spin(cs: MemorySegment): MemorySegment = getSpinH.invokeExact(cs) as MemorySegment
fun ksmps(cs: MemorySegment): Int = getKsmpsH.invokeExact(cs) as Int
fun nchnls(cs: MemorySegment): Int = getNchnlsH.invokeExact(cs) as Int
fun nchnlsInput(cs: MemorySegment): Int = getNchnlsInputH.invokeExact(cs) as Int
fun sr(cs: MemorySegment): Double = getSrH.invokeExact(cs) as Double
fun zeroDbfs(cs: MemorySegment): Double = get0dBFSH.invokeExact(cs) as Double
fun scoreTime(cs: MemorySegment): Double = getScoreTimeH.invokeExact(cs) as Double
fun setControlChannel(cs: MemorySegment, name: MemorySegment, value: Double)
fun setControlChannel(cs: MemorySegment, name: MemorySegment, value: Double) {
setControlChannelH.invokeExact(cs, name, value)
}
fun getControlChannel(cs: MemorySegment, name: MemorySegment, err: MemorySegment): Double
fun getControlChannel(cs: MemorySegment, name: MemorySegment, err: MemorySegment): Double =
getControlChannelH.invokeExact(cs, name, err) as Double
fun createMessageBuffer(cs: MemorySegment) { createMessageBufferH.invokeExact(cs, 0) }
fun firstMessage(cs: MemorySegment): MemorySegment
fun firstMessage(cs: MemorySegment): MemorySegment = getFirstMessageH.invokeExact(cs) as MemorySegment
fun firstMessageAttr(cs: MemorySegment): Int = getFirstMessageAttrH.invokeExact(cs) as Int
fun popFirstMessage(cs: MemorySegment) { popFirstMessageH.invokeExact(cs) }
fun messageCount(cs: MemorySegment): Int = getMessageCntH.invokeExact(cs) as Int
fun destroyMessageBuffer(cs: MemorySegment) { destroyMessageBufferH.invokeExact(cs) }
fun getTable(cs: MemorySegment, out: MemorySegment, number: Int): Int
fun getTable(cs: MemorySegment, out: MemorySegment, number: Int): Int = getTableH.invokeExact(cs, out, number) as Int
fun tableLength(cs: MemorySegment, number: Int): Int
fun tableLength(cs: MemorySegment, number: Int): Int = tableLengthH.invokeExact(cs, number) as Int
fun tableCopyIn(cs: MemorySegment, number: Int, src: MemorySegment)
fun tableCopyIn(cs: MemorySegment, number: Int, src: MemorySegment) { tableCopyInH.invokeExact(cs, number, src) }
fun setStringChannel(cs: MemorySegment, name: MemorySegment, value: MemorySegment)
fun setStringChannel(cs: MemorySegment, name: MemorySegment, value: MemorySegment) { setStringChannelH.invokeExact(cs, name, value) }
fun getStringChannel(cs: MemorySegment, name: MemorySegment, out: MemorySegment)
fun getStringChannel(cs: MemorySegment, name: MemorySegment, out: MemorySegment) { getStringChannelH.invokeExact(cs, name, out) }
fun channelDatasize(cs: MemorySegment, name: MemorySegment): Int
fun channelDatasize(cs: MemorySegment, name: MemorySegment): Int = getChannelDatasizeH.invokeExact(cs, name) as Int
fun setAudioChannel(cs: MemorySegment, name: MemorySegment, samples: MemorySegment)
fun setAudioChannel(cs: MemorySegment, name: MemorySegment, samples: MemorySegment) { setAudioChannelH.invokeExact(cs, name, samples) }
fun getAudioChannel(cs: MemorySegment, name: MemorySegment, samples: MemorySegment)
fun getAudioChannel(cs: MemorySegment, name: MemorySegment, samples: MemorySegment) { getAudioChannelH.invokeExact(cs, name, samples) }
fun channelPtr(cs: MemorySegment, out: MemorySegment, name: MemorySegment, type: Int): Int
fun channelPtr(cs: MemorySegment, out: MemorySegment, name: MemorySegment, type: Int): Int =
getChannelPtrH.invokeExact(cs, out, name, type) as Int
fun controlChannelHints(cs: MemorySegment, name: MemorySegment, hints: MemorySegment): Int
fun controlChannelHints(cs: MemorySegment, name: MemorySegment, hints: MemorySegment): Int =
getControlChannelHintsH.invokeExact(cs, name, hints) as Int
fun listChannels(cs: MemorySegment, out: MemorySegment): Int
csoundListChannels: the channel count (negative on error); *out is a controlChannelInfo_t[] or NULL.
fun listChannels(cs: MemorySegment, out: MemorySegment): Int = listChannelsH.invokeExact(cs, out) as Int
fun deleteChannelList(cs: MemorySegment, list: MemorySegment)
fun deleteChannelList(cs: MemorySegment, list: MemorySegment) { deleteChannelListH.invokeExact(cs, list) }
const val CHANNEL_INFO_SIZE = 72L
sizeof(controlChannelInfo_t) on LP64: char* name; int type; controlChannelHints_t hints (hints at 16, 56 bytes).
const val CHANNEL_INFO_HINTS_OFFSET = 16L
const val CHANNEL_TYPE_MASK = 15
CSOUND_CHANNEL_TYPE_MASK: the low bits of a channel's type say control, audio or string.
fun setOutputChannelCallback(cs: MemorySegment, cb: MemorySegment)
fun setOutputChannelCallback(cs: MemorySegment, cb: MemorySegment) { setOutputChannelCallbackH.invokeExact(cs, cb) }
fun setInputChannelCallback(cs: MemorySegment, cb: MemorySegment)
fun setInputChannelCallback(cs: MemorySegment, cb: MemorySegment) { setInputChannelCallbackH.invokeExact(cs, cb) }
fun newOpcodeList(cs: MemorySegment, out: MemorySegment): Int
fun newOpcodeList(cs: MemorySegment, out: MemorySegment): Int = newOpcodeListH.invokeExact(cs, out) as Int
fun disposeOpcodeList(cs: MemorySegment, list: MemorySegment)
fun disposeOpcodeList(cs: MemorySegment, list: MemorySegment) { disposeOpcodeListH.invokeExact(cs, list) }
fun setHostImplementedMIDIIO(cs: MemorySegment, on: Int)
fun setHostImplementedMIDIIO(cs: MemorySegment, on: Int) { setHostImplementedMIDIIOH.invokeExact(cs, on) }
fun setMidiInOpen(cs: MemorySegment, cb: MemorySegment)
fun setMidiInOpen(cs: MemorySegment, cb: MemorySegment) { setExternalMidiInOpenCallbackH.invokeExact(cs, cb) }
fun setMidiRead(cs: MemorySegment, cb: MemorySegment)
fun setMidiRead(cs: MemorySegment, cb: MemorySegment) { setExternalMidiReadCallbackH.invokeExact(cs, cb) }
fun setMidiInClose(cs: MemorySegment, cb: MemorySegment)
fun setMidiInClose(cs: MemorySegment, cb: MemorySegment) { setExternalMidiInCloseCallbackH.invokeExact(cs, cb) }
fun cString(s: MemorySegment): String
fun cString(s: MemorySegment): String =
if (s == MemorySegment.NULL) "" else s.reinterpret(Long.MAX_VALUE).getString(0)
}