8-bit THX Deep Note sound generation with Scala
import java.util.concurrent.ThreadLocalRandom | |
import javax.sound.sampled._ | |
// Program to generate THX Deep Note sound according to the sheet music | |
// published to celebrate its 35th birthday | |
// https://twitter.com/THX/status/1000077588415447040 | |
object THX extends App { | |
val sampleRate = 44100f | |
val af = new AudioFormat( | |
sampleRate, | |
8, // sample size in bits | |
1, // channels | |
true, // signed | |
false // bigendian | |
); | |
val line = AudioSystem.getSourceDataLine(af) | |
line.open(af, sampleRate.toInt) | |
val detuneFactor = 1.005f | |
val r = ThreadLocalRandom.current | |
// https://www.johndcook.com/blog/2018/06/12/mathematics-of-deep-note/ | |
val freqs = Array[Float](36, 72, 144, 288, 576, 1152, 108, 216, 432, 864, 1458).flatMap { f => | |
def detune = math.pow(detuneFactor, r.nextInt(-1, 1)).toFloat | |
val num = if (f < 600 || f > 1200) 2 else 3 | |
Array.fill(num)(f * detune) | |
} | |
val convergeStartSecond = 4 | |
val convergeEndSecond = 6 | |
val convergeStart = convergeStartSecond * sampleRate | |
val convergeSamples = (convergeEndSecond - convergeStartSecond) * sampleRate | |
val convergeEnd = convergeStart + convergeSamples | |
val startFreqs = Array.fill(freqs.size)(r.nextInt(200, 400).toFloat) | |
val freqFreqs = Array.fill(freqs.size)(r.nextInt(1, 8).toFloat / 6 / convergeStartSecond / sampleRate) | |
val freqPhases = Array.fill(freqs.size)(r.nextFloat) | |
val periods = freqs.map(freq => (1f / freq, 0)) | |
//val period = 1f / freq | |
def phaseAt(period: Float, phaseOffset: Float, frame: Int): Float = { | |
val time = frame.toFloat / sampleRate | |
val p = time / period | |
val phase = (p - p.toInt) + phaseOffset | |
phase | |
} | |
def amplAt(phase: Float): Byte = { | |
//val ampl0: Double = math.sin(2 * math.Pi * phase) * 127 | |
val ampl0 = if (phase < 0.5) 127 else -127 | |
//val ampl0 = (phase * 2 - 1) * 127 | |
//val ampl1 = (ampl0 * (1 + r.nextFloat * 0.1)) | |
//val ampl2 = math.max(-127, math.min(127, ampl1)) | |
ampl0.toByte | |
} | |
def phaseAt(voice: Int, lastPhase: Float, frame: Int): Float = { | |
val freq = | |
if (frame < convergeStart) { | |
val ph = 2f * math.Pi * (freqFreqs(voice) * frame + freqPhases(voice)) | |
val freq = 300f + 100 * math.sin(ph).toFloat | |
//if (frame % 100 == 0) println(frame, freqFreqs(voice), ph, freq) | |
startFreqs(voice) = freq | |
freq | |
} | |
else if (frame >= convergeStart && frame < convergeEnd) { | |
val delta = freqs(voice).toFloat / startFreqs(voice) | |
// linear | |
// val curDelta = 1 + (delta - 1) * (frame - convergeStart) / convergeSamples | |
// exp | |
val curDelta = math.pow(delta, (frame - convergeStart) / convergeSamples).toFloat | |
startFreqs(voice) * curDelta | |
} | |
else freqs(voice) | |
(lastPhase + freq / sampleRate) % 1f | |
} | |
val phases = Array.fill(freqs.size)(0f) | |
def writeSampleAt(frame: Int): Unit = { | |
val sample: Byte = (0 until freqs.size).map { voice => | |
val newPhase = phaseAt(voice, phases(voice), frame) | |
phases(voice) = newPhase | |
amplAt(newPhase) / freqs.size | |
}.sum.toByte | |
line.write(Array[Byte](sample), 0, 1) | |
} | |
def writeSamples(at: Int, remaining: Int): Unit = | |
if (remaining > 0) { | |
writeSampleAt(at) | |
writeSamples(at + 1, remaining - 1) | |
} | |
println("Preparing...") | |
line.start() | |
writeSamples(0, (convergeEndSecond + 2) * sampleRate.toInt) | |
line.drain | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment