Skip to content

Commit 49c00a0

Browse files
committed
Added TinySoundFont bindings
1 parent 4eab06b commit 49c00a0

File tree

7 files changed

+389
-1
lines changed

7 files changed

+389
-1
lines changed

app/src/nativeMain/kotlin/Main.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ fun main() {
3636
next = g
3737
)
3838

39-
4039
val e = MidiMessageNoteOn(
4140
time = 1_000u,
4241
channel = 7u,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package pl.lemanski.pandamidi.soundFont
2+
3+
interface SoundFont {
4+
/**
5+
* Copy a tsf instance from an existing one, use tsf_close to close it as well.
6+
* All copied tsf instances and their original instance are linked, and share the underlying soundfont.
7+
* This allows loading a soundfont only once, but using it for multiple independent playbacks.
8+
* (This function isn't thread-safe without locking.)
9+
*/
10+
fun copy(): SoundFont
11+
12+
/**
13+
* Free the memory related to this tsf instance
14+
*/
15+
fun close()
16+
17+
/**
18+
* Stop all playing notes immediately and reset all channel parameters
19+
*/
20+
fun reset()
21+
22+
/**
23+
* Returns the preset index from a bank and preset number, or -1 if it does not exist in the loaded SoundFont
24+
*/
25+
fun getPresetIndex(bank: Int, presetNumber: Int): Int
26+
27+
/**
28+
* Returns the number of presets in the loaded SoundFont
29+
*/
30+
fun getPresetsCount(): Int
31+
32+
/**
33+
* Returns the name of a preset index >= 0 and < tsf_get_presetcount()
34+
*/
35+
fun getPresetName(presetIndex: Int): String
36+
37+
/**
38+
* Returns the name of a preset by bank and preset number
39+
*/
40+
fun bankGetPresetName(bank: Int, presetNumber: Int): String
41+
42+
/**
43+
* Setup the parameters for the voice render methods
44+
* @param outputMode: if mono or stereo and how stereo channel data is ordered
45+
* @param sampleRate: the number of samples per second (output frequency)
46+
* @param globalGainDb: volume gain in decibels (>0 means higher, <0 means lower)
47+
*/
48+
fun setOutput(outputMode: OutputMode, sampleRate: Int, globalGainDb: Float)
49+
50+
/**
51+
* Set the global gain as a volume factor
52+
* @param globalGain: the desired volume where 1.0 is 100%
53+
*/
54+
fun setVolume(globalGain: Float)
55+
56+
/**
57+
* Set the maximum number of voices to play simultaneously
58+
* Depending on the soundfont, one note can cause many new voices to be started,
59+
* so don't keep this number too low or otherwise sounds may not play.
60+
*
61+
* @param maxVoices: maximum number to pre-allocate and set the limit to
62+
* @throws IllegalArgumentException if allocation failed
63+
*/
64+
fun setMaxVoices(maxVoices: Int)
65+
66+
/**
67+
* Start playing a note
68+
* @param presetIndex preset index >= 0 and < getPresetCount()
69+
* @param key note value between 0 and 127 (60 being middle C)
70+
* @param velocity velocity as a float between 0.0 (equal to note off) and 1.0 (full)
71+
* @throws IllegalArgumentException if allocation of a new voice failed
72+
*/
73+
fun noteOn(presetIndex: Int, key: Int, velocity: Float)
74+
75+
/**
76+
* Start playing a note
77+
* @param bank instrument bank number (alternative to presetIndex)
78+
* @param presetNumber preset number (alternative to presetIndex)
79+
* @param key note value between 0 and 127 (60 being middle C)
80+
* @param velocity velocity as a float between 0.0 (equal to note off) and 1.0 (full)
81+
* @throws IllegalArgumentException if preset does not exist or allocation failed
82+
*/
83+
fun bankNoteOn(bank: Int, presetNumber: Int, key: Int, velocity: Float)
84+
85+
/**
86+
* Stop playing a note
87+
* @param presetIndex preset index >= 0 and < getPresetCount()
88+
* @param key note value between 0 and 127 (60 being middle C)
89+
*/
90+
fun noteOff(presetIndex: Int, key: Int)
91+
92+
/**
93+
* Stop playing a note
94+
* @param bank instrument bank number (alternative to presetIndex)
95+
* @param presetNumber preset number (alternative to presetIndex)
96+
* @param key note value between 0 and 127 (60 being middle C)
97+
* @throws IllegalArgumentException if preset does not exist
98+
*/
99+
fun bankNoteOff(bank: Int, presetNumber: Int, key: Int)
100+
101+
/**
102+
* Stop playing all notes (end with sustain and release)
103+
*/
104+
fun noteOffAll()
105+
106+
/**
107+
* @return the number of active voices
108+
*/
109+
fun activeVoiceCount(): Int
110+
111+
/**
112+
* Supported output modes by the render methods
113+
*/
114+
enum class OutputMode {
115+
/**
116+
* Two channels with single left/right samples one after another
117+
*/
118+
TSF_STEREO_INTERLEAVED,
119+
120+
/**
121+
* Two channels with all samples for the left channel first then right
122+
*/
123+
TSF_STEREO_UNWEAVED,
124+
125+
/**
126+
* A single channel (stereo instruments are mixed into center)
127+
*/
128+
TSF_MONO
129+
}
130+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package pl.lemanski.pandamidi.soundFont
2+
3+
import pl.lemanski.pandamidi.soundFont.internal.SoundFontDelegate
4+
5+
internal typealias Sf = Any
6+
7+
fun SoundFont.load(path: String): SoundFont {
8+
val soundFont = loadFromFile(path)
9+
return object : SoundFont by SoundFontDelegate(soundFont) { }
10+
}
11+
12+
fun SoundFont.load(memory: ByteArray): SoundFont {
13+
val soundFont = loadFromMemory(memory)
14+
return object : SoundFont by SoundFontDelegate(soundFont) { }
15+
}
16+
17+
internal expect fun loadFromFile(path: String): Sf
18+
19+
internal expect fun loadFromMemory(memory: ByteArray): Sf
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package pl.lemanski.pandamidi.soundFont.internal
2+
3+
import pl.lemanski.pandamidi.soundFont.Sf
4+
import pl.lemanski.pandamidi.soundFont.SoundFont
5+
6+
internal open class SoundFontDelegate(val sf: Sf) : SoundFont {
7+
8+
override fun copy(): SoundFont = SoundFontDelegate(sf)
9+
10+
override fun close() = close(this)
11+
12+
override fun reset() = reset(this)
13+
14+
override fun getPresetIndex(bank: Int, presetNumber: Int): Int = getPresetIndex(this, bank, presetNumber)
15+
16+
override fun getPresetsCount(): Int = getPresetsCount(this)
17+
18+
override fun getPresetName(presetIndex: Int): String = getPresetName(this, presetIndex)
19+
20+
override fun bankGetPresetName(bank: Int, presetNumber: Int): String = bankGetPresetName(this, bank, presetNumber)
21+
22+
override fun setOutput(outputMode: SoundFont.OutputMode, sampleRate: Int, globalGainDb: Float) = setOutput(this, outputMode, sampleRate, globalGainDb)
23+
24+
override fun setVolume(globalGain: Float) = setVolume(this, globalGain)
25+
26+
override fun setMaxVoices(maxVoices: Int) = setMaxVoices(this, maxVoices)
27+
28+
override fun noteOn(presetIndex: Int, key: Int, velocity: Float) = noteOn(this, presetIndex, key, velocity)
29+
30+
override fun bankNoteOn(bank: Int, presetNumber: Int, key: Int, velocity: Float) = bankNoteOn(this, bank, presetNumber, key, velocity)
31+
32+
override fun noteOff(presetIndex: Int, key: Int) = noteOff(this, presetIndex, key)
33+
34+
override fun bankNoteOff(bank: Int, presetNumber: Int, key: Int) = bankNoteOff(this, bank, presetNumber, key)
35+
36+
override fun noteOffAll() = noteOffAll(this)
37+
38+
override fun activeVoiceCount(): Int = activeVoiceCount(this)
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package pl.lemanski.pandamidi.soundFont.internal
2+
3+
import pl.lemanski.pandamidi.soundFont.SoundFont
4+
5+
internal expect fun close(delegate: SoundFontDelegate)
6+
7+
internal expect fun reset(delegate: SoundFontDelegate)
8+
9+
internal expect fun getPresetIndex(delegate: SoundFontDelegate, bank: Int, presetNumber: Int): Int
10+
11+
internal expect fun getPresetsCount(delegate: SoundFontDelegate): Int
12+
13+
internal expect fun getPresetName(delegate: SoundFontDelegate, presetIndex: Int): String
14+
15+
internal expect fun bankGetPresetName(delegate: SoundFontDelegate, bank: Int, presetNumber: Int): String
16+
17+
internal expect fun setOutput(delegate: SoundFontDelegate, outputMode: SoundFont.OutputMode, sampleRate: Int, globalGainDb: Float)
18+
19+
internal expect fun setVolume(delegate: SoundFontDelegate, globalGain: Float)
20+
21+
internal expect fun setMaxVoices(delegate: SoundFontDelegate, maxVoices: Int)
22+
23+
internal expect fun noteOn(delegate: SoundFontDelegate, presetIndex: Int, key: Int, velocity: Float)
24+
25+
internal expect fun bankNoteOn(delegate: SoundFontDelegate, bank: Int, presetNumber: Int, key: Int, velocity: Float)
26+
27+
internal expect fun noteOff(delegate: SoundFontDelegate, presetIndex: Int, key: Int)
28+
29+
internal expect fun bankNoteOff(delegate: SoundFontDelegate, bank: Int, presetNumber: Int, key: Int)
30+
31+
internal expect fun noteOffAll(delegate: SoundFontDelegate)
32+
33+
internal expect fun activeVoiceCount(delegate: SoundFontDelegate): Int
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package pl.lemanski.pandamidi.soundFont
2+
3+
import kotlinx.cinterop.ExperimentalForeignApi
4+
import kotlinx.cinterop.refTo
5+
import tinySoundFont.tsf_load_filename
6+
import tinySoundFont.tsf_load_memory
7+
8+
@OptIn(ExperimentalForeignApi::class)
9+
internal actual fun loadFromFile(path: String): Sf {
10+
return tsf_load_filename(path) ?: throw IllegalStateException("SoundFont not loaded")
11+
}
12+
13+
@OptIn(ExperimentalForeignApi::class)
14+
internal actual fun loadFromMemory(memory: ByteArray): Sf {
15+
return tsf_load_memory(memory.refTo(0), memory.size) ?: throw IllegalStateException("SoundFont not loaded")
16+
}
17+

0 commit comments

Comments
 (0)