some reworks
This commit is contained in:
parent
e127450404
commit
a38f0b35d6
8
src/main/kotlin/music/Frac.kt
Normal file
8
src/main/kotlin/music/Frac.kt
Normal file
@ -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();
|
||||
}
|
@ -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);
|
||||
|
@ -3,37 +3,37 @@ package music
|
||||
import music.Params.SAMPLES
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
typealias Samples = Array<Float>
|
||||
typealias TimeSamples = Array<Float>
|
||||
|
||||
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 }
|
||||
|
||||
|
@ -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<Int>
|
||||
typealias SoundSamples = Array<Int>
|
||||
|
||||
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))
|
||||
|
||||
|
@ -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<Beat>()
|
||||
|
||||
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))
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
override fun stream(signal: Signal): Signal {
|
||||
return signal;
|
||||
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)
|
||||
}
|
||||
|
||||
return dBToSignal(db + outputGain);
|
||||
}
|
||||
}
|
@ -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<Int> = 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()
|
||||
|
@ -1,53 +1,40 @@
|
||||
package music.filter
|
||||
|
||||
import music.Signal
|
||||
import music.SoundSamples
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
interface Filter {
|
||||
fun stream(signal: Signal): Signal
|
||||
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)
|
||||
|
||||
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
|
||||
object DefaultEases {
|
||||
val IN = EaseFilter(32, 0)
|
||||
val OUT = EaseFilter(0, 32)
|
||||
val INOUT = EaseFilter(32, 32)
|
||||
}
|
||||
|
||||
class EaseFilter(val inDuration: Int, val outDuration: Int): Filter() {
|
||||
|
||||
override fun stream(soundSamples: SoundSamples): SoundSamples {
|
||||
val inlen = kotlin.math.min(inDuration, soundSamples.size - outDuration)
|
||||
val outlen = kotlin.math.min(outDuration, soundSamples.size - inlen)
|
||||
|
||||
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)
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)})
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user