Skip to content

Instantly share code, notes, and snippets.

@jrudolph
Created September 8, 2018 11:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrudolph/9683c6c4e0d9c90230d35460e302bca7 to your computer and use it in GitHub Desktop.
Save jrudolph/9683c6c4e0d9c90230d35460e302bca7 to your computer and use it in GitHub Desktop.
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