Skip to content

Instantly share code, notes, and snippets.

@SpotlightKid
Last active October 17, 2024 10:51
Show Gist options
  • Save SpotlightKid/054b863b3f47f7ec2d432d328762361e to your computer and use it in GitHub Desktop.
Save SpotlightKid/054b863b3f47f7ec2d432d328762361e to your computer and use it in GitHub Desktop.
Control JACK transport with foot switch connected to Zoom R8 audio interface
import system/ansi_c
export SIG_DFL, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM
when not defined(windows):
export SIGPIPE
var
SIG_IGN* {.importc: "SIG_IGN", header: "<signal.h>".}: cint
SIGHUP* {.importc: "SIGHUP", header: "<signal.h>".}: cint
SIGQUIT* {.importc: "SIGQUIT", header: "<signal.h>".}: cint
type CSighandlerT = proc (a: cint) {.noconv.}
proc setSignalProc* (`proc`: CSighandlerT, signals: varargs[cint]) =
for sig in signals:
discard c_signal(sig, `proc`)
##
## Control JACK transport with foot switch connected to Zoom R8 audio interface
##
## Requires: https://github.com/SpotlightKid/jacket
import std/[cmdline, logging, os, re, strformat]
import threading/channels
import signal
import jacket
const MidiNoteOn: byte = 0x90
const McpRewind: byte = 91
const McpForward: byte = 92
const McpStop: byte = 93
const McpPlay: byte = 94
const McpPunchInOut: byte = 95
type
ConnectionInfo = tuple[name: string, pattern: string]
App = object
client: ClientP
logger: ConsoleLogger
midiIn: PortP
var
jclient: ClientP
event: MidiEvent
midiPort: PortP
status: cint
srcPortPtn: string
portChan: Chan[ConnectionInfo]
portConnecter: Thread[App]
exitSignalled: bool = false
var log = newConsoleLogger(when defined(release): lvlInfo else: lvlDebug)
proc cleanup() =
debug "Cleaning up..."
if jclient != nil:
discard jclient.deactivate()
if portConnecter.running:
debug "Stopping port connecter thread..."
discard portChan.trySend((name: "", pattern: ""))
portConnecter.joinThread()
discard jclient.clientClose()
jclient = nil
proc errorCb(msg: cstring) {.cdecl.} =
# Suppress verbose JACK error messages when server is not available by
# default. Pass ``lvlAll`` when creating the logger to enable them.
debug "JACK error: " & $msg
proc signalCb(sig: cint) {.noconv.} =
debug "Received signal: " & $sig
exitSignalled = true
proc shutdownCb(arg: pointer = nil) {.cdecl.} =
warn "JACK server has shut down."
exitSignalled = true
proc isStatus(event: MidiEvent, status: byte): bool =
return (event.buffer[0] and 0xF0) == status
proc processCb*(nFrames: NFrames, arg: pointer): cint {.cdecl.} =
let app = cast[ptr App](arg)
let inbuf = portGetBuffer(app.midiIn, nFrames)
let count = midiGetEventCount(inbuf)
for i in 0..<count:
if midiEventGet(event.addr, inbuf, i.uint32) == 0:
if isStatus(event, MidiNoteOn):
var pos: Position
let transportState = transportQuery(jclient, pos.addr)
case event.buffer[1].byte
of McpRewind:
app.logger.log(lvlDebug, "Setting transport position to frame = 0")
discard transportLocate(app.client, 0)
of McpForward:
let new_pos = pos.frame + pos.frame_rate
app.logger.log(lvlDebug, "Setting transport position to frame = " & $new_pos)
discard transportLocate(app.client, new_pos)
of McpStop:
if (transportState == TransportRolling or
transportState == TransportStarting or
transportState == TransportNetStarting):
app.logger.log(lvlDebug, "STOPPING JACK transport")
transportStop(app.client)
of McpPlay:
if transportState == TransportStopped:
app.logger.log(lvlDebug, "STARTING JACK transport")
transportStart(app.client)
of McpPunchInOut:
if transportState == TransportStopped:
app.logger.log(lvlDebug, "STARTING JACK transport")
transportStart(app.client)
else:
app.logger.log(lvlDebug, "STOPPING JACK transport")
transportStop(app.client)
else:
discard
proc findPort(client: ClientP, pattern: string): string =
let ports = getPorts(client, pattern, JACK_DEFAULT_MIDI_TYPE, PortIsOutput)
if not ports.isNil():
result = $ports[0]
free(ports)
proc portRegisterCb(port: PortId; flag: cint; arg: pointer) {.cdecl.} =
if flag == 0:
return
let portP = jclient.portById(port)
if not portP.isNil():
let info: ConnectionInfo = (name: $portName(portP), pattern: srcPortPtn)
if not portChan.trySend(info):
writeLine stderr, "Port connecter channel overflow!"
proc portConnecterProc(app: App) {.thread.} =
var info: ConnectionInfo
addHandler(app.logger)
setLogFilter(when defined(release): lvlInfo else: lvlDebug)
while true:
portChan.recv(info)
if info.name == "":
break
debug &"New port: {info.name}"
if contains(info.name, re(info.pattern)):
if portConnectedTo(midiPort, info.name.cstring) != 1:
if app.client.connect(info.name.cstring, portName(app.midiIn)) != 1:
debug &"Connected input to port {info.name}"
else:
warn &"Failed to connect to port {info.name}"
proc main() =
addHandler(log)
# Create JACK client
setErrorFunction(errorCb)
jclient = clientOpen("zoom2transport", NoStartServer or UseExactName, status.addr)
debug "JACK server status: " & $status
if jclient == nil:
error getJackStatusErrorString(status)
quit QuitFailure
# Set up signal handlers to clean up on exit
when defined(windows):
setSignalProc(signalCb, SIGABRT, SIGINT, SIGTERM)
else:
setSignalProc(signalCb, SIGABRT, SIGHUP, SIGINT, SIGQUIT, SIGTERM)
# Create output port
midiPort = jclient.portRegister("midi_in", JACK_DEFAULT_MIDI_TYPE, PortIsInput, 0)
var app = App(client: jclient, logger: log, midiIn: midiPort)
portChan = newChan[ConnectionInfo]()
createThread(portConnecter, portConnecterProc, app)
# Register JACK callbacks
if jclient.setProcessCallback(processCb, app.addr) != 0:
error "Could not set JACK process callback function."
cleanup()
quit QuitFailure
jclient.onShutdown(shutdownCb)
if paramCount() > 0:
srcPortPtn = paramStr(1)
if jclient.setPortRegistrationCallback(portRegisterCb) != 0:
error "Error: could not set JACK port registration callback."
cleanup()
quit QuitFailure
else:
srcPortPtn = ""
# Activate JACK client ...
if jclient.activate() == 0:
# try to connect input to port given via port pattern on command line
if srcPortPtn != "":
let srcPortName = findPort(jclient, srcPortPtn)
if srcPortName != "" and portConnectedTo(midiPort, srcPortName.cstring) != 1:
if jclient.connect(srcPortName.cstring, portName(midiPort)) != 1:
debug &"Connected input to port {srcPortName}"
else:
warn &"Failed to connect to port {srcPortName}"
# ... and keep running until a signal is received
while not exitSignalled:
sleep(100)
cleanup()
when isMainModule:
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment