DEV Community

Andrew (he/him)
Andrew (he/him)

Posted on

Creating audio from raw bits in Scala

I was curious recently if it was possible to create sound in pure Java / Scala, without using some third-party package, when I stumbled across this old code snippet on the Oracle forums which did just that.

With some cleanup and a few small bug fixes, I was able to get it working nicely in Scala.

The core of the example is this Note class

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 } } 
Enter fullscreen mode Exit fullscreen mode

...where, for a given frequency and duration in msecs, we literally build a tone bit-by-bit, including fading the tone in and out to avoid "crackly" discontinuities at the start and end of the tone.

Creating a Tune out of multiple Notes is then pretty straightforward

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 } } 
Enter fullscreen mode Exit fullscreen mode

Add the Notes to a buffer one at a time, then when you want to play the tune, simply copy the buffer to the SourceDataLine and drain the line's buffer.

I wrote a simple tune to test this... can you tell what it is without playing it?

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() } 
Enter fullscreen mode Exit fullscreen mode

P.S. if anyone has any ideas for eliminating the "crackling" at the end of the tune, please let me know! Fading out doesn't seem to help, nor does trimming the end of the buffer. Even when only playing a bit of silence, there's still some crackling at the end.

Top comments (2)

Collapse
 
rrampage profile image
Raunak Ramakrishnan • Edited

Awesome stuff! I made a similar thing in Ruby while following this video in Haskell which derives a lot of musical stuff from first principles. Re the crackling, have you tried saving it as array of floats and playing it in ffplay?


# Create an array of floats of required frequency, duration, sample rate and volume def wave(freq = 440.0, duration = 2.0, sampleRate = 48000, vol = 0.2) (0..sampleRate*duration).step(1).map { |w| Math.sin(w * freq * 2 * Math::PI / sampleRate) * vol} end # Get nth semitone def f(n) return 440.0 * (2 ** (1.0/12)) ** n end # Play nth semitone def note(n, duration = 1.0, sampleRate = 48000, vol = 0.2) return wave( f(n), duration, sampleRate, vol) end # Pack as 32-bit floats to file https://ruby-doc.org/core-2.6.4/Array.html#method-i-pack def save(fname, sound) File.write(fname, sound.pack("F*"), mode: "wb") end # Uses ffplay (installed as part of ffmpeg) def play(fname, sampleRate = 48000) cmd = "ffplay -autoexit -showmode 1 -f f32le -ar #{sampleRate} #{fname}" puts cmd puts `#{cmd}` end x = 'sound.bin' d = 0.3 w = note(0, d) + note(2, d) + note(4, d) + note(5,d) + note(7,d) + note(9, d) + note(11, d) + note(12, d) + note(0, 0.1, 48000, 0.01) save(x, w) play(x) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
awwsmm profile image
Andrew (he/him)

I haven't tried playing it in ffplay... I'll give that a shot. Thanks, Raunak!