OrcaSoundDocs.kt
src/jvmMain/kotlin/afield/audio/synth/OrcaSoundDocs.kt · 178 lines
@file:afield.runtime.completion.CompletionCategory("Patterns")
package afield.audio.synth
import afield.audio.engine.WavFloatArrayWriter
import kotlin.math.roundToLong
Offline renders of the ORCA page's two sound examples, with each note placed on the sample its ORCA frame falls on.
object OrcaSoundDocs
The sound on the ORCA page, rendered offline: ORCA's own operators running in
OrcaHeadless, frame by frame, voiced by the same PatSynthBank calls a box makes live.
Two scenes. hello is the page's first box: one ORCA, a melody and a bass. duet is the
orca_duet starting point: two ORCAs, with the box's exchange between them run after every frame
instead of in a frames { } loop. Each note starts on the sample its ORCA frame falls on, so the
clip is the rhythm ORCA computed, with none of a live run's network jitter.
The grids and the exchange are copies of dev_docs/agents/transcripts/orca_duet.yaml and of the
page's first box.
const val SR = 48_000
val HELLO
val HELLO = """
#.ORCA.IN.FIELD.#
.
.2C8
D2.8TCDEGAGED
.:04
.
D8
.:12C
""".trimIndent()
val LEAD
val LEAD = """
#.LEAD.#
.
.2C8
D2.8TCDEGAGED
.:04
.
D8
.:12C
""".trimIndent()
val ECHO
val ECHO = """
#.ECHO.#
.
.3C8
D3.8TCCCCCCCC
.:25
""".trimIndent()
class Rendered(val left: FloatArray, val right: FloatArray, val notes: List<OrcaNote>)
A rendered scene: the notes ORCA played, and the audio they made.
fun render(name: String): Rendered
fun render(name: String): Rendered = when (name) {
"hello" -> hello(seconds = 8.0)
"duet" -> duet(seconds = 16.0)
"bass" -> bass(seconds = 8.0)
"drone" -> drone(seconds = 8.0)
else -> throw IllegalArgumentException("No ORCA scene '$name'; there are 'hello', 'duet', 'bass' and 'drone'")
}
fun renderWav(name: String, outPath: String)
fun renderWav(name: String, outPath: String) {
val rendered = render(name)
require(rendered.notes.isNotEmpty()) { "scene '$name' played no notes" }
require(rendered.left.any { it != 0f }) { "scene '$name' rendered silence" }
WavFloatArrayWriter.writeStereo16(outPath, rendered.left, rendered.right, SR, normalizePeak = 0.9f)
}
private fun hello(seconds: Double): Rendered = OrcaHeadless(HELLO, rows = 12).use { o ->
val voice = Voicing(o)
o.orca.onNote { voice.play(it) }
voice.run(seconds) { o.step() }
}
orca.md's "Playing a part in the grid": the hello grid with its one onCell line.
private fun bass(seconds: Double): Rendered = OrcaHeadless(HELLO, rows = 12).use { o ->
val voice = Voicing(o)
o.orca.onNote { voice.play(it) }
o.orca.onCell(4, 4) { note -> o.orca[4, 7] = if (note == 'G' || note == 'A') 'F' else 'C' }
voice.run(seconds) { o.step() }
}
orca.md's "Patching ORCA itself": the hello grid after the page's patch makes every : play G.
private fun drone(seconds: Double): Rendered = OrcaHeadless(HELLO, rows = 12).use { o ->
o.evaluate(DRONE_PATCH)
val voice = Voicing(o)
o.orca.onNote { voice.play(it) }
voice.run(seconds) { o.step() }
}
val DRONE_PATCH
body — 44 lines, from line 96
val DRONE_PATCH = """
const Midi = library[':']
library[':'] = function (orca, x, y, passive) {
Midi.call(this, orca, x, y, passive)
const listen = this.listen
this.listen = (port, value) => port === this.ports.note ? 'G' : listen.call(this, port, value)
}
true
""".trimIndent()
private fun duet(seconds: Double): Rendered = OrcaHeadless(LEAD, cols = 40, rows = 10).use { leadEngine ->
OrcaHeadless(ECHO, cols = 40, rows = 10).use { echoEngine ->
val lead = leadEngine.orca
val echo = echoEngine.orca
val voice = Voicing(leadEngine)
// The starting point's box from here on, with `voice` for `synth` and no ledger display.
val up = mapOf('C' to 'D', 'D' to 'E', 'E' to 'G', 'G' to 'A', 'A' to 'C')
val heard = ArrayDeque("CCCCCCCC".toList())
var echoes = 0
lead.onNote {
if (it.channel == 0) { heard.addLast(it.note[0]); heard.removeFirst() }
voice.play(it, pan = 0.35, wave = if (it.channel == 1) PatWave.SINE else PatWave.TRIANGLE)
}
echo.onNote {
echoes++
voice.play(it, pan = 0.7, wave = PatWave.SQUARE, gain = 0.35, cutoffHz = 2400.0)
}
voice.run(seconds) {
leadEngine.step()
echoEngine.step()
val seen = lead.frame
heard.forEachIndexed { i, note -> echo[5 + i, 3] = note }
if (echoes >= 8) {
echoes = 0
val x = 5 + seen % 8
lead[x, 3] = up[lead[x, 3]] ?: 'C'
}
lead[4, 7] = if (lead[4, 4] in "GA") 'F' else 'C'
}
}
}
One bank for a scene, with each note placed on the sample its ORCA frame starts.
private class Voicing(private val clock: OrcaHeadless) {
val bank = PatSynthBank(voices = 32, sampleRate = SR)
private val scheduled = mutableListOf<PatSynthNote>()
val notes = mutableListOf<OrcaNote>()
fun play(note: OrcaNote, pan: Double = 0.5, wave: PatWave = bank.voice.wave, gain: Double = 1.0,
cutoffHz: Double? = null) {
notes += note
val start = (note.frame * clock.frameSeconds * SR).roundToLong()
scheduled += bank.orcaSynthNote(note, start, pan, wave, gain, cutoffHz)
}
fun run(seconds: Double, frame: () -> Unit): Rendered {
repeat((seconds / clock.frameSeconds).toInt()) { frame() }
bank.schedule(scheduled)
val total = (seconds * SR).toInt()
val left = FloatArray(total); val right = FloatArray(total)
val block = 512
val l = FloatArray(block); val r = FloatArray(block)
val ctx = SynthContext(SR.toFloat(), block)
var at = 0
var cycle = 1L
while (at < total) {
val n = minOf(block, total - at)
l.fill(0f); r.fill(0f)
// A node renders once per cycle and hands the same block back for the rest of it.
ctx.cycleId = cycle++
ctx.time = at.toFloat() / SR
bank.render(l, r, n, ctx)
System.arraycopy(l, 0, left, at, n); System.arraycopy(r, 0, right, at, n)
at += n
}
return Rendered(left, right, notes.toList())
}
}
}