Skip to content

Instantly share code, notes, and snippets.

@awwsmm
Last active November 6, 2021 12:23
Show Gist options
  • Save awwsmm/10474fb4dfde823bb53dc246a2f80f3d to your computer and use it in GitHub Desktop.
Save awwsmm/10474fb4dfde823bb53dc246a2f80f3d to your computer and use it in GitHub Desktop.
Create audio from raw bits in Scala
package com.awwsmm
import javax.sound.sampled.{AudioFormat, AudioSystem, SourceDataLine}
// Inspired by https://community.oracle.com/tech/developers/discussion/1273219/example-code-to-generate-audio-tone
// and https://stackoverflow.com/questions/1932490/java-generating-sound/47916383
// still some "crackling" / "popping" at the end of the tune, but most of the way there
object Main extends App {
val G = 196.00 // Hz
val Eb = 155.56
val F = 174.61
val D = 146.83
val bpm = 108.0
val quarter = 1000.0 * 60.0 / bpm
val triplet = quarter / 3.0
val half = quarter * 2.0
val quarterRest = Note(0, quarter, 0)
val tripletG = Note(G, triplet)
val halfEb = Note(Eb, half)
val tripletF = Note(F, triplet)
val halfD = Note(D, half)
val bars12: List[Note] = List(quarterRest, tripletG, tripletG, tripletG, halfEb)
val bars34: List[Note] = List(quarterRest, tripletF, tripletF, tripletF, halfD, quarterRest)
val tune = Tune.empty
(bars12 ++ bars34).foreach(tune.addNote)
tune.play()
tune.close()
}
object Tune {
def empty: Tune = {
val sampleRate = 44100 // samples / sec
val audioFormat = new AudioFormat(sampleRate, 8, 1, true, false)
new Tune(sampleRate, audioFormat)
}
}
class Tune(val sampleRate: Int, audioFormat: AudioFormat) {
private[this] var sourceDataLine: Option[SourceDataLine] = None
private[this] var ready = false
private var bytes = Array[Byte]()
def start(): Unit = {
sourceDataLine = Some(AudioSystem.getSourceDataLine(audioFormat))
sourceDataLine.get.open(audioFormat)
sourceDataLine.get.flush() // this eliminates "crackling" / "popping" at the beginning of the tune
sourceDataLine.get.start()
ready = true
}
def addNote(note: Note): Unit = {
bytes ++= note.bytes(sampleRate)
}
def play(): Unit = {
if (!ready) start()
sourceDataLine.get.write(bytes, 0, bytes.length)
sourceDataLine.get.drain() // this causes the "crackling" / "popping" at the end of the tune
}
def close(): Unit = {
sourceDataLine.foreach(_.flush())
sourceDataLine.foreach(_.stop())
sourceDataLine.foreach(_.close())
ready = false
}
}
case class Note(frequency: Double, msecs: Double, volume: Double = 128.0, fade: Boolean = true) {
// this (mostly) eliminates "crackling" / "popping" at the beginning / end of each tone
def fadeVolume(sampleIndex: Int, nSamples: Int): Double = {
val fadedSamples = 0.1 * nSamples // 10% fade in/out
if (sampleIndex < fadedSamples) { // fade in
val x = sampleIndex / fadedSamples // [0, 1]
x * x * volume
} else if ((nSamples - sampleIndex) < fadedSamples) { // fade out
val x = (nSamples - sampleIndex) / fadedSamples // [0, 1]
x * x * volume
} else volume
}
val wavelength: Double = 2.0 * Math.PI * frequency
def bytes(sampleRate: Int): Array[Byte] = {
val nSamples = (msecs * sampleRate / 1000.0).toInt
(0 to nSamples).map({ sampleIndex =>
val angle = wavelength * sampleIndex / sampleRate
val fadedVolume = if (fade) fadeVolume(sampleIndex, nSamples) else volume
(Math.sin(angle) * fadedVolume).toByte
}).toArray
}
}
// notes
// https://stackoverflow.com/questions/9630324/popping-crackling-when-using-a-java-source-data-line-for-audio
// https://stackoverflow.com/questions/34579950/java-audio-crackling
// https://community.oracle.com/tech/developers/discussion/1273219/example-code-to-generate-audio-tone
// https://stackoverflow.com/questions/59888683/consistent-popping-sound-while-playing-audio-through-a-sourcedataline
// https://stackoverflow.com/questions/57207927/preventing-the-click-when-stopping-sourcedataline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment