Orca.kt
src/jvmMain/kotlin/afield/audio/synth/Orca.kt · 802 lines
@file:afield.runtime.completion.CompletionCategory("Patterns")
package afield.audio.synth
import afield.ai.tools.BoxTool
import afield.ai.tools.BranchConflict
import afield.ai.tools.ToolEffect
import afield.ai.tools.agentExtension
import afield.ai.tools.exposeOutputIframeTools
import afield.document.graph.Box
import afield.ui.output.LiveOutputIframe
import afield.ui.output.displayOpen
import afield.ui.output.outputIframe
import afield.ui.output.outputRequests
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.pow
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.*
ORCA, the two-dimensional livecoding sequencer, running in a box output with its notes, control changes, UDP and OSC messages delivered to Kotlin callbacks; start it with orca.
fun Box.orca
ORCA, Hundred Rabbits' two-dimensional livecoding sequencer, running in one of this box's outputs, with everything it plays coming back to Kotlin.
orca()
is a working ORCA: click into it and type. Its operators that would talk to the outside world — : and % (MIDI
notes), ! (control change), ? (pitch bend), ; (UDP) and = (OSC) — arrive here, one frame at a time, as
onNote, onCC, onBend, onUdp and onOsc. To hear the notes:
val synth = PatSynthBank()
orca().onNote { synth.play(it) }
synth
Input goes the other way: o[x, y] reads a cell and o[x, y] = 'C' sets one, bang fires the operators beside a
cell, variable holds an ORCA variable, write puts text on the grid, grid replaces all of it, command runs
anything ORCA's commander takes ("bpm:140", "play", ...), and onChange / onCell report the cells ORCA
changes, so a box can play a part in the grid as one more operator. eval and js run JavaScript in ORCA's page,
for patching ORCA itself. A grid passed here is loaded when ORCA starts; without one, the box reopens the grid it
had when last edited, which is saved with the document. exposeTools lets AI4 read and write the grid.
ORCA itself is upstream's code, unmodified, in vendor/orca/; orca_field.js wraps its IO objects and talks to this
class through afield.ui.output.outputRequests. Callbacks run on the Runloop with this box in context, so they can
display, print and touch box state.
@param cols the output's width when it opens, in ORCA cells. ORCA fits its grid to the output, and keeps doing so when the output is resized. @param rows the output's height when it opens, in ORCA cells; at least ten, so ORCA's status lines fit. @param guide show ORCA's operator guide when it starts (Escape hides it).
fun Box.orca(
channel: String = "orca",
grid: String? = null,
bpm: Int? = null,
cols: Int = 64,
rows: Int = 24,
guide: Boolean = false,
): Orca {
require(cols in 4..400 && rows in 4..200) { "cols must be 4..400 and rows 4..200" }
val orca = Orca(this, channel, grid?.trimIndent() ?: Orca.savedGrid(this, channel), bpm, guide)
val binding = outputRequests("orca-" + channel.replace(Regex("[^A-Za-z0-9_-]"), "_").take(70)) { orca.receive(it) }
val width = cols * 10 + 60
// ORCA draws its two status lines inside a canvas a fifth taller than the grid, which only has
// room for them from ten rows up; any shorter and the output crops them.
val height = maxOf(rows, 10) * 15 + 90
display(
channel,
"""<iframe src="/orca.html?binding=${binding.id}" allow="midi" """ +
"""style="border:0;display:block;background:#000;width:${width}px;height:${height}px"></iframe>""",
)
displayOpen(channel)
return orca
}
data class OrcaNote
One : (or %, mono) note. frames is ORCA's length; seconds is that at the current bpm.
data class OrcaNote(
val channel: Int,
val octave: Int,
/** The note as ORCA writes it: `C` to `B`, lowercase for sharps, and its wrapped letters. */
val note: String,
val midi: Int,
/** 0 to 1; ORCA's velocity digit over 16. */
val velocity: Double,
val frames: Int,
val seconds: Double,
val mono: Boolean,
/** The ORCA frame that played it. */
val frame: Int,
/** The [System.nanoTime] it should sound at: when ORCA played it, plus [Orca.latency]. */
val time: Long,
) {
val hz: Double
val hz: Double
get() = 440.0 * 2.0.pow((midi - 69) / 12.0)
}
data class OrcaCC(val channel: Int, val knob: Int, val value: Int, val frame: Int)
One ! control change; value is already scaled to 0..127, knob already offset as ORCA sends it.
data class OrcaBend(val channel: Int, val lsb: Int, val msb: Int, val frame: Int)
One ? pitch bend, as the two 7-bit halves ORCA sends.
data class OrcaCell(val x: Int, val y: Int, val before: Char, val after: Char)
A cell that changed between two reports.
data class OrcaOsc(val path: String, val args: List<Int>, val text: String, val frame: Int)
One = message: / plus its path, and the values of the glyphs after it.
class Orca internal constructor
An ORCA: what it is doing, what it plays, and how to change it. Box.orca makes one in a box's output; OrcaHeadless makes one in the JVM with no page, for rendering and tests.
class Orca internal constructor(
/** The box whose output holds it; null for an [OrcaHeadless]. */
private val box: Box?,
val channel: String,
private val initialGrid: String?,
private val initialBpm: Int?,
private val showGuide: Boolean,
) {
companion object
companion object {
The grid each output had when last edited, as {channel: grid} JSON, saved with the document.
private const val SAVED = "orcaGrids"
Redraw ORCA's canvas (a page in the background skips drawing) and return it, scaled down.
private val PICTURE =
"""
(() => {
client.clear(); client.ports = client.findPorts(); client.drawProgram(); client.drawInterface(); client.drawGuide()
const source = client.el, scale = Math.min(1, 1400 / source.width)
const out = document.createElement('canvas')
out.width = Math.round(source.width * scale); out.height = Math.round(source.height * scale)
const g = out.getContext('2d')
g.fillStyle = client.theme.active.background || '#000'; g.fillRect(0, 0, out.width, out.height)
g.drawImage(source, 0, 0, out.width, out.height)
return out.toDataURL('image/png')
})()
"""
.trimIndent()
init
init {
/*
Registered on first use, as syncToProperty does, rather than in Box's own list, which
every saved document copies into its header. A document saved after this lists it,
and loading that document merges it back before any orca() runs.
*/
Box.ioPersistentProperties.add(SAVED)
}
internal fun savedGrid(box: Box, channel: String): String?
internal fun savedGrid(box: Box, channel: String): String? =
savedGrids(box)[channel]?.jsonPrimitive?.contentOrNull
private fun savedGrids(box: Box): JsonObject = (box.getPropertyRaw(SAVED) as? String)?.let {
runCatching { Json.parseToJsonElement(it).jsonObject }.getOrNull()
} ?: JsonObject(emptyMap())
}
private val outgoing = ConcurrentLinkedQueue<JsonObject>()
private val notes = CopyOnWriteArrayList<(OrcaNote) -> Unit>()
private val ccs = CopyOnWriteArrayList<(OrcaCC) -> Unit>()
private val bends = CopyOnWriteArrayList<(OrcaBend) -> Unit>()
private val udps = CopyOnWriteArrayList<(String) -> Unit>()
private val oscs = CopyOnWriteArrayList<(OrcaOsc) -> Unit>()
private val frames = CopyOnWriteArrayList<(Orca) -> Unit>()
private val changes = CopyOnWriteArrayList<(OrcaCell) -> Unit>()
private val evals = ConcurrentHashMap<String, CompletableDeferred<JsonElement>>()
Cells this box set that ORCA has not reported back yet, with the report that will show them.
private val pendingCells = ConcurrentHashMap<Pair<Int, Int>, Pair<Char, Long>>()
private var rows: List<String> = emptyList()
private var lastSaved: String? = null
More than one page can show this output: a second tab, or the browser AI4 takes pictures in. The first to report is the one that plays and takes commands; the others are stopped mirrors of its grid, so a picture of any of them shows the board. A silent player is replaced.
private var player: String? = null
private var playerSeen = 0L
internal var playerTimeoutNanos = 2_000_000_000L
private val mirrors = HashMap<String, String?>()
private val waiting = CopyOnWriteArrayList<Pair<Long, CompletableDeferred<Unit>>>()
private var offsetNanos: Long? = null
private var lastError: String? = null
var latency: Double = 0.05
How far behind ORCA a note's OrcaNote.time is set, in seconds: room for the trip to Field.
private var reportedGrid = ""
private var reportedBpm = initialBpm ?: 120
var grid: String
The grid as ORCA last reported it, rows separated by newlines. Setting it replaces the whole grid; short rows are padded with empty cells, and common indentation is removed.
var grid: String
get() = reportedGrid
set(value) {
send("load", "grid" to JsonPrimitive(value.trimIndent()))
}
var width: Int
var width: Int = 0
private set
var height: Int
var height: Int = 0
private set
var frame: Int
var frame: Int = 0
private set
var playing: Boolean
var playing: Boolean = false
private set
var bpm: Int
ORCA's tempo; setting it is the commander's bpm:, clamped by ORCA to 60..300.
var bpm: Int
get() = reportedBpm
set(value) {
send("bpm", "value" to JsonPrimitive(value))
}
var variables: Map<String, String>
This frame's V variables.
var variables: Map<String, String> = emptyMap()
private set
var cursor: Pair<Int, Int>
Where ORCA's cursor is.
var cursor: Pair<Int, Int> = 0 to 0
private set
var reports: Long
Reports received from the page; zero until ORCA has started.
var reports: Long = 0
private set
var operators: String
ORCA's own operator reference, as its page generated it.
var operators: String = ""
private set
private val recentEvents = ArrayDeque<String>()
val recent: List<String>
The last 64 messages ORCA sent, one line each, newest last.
val recent: List<String>
get() = recentEvents.toList()
fun write(x: Int, y: Int, text: String)
Put text on the grid with its top-left at (x, y); newlines start a new row at x.
fun write(x: Int, y: Int, text: String) =
send("write", "x" to JsonPrimitive(x), "y" to JsonPrimitive(y), "text" to JsonPrimitive(text))
operator fun get(x: Int, y: Int): Char
The glyph at (x, y), '.' for an empty cell or one off the grid. It is the grid ORCA last reported, with any
cell this box has set since then showing the value it was set to.
operator fun get(x: Int, y: Int): Char = pendingCells[x to y]?.first ?: rows.getOrNull(y)?.getOrNull(x) ?: '.'
operator fun set(x: Int, y: Int, glyph: Char)
Put one glyph at (x, y) at ORCA's next frame. Unlike write it stays out of ORCA's undo history and is not
saved as an edit, so a box can set cells every frame: o[4, 2] = 'C'.
operator fun set(x: Int, y: Int, glyph: Char) {
pendingCells[x to y] = glyph to reports + 2
send("set", "x" to JsonPrimitive(x), "y" to JsonPrimitive(y), "glyph" to JsonPrimitive(glyph.toString()))
}
suspend fun eval(javascript: String, timeoutMillis: Long = 5_000): JsonElement
Run javascript in ORCA's page, at global scope beside ORCA's client and library, and suspend until its
value comes back as JSON with the next report; a promise is awaited first. Anything goes: read ORCA's state,
restyle it, replace an operator in library, wrap client.run. A JavaScript error comes back as an
IllegalStateException carrying its stack.
go { println(o.eval("client.orca.w + ' x ' + client.orca.h")) }
suspend fun eval(javascript: String, timeoutMillis: Long = 5_000): JsonElement {
val id = UUID.randomUUID().toString()
val result = CompletableDeferred<JsonElement>()
evals[id] = result
send("eval", "id" to JsonPrimitive(id), "js" to JsonPrimitive(javascript))
try {
return withTimeout(timeoutMillis) { result.await() }
} finally {
evals.remove(id)
}
}
fun js(javascript: String) = send("eval", "js" to JsonPrimitive(javascript))
Run javascript in ORCA's page at its next frame, as eval does, without waiting for a value.
fun bang(x: Int, y: Int) = set(x, y, '*')
Put a * at (x, y): next frame, the operators beside it fire, and ORCA erases it.
fun variable(name: Char, value: Char?)
Hold ORCA variable name at value every frame, as if a V on the grid wrote it, so V reads it there; null
lets it go. A V on the grid that writes the same name wins that frame.
fun variable(name: Char, value: Char?) = send(
"variable",
"name" to JsonPrimitive(name.toString()),
"value" to (value?.let { JsonPrimitive(it.toString()) } ?: JsonNull),
)
fun command(text: String) = send("command", "text" to JsonPrimitive(text))
Run a commander string, as if typed after Cmd-K: "bpm:140", "write:D8;4;2", "find:aa", ...
fun play() = send("play")
fun stop() = send("stop")
fun onNote(callback: (OrcaNote) -> Unit) = apply { notes.add(callback) }
fun onNote(name: String, callback: (OrcaNote) -> Unit) = apply {
Replace this named note callback; Stop or rerun of its registering box removes it.
fun onNote(name: String, callback: (OrcaNote) -> Unit) = apply {
afield.runtime.lifecycle.runOwnedListener(
this,
"note:$name",
{ listener: (OrcaNote) -> Unit ->
notes.add(listener)
val remove: () -> Unit = { notes.remove(listener) }
remove
},
callback,
)
}
fun onCC(callback: (OrcaCC) -> Unit) = apply { ccs.add(callback) }
fun onBend(callback: (OrcaBend) -> Unit) = apply { bends.add(callback) }
fun onUdp(callback: (String) -> Unit) = apply { udps.add(callback) }
fun onOsc(callback: (OrcaOsc) -> Unit) = apply { oscs.add(callback) }
fun onFrame(callback: (Orca) -> Unit) = apply { frames.add(callback) }
After every report: once a frame while ORCA plays, a few times a second while it is paused.
fun onChange(callback: (OrcaCell) -> Unit) = apply { changes.add(callback) }
Every cell that changed since the last report, whoever changed it: an operator, the keyboard, this box. Called row by row, before onFrame. A report can cover more than one frame when the page is busy, so a cell that changed and changed back in between is not seen.
fun onChange(name: String, callback: (OrcaCell) -> Unit) = apply {
Replace this named change callback; owned by the registering box run.
fun onChange(name: String, callback: (OrcaCell) -> Unit) = apply {
afield.runtime.lifecycle.runOwnedListener(
this,
"change:$name",
{ listener: (OrcaCell) -> Unit ->
changes.add(listener)
val remove: () -> Unit = { changes.remove(listener) }
remove
},
callback,
)
}
fun onCell(x: Int, y: Int, callback: (Char) -> Unit) = onChange { if (it.x == x && it.y == y) callback(it.after) }
onChange, for one cell: callback gets the glyph it changed to.
fun onCell(x: Int, y: Int, callback: (Char) -> Unit) = onChange { if (it.x == x && it.y == y) callback(it.after) }
fun onCell(x: Int, y: Int, name: String, callback: (Char) -> Unit) =
A named, run-owned cell callback; replacing it does not accumulate listeners.
fun onCell(x: Int, y: Int, name: String, callback: (Char) -> Unit) =
onChange("cell:$x:$y:$name") { if (it.x == x && it.y == y) callback(it.after) }
suspend fun sync(timeoutMillis: Long = 3_000): Boolean
Suspend until ORCA has applied everything sent so far and reported the result, or timeoutMillis passes.
Commands travel in the reply to one report, so their effect shows in the one after.
suspend fun sync(timeoutMillis: Long = 3_000): Boolean {
val done = CompletableDeferred<Unit>()
waiting.add((reports + 2) to done)
return withTimeoutOrNull(timeoutMillis) { done.await() } != null
}
val page: LiveOutputIframe
The output's iframe, for the live-iframe bridge's evaluate, screenshot, ... in the Electron app.
val page: LiveOutputIframe
get() = owner.outputIframe(channel)
private val owner: Box
get() = box ?: throw IllegalStateException("A headless ORCA has no box output")
suspend fun picture(timeoutMillis: Long = 5_000): String
ORCA as the page playing it has drawn it, as a PNG data URL at most 1400 pixels wide. It comes from ORCA's own canvas, through ORCA's connection, so it works in any browser and shows exactly what is on the screen; it is drawn fresh first, in case the page is in the background.
suspend fun picture(timeoutMillis: Long = 5_000): String =
(eval(PICTURE, timeoutMillis) as? JsonPrimitive)?.contentOrNull
?: throw IllegalStateException("ORCA's page returned no picture")
val numberedGrid: String
The grid with each row prefixed by its y, as AI4's tools show it. Empty cells at the ends of rows, and empty rows at the bottom, are left off: they move nothing, and a window-wide grid is mostly them.
val numberedGrid: String
get() =
grid
.lines()
.map { it.trimEnd('.') }
.dropLastWhile { it.isEmpty() }
.mapIndexed { y, row -> "%3d %s".format(y, row).trimEnd() }
.joinToString("\n")
fun exposeTools(name: String = "orca", about: String = "", iframeTools: Boolean = false): List<BoxTool>
Let the document's AI4 play this ORCA, the way the paint studio lets it paint. AI4 gets tools to look at the
board (<name>_look: the picture ORCA drew, and the grid as text), read it, write cells while it plays, replace
it, command it and run JavaScript in its page; the board, the tempo and how to play are in its context every
turn; and /orca shows the board in the chat.
The board belongs to the conversation: it and the tempo are saved with every AI4 turn, so rewinding the chat puts
the board back as it was before that request, switching branch puts back that branch's, and each saved board is a
node in the chat's States tree. about tells AI4 anything the grid cannot, such as which channel is which
instrument. iframeTools adds the live-iframe tools (Electron only). Re-running the box replaces all of it; not
calling this removes it.
body — 166 lines, from line 423
fun exposeTools(name: String = "orca", about: String = "", iframeTools: Boolean = false): List<BoxTool> {
// The tool DSL's scopes have their own `box`, `name` and `frame()`; say which ORCA is meant.
val orca = this
val box = owner
val where = "output '$channel' of box '${box.name.ifBlank { box.id }}'"
val tools = mutableListOf<BoxTool>()
box.agentExtension(name) {
tools +=
tool("${name}_look") {
namespace("orca")
effects(ToolEffect.CAPTURE_IMAGE, ToolEffect.READ_DOCUMENT)
describe(
"Look at the ORCA in $where as it plays: the picture ORCA has drawn in the page that is " +
"playing it, and the grid as text with each row prefixed by its y.",
)
action {
image(
orca.picture(),
"Frame ${orca.frame} at ${orca.bpm} bpm, " +
(if (orca.playing) "playing" else "paused") +
". The grid:\n" +
orca.numberedGrid,
)
}
}
tools +=
tool("${name}_read") {
namespace("orca")
effects(ToolEffect.READ_DOCUMENT)
describe(
"Read the ORCA grid in $where as text: rows prefixed with their y (x counts from 0 at the " +
"first glyph), size, frame, bpm, whether it plays, the last messages it sent (notes, cc, udp, osc), " +
"and ORCA's operator reference.",
)
action {
mapOf(
"grid" to orca.numberedGrid,
"width" to orca.width,
"height" to orca.height,
"frame" to orca.frame,
"bpm" to orca.bpm,
"playing" to orca.playing,
"cursor" to listOf(orca.cursor.first, orca.cursor.second),
"recent" to orca.recent.takeLast(32),
"operators" to orca.operators,
)
}
}
tools +=
tool("${name}_write") {
namespace("orca")
effects(ToolEffect.CONTROL_RUNTIME)
describe(
"Write glyphs onto the ORCA grid in $where while it plays, top-left at (x, y); it lands at " +
"ORCA's next frame, with no need to edit or re-run the box. Newlines in text start the next row " +
"at the same x; '.' is an empty cell. Returns the grid afterwards.",
)
val x by param<Int>("Column, from 0.")
val y by param<Int>("Row, from 0.")
val text by param<String>("Glyphs to write, possibly several rows.")
action {
orca.write(x(), y(), text())
orca.sync()
mapOf("grid" to orca.numberedGrid)
}
}
tools +=
tool("${name}_load") {
namespace("orca")
effects(ToolEffect.CONTROL_RUNTIME)
describe(
"Replace the whole ORCA grid in $where with these rows ('.' for empty) while it plays. " +
"Returns the grid afterwards.",
)
val rows by param<String>("The new grid, one row per line.")
action {
orca.grid = rows()
orca.sync()
mapOf("grid" to orca.numberedGrid)
}
}
tools +=
tool("${name}_command") {
namespace("orca")
effects(ToolEffect.CONTROL_RUNTIME)
describe(
"Run an ORCA commander string in $where: play, stop, run, bpm:140, frame:0, " +
"write:TEXT;x;y, find:TEXT, select:x;y;w;h, erase, copy, paste. Returns the grid afterwards.",
)
val text by param<String>("The command.")
action {
orca.command(text())
orca.sync()
mapOf(
"bpm" to orca.bpm,
"playing" to orca.playing,
"frame" to orca.frame,
"grid" to orca.numberedGrid,
)
}
}
tools +=
tool("${name}_js") {
namespace("orca")
effects(ToolEffect.EXECUTE_CODE, ToolEffect.CONTROL_RUNTIME)
describe(
"Run JavaScript in the page of the ORCA in $where, at global scope where ORCA's `client` " +
"(client.orca: the grid, client.clock, client.theme, client.io) and `library` (the operators, by " +
"lowercase glyph) live, and return its JSON value. Use it to inspect or patch ORCA itself: " +
"restyle it, redefine an operator, wrap client.run. Works in any browser, not only Electron.",
)
val code by param<String>("JavaScript expression, or statements whose last value is returned.")
action { mapOf("json" to orca.eval(code()).toString()) }
}
context("$name-board") {
"The ORCA in $where is " +
(if (orca.playing) "playing" else "paused") +
" at ${orca.bpm} bpm, " +
"frame ${orca.frame}, ${orca.width}x${orca.height}. Its grid, rows prefixed with y:\n" +
orca.numberedGrid +
(if (about.isBlank()) "" else "\n\nAbout it, from the box: $about")
}
context("$name-craft", changesOften = false) {
"""
|You can play the ORCA in $where, a livecoding sequencer: a grid where each letter is an operator,
|uppercase ones run every frame, lowercase ones only when a bang (*) lands beside them, and four frames
|make a beat. Play it while it runs: write cells with ${name}_write (or replace the grid with
|${name}_load); a change lands at the next frame, and the music carries on. Look with ${name}_look
|after a change and listen through ${name}_read's recent notes. Leave the box's code alone unless asked:
|the grid is the instrument. Rewinding the chat brings the board back as it was, so be bold.
|ORCA's operators:
|${orca.operators}
"""
.trimMargin()
}
command(name, "show the ORCA board") {
"```\n${orca.numberedGrid}\n```\n\nFrame ${orca.frame}, ${orca.bpm} bpm"
}
// The board belongs to the conversation. ORCA's operators rewrite cells every frame, so it never
// matches the board the conversation last saw: restore anyway (the replaced board is saved first).
branchState(
"board",
save = {
it.writeText(
buildJsonObject {
put("grid", orca.grid)
put("bpm", orca.bpm)
}
.toString(),
)
},
restore = { f ->
val saved = f?.readText()?.let { Json.parseToJsonElement(it).jsonObject }
saved?.get("grid")?.jsonPrimitive?.contentOrNull?.let { orca.grid = it }
saved?.get("bpm")?.jsonPrimitive?.intOrNull?.let { orca.bpm = it }
},
onConflict = BranchConflict.RESTORE,
)
}
return tools + (if (iframeTools) box.exposeOutputIframeTools(channel, name) else emptyList())
}
private fun send(op: String, vararg fields: Pair<String, JsonElement>) {
outgoing.add(JsonObject(mapOf("op" to JsonPrimitive(op)) + fields))
}
internal fun receive(data: JsonElement): JsonElement
body — 126 lines, from line 590
internal fun receive(data: JsonElement): JsonElement {
val received = System.nanoTime()
val report = data.jsonObject
val commands = mutableListOf<JsonObject>()
val page = report["page"]?.jsonPrimitive?.contentOrNull ?: ""
if (player != null && page != player && received - playerSeen < playerTimeoutNanos) return mirror(page)
if (player != null && page != player) {
// Taking over from a player that went quiet: this page may have been a stopped mirror.
mirrors.remove(page)
commands += op("load", "grid" to JsonPrimitive(reportedGrid))
if (playing) commands += op("play")
}
player = page
playerSeen = received
if (report.bool("hello")) {
initialGrid?.let { commands += op("load", "grid" to JsonPrimitive(it)) }
initialBpm?.let { commands += op("bpm", "value" to JsonPrimitive(it)) }
commands += op("guide", "on" to JsonPrimitive(showGuide))
report["operators"]?.jsonPrimitive?.contentOrNull?.let { operators = it }
}
frame = report.int("frame") ?: frame
reportedBpm = report.int("bpm") ?: reportedBpm
playing = report["paused"]?.jsonPrimitive?.booleanOrNull?.not() ?: playing
val before = rows
report["grid"]?.jsonPrimitive?.contentOrNull?.let {
reportedGrid = it
rows = it.lines()
}
width = report.int("w") ?: width
height = report.int("h") ?: height
report["cursor"]?.jsonObject?.let { cursor = (it.int("x") ?: 0) to (it.int("y") ?: 0) }
variables = report["variables"]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap()
if (report.bool("edited")) save()
for (element in report["results"]?.jsonArray.orEmpty()) {
val result = element.jsonObject
val waiting = evals.remove(result["id"]?.jsonPrimitive?.contentOrNull ?: continue) ?: continue
val error = result["error"]?.jsonPrimitive?.contentOrNull
if (error != null)
waiting.completeExceptionally(IllegalStateException("JavaScript in ORCA's page failed: $error"))
else waiting.complete(result["value"] ?: JsonNull)
}
// The page's clock and this JVM's differ by a constant plus the trip; the quickest trip seen
// (allowed to creep up 0.1 ms a report, for drift) puts each event back where ORCA played it.
val sentMillis = report["t"]?.jsonPrimitive?.doubleOrNull ?: 0.0
val observed = received - (sentMillis * 1e6).toLong()
offsetNanos = offsetNanos?.let { minOf(it + 100_000, observed) } ?: observed
val frameSeconds = 60.0 / bpm.coerceAtLeast(1) / 4.0
for (element in report["events"]?.jsonArray.orEmpty()) {
val e = element.jsonObject
val at = ((e["at"]?.jsonPrimitive?.doubleOrNull ?: sentMillis) * 1e6).toLong() + offsetNanos!!
val f = e.int("frame") ?: frame
when (val kind = e["kind"]?.jsonPrimitive?.contentOrNull) {
"note",
"mono" -> {
val length = e.int("length") ?: 1
val note = OrcaNote(
channel = e.int("channel") ?: 0,
octave = e.int("octave") ?: 3,
note = e["note"]?.jsonPrimitive?.contentOrNull ?: "",
midi = e.int("midi") ?: -1,
velocity = ((e.int("velocity") ?: 15) / 16.0).coerceIn(0.0, 1.0),
frames = length,
seconds = length * frameSeconds,
mono = kind == "mono",
frame = f,
time = at + (latency * 1e9).toLong(),
)
remember(
"$f ${if (note.mono) "%" else ":"} ch${note.channel} ${note.note}${note.octave} midi ${note.midi} vel ${e.int("velocity")} len $length",
)
if (note.midi >= 0) notes.forEach { call { it(note) } }
}
"cc" ->
OrcaCC(e.int("channel") ?: 0, e.int("knob") ?: 0, e.int("value") ?: 0, f).let { cc ->
remember("$f ! ch${cc.channel} knob ${cc.knob} value ${cc.value}")
ccs.forEach { call { it(cc) } }
}
"pb" ->
OrcaBend(e.int("channel") ?: 0, e.int("lsb") ?: 0, e.int("msb") ?: 0, f).let { pb ->
remember("$f ? ch${pb.channel} lsb ${pb.lsb} msb ${pb.msb}")
bends.forEach { call { it(pb) } }
}
"udp" ->
(e["message"]?.jsonPrimitive?.contentOrNull ?: "").let { message ->
remember("$f ; $message")
udps.forEach { call { it(message) } }
}
"osc" ->
OrcaOsc(
e["path"]?.jsonPrimitive?.contentOrNull ?: "/",
e["args"]?.jsonArray?.map { it.jsonPrimitive.intOrNull ?: 0 }.orEmpty(),
e["message"]?.jsonPrimitive?.contentOrNull ?: "",
f,
)
.let { osc ->
remember("$f = ${osc.path} ${osc.args}")
oscs.forEach { call { it(osc) } }
}
}
}
reports++
pendingCells.entries.removeIf { it.value.second <= reports }
if (rows !== before && changes.isNotEmpty() && before.isNotEmpty()) {
for (y in 0 until minOf(before.size, rows.size)) {
val old = before[y]
val new = rows[y]
for (x in 0 until minOf(old.length, new.length)) {
if (old[x] != new[x])
OrcaCell(x, y, old[x], new[x]).let { cell -> changes.forEach { call { it(cell) } } }
}
}
}
frames.forEach { call { it(this) } }
waiting
.filter { it.first <= reports }
.forEach {
waiting.remove(it)
it.second.complete(Unit)
}
// Drained last, so what the callbacks above asked for goes out in this reply.
while (true) commands += outgoing.poll() ?: break
return buildJsonObject { put("commands", JsonArray(commands)) }
}
A page that is not the player: keep it stopped, showing the player's grid.
source — 49 lines, from line 718
private fun mirror(page: String): JsonElement {
val commands = mutableListOf<JsonObject>()
if (page !in mirrors) {
commands += op("guide", "on" to JsonPrimitive(false))
commands += op("stop")
mirrors[page] = null
}
if (mirrors[page] != reportedGrid) {
commands += op("load", "grid" to JsonPrimitive(reportedGrid))
mirrors[page] = reportedGrid
}
return buildJsonObject { put("commands", JsonArray(commands)) }
}
private fun save() {
val box = box ?: return
if (grid == lastSaved) return
lastSaved = grid
val all = savedGrids(box).toMutableMap()
all[channel] = JsonPrimitive(grid)
box.setPropertyRaw(SAVED, JsonObject(all).toString())
}
private fun remember(line: String) {
recentEvents.addLast(line)
while (recentEvents.size > 64) recentEvents.removeFirst()
}
private inline fun call(block: () -> Unit) {
try {
block()
} catch (e: Exception) {
// With no box to paint the error on, a headless render fails rather than carry on wrong.
val box = box ?: throw e
val message = "orca: ${e.message ?: e.javaClass.simpleName}"
if (message != lastError) {
lastError = message
box.displayError(message)
}
}
}
private fun op(op: String, vararg fields: Pair<String, JsonElement>) =
JsonObject(mapOf("op" to JsonPrimitive(op)) + fields)
private fun JsonObject.int(key: String) = this[key]?.jsonPrimitive?.intOrNull
private fun JsonObject.bool(key: String) = this[key]?.jsonPrimitive?.booleanOrNull == true
}
fun PatSynthBank.play
Play note on this bank at OrcaNote.time: held for ORCA's length, at its velocity. The bank has to be playing —
returned from the box — for the note to be heard.
fun PatSynthBank.play(
note: OrcaNote,
pan: Double = 0.5,
wave: PatWave = voice.wave,
gain: Double = 1.0,
cutoffHz: Double? = null,
) {
val delay = ((note.time - System.nanoTime()) * sampleRate / 1e9).toLong().coerceAtLeast(0)
schedule(orcaSynthNote(note, frame + delay, pan, wave, gain, cutoffHz))
}
internal fun PatSynthBank.orcaSynthNote
note as a PatSynthNote on this bank, starting at startFrame on its clock.
internal fun PatSynthBank.orcaSynthNote(
note: OrcaNote,
startFrame: Long,
pan: Double,
wave: PatWave,
gain: Double,
cutoffHz: Double?,
) = PatSynthNote(
startFrame = startFrame,
holdFrames = (note.seconds * sampleRate).toInt(),
tailFrames = (voice.decay * 4.0 * sampleRate).toInt().coerceAtLeast(1),
hz = note.hz,
wave = wave,
gain = note.velocity * gain,
pan = pan.coerceIn(0.0, 1.0),
cutoffHz = cutoffHz,
seed = note.midi.toLong(),
)