Last active
November 6, 2021 12:23
-
-
Save awwsmm/10474fb4dfde823bb53dc246a2f80f3d to your computer and use it in GitHub Desktop.
Create audio from raw bits in Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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