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
import music.Filter.Filters
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.AudioSystem
import javax.sound.sampled.DataLine
@ -11,6 +13,7 @@ import javax.sound.sampled.SourceDataLine
object Params {
const val SAMPLES = 44100
const val PI = kotlin.math.PI.toFloat()
const val MAX_VAL = 32767 // 2**15 - 1
val CORES = Runtime.getRuntime().availableProcessors()
}
@ -23,9 +26,115 @@ fun main() {
line.open(format)
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
import music.Params.SAMPLES
import kotlin.math.absoluteValue
typealias Samples = Array<Float>
fun Samples(len: Int, start: Int = 0): Samples =
Samples(len * SAMPLES) { i -> start.toFloat() + (i / SAMPLES.toFloat())}
fun Samples(len: Float, start: Float = 0f): Samples =
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 =
Array(size) { mapper(this[it]) }
@ -17,7 +20,10 @@ fun Samples.sin(): Samples =
map { kotlin.math.sin(it) }
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 =
if (scalar == 1f) this else map { it * scalar }

@ -1,31 +1,58 @@
package music
import music.Params.MAX_VAL
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
*/
typealias Signal = Array<Int>
val MAX_VAL = 2f.pow(15) - 1
fun Signal.toBytes(): ByteArray =
Array(size * 2) { i -> (this[i/2] shr ((i % 2) * 8)).toByte() }.toByteArray()
fun Signal(samples: Samples): Signal =
Signal(samples.size) {i -> (samples[i] * MAX_VAL).toInt() }
object WaveformTransformations {
fun sine(samples: Samples, volume: Float): Signal
= Signal(((samples * (2f * PI)).sin()) * volume)
enum class WaveformTransformation {
SINE {
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
= Signal((((samples * (2f * PI)).sin() / 1f).ceil() * 2f + 1f) * volume)
fun Signal.map(mapper: (Int) -> (Int)): Signal =
Array(size) { mapper(this[it]) }
fun sawtooth(samples: Samples, volume: Float): Signal
= Signal(((samples + 1f).mod(2) - 1f) * volume)
}
operator fun Signal.times(scalar: Float): Signal =
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
import music.Params.SAMPLES
import music.filter.EaseFilter
import music.filter.FilterQueue
import music.generators.Generator
import java.util.logging.Filter
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.WaveformTransformation
abstract class Generator(private val volume: Float, private val waveform: WaveformTransformation) {
fun get(samples: Samples): Signal {
return waveform(generate(samples), volume)
}
protected abstract fun generate(samples: Samples): Samples
interface Generator {
fun get(samples: Samples): Signal
}

@ -3,11 +3,19 @@ package music.generators
import music.Samples
import music.Signal
import music.WaveformTransformation
import music.filter.EaseFilter
import music.times
import kotlin.math.pow
import kotlin.math.sign
open class ToneGenerator(volume: Float, private val tone: Int, waveform: WaveformTransformation) : Generator(volume, waveform) {
override fun generate(samples: Samples): Samples =
open class ToneGenerator(private val volume: Float, private val tone: Int, private val waveform: WaveformTransformation) : Generator {
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)
protected fun getFreqOfTone(tone: Float): Float =
@ -20,3 +28,8 @@ open class ToneGenerator(volume: Float, private val tone: Int, waveform: Wavefor
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