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

<span>case</span> <span>class</span> <span>Note</span><span>(</span><span>frequency</span><span>:</span> <span>Double</span><span>,</span> <span>msecs</span><span>:</span> <span>Double</span><span>,</span> <span>volume</span><span>:</span> <span>Double</span> <span>=</span> <span>128.0</span><span>,</span> <span>fade</span><span>:</span> <span>Boolean</span> <span>=</span> <span>true</span><span>)</span> <span>{</span>
<span>// this (mostly) eliminates "crackling" / "popping" at the beginning / end of each tone</span>
<span>def</span> <span>fadeVolume</span><span>(</span><span>sampleIndex</span><span>:</span> <span>Int</span><span>,</span> <span>nSamples</span><span>:</span> <span>Int</span><span>)</span><span>:</span> <span>Double</span> <span>=</span> <span>{</span>
<span>val</span> <span>fadedSamples</span> <span>=</span> <span>0.1</span> <span>*</span> <span>nSamples</span> <span>// 10% fade in/out</span>
<span>if</span> <span>(</span><span>sampleIndex</span> <span><</span> <span>fadedSamples</span><span>)</span> <span>{</span> <span>// fade in</span>
<span>val</span> <span>x</span> <span>=</span> <span>sampleIndex</span> <span>/</span> <span>fadedSamples</span> <span>// [0, 1]</span>
<span>x</span> <span>*</span> <span>x</span> <span>*</span> <span>volume</span>
<span>}</span> <span>else</span> <span>if</span> <span>((</span><span>nSamples</span> <span>-</span> <span>sampleIndex</span><span>)</span> <span><</span> <span>fadedSamples</span><span>)</span> <span>{</span> <span>// fade out</span>
<span>val</span> <span>x</span> <span>=</span> <span>(</span><span>nSamples</span> <span>-</span> <span>sampleIndex</span><span>)</span> <span>/</span> <span>fadedSamples</span> <span>// [0, 1]</span>
<span>x</span> <span>*</span> <span>x</span> <span>*</span> <span>volume</span>
<span>}</span> <span>else</span> <span>volume</span>
<span>}</span>
<span>val</span> <span>wavelength</span><span>:</span> <span>Double</span> <span>=</span> <span>2.0</span> <span>*</span> <span>Math</span><span>.</span><span>PI</span> <span>*</span> <span>frequency</span>
<span>def</span> <span>bytes</span><span>(</span><span>sampleRate</span><span>:</span> <span>Int</span><span>)</span><span>:</span> <span>Array</span><span>[</span><span>Byte</span><span>]</span> <span>=</span> <span>{</span>
<span>val</span> <span>nSamples</span> <span>=</span> <span>(</span><span>msecs</span> <span>*</span> <span>sampleRate</span> <span>/</span> <span>1000.0</span><span>).</span><span>toInt</span>
<span>(</span><span>0</span> <span>to</span> <span>nSamples</span><span>).</span><span>map</span><span>({</span> <span>sampleIndex</span> <span>=></span>
<span>val</span> <span>angle</span> <span>=</span> <span>wavelength</span> <span>*</span> <span>sampleIndex</span> <span>/</span> <span>sampleRate</span>
<span>val</span> <span>fadedVolume</span> <span>=</span> <span>if</span> <span>(</span><span>fade</span><span>)</span> <span>fadeVolume</span><span>(</span><span>sampleIndex</span><span>,</span> <span>nSamples</span><span>)</span> <span>else</span> <span>volume</span>
<span>(</span><span>Math</span><span>.</span><span>sin</span><span>(</span><span>angle</span><span>)</span> <span>*</span> <span>fadedVolume</span><span>).</span><span>toByte</span>
<span>}).</span><span>toArray</span>
<span>}</span>
<span>}</span>
<span>case</span> <span>class</span> <span>Note</span><span>(</span><span>frequency</span><span>:</span> <span>Double</span><span>,</span> <span>msecs</span><span>:</span> <span>Double</span><span>,</span> <span>volume</span><span>:</span> <span>Double</span> <span>=</span> <span>128.0</span><span>,</span> <span>fade</span><span>:</span> <span>Boolean</span> <span>=</span> <span>true</span><span>)</span> <span>{</span>

  <span>// this (mostly) eliminates "crackling" / "popping" at the beginning / end of each tone</span>
  <span>def</span> <span>fadeVolume</span><span>(</span><span>sampleIndex</span><span>:</span> <span>Int</span><span>,</span> <span>nSamples</span><span>:</span> <span>Int</span><span>)</span><span>:</span> <span>Double</span> <span>=</span> <span>{</span>
    <span>val</span> <span>fadedSamples</span> <span>=</span> <span>0.1</span> <span>*</span> <span>nSamples</span> <span>// 10% fade in/out</span>

    <span>if</span> <span>(</span><span>sampleIndex</span> <span><</span> <span>fadedSamples</span><span>)</span> <span>{</span> <span>// fade in</span>
      <span>val</span> <span>x</span> <span>=</span> <span>sampleIndex</span> <span>/</span> <span>fadedSamples</span> <span>// [0, 1]</span>
      <span>x</span> <span>*</span> <span>x</span> <span>*</span> <span>volume</span>
    <span>}</span> <span>else</span> <span>if</span> <span>((</span><span>nSamples</span> <span>-</span> <span>sampleIndex</span><span>)</span> <span><</span> <span>fadedSamples</span><span>)</span> <span>{</span> <span>// fade out</span>
      <span>val</span> <span>x</span> <span>=</span> <span>(</span><span>nSamples</span> <span>-</span> <span>sampleIndex</span><span>)</span> <span>/</span> <span>fadedSamples</span> <span>// [0, 1]</span>
      <span>x</span> <span>*</span> <span>x</span> <span>*</span> <span>volume</span>
    <span>}</span> <span>else</span> <span>volume</span>
  <span>}</span>

  <span>val</span> <span>wavelength</span><span>:</span> <span>Double</span> <span>=</span> <span>2.0</span> <span>*</span> <span>Math</span><span>.</span><span>PI</span> <span>*</span> <span>frequency</span>

  <span>def</span> <span>bytes</span><span>(</span><span>sampleRate</span><span>:</span> <span>Int</span><span>)</span><span>:</span> <span>Array</span><span>[</span><span>Byte</span><span>]</span> <span>=</span> <span>{</span>
    <span>val</span> <span>nSamples</span> <span>=</span> <span>(</span><span>msecs</span> <span>*</span> <span>sampleRate</span> <span>/</span> <span>1000.0</span><span>).</span><span>toInt</span>
    <span>(</span><span>0</span> <span>to</span> <span>nSamples</span><span>).</span><span>map</span><span>({</span> <span>sampleIndex</span> <span>=></span>
      <span>val</span> <span>angle</span> <span>=</span> <span>wavelength</span> <span>*</span> <span>sampleIndex</span> <span>/</span> <span>sampleRate</span>
      <span>val</span> <span>fadedVolume</span> <span>=</span> <span>if</span> <span>(</span><span>fade</span><span>)</span> <span>fadeVolume</span><span>(</span><span>sampleIndex</span><span>,</span> <span>nSamples</span><span>)</span> <span>else</span> <span>volume</span>
      <span>(</span><span>Math</span><span>.</span><span>sin</span><span>(</span><span>angle</span><span>)</span> <span>*</span> <span>fadedVolume</span><span>).</span><span>toByte</span>
    <span>}).</span><span>toArray</span>
  <span>}</span>
<span>}</span>
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

<span>class</span> <span>Tune</span><span>(</span><span>val</span> <span>sampleRate</span><span>:</span> <span>Int</span><span>,</span> <span>audioFormat</span><span>:</span> <span>AudioFormat</span><span>)</span> <span>{</span>
<span>private</span><span>[</span><span>this</span><span>]</span> <span>var</span> <span>sourceDataLine</span><span>:</span> <span>Option</span><span>[</span><span>SourceDataLine</span><span>]</span> <span>=</span> <span>None</span>
<span>private</span><span>[</span><span>this</span><span>]</span> <span>var</span> <span>ready</span> <span>=</span> <span>false</span>
<span>private</span> <span>var</span> <span>bytes</span> <span>=</span> <span>Array</span><span>[</span><span>Byte</span><span>]()</span>
<span>def</span> <span>start</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
<span>sourceDataLine</span> <span>=</span> <span>Some</span><span>(</span><span>AudioSystem</span><span>.</span><span>getSourceDataLine</span><span>(</span><span>audioFormat</span><span>))</span>
<span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>open</span><span>(</span><span>audioFormat</span><span>)</span>
<span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>flush</span><span>()</span> <span>// this eliminates "crackling" / "popping" at the beginning of the tune</span>
<span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>start</span><span>()</span>
<span>ready</span> <span>=</span> <span>true</span>
<span>}</span>
<span>def</span> <span>addNote</span><span>(</span><span>note</span><span>:</span> <span>Note</span><span>)</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
<span>bytes</span> <span>++=</span> <span>note</span><span>.</span><span>bytes</span><span>(</span><span>sampleRate</span><span>)</span>
<span>}</span>
<span>def</span> <span>play</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
<span>if</span> <span>(!</span><span>ready</span><span>)</span> <span>start</span><span>()</span>
<span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>write</span><span>(</span><span>bytes</span><span>,</span> <span>0</span><span>,</span> <span>bytes</span><span>.</span><span>length</span><span>)</span>
<span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>drain</span><span>()</span> <span>// this causes the "crackling" / "popping" at the end of the tune</span>
<span>}</span>
<span>def</span> <span>close</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
<span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>flush</span><span>())</span>
<span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>stop</span><span>())</span>
<span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>close</span><span>())</span>
<span>ready</span> <span>=</span> <span>false</span>
<span>}</span>
<span>}</span>
<span>class</span> <span>Tune</span><span>(</span><span>val</span> <span>sampleRate</span><span>:</span> <span>Int</span><span>,</span> <span>audioFormat</span><span>:</span> <span>AudioFormat</span><span>)</span> <span>{</span>

  <span>private</span><span>[</span><span>this</span><span>]</span> <span>var</span> <span>sourceDataLine</span><span>:</span> <span>Option</span><span>[</span><span>SourceDataLine</span><span>]</span> <span>=</span> <span>None</span>
  <span>private</span><span>[</span><span>this</span><span>]</span> <span>var</span> <span>ready</span> <span>=</span> <span>false</span>

  <span>private</span> <span>var</span> <span>bytes</span> <span>=</span> <span>Array</span><span>[</span><span>Byte</span><span>]()</span>

  <span>def</span> <span>start</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
    <span>sourceDataLine</span> <span>=</span> <span>Some</span><span>(</span><span>AudioSystem</span><span>.</span><span>getSourceDataLine</span><span>(</span><span>audioFormat</span><span>))</span>
    <span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>open</span><span>(</span><span>audioFormat</span><span>)</span>
    <span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>flush</span><span>()</span> <span>// this eliminates "crackling" / "popping" at the beginning of the tune</span>
    <span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>start</span><span>()</span>
    <span>ready</span> <span>=</span> <span>true</span>
  <span>}</span>

  <span>def</span> <span>addNote</span><span>(</span><span>note</span><span>:</span> <span>Note</span><span>)</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
    <span>bytes</span> <span>++=</span> <span>note</span><span>.</span><span>bytes</span><span>(</span><span>sampleRate</span><span>)</span>
  <span>}</span>

  <span>def</span> <span>play</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
    <span>if</span> <span>(!</span><span>ready</span><span>)</span> <span>start</span><span>()</span>
    <span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>write</span><span>(</span><span>bytes</span><span>,</span> <span>0</span><span>,</span> <span>bytes</span><span>.</span><span>length</span><span>)</span>
    <span>sourceDataLine</span><span>.</span><span>get</span><span>.</span><span>drain</span><span>()</span> <span>// this causes the "crackling" / "popping" at the end of the tune</span>
  <span>}</span>

  <span>def</span> <span>close</span><span>()</span><span>:</span> <span>Unit</span> <span>=</span> <span>{</span>
    <span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>flush</span><span>())</span>
    <span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>stop</span><span>())</span>
    <span>sourceDataLine</span><span>.</span><span>foreach</span><span>(</span><span>_</span><span>.</span><span>close</span><span>())</span>
    <span>ready</span> <span>=</span> <span>false</span>
  <span>}</span>
<span>}</span>
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?

<span>object</span> <span>Main</span> <span>extends</span> <span>App</span> <span>{</span>
<span>val</span> <span>G</span> <span>=</span> <span>196.00</span> <span>// Hz</span>
<span>val</span> <span>Eb</span> <span>=</span> <span>155.56</span>
<span>val</span> <span>F</span> <span>=</span> <span>174.61</span>
<span>val</span> <span>D</span> <span>=</span> <span>146.83</span>
<span>val</span> <span>bpm</span> <span>=</span> <span>108.0</span>
<span>val</span> <span>quarter</span> <span>=</span> <span>1000.0</span> <span>*</span> <span>60.0</span> <span>/</span> <span>bpm</span>
<span>val</span> <span>triplet</span> <span>=</span> <span>quarter</span> <span>/</span> <span>3.0</span>
<span>val</span> <span>half</span> <span>=</span> <span>quarter</span> <span>*</span> <span>2.0</span>
<span>val</span> <span>quarterRest</span> <span>=</span> <span>Note</span><span>(</span><span>0</span><span>,</span> <span>quarter</span><span>,</span> <span>0</span><span>)</span>
<span>val</span> <span>tripletG</span> <span>=</span> <span>Note</span><span>(</span><span>G</span><span>,</span> <span>triplet</span><span>)</span>
<span>val</span> <span>halfEb</span> <span>=</span> <span>Note</span><span>(</span><span>Eb</span><span>,</span> <span>half</span><span>)</span>
<span>val</span> <span>tripletF</span> <span>=</span> <span>Note</span><span>(</span><span>F</span><span>,</span> <span>triplet</span><span>)</span>
<span>val</span> <span>halfD</span> <span>=</span> <span>Note</span><span>(</span><span>D</span><span>,</span> <span>half</span><span>)</span>
<span>val</span> <span>bars12</span><span>:</span> <span>List</span><span>[</span><span>Note</span><span>]</span> <span>=</span> <span>List</span><span>(</span><span>quarterRest</span><span>,</span> <span>tripletG</span><span>,</span> <span>tripletG</span><span>,</span> <span>tripletG</span><span>,</span> <span>halfEb</span><span>)</span>
<span>val</span> <span>bars34</span><span>:</span> <span>List</span><span>[</span><span>Note</span><span>]</span> <span>=</span> <span>List</span><span>(</span><span>quarterRest</span><span>,</span> <span>tripletF</span><span>,</span> <span>tripletF</span><span>,</span> <span>tripletF</span><span>,</span> <span>halfD</span><span>,</span> <span>quarterRest</span><span>)</span>
<span>val</span> <span>tune</span> <span>=</span> <span>Tune</span><span>.</span><span>empty</span>
<span>(</span><span>bars12</span> <span>++</span> <span>bars34</span><span>).</span><span>foreach</span><span>(</span><span>tune</span><span>.</span><span>addNote</span><span>)</span>
<span>tune</span><span>.</span><span>play</span><span>()</span>
<span>tune</span><span>.</span><span>close</span><span>()</span>
<span>}</span>
<span>object</span> <span>Main</span> <span>extends</span> <span>App</span> <span>{</span>

  <span>val</span> <span>G</span>  <span>=</span> <span>196.00</span> <span>// Hz</span>
  <span>val</span> <span>Eb</span> <span>=</span> <span>155.56</span>
  <span>val</span> <span>F</span>  <span>=</span> <span>174.61</span>
  <span>val</span> <span>D</span>  <span>=</span> <span>146.83</span>

  <span>val</span> <span>bpm</span> <span>=</span> <span>108.0</span>
  <span>val</span> <span>quarter</span> <span>=</span> <span>1000.0</span> <span>*</span> <span>60.0</span> <span>/</span> <span>bpm</span>
  <span>val</span> <span>triplet</span> <span>=</span> <span>quarter</span> <span>/</span> <span>3.0</span>
  <span>val</span> <span>half</span> <span>=</span> <span>quarter</span> <span>*</span> <span>2.0</span>

  <span>val</span> <span>quarterRest</span> <span>=</span> <span>Note</span><span>(</span><span>0</span><span>,</span> <span>quarter</span><span>,</span> <span>0</span><span>)</span>
  <span>val</span> <span>tripletG</span> <span>=</span> <span>Note</span><span>(</span><span>G</span><span>,</span> <span>triplet</span><span>)</span>
  <span>val</span> <span>halfEb</span> <span>=</span> <span>Note</span><span>(</span><span>Eb</span><span>,</span> <span>half</span><span>)</span>
  <span>val</span> <span>tripletF</span> <span>=</span> <span>Note</span><span>(</span><span>F</span><span>,</span> <span>triplet</span><span>)</span>
  <span>val</span> <span>halfD</span> <span>=</span> <span>Note</span><span>(</span><span>D</span><span>,</span> <span>half</span><span>)</span>

  <span>val</span> <span>bars12</span><span>:</span> <span>List</span><span>[</span><span>Note</span><span>]</span> <span>=</span> <span>List</span><span>(</span><span>quarterRest</span><span>,</span> <span>tripletG</span><span>,</span> <span>tripletG</span><span>,</span> <span>tripletG</span><span>,</span> <span>halfEb</span><span>)</span>
  <span>val</span> <span>bars34</span><span>:</span> <span>List</span><span>[</span><span>Note</span><span>]</span> <span>=</span> <span>List</span><span>(</span><span>quarterRest</span><span>,</span> <span>tripletF</span><span>,</span> <span>tripletF</span><span>,</span> <span>tripletF</span><span>,</span> <span>halfD</span><span>,</span> <span>quarterRest</span><span>)</span>

  <span>val</span> <span>tune</span> <span>=</span> <span>Tune</span><span>.</span><span>empty</span>

  <span>(</span><span>bars12</span> <span>++</span> <span>bars34</span><span>).</span><span>foreach</span><span>(</span><span>tune</span><span>.</span><span>addNote</span><span>)</span>

  <span>tune</span><span>.</span><span>play</span><span>()</span>
  <span>tune</span><span>.</span><span>close</span><span>()</span>
<span>}</span>
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.

原文链接:Creating audio from raw bits in Scala

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
I am the luckiest person in the world.
我是世界上最幸运的人
评论 抢沙发

请登录后发表评论

    暂无评论内容