commonMain.korlibs.audio.format.mod.S3M.kt Maven / Gradle / Ivy
The newest version!
@file:Suppress("unused", "NAME_SHADOWING", "UNUSED_PARAMETER", "FunctionName", "MemberVisibilityCanBePrivate")
package korlibs.audio.format.mod
import korlibs.datastructure.IntDeque
import korlibs.memory.*
import korlibs.audio.sound.NativeSoundProvider
import korlibs.audio.sound.Sound
import korlibs.audio.sound.nativeSoundProvider
import korlibs.io.file.VfsFile
import korlibs.io.stream.AsyncStream
import korlibs.io.stream.readBytesExact
import korlibs.io.stream.sliceStart
import korlibs.math.*
import kotlin.random.Random
import kotlin.time.*
/*
(c) 2012-2021 Noora Halme et al. (see AUTHORS)
This code is licensed under the MIT license:
http://www.opensource.org/licenses/mit-license.php
Scream Tracker 3 module player class
todo:
- are Exx, Fxx and Gxx supposed to share a single
command data memory?
*/
object S3M : BaseModuleTracker.Format("s3m") {
override fun createTracker(): BaseModuleTracker = Screamtracker()
override suspend fun fastValidate(data: AsyncStream): Boolean {
val buffer = data.sliceStart(0x2c).readBytesExact(4)
val signature = CharArray(4) { buffer[it].toChar() }.concatToString()
return signature == "SCRM"
}
}
suspend fun VfsFile.readS3M(soundProvider: NativeSoundProvider = nativeSoundProvider): Sound =
Screamtracker().createSoundFromFile(this)
class Screamtracker : BaseModuleTracker() {
var paused = false
var repeat = false
var filter = false
var syncqueue = IntDeque()
var title: String = ""
var signature: String = ""
var songlen: Int = 1
var repeatpos: Int = 0
var patterntable: Uint8Buffer = Uint8Buffer(0)
var channels: Int = 0
var ordNum: Int = 0
var insNum: Int = 0
var patNum: Int = 0
var globalVol: Int = 64
var initSpeed: Int = 6
var initBPM: Int = 125
var fastslide: Int = 0
var mixval: Double = 8.0
var sample: Array = emptyArray()
var pattern: Array = emptyArray()
var channel: Array = emptyArray()
var looprow: Int = 0
var loopstart: Int = 0
var loopcount: Int = 0
var patterndelay: Int = 0
var patternwait: Int = 0
var tick: Int = -1
var position: Int = 0
var row: Int = 0
var flags: Int = 0
var patterns: Int = 0
var chvu: FloatArray = FloatArray(0)
var volume: Int = globalVol
var speed: Int = initSpeed
var bpm: Int = initBPM
var stt: Double = 0.0
var breakrow: Int = 0
var patternjump: Int = 0
val mod = this
val periodtable = floatArrayOf(
27392f, 25856f, 24384f, 23040f, 21696f, 20480f, 19328f, 18240f, 17216f, 16256f, 15360f, 14496f,
13696f, 12928f, 12192f, 11520f, 10848f, 10240f, 9664f, 9120f, 8608f, 8128f, 7680f, 7248f,
6848f, 6464f, 6096f, 5760f, 5424f, 5120f, 4832f, 4560f, 4304f, 4064f, 3840f, 3624f,
3424f, 3232f, 3048f, 2880f, 2712f, 2560f, 2416f, 2280f, 2152f, 2032f, 1920f, 1812f,
1712f, 1616f, 1524f, 1440f, 1356f, 1280f, 1208f, 1140f, 1076f, 1016f, 960f, 906f,
856f, 808f, 762f, 720f, 678f, 640f, 604f, 570f, 538f, 508f, 480f, 453f,
428f, 404f, 381f, 360f, 339f, 320f, 302f, 285f, 269f, 254f, 240f, 226f,
214f, 202f, 190f, 180f, 170f, 160f, 151f, 143f, 135f, 127f, 120f, 113f,
107f, 101f, 95f, 90f, 85f, 80f, 75f, 71f, 67f, 63f, 60f, 56f
)
val retrigvoltab = floatArrayOf(
0f, -1f, -2f, -4f, -8f, -16f, 0.66f, 0.5f,
0f, 1f, 2f, 4f, 8f, 16f, 1.50f, 2.0f
)
val pan_r = FloatArray(32) { 0.5f }
val pan_l = FloatArray(32) { 0.5f }
// calc tables for vibrato waveforms
val vibratotable = arrayOf(
FloatArray(256) { 127f * kotlin.math.sin(kotlin.math.PI * 2f * (it / 256f)).toFloat() },
FloatArray(256) { 127f - it },
FloatArray(256) { if (it < 128) 127f else -128f },
FloatArray(256) { Random.nextFloat() * 255f - 128f },
)
// effect jumptables for tick 0 and tick 1+
val effects_t0 = arrayOf(
null, // zero is ignored
::effect_t0_a, ::effect_t0_b, ::effect_t0_c, ::effect_t0_d, ::effect_t0_e,
::effect_t0_f, ::effect_t0_g, ::effect_t0_h, ::effect_t0_i, ::effect_t0_j,
::effect_t0_k, ::effect_t0_l, ::effect_t0_m, ::effect_t0_n, ::effect_t0_o,
::effect_t0_p, ::effect_t0_q, ::effect_t0_r, ::effect_t0_s, ::effect_t0_t,
::effect_t0_u, ::effect_t0_v, ::effect_t0_w, ::effect_t0_x, ::effect_t0_y,
::effect_t0_z
)
val effects_t0_s = arrayOf(
::effect_t0_s0, ::effect_t0_s1, ::effect_t0_s2, ::effect_t0_s3, ::effect_t0_s4,
::effect_t0_s5, ::effect_t0_s6, ::effect_t0_s7, ::effect_t0_s8, ::effect_t0_s9,
::effect_t0_sa, ::effect_t0_sb, ::effect_t0_sc, ::effect_t0_sd, ::effect_t0_se,
::effect_t0_sf
)
val effects_t1 = arrayOf(
null, // zero is ignored
::effect_t1_a, ::effect_t1_b, ::effect_t1_c, ::effect_t1_d, ::effect_t1_e,
::effect_t1_f, ::effect_t1_g, ::effect_t1_h, ::effect_t1_i, ::effect_t1_j,
::effect_t1_k, ::effect_t1_l, ::effect_t1_m, ::effect_t1_n, ::effect_t1_o,
::effect_t1_p, ::effect_t1_q, ::effect_t1_r, ::effect_t1_s, ::effect_t1_t,
::effect_t1_u, ::effect_t1_v, ::effect_t1_w, ::effect_t1_x, ::effect_t1_y,
::effect_t1_z
)
val effects_t1_s = arrayOf(
::effect_t1_s0, ::effect_t1_s1, ::effect_t1_s2, ::effect_t1_s3, ::effect_t1_s4,
::effect_t1_s5, ::effect_t1_s6, ::effect_t1_s7, ::effect_t1_s8, ::effect_t1_s9,
::effect_t1_sa, ::effect_t1_sb, ::effect_t1_sc, ::effect_t1_sd, ::effect_t1_se,
::effect_t1_sf
)
init {
clearsong()
initialize()
}
class Sample(
var length: Int = 0,
var loopstart: Int = 0,
var loopend: Int = 0,
var looplength: Int = 0,
var volume: Int = 64,
var loop: Int = 0,
var c2spd: Int = 8363,
var name: String = "",
var data: FloatArray = FloatArray(0),
var stereo: Int = 0,
var bits: Int = 8,
)
// clear song data
fun clearsong() {
title = ""
signature = ""
songlen = 1
repeatpos = 0
patterntable = Uint8Buffer(256)
channels = 0
ordNum = 0
insNum = 0
patNum = 0
globalVol = 64
initSpeed = 6
initBPM = 125
fastslide = 0
mixval = 8.0
sample = Array(256) { Sample() }
pattern = emptyArray()
looprow = 0
loopstart = 0
loopcount = 0
patterndelay = 0
patternwait = 0
}
class Channel(
var sample: Int = 0,
var note: Int = 24,
var command: Int = 0,
var data: Int = 0,
var samplepos: Double = 0.0,
var samplespeed: Double = 0.0,
var flags: Int = 0,
var noteon: Int = 0,
var slidespeed: Int = 0,
var slideto: Double = 0.0,
var slidetospeed: Int = 0,
var arpeggio: Int = 0,
var period: Double = 0.0,
var volume: Int = 64,
var voiceperiod: Double = 0.0,
var voicevolume: Int = 0,
var oldvoicevolume: Int = 0,
var semitone: Int = 12,
var vibratospeed: Int = 0,
var vibratodepth: Int = 0,
var vibratopos: Int = 0,
var vibratowave: Int = 0,
var lastoffset: Int = 0,
var lastretrig: Int = 0,
var volramp: Double = 0.0,
var volrampfrom: Int = 0,
var trigramp: Double = 0.0,
var trigrampfrom: Double = 0.0,
var currentsample: Double = 0.0,
var lastsample: Double = 0.0,
var volslide: Int = 0,
)
// initialize all player variables to defaults prior to starting playback
override fun initialize() {
syncqueue = IntDeque()
tick = -1
position = 0
row = 0
flags = 0
volume = globalVol
speed = initSpeed
bpm = initBPM
stt = 0.0
breakrow = 0
patternjump = 0
patterndelay = 0
patternwait = 0
endofsong = false
channel = Array(channels) { Channel() }
}
// parse the module from local buffer
override fun parse(buffer: Uint8Buffer): Boolean {
// check s3m signature and type
signature = CharArray(4) { buffer[0x002c + it].toChar() }.concatToString()
if (signature != "SCRM") return false
if (buffer[0x001d] != 0x10) return false
// get channel count
channels = 0
for (i in 0 until 32) {
if ((buffer[0x0040 + i] and 0x80) != 0) break
channels++
}
// default panning 3/C/3/...
for (i in 0 until 32) {
if ((buffer[0x0040 + i] and 0x80) == 0) {
val c = buffer[0x0040 + i] and 15
pan_r[i] = if (c < 8) 0.2f else 0.8f
pan_l[i] = if (c < 8) 0.8f else 0.2f
}
}
title = CharArray(0x1c) { dos2utf(buffer[it]) }.concatToString().trimEnd('\u0000')
ordNum = buffer[0x0020] or (buffer[0x0021] shl 8)
insNum = buffer[0x0022] or (buffer[0x0023] shl 8)
patNum = buffer[0x0024] or (buffer[0x0025] shl 8)
globalVol = buffer[0x0030]
initSpeed = buffer[0x0031]
initBPM = buffer[0x0032]
fastslide = if ((buffer[0x0026] and 64) != 0) 1 else 0
speed = initSpeed
bpm = initBPM
// check for additional panning info
if (buffer[0x0035] == 0xfc) {
for (i in 0 until 32) {
var c = buffer[0x0070 + ordNum + insNum * 2 + patNum * 2 + i]
if ((c and 0x10) != 0) {
c = c and 0x0f
pan_r[i] = c / 15f
pan_l[i] = 1f - pan_r[i]
}
}
}
// check for mono panning
mixval = buffer[0x0033].toDouble()
if ((mixval.toInt() and 0x80) == 0x80) {
for (i in 0 until 32) {
pan_r[i] = 0.5f
pan_l[i] = 0.5f
}
}
// calculate master mix scaling factor
mixval = 128.0 / kotlin.math.max(0x10, mixval.toInt() and 0x7f).toDouble() // (8.0 when mastervol is 0x10, 1.0 when mastervol is 0x7f)
// load orders
for (i in 0 until ordNum) patterntable[i] = buffer[0x0060 + i]
songlen = 0
for (i in 0 until ordNum) if (patterntable[i] != 255) songlen++
// load instruments
sample = Array(insNum) { i ->
val offset = (buffer[0x0060 + ordNum + i * 2] or (buffer[0x0060 + ordNum + i * 2 + 1] shl 8)) * 16
// sample data
val smpoffset = (((buffer[offset + 0x0d] shl 16) or (buffer[offset + 0x0e]) or (buffer[offset + 0x0f] shl 8)) * 16)
val loopstart = buffer[offset + 0x14] or (buffer[offset + 0x15] shl 8)
val loopend = buffer[offset + 0x18] or (buffer[offset + 0x19] shl 8)
val length = buffer[offset + 0x10] or (buffer[offset + 0x11] shl 8)
//println("SAMPLE:offset=${offset},smpoffset=$smpoffset,loopstart=$loopstart,loopend=$loopend,length=$length")
Sample(
name = CharArray(28) { dos2utf(buffer[offset + 0x0030 + it]) }.concatToString().trimEnd('\u0000'),
length = length,
loopstart = loopstart,
loopend = loopend,
looplength = loopend - loopstart,
volume = buffer[offset + 0x1c],
loop = buffer[offset + 0x1f] and 1,
stereo = (buffer[offset + 0x1f] and 2) ushr 1,
bits = if ((buffer[offset + 0x1f] and 4) != 0) 16 else 8,
c2spd = buffer[offset + 0x20] or (buffer[offset + 0x21] shl 8),
data = FloatArray(length) { (buffer[smpoffset + it] - 128).toFloat() / 128f } // convert to mono float signed,
)
}
// load and unpack patterns
var max_ch = 0
pattern = Array(patNum) { Uint8Buffer(channels * 64 * 5) }
for (i in 0 until patNum) {
val boffset = 0x0060 + ordNum + insNum * 2 + i * 2
var offset = (buffer[boffset] or (buffer[boffset + 1] shl 8)) * 16
var patlen = buffer[offset] or (buffer[offset + 1] shl 8)
val pattern = pattern[i]
for (row in 0 until 64) {
for (ch in 0 until channels) {
val opattern = row * channels * 5 + ch * 5
pattern[opattern + 0] = 255
pattern[opattern + 1] = 0
pattern[opattern + 2] = 255
pattern[opattern + 3] = 255
pattern[opattern + 4] = 0
}
}
if (offset == 0) continue // fix for control_e.s3m
var row = 0
var pos = 0
offset += 2
while (row < 64) {
val c = buffer[offset + pos++]
if (c == 0) {
row++
continue
}
val ch = c and 31
if (ch < channels) {
if (ch > max_ch) {
for (j in 0 until songlen) {
if (patterntable[j] == i) max_ch = ch
} // only if pattern is actually used
}
val opattern = row * channels * 5 + ch * 5
if ((c and 32) != 0) {
pattern[opattern + 0] = buffer[offset + pos++] // note
pattern[opattern + 1] = buffer[offset + pos++] // instrument
}
if ((c and 64) != 0) {
pattern[opattern + 2] = buffer[offset + pos++]
} // volume
if ((c and 128) != 0) {
pattern[opattern + 3] = buffer[offset + pos++] // command
pattern[opattern + 4] = buffer[offset + pos++] // parameter
if (pattern[opattern + 3] == 0 || pattern[opattern + 3] > 26) {
pattern[opattern + 3] = 255
}
}
} else {
if ((c and 32) != 0) pos += 2
if ((c and 64) != 0) pos++
if ((c and 128) != 0) pos += 2
}
}
}
patterns = patNum
// how many channels had actually pattern data on them? trim off the extra channels
val oldch = channels
channels = max_ch + 1
for (i in 0 until patNum) {
val oldpat = Uint8Buffer(pattern[i].size)
arraycopy(pattern[i], 0, oldpat, 0, pattern[i].size)
pattern[i] = Uint8Buffer(channels * 64 * 5)
val pattern = pattern[i]
for (j in 0 until 64) {
for (c in 0 until channels) {
val op = j * channels * 5 + c * 5
val oop = j * oldch * 5 + c * 5
pattern[op + 0] = oldpat[oop + 0]
pattern[op + 1] = oldpat[oop + 1]
pattern[op + 2] = oldpat[oop + 2]
pattern[op + 3] = oldpat[oop + 3]
pattern[op + 4] = oldpat[oop + 4]
}
}
}
chvu = FloatArray(channels) { 0f }
val computeTime = measureTime {
totalLengthInSamples = computeTime().toLong()
}
println("COMPUTED LENGTH IN: $computeTime, totalLengthInSamples=$totalLengthInSamples")
return true
}
fun computeTime(): Int {
initialize()
var samples = 0
var speed = this.speed
var bpm = this.bpm
var row = 0
var position = 0
val channels = this.channels
val samplerate = this.samplerate
while (position < songlen) {
val pat = pattern.getOrNull(patterntable[position]) ?: continue
for (ch in 0 until channels) {
val pp = row * 5 * channels + ch * 5
val command = pat[pp + 3]
val data = pat[pp + 4]
if (command == 1) speed = data
if (command == 20 && data > 32) bpm = data
// @TODO: Pattern Delay/patternwait
// @TODO: Loops?
}
val stt = samplerate * 60 / bpm / 4 / 6 // samples to tick
samples += (stt * speed)
row++
if (row >= 64) {
row = 0
position++
}
//println("tick=$tick, speed=$speed, row=$row, position=$position, songlen=$songlen, loopstart=$loopstart,looprow=$looprow,loopcount=$loopcount, stt=$stt, samples=$samples")
}
return samples
}
// advance player
fun advance() {
stt = (((samplerate * 60).toDouble() / bpm.toDouble()) / 4.0) / 6.0 // samples to tick
// advance player
tick++
flags = flags or 1
// new row on this tick?
if (tick >= speed) {
if (patterndelay != 0) { // delay pattern
if (tick < ((patternwait + 1) * speed)) {
patternwait++
} else {
row++; tick = 0; flags = flags or 2; patterndelay = 0
}
} else {
if ((flags and (16 + 32 + 64)) != 0) {
if ((flags and 64) != 0) { // loop pattern?
row = looprow
flags = flags and 0xa1
flags = flags or 2
} else {
if ((flags and 16) != 0) { // pattern jump/break?
position = patternjump
row = breakrow
patternjump = 0
breakrow = 0
flags = flags and 0xe1
flags = flags or 2
}
}
tick = 0
} else {
row++
tick = 0
flags = flags or 2
}
}
}
// step to new pattern?
if (row >= 64) {
position++
row = 0
flags = flags or 4
while (patterntable[position] == 254) position++ // skip markers
}
// end of song?
if (position >= songlen || patterntable[position] == 255) {
if (repeat) {
position = 0
} else {
endofsong = true
}
return
}
}
// process one channel on a row in pattern p, pp is an offset to pattern data
fun process_note(p: Int, ch: Int) {
val pp: Int = row * 5 * channels + ch * 5
val pat = pattern[p]
val n = pat[pp]
val s = pat[pp + 1]
val channel = channel[ch]
if (s != 0) {
channel.sample = s - 1
channel.volume = sample[s - 1].volume
channel.voicevolume = channel.volume
if (n == 255 && (channel.samplepos > sample[s - 1].length)) {
channel.trigramp = 0.0
channel.trigrampfrom = channel.currentsample
channel.samplepos = 0.0
}
}
if (n < 254) {
// calc period for note
val n = (n and 0x0f) + (n ushr 4) * 12
val pv = (8363.0 * periodtable[n]) / sample[channel.sample].c2spd.toDouble()
// noteon, except if command=0x07 ('G') (porta to note) or 0x0c ('L') (porta+volslide)
if ((channel.command != 0x07) && (channel.command != 0x0c)) {
channel.note = n
channel.period = pv
channel.voiceperiod = channel.period
channel.samplepos = 0.0
if (channel.vibratowave > 3) channel.vibratopos = 0
channel.trigramp = 0.0
channel.trigrampfrom = channel.currentsample
channel.flags = channel.flags or 3 // force sample speed recalc
channel.noteon = 1
}
// in either case, set the slide to note target to note period
channel.slideto = pv
} else if (n == 254) {
channel.noteon = 0 // sample off
channel.voicevolume = 0
}
if (pat[pp + 2] <= 64) {
channel.volume = pat[pp + 2]
channel.voicevolume = channel.volume
}
}
// advance player and all channels by a tick
fun process_tick() {
// advance global player state by a tick
advance()
// advance all channels
for (ch in 0 until channels) {
// calculate playback position
val p = patterntable[position]
val pp = row * 5 * channels + ch * 5
val channel = channel[ch]
channel.oldvoicevolume = channel.voicevolume
if ((flags and 2) != 0) { // new row
val pat = pattern.getOrNull(p) ?: continue
channel.command = pat[pp + 3]
channel.data = pat[pp + 4]
if (!(channel.command == 0x13 && (channel.data and 0xf0) == 0xd0)) { // note delay?
process_note(p, ch)
}
}
// kill empty samples
if (sample[channel.sample].length == 0) channel.noteon = 0
// run effects on each new tick
if (channel.command < 27) {
if (tick == 0) {
// process only on tick 0 effects
effects_t0[channel.command]?.invoke(ch)
} else {
effects_t1[channel.command]?.invoke(ch)
}
}
// advance vibrato on each new tick
channel.vibratopos += channel.vibratospeed * 2
channel.vibratopos = channel.vibratopos and 0xff
if (channel.oldvoicevolume != channel.voicevolume) {
channel.volrampfrom = channel.oldvoicevolume
channel.volramp = 0.0
}
// recalc sample speed if voiceperiod has changed
if (((channel.flags and 1) != 0 || (flags and 2) != 0) && channel.voiceperiod != 0.0) {
channel.samplespeed = (14317056.0 / channel.voiceperiod) / samplerate.toDouble()
}
// clear channel flags
channel.flags = 0
}
// clear global flags after all channels are processed
flags = flags and 0x70
}
override fun skip(samples: Int) {
var rem = samples.toDouble()
while (rem > 0 && !endofsong && playing) {
rem -= stt
process_tick()
}
}
// mix an audio buffer with data
override fun mix(bufs: Array?, buflen: Int) {
val outp = FloatArray(2)
// return a buffer of silence if not playing
if (paused || endofsong || !playing) {
if (bufs != null) {
bufs[0].fill(0f)
bufs[1].fill(0f)
}
chvu.fill(0f)
return
}
// fill audiobuffer
for (s in 0 until buflen) {
// if STT has run out, step player forward by tick
if (stt <= 0) process_tick()
// mix channels
//var count = 0
//var line = ""
//sampleCount++
//val doLog = sampleCount >= 5294;
if (bufs != null) {
outp[0] = 0f
outp[1] = 0f
for (ch in 0 until channels) {
var fl = 0.0
var fr = 0.0
var fs = 0.0
val channel = channel[ch]
val si = channel.sample
// add channel output to left/right master outputs
channel.currentsample = 0.0 // assume note is off
if (channel.noteon != 0 || (channel.noteon == 0 && channel.volramp < 1.0)) {
if (sample[si].length > channel.samplepos) {
fl = channel.lastsample
// interpolate towards current sample
var f = channel.samplepos - kotlin.math.floor(channel.samplepos)
fs = sample[si].data[kotlin.math.floor(channel.samplepos).toInt()].toDouble()
fl = f * fs + (1.0 - f) * fl
//if (doLog) {
// count++
// line += "${fl.niceStr},"
//}
//println(fl)
// smooth out discontinuities from retrig and sample offset
f = channel.trigramp
fl = f * fl + (1.0 - f) * channel.trigrampfrom
f += 1.0 / 128.0
channel.trigramp = kotlin.math.min(1.0, f)
channel.currentsample = fl
// ramp volume changes over 64 samples to avoid clicks
fr = fl * (channel.voicevolume.toDouble() / 64.0)
f = channel.volramp
fl = f * fr + (1.0 - f) * (fl * (channel.volrampfrom / 64.0))
f += (1.0 / 64.0)
channel.volramp = kotlin.math.min(1.0, f)
// pan samples
fr = fl * pan_r[ch]
fl *= pan_l[ch]
}
outp[0] = (outp[0] + fl).toFloat()
outp[1] = (outp[1] + fr).toFloat()
val oldpos = channel.samplepos
channel.samplepos += channel.samplespeed
if (kotlin.math.floor(channel.samplepos) > kotlin.math.floor(oldpos)) {
channel.lastsample = fs
}
// loop or stop sample?
val sample = sample[channel.sample]
when {
sample.loop != 0 -> {
if (channel.samplepos >= sample.loopend) {
channel.samplepos -= sample.looplength
channel.lastsample = channel.currentsample
}
}
channel.samplepos >= sample.length -> channel.noteon = 0
}
}
chvu[ch] = kotlin.math.max(chvu[ch].toDouble(), kotlin.math.abs(fl + fr)).toFloat()
//print("${chvu[ch].niceStr},")
}
//if (doLog) println("$sampleCount:$count:${line}channels=$channels")
// done - store to output buffer
val t = volume / 64.0
bufs[0][s] = (outp[0] * t).toFloat()
bufs[1][s] = (outp[1] * t).toFloat()
}
stt--
}
}
var sampleCount = 0
//
// tick 0 effect functions
//
fun effect_t0_a(ch: Int) { // set speed
val channel = channel[ch]
if (channel.data > 0) speed = channel.data
}
fun effect_t0_b(ch: Int) { // pattern jump
val channel = channel[ch]
breakrow = 0
patternjump = channel.data
flags = flags or 16
}
fun effect_t0_c(ch: Int) { // pattern break
val channel = channel[ch]
breakrow = ((channel.data and 0xf0) ushr 4) * 10 + (channel.data and 0x0f)
if ((flags and 16) == 0) patternjump = position + 1
flags = flags or 16
}
fun effect_t0_d(ch: Int) { // volume slide
val channel = channel[ch]
if (channel.data != 0) channel.volslide = channel.data
// DxF fine up
// DFx fine down
when {
(channel.volslide and 0x0f) == 0x0f -> channel.voicevolume += channel.volslide ushr 4
(channel.volslide ushr 4) == 0x0f -> channel.voicevolume -= channel.volslide and 0x0f
else -> if (fastslide != 0) effect_t1_d(ch)
}
if (channel.voicevolume < 0) channel.voicevolume = 0
if (channel.voicevolume > 64) channel.voicevolume = 64
}
fun effect_t0_e(ch: Int) { // slide down
val channel = channel[ch]
if (channel.data != 0) channel.slidespeed = channel.data
if ((channel.slidespeed and 0xf0) == 0xf0) channel.voiceperiod += (channel.slidespeed and 0x0f) shl 2
if ((channel.slidespeed and 0xf0) == 0xe0) channel.voiceperiod += (channel.slidespeed and 0x0f)
if (channel.voiceperiod > 27392) channel.noteon = 0
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t0_f(ch: Int) { // slide up
val channel = channel[ch]
if (channel.data != 0) channel.slidespeed = channel.data
if ((channel.slidespeed and 0xf0) == 0xf0) channel.voiceperiod -= (channel.slidespeed and 0x0f) shl 2
if ((channel.slidespeed and 0xf0) == 0xe0) channel.voiceperiod -= (channel.slidespeed and 0x0f)
if (channel.voiceperiod < 56) channel.noteon = 0
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t0_g(ch: Int) { // slide to note
val channel = channel[ch]
// if (channel.data) channel.slidetospeed=channel.data;
if (channel.data != 0) channel.slidespeed = channel.data
}
fun effect_t0_h(ch: Int) { // vibrato
val channel = channel[ch]
if ((channel.data and 0x0f) != 0 && (channel.data and 0xf0) != 0) {
channel.vibratodepth = (channel.data and 0x0f)
channel.vibratospeed = (channel.data and 0xf0) ushr 4
}
}
fun effect_t0_i(ch: Int) { // tremor
}
fun effect_t0_j(ch: Int) { // arpeggio
val channel = channel[ch]
if (channel.data != 0) channel.arpeggio = channel.data
channel.voiceperiod = channel.period
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t0_k(ch: Int) { // vibrato + volslide
effect_t0_d(ch)
}
fun effect_t0_l(ch: Int) { // slide to note + volslide
effect_t0_d(ch)
}
fun effect_t0_m(ch: Int) { // -
}
fun effect_t0_n(ch: Int) { // -
}
fun effect_t0_o(ch: Int) { // set sample offset
val channel = channel[ch]
if (channel.data != 0) channel.lastoffset = channel.data
if (channel.lastoffset * 256 < sample[channel.sample].length) {
channel.samplepos = (channel.lastoffset * 256).toDouble()
channel.trigramp = 0.0
channel.trigrampfrom = channel.currentsample
}
}
fun effect_t0_p(ch: Int) { // -
}
fun effect_t0_q(ch: Int) { // retrig note
val channel = channel[ch]
if (channel.data != 0) channel.lastretrig = channel.data
effect_t1_q(ch) // to retrig also on lines with no note but Qxy command
}
fun effect_t0_r(ch: Int) { // tremolo
}
fun effect_t0_s(ch: Int) { // Sxy effects
val channel = channel[ch]
val i = (channel.data and 0xf0) ushr 4
effects_t0_s[i](ch)
}
fun effect_t0_t(ch: Int) { // set tempo
val channel = channel[ch]
if (channel.data > 32) bpm = channel.data
}
fun effect_t0_u(ch: Int) { // fine vibrato
}
fun effect_t0_v(ch: Int) { // set global volume
val channel = channel[ch]
volume = channel.data
}
fun effect_t0_w(ch: Int) { // -
}
fun effect_t0_x(ch: Int) { // -
}
fun effect_t0_y(ch: Int) { // -
}
fun effect_t0_z(ch: Int) { // sync for FMOD (was: unused)
val channel = channel[ch]
syncqueue.addFirst(channel.data and 0x0f)
}
//
// tick 0 special Sxy effect functions
//
fun effect_t0_s0(ch: Int) { // set filter (not implemented)
}
fun effect_t0_s1(ch: Int) { // set glissando control
}
fun effect_t0_s2(ch: Int) { // sync for BASS (was: set finetune)
val channel = channel[ch]
syncqueue.addFirst(channel.data and 0x0f)
}
fun effect_t0_s3(ch: Int) { // set vibrato waveform
val channel = channel[ch]
channel.vibratowave = channel.data and 0x07
}
fun effect_t0_s4(ch: Int) { // set tremolo waveform
}
fun effect_t0_s5(ch: Int) { // -
}
fun effect_t0_s6(ch: Int) { // -
}
fun effect_t0_s7(ch: Int) { // -
}
fun effect_t0_s8(ch: Int) { // set panning position
val channel = channel[ch]
pan_r[ch] = ((channel.data and 0x0f) / 15.0).toFloat()
pan_l[ch] = (1.0 - pan_r[ch]).toFloat()
}
fun effect_t0_s9(ch: Int) { // -
}
fun effect_t0_sa(ch: Int) { // old stereo control (not implemented)
}
fun effect_t0_sb(ch: Int) { // loop pattern
val channel = channel[ch]
when {
(channel.data and 0x0f) != 0 -> {
when {
loopcount != 0 -> loopcount--
else -> loopcount = channel.data and 0x0f
}
if (loopcount != 0) {
flags = flags or 64
}
}
else -> {
looprow = row
}
}
}
fun effect_t0_sc(ch: Int) { // note cut
}
fun effect_t0_sd(ch: Int) { // note delay
val channel = channel[ch]
if (tick == (channel.data and 0x0f)) {
process_note(patterntable[position], ch)
}
}
fun effect_t0_se(ch: Int) { // pattern delay
val channel = channel[ch]
patterndelay = channel.data and 0x0f
patternwait = 0
}
fun effect_t0_sf(ch: Int) { // funkrepeat (not implemented)
}
//
// tick 1+ effect functions
//
fun effect_t1_a(ch: Int) { // set speed
}
fun effect_t1_b(ch: Int) { // order jump
}
fun effect_t1_c(ch: Int) { // jump to row
}
fun effect_t1_d(ch: Int) { // volume slide
val channel = channel[ch]
if ((channel.volslide and 0x0f) == 0) {
// slide up
channel.voicevolume += channel.volslide ushr 4
} else if ((channel.volslide ushr 4) == 0) {
// slide down
channel.voicevolume -= channel.volslide and 0x0f
}
channel.voicevolume = channel.voicevolume.clamp(0, 64)
}
fun effect_t1_e(ch: Int) { // slide down
val channel = channel[ch]
if (channel.slidespeed < 0xe0) channel.voiceperiod += channel.slidespeed * 4
if (channel.voiceperiod > 27392) channel.noteon = 0
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t1_f(ch: Int) { // slide up
val channel = channel[ch]
if (channel.slidespeed < 0xe0) channel.voiceperiod -= channel.slidespeed * 4
if (channel.voiceperiod < 56) channel.noteon = 0
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t1_g(ch: Int) { // slide to note
val channel = channel[ch]
if (channel.voiceperiod < channel.slideto) {
//channelvoiceperiod+=4*channel.slidetospeed;
channel.voiceperiod += 4 * channel.slidespeed
if (channel.voiceperiod > channel.slideto) channel.voiceperiod = channel.slideto
} else if (channel.voiceperiod > channel.slideto) {
//channel.voiceperiod-=4*channel.slidetospeed;
channel.voiceperiod -= 4 * channel.slidespeed
if (channel.voiceperiod < channel.slideto) channel.voiceperiod = channel.slideto
}
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t1_h(ch: Int) { // vibrato
val channel = channel[ch]
channel.voiceperiod = (channel.voiceperiod + vibratotable[channel.vibratowave and 3][channel.vibratopos] * channel.vibratodepth / 128.0).clamp(56.0, 27392.0)
channel.flags = channel.flags or 1
}
fun effect_t1_i(ch: Int) { // tremor
}
fun effect_t1_j(ch: Int) { // arpeggio
val channel = channel[ch]
var n = channel.note
if ((tick and 3) == 1) n += channel.arpeggio ushr 4
if ((tick and 3) == 2) n += channel.arpeggio and 0x0f
channel.voiceperiod = (8363.0 * periodtable[n]) / sample[channel.sample].c2spd.toDouble()
channel.flags = channel.flags or 3 // recalc speed
}
fun effect_t1_k(ch: Int) { // vibrato + volslide
effect_t1_h(ch)
effect_t1_d(ch)
}
fun effect_t1_l(ch: Int) { // slide to note + volslide
effect_t1_g(ch)
effect_t1_d(ch)
}
fun effect_t1_m(ch: Int) { // -
}
fun effect_t1_n(ch: Int) { // -
}
fun effect_t1_o(ch: Int) { // set sample offset
}
fun effect_t1_p(ch: Int) { // -
}
fun effect_t1_q(ch: Int) { // retrig note
val channel = channel[ch]
if ((tick % (channel.lastretrig and 0x0f)) == 0) {
channel.samplepos = 0.0
channel.trigramp = 0.0
channel.trigrampfrom = channel.currentsample
val v = channel.lastretrig ushr 4
if ((v and 7) >= 6) {
channel.voicevolume = kotlin.math.floor(channel.voicevolume * retrigvoltab[v]).toInt()
} else {
channel.voicevolume += retrigvoltab[v].toInt()
}
channel.voicevolume = channel.voicevolume.clamp(0, 64)
}
}
fun effect_t1_r(ch: Int) { // tremolo
}
fun effect_t1_s(ch: Int) { // special effects
val channel = channel[ch]
val i = (channel.data and 0xf0) ushr 4
effects_t1_s[i](ch)
}
fun effect_t1_t(ch: Int) { // set tempo
}
fun effect_t1_u(ch: Int) { // fine vibrato
}
fun effect_t1_v(ch: Int) { // set global volume
}
fun effect_t1_w(ch: Int) { // -
}
fun effect_t1_x(ch: Int) { // -
}
fun effect_t1_y(ch: Int) { // -
}
fun effect_t1_z(ch: Int) { // -
}
//
// tick 1+ special Sxy effect functions
//
fun effect_t1_s0(ch: Int) { // set filter (not implemented)
}
fun effect_t1_s1(ch: Int) { // set glissando control
}
fun effect_t1_s2(ch: Int) { // set finetune
}
fun effect_t1_s3(ch: Int) { // set vibrato waveform
}
fun effect_t1_s4(ch: Int) { // set tremolo waveform
}
fun effect_t1_s5(ch: Int) { // -
}
fun effect_t1_s6(ch: Int) { // -
}
fun effect_t1_s7(ch: Int) { // -
}
fun effect_t1_s8(ch: Int) { // set panning position
}
fun effect_t1_s9(ch: Int) { // -
}
fun effect_t1_sa(ch: Int) { // old stereo control (not implemented)
}
fun effect_t1_sb(ch: Int) { // loop pattern
}
fun effect_t1_sc(ch: Int) { // note cut
val channel = channel[ch]
if (tick == (channel.data and 0x0f)) {
channel.volume = 0
channel.voicevolume = 0
}
}
fun effect_t1_sd(ch: Int) { // note delay
effect_t0_sd(ch)
}
fun effect_t1_se(ch: Int) { // pattern delay
}
fun effect_t1_sf(ch: Int) { // funkrepeat (not implemented)
}
}