脚本
JavaScript 节点:读取并修改实时数据,控制节点和设备,绘制图形。
脚本(JavaScript)#
脚本在 JavaScriptCore(macOS 系统自带的 JS 引擎)上运行。目前这是唯一支持的语言,以前弹出菜单中的 Lua/Python 选项只是占位,已经移除。
Script 节点用直接写名字的方式转换数据帧:读取看到的是当前数据,写入的内容成为节点的输出(并通过回写驱动设备 / 节点)。读取时拼错会报错并显示完整路径。没有设置的字段原样通过。
system.beep = CDJ1.beat.bar; // beep every bar
value.inChorus = (CDJ1.phrase.current == "CHORUS") ? 1 : 0;
if (CDJ1.beat.currentBpm > 130) { CDJ1.select.id = 2; }
CDJ1.status.isQuantizeOn = 1; // device write via CDJ1's deck
source.* 和 output.* 仍然存在,用于需要同时用到一个数据键两侧的情况,即原始值和修改后的值:metadata.title = source.metadata.title + " [LIVE]";
检查器中的单行脚本框适合写一行代码;要认真写脚本,请按 Editor ⤢(在检查器中 Script 标签下面),或者直接双击 Script 节点,Patch 会切换为一个风格和其他面板一致的全屏编辑器:
- 左侧边栏显示行号。
- 语法着色(注释、字符串、数字、关键字、
source/output/g),在你停止输入后不久刷新。 - 实时检查:最后一次按键约 0.6 秒后,脚本会被编译,并用节点当前的实时数据帧试运行,所以乱码、ReferenceError 和
source.*拼写错误都会在底部显示为红色错误(没有问题时显示 ✓ runs on live data)。你输入时,节点会继续运行它上一个可用的代码。 - Tab 补全:在单词中间按 Tab,可以根据这个节点的实际数据键(
source.beat.…)、g.*辅助函数和 JS 基础语法补全;候选项显示在底部。 - ▶ Run(右上角)应用代码并试触发节点一次,和节点上的 ▶ 按钮一样。
- Close(右下角)应用代码并返回 Patch。除此之外,你输入时修改不会实时生效。
system.beep 会发出提示音(边沿触发)。由触发器驱动的 Script 每次触发运行一次;由其他节点驱动时,每一帧都运行。
数据流中的节点参数#
节点名称就是标识符:空格和符号会被自动去掉(输入 "CDJ 1" → 名称变为 CDJ1)。Description 字段(每个节点的 Node 区域中)是自由文本,设置后画布上会显示它而不是名称;名称仍然是地址。新节点会得到唯一的小写带编号名称(device1、router1、matrix1、source1、group1……),你可以把它们改成任何你喜欢的地址(CDJ1、MainDeck)。有一个保留的特例:一个单独的全小写数据命名空间词(device、value、select……)不能作为节点名称;默认名称末尾的数字正是为了避免这种冲突,所以你起的名字里请保留一个数字或大写字母。
数据帧经过一个节点时,这个节点的所有设置都会以 <Name>.<setting> 数据键的形式随之传递,所以下游任何 Live Data monitor 都能看到上游链路的状态,脚本也可以用普通的点语法读取和控制这些节点:
var active = source.CDJ1.select.id; // read an upstream node's state
output.CDJ1.select.id = 2; // control any node by NAME
output.MainRouter.router.active = "Input 2";
output.FXScript.node.bypassed = true; // bypass/stop work too
寻址是按节点名称进行的。它适用于检查器中你看到的每一项设置,适用于 Patch 中的每个节点,可以在任何脚本中使用(包括 Cue 中的脚本),无论数据是否经过了那个节点。读取也是一样:source.FXScript.node.bypassed 会返回那个节点的当前设置。
output.OSC.node.bypassed = true; // bypass the OSC node while the cue runs
output.Resolume.node.stopped = true; // stop a node
output.MainRouter.router.active = "Input 2";
Cue 中的脚本只在 Cue 激活时运行,而节点会保持最后收到的值。如果想在 Cue 结束时恢复原状,可以用 cue.active 上的 State Off 触发器,连接到一个写入 output.OSC.node.bypassed = false; 的脚本。在 Live Data monitor 中覆盖 <Name>.* 行也会以同样的方式驱动节点。嵌套数据现在也使用普通的点:source.mixer.ch1.fader、source.cue.1.color。
parent 和 child(不用名字的寻址)#
两个别名可以让你在脚本中不必把名字写死:
parent= 拥有输入数据的那个身份节点(Device,或数据最后经过的 Router/Matrix;中间的延迟 / 触发器不算)。可读可写:var bar = source.parent.beat.bar; // same as source.device1.beat.bar output.parent.led.play = 1; // press Play on the routed deck output.parent.select.id = 2; // retarget the feeding Device node
child= 直接连在这个脚本后面的节点,只能写(数据向下游流动,所以没有可读的内容)。一次写入会分发给每个直接相连的子节点;子节点没有的设置会被忽略:output.child.gen.fps = "30"; // configure the MTC node after me output.child.node.bypassed = true; // bypass whatever I feed
需要在重新连线后继续工作的脚本使用 parent/child;针对某一个特定节点的脚本使用它的名字。
在脚本中写入设备设置#
在脚本中修改 util.*(DJM 设置)或 setting.*(CDJ 播放器设置)数据键,会把它写入硬件:
output.util.digitalLevel = 1; // on the mixer's frame
output.setting.quantize = 1; // on a deck's frame (deck = device.number)
只有脚本*修改过*(相对于输入)的数据键才会被发送。写入每 2 秒重复一次,直到设备上报的状态确认了它,然后停止发送,所以持续设置某个值的脚本实际上会*强制保持*这个值。对 util.*/setting.* 数据键的 Live Data 覆盖也是一样:覆盖它就会把它推送到设备。output.setting / output.util / output.led / output.status / output.playback 这几部分始终存在(预先创建),所以即使设备还没有上报这些数据键,上面的赋值也能生效。
面板状态也可以:设置 led.* / status.* 数据键会驱动播放器,直到状态一致。有对应设置的开关(status.isMasterTempoOn、status.isQuantizeOn、status.isAutoCueOn、status.isVinylModeOn)通过设置写入完成;其余的只在播放器状态不一致时按下真实按钮(每 2 秒最多按一次):
output.led.play = 1; // deck not playing? → press Play/Pause
output.led.sync = 1; // sync off? → press Sync
output.led.loop = 0; // looping? → press Reloop/Exit
output.status.isQuantizeOn = 1; // via the settings write
也可以通过 playback.state 按名称控制播放,可接受的值:"playing"、"paused"、"cued"(其他状态不能作为命令):
output.playback.state = "paused"; // playing? → press Play/Pause
单向状态:led.cue 和 led.master 只对 1 起作用(播放器不能通过按钮取消 Cue 或交出主控)。led.onAir 不能控制(那是混音台的推子)。*持续*存在的脚本值或覆盖会强制保持这个状态:手动暂停播放器后,2 秒内会被重新按下,直到你删除这一行或恢复覆盖。
任何设备,任何脚本都可以,包括 Cue 中的脚本。 在这部分前面写上 Device 节点的名称(它在 Patch 中的标题)。无论脚本连到哪里,output.<Device>.setting / .util / .led / .status / .playback 都始终存在。读取 source.<Device>.… 也可以:脚本自己的输入中没有的数据键,会从那个 Device 节点的实时数据中补上。
var periodMs = 600; // each on AND each off phase
var on = Math.floor(Date.now() / periodMs) % 2 === 0;
output.cdj2.setting.jogRingBrightness = on ? 2 : 0; // 0-2
output.cdj2.setting.jogLcdBrightness = on ? 5 : 1; // 1-5 (no "off")
if (source.cdj2.status.isQuantizeOn) { /* … */ }
Cue 中的脚本只在 Cue 激活时运行。 在此之外,它的输入原样通过,不会写入任何东西。由触发器触发的脚本仍然会运行,例如在 cue.active State Off 时触发、用来恢复某个设置的脚本。Schedule 项目的图(item.active)也是一样。设备设置会保持脚本最后发送的值,所以如果想恢复,请用 State-Off 触发器写回"正常"值。
注意 setting. 这一部分:output.cdj2.jogRingBrightness(没有它)是 cdj2 这个*节点*的设置,而不是播放器的。超出设置范围的值会被限制在范围内。播放器设置通过网络写入,需要一点时间才能生效,所以非常快的闪烁(远小于半秒)会跳过一些步骤。
在 Graphic 节点上绘图#
Script 可以用 g 辅助对象在连在它后面的 Graphic 节点上绘图。坐标是画布像素(原点在左上角);颜色是 "#RRGGBB"。
g.clear("#101014");
g.rect(4, 4, source.beat.beatInBar * 40, 24, "#33aaff");
g.circle(180, 60, 20 + source.beat.currentBpm % 20, "#ff3399");
g.text(8, 110, source.metadata.title, "#ffffff");
辅助函数:g.clear(color)、g.rect(x,y,w,h,color)、g.line(x1,y1,x2,y2,color)、g.circle(x,y,r,color)、g.text(x,y,string,color)、g.waveform(x,y,w,h,mode,zoom)(缓存的波形,mode 为 "detail"/"overview")、g.artwork(x,y,w,h)(曲目的封面图,已缓存时)。连接 Script → Graphic;Graphic 节点会实时渲染这些命令。
双击 Graphic 节点会在 Patch 的位置打开一个全屏实时查看器(按渲染分辨率加黑边显示,每个引擎周期刷新一次);Close 返回 Patch。
Graphic 节点设置:
- Render Width / Height:脚本绘制所针对的真实输出分辨率(例如 1280×720)。
g.*中的坐标就以这些像素为单位。 - Node Width / Height:节点在画布上的预览大小(也可以拖动节点的右下角)。渲染结果会缩放到合适的大小显示。
- Preview FPS:限制 Patch 中预览的重绘频率(输出运行得更快)。
- Output:Off、Screen(打开一个可调整大小的输出窗口,按完整渲染分辨率加黑边显示,帧率为 Output FPS)或 NDI(发送一个名为 NDI Source Name 的 NDI 源)。
Only Keys the Script Names(更快的脚本)#
Script 节点通常会收到完整的输入数据帧(一台播放器就有几百个数据键),并在每个引擎周期交还一个完整的数据帧,即使脚本只读取一个数据键也是如此。打开 Only Keys the Script Names(节点的 Script 区域),脚本就只会收到它的代码中提到的数据键,也只有它写入的数据键会被修改;其他一切原样通过。它默认是关闭的。
什么算"提到":脚本中每个带点的名称,包括直接写的 CDJ1.beat.bar、source.…、output.…、parent.…(数据帧所属的播放器)以及方括号形式的数据键 x["3.inMs"]。提到一个分组(source.CDJ1.beat)会带上它下面的所有数据键。
以下情况请保持关闭:
- 脚本在运行时才拼出数据键名称(
get("playback." + x)、source[name]、for (k in source)),否则这些数据键会悄悄缺失;或者 - 脚本通过删除数据键来筛选向下游传递的内容,否则被删除的数据键会保留下来。
适合打开的情况:读取几个固定数据键、并写入 value.*、setting.* 或子节点开关的脚本(例如"只在 Drop 期间"的开关,或 jog 灯环闪烁)。
