diff --git a/src/main/kotlin/music/Frac.kt b/src/main/kotlin/music/Frac.kt new file mode 100644 index 0000000..cae209c --- /dev/null +++ b/src/main/kotlin/music/Frac.kt @@ -0,0 +1,8 @@ +package music + +class Frac(val upper: Int, val lower: Int) { + val numerator = upper; + val denominator = lower; + + fun value(): Double = upper / lower.toDouble(); +} \ No newline at end of file diff --git a/src/main/kotlin/music/Main.kt b/src/main/kotlin/music/Main.kt index 4ea35d3..87caa74 100644 --- a/src/main/kotlin/music/Main.kt +++ b/src/main/kotlin/music/Main.kt @@ -28,7 +28,7 @@ fun main() { //val gen = ToneGenerator(.2f, -24, WaveformTransformation.SAWTOOTH) - val bpm = 60; + val bpm = 90; val volume = 0.1f; val tetrisSound = { volume: Float, tone: Int -> EasedToneGenerator(volume, tone, WaveformTransformation.SQUARE) } @@ -116,10 +116,6 @@ fun main() { .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); diff --git a/src/main/kotlin/music/Samples.kt b/src/main/kotlin/music/Samples.kt index 2d42620..c00a315 100644 --- a/src/main/kotlin/music/Samples.kt +++ b/src/main/kotlin/music/Samples.kt @@ -3,37 +3,37 @@ package music import music.Params.SAMPLES import kotlin.math.absoluteValue -typealias Samples = Array +typealias TimeSamples = Array -fun Samples(len: Float, start: Float = 0f): Samples = - Samples((len * SAMPLES).toInt()) { i -> start + (i / SAMPLES.toFloat())} +fun Samples(len: Float, start: Float = 0f): TimeSamples = + TimeSamples((len * SAMPLES).toInt()) { i -> start + (i / SAMPLES.toFloat())} -fun Samples(len: Int, start: Float = 0f): Samples = Samples(len.toFloat(), start) +fun Samples(len: Int, start: Float = 0f): TimeSamples = Samples(len.toFloat(), start) -fun Samples.map(mapper: (Float) -> (Float)): Samples = +fun TimeSamples.map(mapper: (Float) -> (Float)): TimeSamples = Array(size) { mapper(this[it]) } -fun Samples.ceil(): Samples = +fun TimeSamples.ceil(): TimeSamples = map { kotlin.math.ceil(it) } -fun Samples.sin(): Samples = +fun TimeSamples.sin(): TimeSamples = map { kotlin.math.sin(it) } -fun Samples.mod(num: Int): Samples = +fun TimeSamples.mod(num: Int): TimeSamples = map { it % num } -fun Samples.abs(): Samples = +fun TimeSamples.abs(): TimeSamples = map { it.absoluteValue } -operator fun Samples.times(scalar: Float): Samples = +operator fun TimeSamples.times(scalar: Float): TimeSamples = if (scalar == 1f) this else map { it * scalar } -operator fun Samples.plus(value: Float): Samples = +operator fun TimeSamples.plus(value: Float): TimeSamples = map { it + value } -operator fun Samples.minus(value: Float): Samples = +operator fun TimeSamples.minus(value: Float): TimeSamples = map { it - value } -operator fun Samples.div(value: Float): Samples = +operator fun TimeSamples.div(value: Float): TimeSamples = map { it / value } diff --git a/src/main/kotlin/music/Signal.kt b/src/main/kotlin/music/Signal.kt index bc0bf6d..6b00941 100644 --- a/src/main/kotlin/music/Signal.kt +++ b/src/main/kotlin/music/Signal.kt @@ -6,53 +6,53 @@ import music.Params.PI /** * A Signal is string of Integer values which describe a curve, they go from - MAX_VAL to MAX_VAL */ -typealias Signal = Array +typealias SoundSamples = Array -fun Signal.toBytes(): ByteArray = +fun SoundSamples.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() } +fun SoundSamples(timeSamples: TimeSamples): SoundSamples = + SoundSamples(timeSamples.size) { i -> (timeSamples[i] * MAX_VAL).toInt() } enum class WaveformTransformation { SINE { - override fun apply(samples: Samples, volume: Float): Signal - = Signal(((samples * (2f * PI)).sin()) * volume) + override fun apply(timeSamples: TimeSamples, volume: Float): SoundSamples + = SoundSamples(((timeSamples * (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)) + override fun apply(timeSamples: TimeSamples, volume: Float): SoundSamples + = SoundSamples(timeSamples.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) + override fun apply(timeSamples: TimeSamples, volume: Float): SoundSamples + = SoundSamples(((timeSamples * 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)) + override fun apply(timeSamples: TimeSamples, volume: Float): SoundSamples + = SoundSamples(timeSamples.times(4f).mod(4).minus(2f).abs().minus(1f) * (volume)) }; - open fun apply(samples: Samples, volume: Float): Signal = Signal(samples * volume) + open fun apply(timeSamples: TimeSamples, volume: Float): SoundSamples = SoundSamples(timeSamples * volume) } -fun Signal.map(mapper: (Int) -> (Int)): Signal = +fun SoundSamples.map(mapper: (Int) -> (Int)): SoundSamples = Array(size) { mapper(this[it]) } -operator fun Signal.times(scalar: Float): Signal = +operator fun SoundSamples.times(scalar: Float): SoundSamples = if (scalar == 1f) this else map { (it * scalar).toInt() } -operator fun Signal.plus(value: Int): Signal = +operator fun SoundSamples.plus(value: Int): SoundSamples = map { it + value } -operator fun Signal.minus(value: Int): Signal = +operator fun SoundSamples.minus(value: Int): SoundSamples = map { it - value } -operator fun Signal.div(value: Float): Signal = +operator fun SoundSamples.div(value: Float): SoundSamples = map { (it / value).toInt() } -fun Signal.join(start: Int, signal: Signal): Signal +fun SoundSamples.join(start: Int, soundSamples: SoundSamples): SoundSamples = (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)) + + soundSamples.withIndex().map { i -> this.elementAtOrElse(i.index + start) {0} + i.value } + + this.sliceArray(start + soundSamples.size until this.size)) diff --git a/src/main/kotlin/music/Track.kt b/src/main/kotlin/music/Track.kt index 738e1f5..b294f23 100644 --- a/src/main/kotlin/music/Track.kt +++ b/src/main/kotlin/music/Track.kt @@ -1,10 +1,7 @@ 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 { @@ -19,8 +16,8 @@ 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} + fun getSignal():SoundSamples { + var base = SoundSamples((SAMPLES * beats / bps).toInt()) {0} for (tone in tones) { base = base.join((SAMPLES * tone.start / bps).toInt(), tone.getSignal(bps)) } @@ -30,6 +27,6 @@ class Bar(val beats: Int, val bpm: Int) { } class Beat(private val generator: Generator, private val duration: Float, val start: Float) { - fun getSignal(bps: Float): Signal = + fun getSignal(bps: Float): SoundSamples = 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 index 76b4e1d..a18df15 100644 --- a/src/main/kotlin/music/filter/Compressor.kt +++ b/src/main/kotlin/music/filter/Compressor.kt @@ -1,10 +1,29 @@ package music.filter -import music.Signal +import music.Frac +import music.SoundSamples +import music.map +import kotlin.math.log10 -class Compressor(val threshold: Int, val ratio: Float, val outputGain: Float):Filter { +/** + * @param threshold the threshold when the compressor starts in decibel + * @param ratio how much to compress when crossing the threshold. A ration of 6/1 means 1 decibel output for each 6 decibel over the threshold + * @param outputGain how much the signal should be amplified after compression + */ +class Compressor(private val threshold: Int, val ratio: Frac, val outputGain: Float):Filter() { + override fun stream(soundSamples: SoundSamples): SoundSamples { + return soundSamples.map(this::apply) + } + + private fun apply(sample: Int): Int { + val db = signalToDB(sample) + + if (db > threshold) { + val surplus = db - threshold; + val reduction = surplus - (surplus / ratio.value()) + return dBToSignal(db - reduction + outputGain) + } - override fun stream(signal: Signal): Signal { - return signal; + return dBToSignal(db + outputGain); } } \ No newline at end of file diff --git a/src/main/kotlin/music/filter/EchoFilter.kt b/src/main/kotlin/music/filter/EchoFilter.kt index 38ea1ec..6861b68 100644 --- a/src/main/kotlin/music/filter/EchoFilter.kt +++ b/src/main/kotlin/music/filter/EchoFilter.kt @@ -1,9 +1,7 @@ package music.filter -import music.Signal +import music.SoundSamples import music.map -import music.div -import music.times import java.util.* import kotlin.math.absoluteValue import kotlin.math.pow @@ -12,8 +10,8 @@ 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) + override fun stream(soundSamples: SoundSamples): SoundSamples = + soundSamples.map(this::apply) private fun apply(value: Int): Int { val effect = value + cache.remove() diff --git a/src/main/kotlin/music/filter/Filter.kt b/src/main/kotlin/music/filter/Filter.kt index c616406..886d939 100644 --- a/src/main/kotlin/music/filter/Filter.kt +++ b/src/main/kotlin/music/filter/Filter.kt @@ -1,53 +1,40 @@ package music.filter -import music.Signal - -interface Filter { - fun stream(signal: Signal): Signal -} - - +import music.SoundSamples +import kotlin.math.log10 +import kotlin.math.pow +abstract class Filter { + abstract fun stream(soundSamples: SoundSamples): SoundSamples + protected fun signalToDB(signal: Int): Double = + 20 * log10(signal.toDouble()) + protected fun dBToSignal(db: Double): Int = + 10.0.pow(db / 20).toInt() +} -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) +object DefaultEases { + val IN = EaseFilter(32, 0) + val OUT = EaseFilter(0, 32) + val INOUT = EaseFilter(32, 32) +} - return (start + end.withIndex().map { i -> (i.value * ((len - i.index) / len.toFloat())).toInt() }).toTypedArray(); - } - }, +class EaseFilter(val inDuration: Int, val outDuration: Int): Filter() { - 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) + override fun stream(soundSamples: SoundSamples): SoundSamples { + val inlen = kotlin.math.min(inDuration, soundSamples.size - outDuration) + val outlen = kotlin.math.min(outDuration, soundSamples.size - inlen) - 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(); - } - }; + val sep = soundSamples.size - outlen - 1; + val start = soundSamples.slice(0 until inlen) + val mid = soundSamples.slice(inlen until sep) + val end = soundSamples.slice(sep until soundSamples.size) - override fun stream(signal: Signal): Signal = signal + return (start.withIndex().map { i -> (i.value * (i.index / inlen.toFloat())).toInt() } + + mid + + end.withIndex().map { i -> (i.value * ((outlen - i.index) / outlen.toFloat())).toInt() } + ).toTypedArray(); + } } diff --git a/src/main/kotlin/music/filter/FilterQueue.kt b/src/main/kotlin/music/filter/FilterQueue.kt index a9bf951..1e6de61 100644 --- a/src/main/kotlin/music/filter/FilterQueue.kt +++ b/src/main/kotlin/music/filter/FilterQueue.kt @@ -1,8 +1,8 @@ package music.filter -import music.Signal +import music.SoundSamples class FilterQueue(private vararg val filters: Filter): Filter { - override fun stream(signal: Signal): Signal = - filters.fold(signal, {acc, filter -> filter.stream(acc)}) + override fun stream(soundSamples: SoundSamples): SoundSamples = + filters.fold(soundSamples, { 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 index f1246de..7df73ef 100644 --- a/src/main/kotlin/music/filter/Limiter.kt +++ b/src/main/kotlin/music/filter/Limiter.kt @@ -1,7 +1,7 @@ package music.filter import music.Params.MAX_VAL -import music.Signal +import music.SoundSamples import music.map import kotlin.math.max import kotlin.math.min @@ -9,6 +9,6 @@ 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) } + override fun stream(soundSamples: SoundSamples): SoundSamples = + soundSamples.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 index 65cc43e..e082713 100644 --- a/src/main/kotlin/music/generators/Drum.kt +++ b/src/main/kotlin/music/generators/Drum.kt @@ -13,10 +13,10 @@ class Drum(volume: Float, tone: Int = -36): Generator{ 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))) + override fun get(timeSamples: TimeSamples): SoundSamples { + val kick1 = gen1.get(timeSamples.sliceArray(0 until kickLen)) + val kick2 = gen2.get(timeSamples.sliceArray(0 until (kickLen / 2))) - return filter.stream(kick1.join(0, kick2) + Signal(max(samples.size - kickLen, 0)) {0}) / 1.5f; + return filter.stream(kick1.join(0, kick2) + SoundSamples(max(timeSamples.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 b42637b..b0fe6e2 100644 --- a/src/main/kotlin/music/generators/Generator.kt +++ b/src/main/kotlin/music/generators/Generator.kt @@ -1,11 +1,10 @@ package music.generators -import music.Samples -import music.Signal -import music.WaveformTransformation +import music.TimeSamples +import music.SoundSamples interface Generator { - fun get(samples: Samples): Signal + fun get(timeSamples: TimeSamples): SoundSamples } diff --git a/src/main/kotlin/music/generators/ToneGenerator.kt b/src/main/kotlin/music/generators/ToneGenerator.kt index c5b5184..1951e6b 100644 --- a/src/main/kotlin/music/generators/ToneGenerator.kt +++ b/src/main/kotlin/music/generators/ToneGenerator.kt @@ -1,22 +1,18 @@ package music.generators -import music.Samples -import music.Signal +import music.TimeSamples +import music.SoundSamples import music.WaveformTransformation import music.filter.EaseFilter import music.times import kotlin.math.pow -import kotlin.math.sign 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 + override fun get(timeSamples: TimeSamples): SoundSamples = + applyEffects(waveform.apply(timeSamples * getFreqOfTone(tone), volume)) - protected open fun generate(samples: Samples): Samples = - samples * getFreqOfTone(tone) + protected open fun applyEffects(soundSamples: SoundSamples): SoundSamples = soundSamples protected fun getFreqOfTone(tone: Float): Float = F0 * 2f.pow(tone / 12f) @@ -30,6 +26,6 @@ open class ToneGenerator(private val volume: Float, private val tone: Int, priva } class EasedToneGenerator(volume: Float, tone: Int, waveform: WaveformTransformation) : ToneGenerator(volume, tone, waveform) { - override fun applyEffects(signal: Signal): Signal = - EaseFilter.INOUT.stream(signal) + override fun applyEffects(soundSamples: SoundSamples): SoundSamples = + EaseFilter.INOUT.stream(soundSamples) } \ No newline at end of file