diff --git a/src/main/kotlin/music/Filter/Filter.kt b/src/main/kotlin/music/Filter/Filter.kt deleted file mode 100644 index 3ac22b9..0000000 --- a/src/main/kotlin/music/Filter/Filter.kt +++ /dev/null @@ -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(); - } -} diff --git a/src/main/kotlin/music/Main.kt b/src/main/kotlin/music/Main.kt index 19da707..4ea35d3 100644 --- a/src/main/kotlin/music/Main.kt +++ b/src/main/kotlin/music/Main.kt @@ -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) } diff --git a/src/main/kotlin/music/Melody.kt b/src/main/kotlin/music/Melody.kt new file mode 100644 index 0000000..8c79cd1 --- /dev/null +++ b/src/main/kotlin/music/Melody.kt @@ -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() + 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)!! + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/music/Samples.kt b/src/main/kotlin/music/Samples.kt index 0287689..2d42620 100644 --- a/src/main/kotlin/music/Samples.kt +++ b/src/main/kotlin/music/Samples.kt @@ -1,11 +1,14 @@ package music import music.Params.SAMPLES +import kotlin.math.absoluteValue typealias Samples = Array -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 } diff --git a/src/main/kotlin/music/Signal.kt b/src/main/kotlin/music/Signal.kt index 8a00c04..bc0bf6d 100644 --- a/src/main/kotlin/music/Signal.kt +++ b/src/main/kotlin/music/Signal.kt @@ -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 -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) diff --git a/src/main/kotlin/music/Track.kt b/src/main/kotlin/music/Track.kt index af548c8..738e1f5 100644 --- a/src/main/kotlin/music/Track.kt +++ b/src/main/kotlin/music/Track.kt @@ -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() + + 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)) } \ No newline at end of file diff --git a/src/main/kotlin/music/filter/Compressor.kt b/src/main/kotlin/music/filter/Compressor.kt new file mode 100644 index 0000000..76b4e1d --- /dev/null +++ b/src/main/kotlin/music/filter/Compressor.kt @@ -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; + } +} \ No newline at end of file diff --git a/src/main/kotlin/music/filter/EchoFilter.kt b/src/main/kotlin/music/filter/EchoFilter.kt new file mode 100644 index 0000000..38ea1ec --- /dev/null +++ b/src/main/kotlin/music/filter/EchoFilter.kt @@ -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 = 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; + } + +} \ No newline at end of file diff --git a/src/main/kotlin/music/filter/Filter.kt b/src/main/kotlin/music/filter/Filter.kt new file mode 100644 index 0000000..c616406 --- /dev/null +++ b/src/main/kotlin/music/filter/Filter.kt @@ -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 +} + diff --git a/src/main/kotlin/music/filter/FilterQueue.kt b/src/main/kotlin/music/filter/FilterQueue.kt new file mode 100644 index 0000000..a9bf951 --- /dev/null +++ b/src/main/kotlin/music/filter/FilterQueue.kt @@ -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)}) +} \ No newline at end of file diff --git a/src/main/kotlin/music/filter/Limiter.kt b/src/main/kotlin/music/filter/Limiter.kt new file mode 100644 index 0000000..f1246de --- /dev/null +++ b/src/main/kotlin/music/filter/Limiter.kt @@ -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) } +} \ No newline at end of file diff --git a/src/main/kotlin/music/generators/Drum.kt b/src/main/kotlin/music/generators/Drum.kt new file mode 100644 index 0000000..65cc43e --- /dev/null +++ b/src/main/kotlin/music/generators/Drum.kt @@ -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; + } +} \ No newline at end of file diff --git a/src/main/kotlin/music/generators/Generator.kt b/src/main/kotlin/music/generators/Generator.kt index 6720569..b42637b 100644 --- a/src/main/kotlin/music/generators/Generator.kt +++ b/src/main/kotlin/music/generators/Generator.kt @@ -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 } + diff --git a/src/main/kotlin/music/generators/ToneGenerator.kt b/src/main/kotlin/music/generators/ToneGenerator.kt index c919d43..c5b5184 100644 --- a/src/main/kotlin/music/generators/ToneGenerator.kt +++ b/src/main/kotlin/music/generators/ToneGenerator.kt @@ -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 = @@ -19,4 +27,9 @@ open class ToneGenerator(volume: Float, private val tone: Int, waveform: Wavefor companion object { 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) } \ No newline at end of file diff --git a/src/test/kotlin/music/filter/EaseFilterTest.kt b/src/test/kotlin/music/filter/EaseFilterTest.kt new file mode 100644 index 0000000..83c8837 --- /dev/null +++ b/src/test/kotlin/music/filter/EaseFilterTest.kt @@ -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") + } + + + } +} \ No newline at end of file