added tetris theme

master
Anton Lydike 4 years ago
parent e02878f0d7
commit e127450404

@ -1,38 +0,0 @@
package music.Filter
import music.Params.SAMPLES
import music.Signal
typealias Filter = (Signal) -> (Signal)
object Filters {
fun easeIn(signal: Signal): Signal {
val len = kotlin.math.min(SAMPLES / 64, signal.size)
val start = signal.slice(0 until len)
val end = signal.slice(len until signal.size)
return (start.withIndex().map { i -> (i.value * (i.index / len.toFloat())).toInt() } + end).toTypedArray();
}
fun easeOut(signal: Signal): Signal {
val len = kotlin.math.min(SAMPLES / 64, signal.size)
val sep = signal.size - len - 1;
val start = signal.slice(0 until sep)
val end = signal.slice(sep until signal.size)
return (start + end.withIndex().map { i -> (i.value * ((len - i.index) / len.toFloat())).toInt() }).toTypedArray();
}
fun ease(signal: Signal): Signal {
val len = kotlin.math.min(SAMPLES / 64, signal.size / 2)
val sep = signal.size - len - 1;
val start = signal.slice(0 until len)
val mid = signal.slice(len until sep)
val end = signal.slice(sep until signal.size)
return (start.withIndex().map { i -> (i.value * (i.index / len.toFloat())).toInt() }
+ mid
+ end.withIndex().map { i -> (i.value * ((len - i.index) / len.toFloat())).toInt() }
).toTypedArray();
}
}

@ -1,8 +1,10 @@
package music package music
import music.Filter.Filters
import music.Params.SAMPLES import music.Params.SAMPLES
import music.generators.ToneGenerator import music.filter.EchoFilter
import music.filter.FilterQueue
import music.filter.Limiter
import music.generators.EasedToneGenerator
import javax.sound.sampled.AudioFormat import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioSystem import javax.sound.sampled.AudioSystem
import javax.sound.sampled.DataLine import javax.sound.sampled.DataLine
@ -11,6 +13,7 @@ import javax.sound.sampled.SourceDataLine
object Params { object Params {
const val SAMPLES = 44100 const val SAMPLES = 44100
const val PI = kotlin.math.PI.toFloat() const val PI = kotlin.math.PI.toFloat()
const val MAX_VAL = 32767 // 2**15 - 1
val CORES = Runtime.getRuntime().availableProcessors() val CORES = Runtime.getRuntime().availableProcessors()
} }
@ -23,9 +26,115 @@ fun main() {
line.open(format) line.open(format)
line.start() line.start()
val gen = ToneGenerator(.2f, -24, WaveformTransformations::sawtooth) //val gen = ToneGenerator(.2f, -24, WaveformTransformation.SAWTOOTH)
val bpm = 60;
val volume = 0.1f;
val tetrisSound = { volume: Float, tone: Int -> EasedToneGenerator(volume, tone, WaveformTransformation.SQUARE) }
// C C# D D# E F F# G G# A A# B C
// C Db D Eb E F Gb G Ab A Bb B C
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// TETRIS
// base notes
val bar1 = Bar(8, bpm);
val rightHand = Melody(tetrisSound, volume/ 2, -1);
rightHand.add("E", 1f)
.add("E", 1f)
.add("A", 1f)
.add("A", 1f)
.add("Ab", 1f)
.add("E", 1f)
.add("A", 1.5f)
.applyTo(bar1)
val leftHand = Melody(tetrisSound, volume, 0)
leftHand.add("E", .5f)
.add(-1, .25f)
.add("C", .25f) // 1
.add("D", .5f)
.add("C", .25f)
.add(-1, .25f) // 2
.add(-3, .25)
.pause(.25)
.add(-3, .25)
.add("C", 0.25) // 3
.add("E", .5)
.add(2, 0.25)
.add(0, 0.25) // 4
.add(-1, 0.5)
.pause(0.25)
.add("C", 0.25) // 5
.add("D", 0.5)
.add("E",0.5) // 6
.add("C", 0.5)
.add(-3, 0.25)
.pause(0.25) // 7
.add(-3, 0.5)
.applyTo(bar1)
val bar2 = Bar(8, bpm);
rightHand.reset()
leftHand.reset()
rightHand.add("D", 1f)
.add("D", 1f)
.add("C", 1f)
.add("C", 1f)
.add("E", 1f)
.add("E", 1f)
.add("A", 2f)
.applyTo(bar2)
leftHand.pause(0.25)
.add("D", .5)
.add("F", .25) // 1
.add("A", .5)
.add("G", .25)
.add("F", .25) // 2
.add("E", 0.5)
.pause(.25)
.add("C", .25) // 3
.add("E", .5)
.add("D", .25)
.add("C", .25) // 4
.add(-1, .25)
.pause(.25)
.add(-1, .25)
.add("C",.25) // 5
.add("D", .5)
.add("E", .5) // 6
.add("C", .5)
.add("A", .25, offset = -1)
.pause(0.25) // 7
.add("A", 0.5, offset = -1)
.applyTo(bar2)
// D F A G F E
// C E D C B
// B C D E C A A
val echo = EchoFilter(SAMPLES / 12, .9f)
val limiter = Limiter(.9f);
val filters = FilterQueue(echo, limiter)
//println(output.size / SAMPLES.toFloat())
var output = filters.stream(bar1.getSignal())
line.write(output.toBytes(), 0, output.size * 2)
output = filters.stream(bar2.getSignal())
line.write(output.toBytes(), 0, output.size * 2)
val signal = Filters.ease(gen.get(Samples(2)))
line.write(signal.toBytes(), 0, signal.size * 2)
} }

@ -0,0 +1,71 @@
package music
import music.generators.Generator
import music.generators.ToneGenerator
class Melody(val getGen: (volume: Float, tone: Int) -> (Generator), val volume: Float, val offset: Int = 0) {
val tones = mutableSetOf<Beat>()
var pos = 0f
fun add(tone: String, len: Double, offset: Int? = null): Melody {
return add(tone, len.toFloat(), offset)
}
fun add(tone: Int, len: Double, offset: Int? = null): Melody {
return add(tone, len.toFloat(), offset)
}
fun add(tone: String, len: Float, offset: Int? = null): Melody {
return add(toneFromString(tone), len, offset)
}
fun add(tone: Int, len: Float, offset: Int? = null): Melody {
val ofs = offset ?: this.offset;
tones.add(Beat(getGen(volume, tone + (ofs * 12)), len, pos))
pos += len
return this
}
fun pause(len: Float): Melody {
pos += len
return this
}
fun pause(len: Double): Melody {
pos += len.toFloat()
return this
}
fun applyTo(bar: Bar) {
for (tone in tones) {
bar.tones.add(tone)
}
}
fun reset() {
pos = 0f
tones.removeAll { true }
}
companion object {
// C C# D D# E F F# G G# A A# B C
// C Db D Eb E F Gb G Ab A Bb B C
// 0 1 2 3 4 5 6 7 8 9 10 11 12
private val tones = mapOf(
"C" to 0,
"Db" to 1, "C#" to 1,
"D" to 2,
"D#" to 3, "Eb" to 3,
"E" to 4,
"F" to 5,
"F#" to 6, "Gb" to 6,
"G" to 7,
"G#" to 8, "Ab" to 8,
"A" to 9,
"A#" to 10, "Bb" to 10,
"B" to 11
)
fun toneFromString(tone: String, offset: Int = 0): Int {
return Companion.tones[tone]?.plus(offset * 12)!!
}
}
}

@ -1,11 +1,14 @@
package music package music
import music.Params.SAMPLES import music.Params.SAMPLES
import kotlin.math.absoluteValue
typealias Samples = Array<Float> typealias Samples = Array<Float>
fun Samples(len: Int, start: Int = 0): Samples = fun Samples(len: Float, start: Float = 0f): Samples =
Samples(len * SAMPLES) { i -> start.toFloat() + (i / SAMPLES.toFloat())} Samples((len * SAMPLES).toInt()) { i -> start + (i / SAMPLES.toFloat())}
fun Samples(len: Int, start: Float = 0f): Samples = Samples(len.toFloat(), start)
fun Samples.map(mapper: (Float) -> (Float)): Samples = fun Samples.map(mapper: (Float) -> (Float)): Samples =
Array(size) { mapper(this[it]) } Array(size) { mapper(this[it]) }
@ -17,7 +20,10 @@ fun Samples.sin(): Samples =
map { kotlin.math.sin(it) } map { kotlin.math.sin(it) }
fun Samples.mod(num: Int): Samples = fun Samples.mod(num: Int): Samples =
map { it % num } map { it % num }
fun Samples.abs(): Samples =
map { it.absoluteValue }
operator fun Samples.times(scalar: Float): Samples = operator fun Samples.times(scalar: Float): Samples =
if (scalar == 1f) this else map { it * scalar } if (scalar == 1f) this else map { it * scalar }

@ -1,31 +1,58 @@
package music package music
import music.Params.MAX_VAL
import music.Params.PI import music.Params.PI
import kotlin.math.pow
/** /**
* A Signal is string of Integer values which describe a curve, they go from - MAX_VAL to MAX_VAL * A Signal is string of Integer values which describe a curve, they go from - MAX_VAL to MAX_VAL
*/ */
typealias Signal = Array<Int> typealias Signal = Array<Int>
val MAX_VAL = 2f.pow(15) - 1
fun Signal.toBytes(): ByteArray = fun Signal.toBytes(): ByteArray =
Array(size * 2) { i -> (this[i/2] shr ((i % 2) * 8)).toByte() }.toByteArray() Array(size * 2) { i -> (this[i/2] shr ((i % 2) * 8)).toByte() }.toByteArray()
fun Signal(samples: Samples): Signal = fun Signal(samples: Samples): Signal =
Signal(samples.size) {i -> (samples[i] * MAX_VAL).toInt() } Signal(samples.size) {i -> (samples[i] * MAX_VAL).toInt() }
object WaveformTransformations { enum class WaveformTransformation {
fun sine(samples: Samples, volume: Float): Signal SINE {
= Signal(((samples * (2f * PI)).sin()) * volume) override fun apply(samples: Samples, volume: Float): Signal
= Signal(((samples * (2f * PI)).sin()) * volume)
},
SQUARE {
override fun apply(samples: Samples, volume: Float): Signal
= Signal(samples.times(2f).ceil().mod(2).times(2f).minus(1f) * (volume))
},
SAWTOOTH {
override fun apply(samples: Samples, volume: Float): Signal
= Signal(((samples * 2f).mod(2) - 1f) * volume)
},
TRIANGLE {
override fun apply(samples: Samples, volume: Float): Signal
= Signal(samples.times(4f).mod(4).minus(2f).abs().minus(1f) * (volume))
};
open fun apply(samples: Samples, volume: Float): Signal = Signal(samples * volume)
}
fun square(samples: Samples, volume: Float): Signal fun Signal.map(mapper: (Int) -> (Int)): Signal =
= Signal((((samples * (2f * PI)).sin() / 1f).ceil() * 2f + 1f) * volume) Array(size) { mapper(this[it]) }
fun sawtooth(samples: Samples, volume: Float): Signal operator fun Signal.times(scalar: Float): Signal =
= Signal(((samples + 1f).mod(2) - 1f) * volume) if (scalar == 1f) this else map { (it * scalar).toInt() }
}
operator fun Signal.plus(value: Int): Signal =
map { it + value }
operator fun Signal.minus(value: Int): Signal =
map { it - value }
operator fun Signal.div(value: Float): Signal =
map { (it / value).toInt() }
fun Signal.join(start: Int, signal: Signal): Signal
= (this.sliceArray(0 until start)
+ signal.withIndex().map { i -> this.elementAtOrElse(i.index + start) {0} + i.value }
+ this.sliceArray(start + signal.size until this.size))
typealias WaveformTransformation = (Samples, Float) -> (Signal)

@ -1,5 +1,35 @@
package music package music
import music.Params.SAMPLES
import music.filter.EaseFilter
import music.filter.FilterQueue
import music.generators.Generator
import java.util.logging.Filter
class Track { class Track {
}
class Bar(val beats: Int, val bpm: Int) {
val bps = bpm / 60f
val tones = mutableSetOf<Beat>()
fun getSignal():Signal {
var base = Signal((SAMPLES * beats / bps).toInt()) {0}
for (tone in tones) {
base = base.join((SAMPLES * tone.start / bps).toInt(), tone.getSignal(bps))
}
return base;
}
}
class Beat(private val generator: Generator, private val duration: Float, val start: Float) {
fun getSignal(bps: Float): Signal =
generator.get(Samples(duration / bps, start / bps))
} }

@ -0,0 +1,10 @@
package music.filter
import music.Signal
class Compressor(val threshold: Int, val ratio: Float, val outputGain: Float):Filter {
override fun stream(signal: Signal): Signal {
return signal;
}
}

@ -0,0 +1,24 @@
package music.filter
import music.Signal
import music.map
import music.div
import music.times
import java.util.*
import kotlin.math.absoluteValue
import kotlin.math.pow
import kotlin.math.sign
class EchoFilter(delay: Int, private val volume: Float = 0.5f): Filter {
private var cache: Queue<Int> = LinkedList(List(delay) {0})
override fun stream(signal: Signal): Signal =
signal.map(this::apply)
private fun apply(value: Int): Int {
val effect = value + cache.remove()
cache.add(effect.toFloat().absoluteValue.pow(volume).toInt() * effect.sign)
return effect;
}
}

@ -0,0 +1,53 @@
package music.filter
import music.Signal
interface Filter {
fun stream(signal: Signal): Signal
}
enum class EaseFilter(val duration: Int = 32): Filter {
IN {
override fun stream (signal: Signal): Signal {
val len = kotlin.math.min(duration, signal.size)
val start = signal.slice(0 until len)
val end = signal.slice(len until signal.size)
return (start.withIndex().map { i -> (i.value * (i.index / len.toFloat())).toInt() } + end).toTypedArray();
}
},
OUT {
override fun stream(signal: Signal): Signal {
val len = kotlin.math.min(duration, signal.size)
val sep = signal.size - len - 1;
val start = signal.slice(0 until sep)
val end = signal.slice(sep until signal.size)
return (start + end.withIndex().map { i -> (i.value * ((len - i.index) / len.toFloat())).toInt() }).toTypedArray();
}
},
INOUT {
override fun stream(signal: Signal): Signal {
val len = kotlin.math.min(duration, signal.size / 2)
val sep = signal.size - len - 1;
val start = signal.slice(0 until len)
val mid = signal.slice(len until sep)
val end = signal.slice(sep until signal.size)
return (start.withIndex().map { i -> (i.value * (i.index / len.toFloat())).toInt() }
+ mid
+ end.withIndex().map { i -> (i.value * ((len - i.index) / len.toFloat())).toInt() }
).toTypedArray();
}
};
override fun stream(signal: Signal): Signal = signal
}

@ -0,0 +1,8 @@
package music.filter
import music.Signal
class FilterQueue(private vararg val filters: Filter): Filter {
override fun stream(signal: Signal): Signal =
filters.fold(signal, {acc, filter -> filter.stream(acc)})
}

@ -0,0 +1,14 @@
package music.filter
import music.Params.MAX_VAL
import music.Signal
import music.map
import kotlin.math.max
import kotlin.math.min
class Limiter(val threshold: Float): Filter {
val maxVal = (MAX_VAL * threshold).toInt()
override fun stream(signal: Signal): Signal =
signal.map { max(min(it, maxVal), - maxVal) }
}

@ -0,0 +1,22 @@
package music.generators
import music.*
import music.Params.SAMPLES
import music.filter.EchoFilter
import music.filter.FilterQueue
import kotlin.math.max
class Drum(volume: Float, tone: Int = -36): Generator{
val kickLen = (SAMPLES * 0.08).toInt();
val filter = FilterQueue(EchoFilter(kickLen, .7f));
val gen1 = EasedToneGenerator(volume, tone, WaveformTransformation.SINE)
val gen2 = EasedToneGenerator(volume / 2, tone + 12, WaveformTransformation.SINE)
override fun get(samples: Samples): Signal {
val kick1 = gen1.get(samples.sliceArray(0 until kickLen))
val kick2 = gen2.get(samples.sliceArray(0 until (kickLen / 2)))
return filter.stream(kick1.join(0, kick2) + Signal(max(samples.size - kickLen, 0)) {0}) / 1.5f;
}
}

@ -4,12 +4,8 @@ import music.Samples
import music.Signal import music.Signal
import music.WaveformTransformation import music.WaveformTransformation
abstract class Generator(private val volume: Float, private val waveform: WaveformTransformation) { interface Generator {
fun get(samples: Samples): Signal
fun get(samples: Samples): Signal {
return waveform(generate(samples), volume)
}
protected abstract fun generate(samples: Samples): Samples
} }

@ -3,11 +3,19 @@ package music.generators
import music.Samples import music.Samples
import music.Signal import music.Signal
import music.WaveformTransformation import music.WaveformTransformation
import music.filter.EaseFilter
import music.times import music.times
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sign
open class ToneGenerator(volume: Float, private val tone: Int, waveform: WaveformTransformation) : Generator(volume, waveform) { open class ToneGenerator(private val volume: Float, private val tone: Int, private val waveform: WaveformTransformation) : Generator {
override fun generate(samples: Samples): Samples = override fun get(samples: Samples): Signal {
return applyEffects(waveform.apply(generate(samples), volume))
}
protected open fun applyEffects(signal: Signal): Signal = signal
protected open fun generate(samples: Samples): Samples =
samples * getFreqOfTone(tone) samples * getFreqOfTone(tone)
protected fun getFreqOfTone(tone: Float): Float = protected fun getFreqOfTone(tone: Float): Float =
@ -19,4 +27,9 @@ open class ToneGenerator(volume: Float, private val tone: Int, waveform: Wavefor
companion object { companion object {
private const val F0: Float = 440f; private const val F0: Float = 440f;
} }
}
class EasedToneGenerator(volume: Float, tone: Int, waveform: WaveformTransformation) : ToneGenerator(volume, tone, waveform) {
override fun applyEffects(signal: Signal): Signal =
EaseFilter.INOUT.stream(signal)
} }

@ -0,0 +1,25 @@
package music.filter
import music.Samples
import music.WaveformTransformation
import music.generators.ToneGenerator
import org.junit.Test
import kotlin.test.assertEquals
class EaseFilterTest {
@Test
fun testOutputLength() {
val gen = ToneGenerator(1f, 0, WaveformTransformation.SINE);
val samples = Samples(1)
val signal = gen.get(samples)
assertEquals(samples.size, signal.size, "Generated samples should have same size as input")
for (filter in EaseFilter.values()) {
assertEquals(samples.size, filter.stream(signal).size, "Filter $filter should not increase length")
}
}
}
Loading…
Cancel
Save