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 Note
s 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 Note
s 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.
暂无评论内容