cue_dsl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +153 -0
- data/bin/run.rb +31 -0
- data/lib/cue_dsl/chain.rb +16 -0
- data/lib/cue_dsl/cheatsheet.rb +59 -0
- data/lib/cue_dsl/connect.rb +28 -0
- data/lib/cue_dsl/cue.rb +65 -0
- data/lib/cue_dsl/device.rb +61 -0
- data/lib/cue_dsl/devices_config.rb +52 -0
- data/lib/cue_dsl/errors.rb +7 -0
- data/lib/cue_dsl/filter_bank.rb +17 -0
- data/lib/cue_dsl/matrix.rb +41 -0
- data/lib/cue_dsl/parameter.rb +33 -0
- data/lib/cue_dsl/receivers_loader.rb +74 -0
- data/lib/cue_dsl/version.rb +4 -0
- data/lib/cue_dsl.rb +211 -0
- metadata +96 -0
data/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# cue_dsl
|
|
2
|
+
|
|
3
|
+
[](https://gitlab.com/nicb/cue_dsl/-/commits/%{default_branch})
|
|
4
|
+
[](https://gitlab.com/nicb/cue_dsl/-/commits/%{default_branch})
|
|
5
|
+
|
|
6
|
+
A small Ruby DSL for generating Pure Data `[text sequence]` cue lists for
|
|
7
|
+
live-electronics performances, plus a cheatsheet of every device parameter.
|
|
8
|
+
|
|
9
|
+
The cue sheet is written to **STDOUT**; the cheatsheet to a file.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
gem build cue_dsl.gemspec
|
|
15
|
+
gem install cue_dsl-*.gem
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Ruby >= 3.0. Runtime deps: none (stdlib `yaml` only).
|
|
19
|
+
|
|
20
|
+
## Files needed next to your DSL
|
|
21
|
+
|
|
22
|
+
- `receivers.txt` — one Pd receiver name per line, e.g. `FILTER_cutoff_recv`.
|
|
23
|
+
Lines starting with a lowercase letter that don't match `reset_<name>_recv`
|
|
24
|
+
or one of the known `system_*` receivers are ignored. Lines containing
|
|
25
|
+
`$1` / `$2` templates are skipped.
|
|
26
|
+
- `devices.yml` — static routing for each device:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
matrix:
|
|
30
|
+
rows: 9
|
|
31
|
+
cols: 8
|
|
32
|
+
|
|
33
|
+
devices:
|
|
34
|
+
DRY:
|
|
35
|
+
label: "Dry"
|
|
36
|
+
inputs: [0]
|
|
37
|
+
outputs: [0]
|
|
38
|
+
FILTER:
|
|
39
|
+
label: "Filter"
|
|
40
|
+
inputs: [1]
|
|
41
|
+
outputs: [1]
|
|
42
|
+
filters: 12 # enables filter.fc(0..11).cutoff / .Q
|
|
43
|
+
CE:
|
|
44
|
+
label: "Echo Chamber"
|
|
45
|
+
inputs: [2]
|
|
46
|
+
outputs: [2, 3] # two outputs
|
|
47
|
+
OUT:
|
|
48
|
+
label: "Output"
|
|
49
|
+
inputs: [6, 7] # two inputs
|
|
50
|
+
outputs: []
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Writing cues
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# cues.rb
|
|
57
|
+
cue "Intro" do
|
|
58
|
+
comment "Opening — filter sweep in", kind: :gui
|
|
59
|
+
connect DRY >> FILTER >> CE >> OUT
|
|
60
|
+
filter.cutoff 800
|
|
61
|
+
filter.mod_onoff :off
|
|
62
|
+
filter.fc(0).cutoff 400
|
|
63
|
+
rm.mod_freq 2.5
|
|
64
|
+
mtx.at(0, 1) 1
|
|
65
|
+
system.cue_step "go"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
cue "Climax" do
|
|
69
|
+
connect DRY >> FILTER >> CE >> OUT
|
|
70
|
+
filter.cutoff 3000
|
|
71
|
+
reset :CE
|
|
72
|
+
system.cue_step "pause"
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### DSL verbs
|
|
77
|
+
|
|
78
|
+
- `cue "name" do … end` — one cue. Name is required and non-empty.
|
|
79
|
+
- `connect A >> B >> C …` — installs matrix cells from the static routing.
|
|
80
|
+
Supports 1→1, 1→2 (fan-out), 2→1 (output 1 only), 2→2 (pairwise).
|
|
81
|
+
Any other geometry raises `ConnectionError`.
|
|
82
|
+
- `device.param value` — emits `<receiver> <value>` in the cue sheet.
|
|
83
|
+
- `device.fc(i).param value` — filter-bank parameter (`i` in `0..11`).
|
|
84
|
+
- `mtx.at(r, c) value` — explicit matrix cell write, in source order.
|
|
85
|
+
- `reset :CE` / `:PS` / `:RM` / `:SA` / `:matrix` — emits `reset_<name>_recv bang`.
|
|
86
|
+
- `system.cue_step "go"` — emits `cue_step_recv go`.
|
|
87
|
+
`system.header_message` is **forbidden** — see below.
|
|
88
|
+
- `comment "text"` (or `kind: :gui` for `##`) — passthrough comment.
|
|
89
|
+
|
|
90
|
+
### Matrix semantics
|
|
91
|
+
|
|
92
|
+
Rows are inputs, columns are outputs. A receiver MTX_<row>_<col>_recv addresses input <row> and output <col>.
|
|
93
|
+
In devices.yml, inputs: lists rows and outputs: lists columns. A link A >> B enables MTX[B_input_row][A_output_col].
|
|
94
|
+
|
|
95
|
+
### Header & pause
|
|
96
|
+
|
|
97
|
+
Every rendered cue starts with:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
reset_matrix_recv bang # if CueDSL.reset_matrix is on
|
|
101
|
+
header_message_recv N\ -\ <cue name> # N is 0-based
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Spaces, commas and semicolons in the header are escaped (`\ `, `\,`, `\;`).
|
|
105
|
+
|
|
106
|
+
Every rendered cue ends with a literal `pause` line.
|
|
107
|
+
|
|
108
|
+
`system.header_message` raises an error; the cue name is the source of truth.
|
|
109
|
+
|
|
110
|
+
## Running
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
require "cue_dsl"
|
|
114
|
+
|
|
115
|
+
CueDSL.configure do |c|
|
|
116
|
+
c.reset_matrix = true # default true
|
|
117
|
+
c.cheatsheet_path = "cheatsheet.txt" # default nil (no file)
|
|
118
|
+
c.cue_numbers_path = "cue_list_numbers.txt" # default nil (no file)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
CueDSL.run("cues.rb") # writes cue sheet to $stdout
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`CueDSL.cue_numbers_path = "cue_list_numbers.txt"` (default `nil`) writes an index of `<line> <escaped cue name>`,
|
|
125
|
+
one entry per cue, where `<line>` is the 0-based line in the cue sheet where
|
|
126
|
+
the cue starts (the `reset_system_recv` bang line). Only cues rendered with `reset_system: true` appear;
|
|
127
|
+
the file is intended for building a goto mechanism against `[text sequence]`.
|
|
128
|
+
|
|
129
|
+
## Tests
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
rake test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Layout
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
lib/cue_dsl.rb # Context, DSL verbs, run()
|
|
139
|
+
lib/cue_dsl/device.rb
|
|
140
|
+
lib/cue_dsl/filter_bank.rb
|
|
141
|
+
lib/cue_dsl/matrix.rb
|
|
142
|
+
lib/cue_dsl/connect.rb
|
|
143
|
+
lib/cue_dsl/cue.rb
|
|
144
|
+
lib/cue_dsl/parameter.rb
|
|
145
|
+
lib/cue_dsl/receivers_loader.rb
|
|
146
|
+
lib/cue_dsl/devices_config.rb
|
|
147
|
+
lib/cue_dsl/cheatsheet.rb
|
|
148
|
+
test/** # minitest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
{width=66} [GNU GPL 3 License](./LICENSE)
|
data/bin/run.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run the cue DSL for a performance directory.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ruby -I path/to/cue_dsl/lib path/to/cue_dsl/bin/run.rb [cue_list.rb] > cue_list.txt
|
|
8
|
+
#
|
|
9
|
+
# Run from the performance directory (the one containing cue_list.rb,
|
|
10
|
+
# receivers.txt and devices.yml). All relative paths below are resolved
|
|
11
|
+
# against the current working directory, not against this script.
|
|
12
|
+
#
|
|
13
|
+
# With no argument, defaults to "cue_list.rb".
|
|
14
|
+
|
|
15
|
+
require "cue_dsl"
|
|
16
|
+
|
|
17
|
+
dsl_path = ARGV[0] || "cue_list.rb"
|
|
18
|
+
|
|
19
|
+
unless File.exist?(dsl_path)
|
|
20
|
+
warn "run.rb: DSL file not found: #{dsl_path}"
|
|
21
|
+
warn "run.rb: usage: ruby -I <lib> bin/run.rb [cue_list.rb] > cue_list.txt"
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
CueDSL.configure do |c|
|
|
26
|
+
c.reset_system = false # default false
|
|
27
|
+
c.cheatsheet_path = "cheatsheet.txt" # default nil (no file)
|
|
28
|
+
c.cue_numbers_path = "cue_list_numbers.txt" # default nil (no file)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
CueDSL.run(dsl_path) # writes cue sheet to $stdout
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# lib/cue_dsl/chain.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
# Built up by Device#> and Chain#>. Passed to Context#connect.
|
|
4
|
+
class Chain
|
|
5
|
+
attr_reader :devices
|
|
6
|
+
|
|
7
|
+
def initialize(devices)
|
|
8
|
+
@devices = devices.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def >>(other)
|
|
12
|
+
raise ConnectionError, "cannot append #{other.inspect} to chain" unless other.is_a?(Device)
|
|
13
|
+
Chain.new(@devices + [other])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# lib/cue_dsl/cheatsheet.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class Cheatsheet
|
|
4
|
+
AUTO_EMITTED = {
|
|
5
|
+
header_message: "auto-emitted from cue name; do not call",
|
|
6
|
+
preview_cue: "auto-emitted from next cue name; do not call"
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
def self.render(devices:, matrix:, system_params:, reset_verbs:)
|
|
10
|
+
out = []
|
|
11
|
+
devices.each do |dev|
|
|
12
|
+
out << "=== #{dev.name} (#{dev.label}) ==="
|
|
13
|
+
dev.parameters.sort_by { |k, _| k.to_s }.each do |short, param|
|
|
14
|
+
next unless param.kind == :normal
|
|
15
|
+
out << format(" %-24s %-36s %s", short, param.pd_name, "#{dev.name.downcase}.#{short}(value)")
|
|
16
|
+
end
|
|
17
|
+
if dev.filter_count.positive?
|
|
18
|
+
dev.filter_count.times do |i|
|
|
19
|
+
params = dev.filter_fc_params_for(i)
|
|
20
|
+
params.sort_by { |k, _| k.to_s }.each do |name, param|
|
|
21
|
+
out << format(" %-24s %-36s %s",
|
|
22
|
+
"fc(#{i}).#{name}", param.pd_name,
|
|
23
|
+
"#{dev.name.downcase}.fc(#{i}).#{name}(value)")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
out << ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
out << "=== MTX (Matrix #{matrix.rows}x#{matrix.cols} — rows=inputs, cols=outputs) ==="
|
|
31
|
+
matrix.rows.times do |r|
|
|
32
|
+
matrix.cols.times do |c|
|
|
33
|
+
out << format(" %-24s %-36s %s",
|
|
34
|
+
"at(#{r},#{c})", "MTX_#{r}_#{c}_recv",
|
|
35
|
+
"mtx.at(#{r}, #{c}, value)")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
out << ""
|
|
39
|
+
|
|
40
|
+
out << "=== system ==="
|
|
41
|
+
system_params.each do |sym, param|
|
|
42
|
+
if AUTO_EMITTED.key?(sym)
|
|
43
|
+
out << format(" %-24s %-36s (%s)", sym, param.pd_name, AUTO_EMITTED[sym])
|
|
44
|
+
else
|
|
45
|
+
out << format(" %-24s %-36s %s", sym, param.pd_name, "system.#{sym}(value)")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
out << ""
|
|
49
|
+
|
|
50
|
+
out << "=== resets ==="
|
|
51
|
+
reset_verbs.each do |sym|
|
|
52
|
+
out << format(" %-24s %-36s %s", sym, "reset_#{sym}_recv", "reset :#{sym}")
|
|
53
|
+
end
|
|
54
|
+
out << ""
|
|
55
|
+
|
|
56
|
+
out.join("\n")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# lib/cue_dsl/connect.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
module Connect
|
|
4
|
+
# Matrix semantics: rows = inputs, cols = outputs.
|
|
5
|
+
# A >> B sets MTX[B_input_row][A_output_col] = 1.
|
|
6
|
+
# Returns [[row, col], ...] of matrix cells to set to 1.
|
|
7
|
+
def self.cells_for(a, b)
|
|
8
|
+
a_out = a.outputs
|
|
9
|
+
b_in = b.inputs
|
|
10
|
+
raise ConnectionError, "#{a.name} has no outputs" if a_out.empty?
|
|
11
|
+
raise ConnectionError, "#{b.name} has no inputs" if b_in.empty?
|
|
12
|
+
|
|
13
|
+
if a_out.length == 1 && b_in.length == 1
|
|
14
|
+
[[b_in[0], a_out[0]]]
|
|
15
|
+
elsif a_out.length == 1 && b_in.length == 2
|
|
16
|
+
[[b_in[0], a_out[0]], [b_in[1], a_out[0]]]
|
|
17
|
+
elsif a_out.length == 2 && b_in.length == 1
|
|
18
|
+
[[b_in[0], a_out[0]]]
|
|
19
|
+
elsif a_out.length == 2 && b_in.length == 2
|
|
20
|
+
[[b_in[0], a_out[0]], [b_in[1], a_out[1]]]
|
|
21
|
+
else
|
|
22
|
+
raise ConnectionError,
|
|
23
|
+
"cannot connect #{a.name}(#{a_out.length} out) >> " \
|
|
24
|
+
"#{b.name}(#{b_in.length} in); unsupported port geometry"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/cue_dsl/cue.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# lib/cue_dsl/cue.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class Cue
|
|
4
|
+
Action = Struct.new(:kind, :payload, keyword_init: true)
|
|
5
|
+
# kind: :param, :matrix, :reset, :system, :comment, :chain
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :line, :devices, :actions, :resets
|
|
8
|
+
|
|
9
|
+
def initialize(name, line, reset_system: nil)
|
|
10
|
+
raise ConfigurationError, "cue name cannot be empty" if name.nil? || name.to_s.strip.empty?
|
|
11
|
+
@name = name.to_s
|
|
12
|
+
@line = line
|
|
13
|
+
@devices = []
|
|
14
|
+
@actions = []
|
|
15
|
+
@resets = []
|
|
16
|
+
@reset_system = reset_system # nil means "use the global default"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_device(dev)
|
|
20
|
+
@devices << dev unless @devices.include?(dev)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record_param(line); @actions << Action.new(kind: :param, payload: line); end
|
|
24
|
+
def record_matrix(line); @actions << Action.new(kind: :matrix, payload: line); end
|
|
25
|
+
def record_system(line); @actions << Action.new(kind: :system, payload: line); end
|
|
26
|
+
def record_comment(text); @actions << Action.new(kind: :comment, payload: text); end
|
|
27
|
+
def record_reset(line); @actions << Action.new(kind: :reset, payload: line); end
|
|
28
|
+
def record_chain_marker(chain_devices)
|
|
29
|
+
@actions << Action.new(kind: :chain, payload: chain_devices)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# preview: nil (no preview line), or a string already formatted as "<n> - <name>".
|
|
33
|
+
def render(index:, matrix:, reset_system: true, header: nil, preview: nil)
|
|
34
|
+
header ||= "#{index} - #{name}"
|
|
35
|
+
out = []
|
|
36
|
+
out << "reset_system_recv bang" if reset_system
|
|
37
|
+
out << "header_message_recv #{escape(header)}"
|
|
38
|
+
out << "preview_cue_recv #{escape(preview)}" unless preview.nil?
|
|
39
|
+
|
|
40
|
+
@actions.each do |a|
|
|
41
|
+
case a.kind
|
|
42
|
+
when :chain
|
|
43
|
+
a.payload.each_cons(2) do |x, y|
|
|
44
|
+
Connect.cells_for(x, y).each { |(r, c)| out << "MTX_#{r}_#{c}_recv 1" }
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
out << a.payload
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
out << "pause"
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reset_system?
|
|
56
|
+
@reset_system
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def escape(str)
|
|
62
|
+
str.gsub(/[ ,;]/) { |m| "\\#{m}" }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# lib/cue_dsl/device.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class Device
|
|
4
|
+
attr_reader :name, :label, :inputs, :outputs, :parameters, :filter_count
|
|
5
|
+
|
|
6
|
+
def initialize(name:, label:, inputs:, outputs:, parameters:, filter_count: 0)
|
|
7
|
+
@name = name
|
|
8
|
+
@label = label
|
|
9
|
+
@inputs = inputs
|
|
10
|
+
@outputs = outputs
|
|
11
|
+
@parameters = parameters
|
|
12
|
+
@filter_count = filter_count
|
|
13
|
+
@filter_bank = {}
|
|
14
|
+
@cue = nil
|
|
15
|
+
@filter_fc_params_by_index = Hash.new { |h, k| h[k] = {} }
|
|
16
|
+
|
|
17
|
+
parameters.each do |short, param|
|
|
18
|
+
next unless param.kind == :normal
|
|
19
|
+
next unless short.is_a?(Symbol) || short.is_a?(String)
|
|
20
|
+
define_singleton_method(short) do |value, ms = nil|
|
|
21
|
+
record_param(param.emit(value, ms))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if filter_count.positive?
|
|
26
|
+
define_singleton_method(:fc) do |i|
|
|
27
|
+
unless (0...filter_count).cover?(i)
|
|
28
|
+
raise ArgumentError, "FC index out of range 0..#{filter_count - 1}"
|
|
29
|
+
end
|
|
30
|
+
@filter_bank[i] ||= FilterBank.new(self, i)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def >>(other)
|
|
36
|
+
Chain.new([self, other])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def filter_fc_params_for(index)
|
|
40
|
+
@filter_fc_params_by_index[index] || {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register_filter_fc(param)
|
|
44
|
+
name = param.short_name.to_s.sub(/\Afc_#{param.index}_/, "").to_sym
|
|
45
|
+
@filter_fc_params_by_index[param.index][name] = param
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bind(cue); @cue = cue; end
|
|
49
|
+
def unbind; @cue = nil; end
|
|
50
|
+
|
|
51
|
+
def record_param(line)
|
|
52
|
+
raise Error, "device #{name} used outside a cue" if @cue.nil?
|
|
53
|
+
@cue.record_param(line)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_matrix(line)
|
|
57
|
+
raise Error, "matrix used outside a cue" if @cue.nil?
|
|
58
|
+
@cue.record_matrix(line)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# lib/cue_dsl/devices_config.rb
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module CueDSL
|
|
5
|
+
class DevicesConfig
|
|
6
|
+
Entry = Struct.new(:name, :label, :inputs, :outputs, :filter_count, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
attr_reader :matrix_rows, :matrix_cols, :entries
|
|
9
|
+
|
|
10
|
+
def self.load(path)
|
|
11
|
+
new(YAML.safe_load(File.read(path), permitted_classes: [Symbol]) || {})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(hash)
|
|
15
|
+
@matrix_rows = hash.dig("matrix", "rows") or
|
|
16
|
+
raise ConfigurationError, "devices.yml: missing matrix.rows"
|
|
17
|
+
@matrix_cols = hash.dig("matrix", "cols") or
|
|
18
|
+
raise ConfigurationError, "devices.yml: missing matrix.cols"
|
|
19
|
+
@entries = {}
|
|
20
|
+
(hash["devices"] || {}).each do |name, spec|
|
|
21
|
+
@entries[name] = Entry.new(
|
|
22
|
+
name: name,
|
|
23
|
+
label: spec["label"] || name,
|
|
24
|
+
inputs: Array(spec["inputs"]),
|
|
25
|
+
outputs: Array(spec["outputs"]),
|
|
26
|
+
filter_count: spec["filters"].to_i
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve(token)
|
|
32
|
+
key = normalize(token)
|
|
33
|
+
return @entries[key] if @entries.key?(key)
|
|
34
|
+
found = @entries.values.find { |e| normalize(e.label) == key }
|
|
35
|
+
return found if found
|
|
36
|
+
raise UnknownDeviceError,
|
|
37
|
+
"unknown device #{token.inspect} (try one of #{@entries.keys.join(', ')})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def normalize(str)
|
|
43
|
+
str.to_s
|
|
44
|
+
.unicode_normalize(:nfkd)
|
|
45
|
+
.encode("ASCII", invalid: :replace, undef: :replace, replace: "")
|
|
46
|
+
.upcase
|
|
47
|
+
.gsub(/[^\p{Alnum}]+/, "_")
|
|
48
|
+
.sub(/\A_+/, "")
|
|
49
|
+
.sub(/_+\z/, "")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# lib/cue_dsl/filter_bank.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class FilterBank
|
|
4
|
+
attr_reader :index
|
|
5
|
+
|
|
6
|
+
def initialize(owner, index)
|
|
7
|
+
@owner = owner
|
|
8
|
+
@index = index
|
|
9
|
+
params = owner.filter_fc_params_for(index)
|
|
10
|
+
params.each do |short, param|
|
|
11
|
+
define_singleton_method(short) do |value, ms = nil|
|
|
12
|
+
@owner.record_param(param.emit(value, ms))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# lib/cue_dsl/matrix.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class Matrix < Device
|
|
4
|
+
UNSET = Object.new.freeze
|
|
5
|
+
attr_reader :rows, :cols
|
|
6
|
+
|
|
7
|
+
def initialize(rows:, cols:, parameters:)
|
|
8
|
+
super(name: "MTX", label: "Matrix", inputs: [], outputs: [], parameters: parameters)
|
|
9
|
+
@rows = rows
|
|
10
|
+
@cols = cols
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# mtx.at(r, c, v) -> records the message immediately
|
|
14
|
+
# mtx.at(r, c).(v) -> defers via CellSetter#call
|
|
15
|
+
def at(row, col, value = UNSET)
|
|
16
|
+
raise ArgumentError, "row #{row} out of range 0..#{rows - 1}" unless (0...rows).cover?(row)
|
|
17
|
+
raise ArgumentError, "col #{col} out of range 0..#{cols - 1}" unless (0...cols).cover?(col)
|
|
18
|
+
return CellSetter.new(self, row, col) if value.equal?(UNSET)
|
|
19
|
+
record_matrix("MTX_#{row}_#{col}_recv #{self.class.format_value(value)}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.format_value(v)
|
|
23
|
+
case v
|
|
24
|
+
when Symbol then v.to_s
|
|
25
|
+
when Float then v == v.to_i ? v.to_i.to_s : v.to_s
|
|
26
|
+
else v.to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class CellSetter
|
|
31
|
+
def initialize(matrix, row, col)
|
|
32
|
+
@matrix, @row, @col = matrix, row, col
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set(v)
|
|
36
|
+
@matrix.record_matrix("MTX_#{@row}_#{@col}_recv #{Matrix.format_value(v)}")
|
|
37
|
+
end
|
|
38
|
+
alias_method :call, :set
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# lib/cue_dsl/parameter.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class Parameter
|
|
4
|
+
attr_reader :pd_name, :device_prefix, :short_name, :kind, :index
|
|
5
|
+
|
|
6
|
+
def initialize(pd_name:, device_prefix:, short_name:, kind: :normal, index: nil)
|
|
7
|
+
@pd_name = pd_name
|
|
8
|
+
@device_prefix = device_prefix
|
|
9
|
+
@short_name = short_name
|
|
10
|
+
@kind = kind
|
|
11
|
+
@index = index
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def emit(value, ms = nil)
|
|
15
|
+
if ms.nil?
|
|
16
|
+
"#{pd_name} #{format_value(value)}"
|
|
17
|
+
else
|
|
18
|
+
"#{pd_name} #{format_value(value)} #{format_value(ms)}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def format_value(value)
|
|
23
|
+
case value
|
|
24
|
+
when Symbol then value.to_s
|
|
25
|
+
when Float
|
|
26
|
+
return value.to_i.to_s if value == value.to_i
|
|
27
|
+
value.to_s
|
|
28
|
+
else
|
|
29
|
+
value.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# lib/cue_dsl/receivers_loader.rb
|
|
2
|
+
module CueDSL
|
|
3
|
+
class ReceiversLoader
|
|
4
|
+
RESET_RE = /\Areset_(\w+)_recv\z/
|
|
5
|
+
TEMPLATE_RE = /\$[0-9]/
|
|
6
|
+
SYSTEM_RE = /\A(cue_step|git_describe|header_message|preview_cue|system_init)_recv\z/
|
|
7
|
+
FILTER_FC_RE = /\AFILTER_fc_(\d+)_(\w+)_recv\z/
|
|
8
|
+
|
|
9
|
+
Result = Struct.new(:devices, :matrix_params, :system_params, :reset_verbs, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
def self.load(path)
|
|
12
|
+
new(File.readlines(path, chomp: true)).load
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(lines)
|
|
16
|
+
@lines = lines.map(&:strip).reject(&:empty?)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load
|
|
20
|
+
devices = Hash.new { |h, k| h[k] = {} }
|
|
21
|
+
matrix_params = {}
|
|
22
|
+
system_params = {}
|
|
23
|
+
reset_verbs = []
|
|
24
|
+
|
|
25
|
+
@lines.each do |line|
|
|
26
|
+
next if line.match?(TEMPLATE_RE)
|
|
27
|
+
|
|
28
|
+
if (m = RESET_RE.match(line))
|
|
29
|
+
reset_verbs << m[1].to_sym
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if (m = SYSTEM_RE.match(line))
|
|
34
|
+
system_params[m[1].to_sym] = Parameter.new(
|
|
35
|
+
pd_name: line, device_prefix: "system", short_name: m[1], kind: :system
|
|
36
|
+
)
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if (m = /\AMTX_(\d+)_(\d+)_recv\z/.match(line))
|
|
41
|
+
r, c = m[1].to_i, m[2].to_i
|
|
42
|
+
matrix_params[[r, c]] = Parameter.new(
|
|
43
|
+
pd_name: line, device_prefix: "MTX", short_name: "#{r}_#{c}"
|
|
44
|
+
)
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
next unless line =~ /\A[A-Z]/
|
|
49
|
+
|
|
50
|
+
if (m = FILTER_FC_RE.match(line))
|
|
51
|
+
idx = m[1].to_i
|
|
52
|
+
name = m[2]
|
|
53
|
+
devices["FILTER"][:"fc_#{idx}_#{name}"] = Parameter.new(
|
|
54
|
+
pd_name: line, device_prefix: "FILTER",
|
|
55
|
+
short_name: "fc_#{idx}_#{name}", kind: :filter_fc, index: idx
|
|
56
|
+
)
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
prefix, *rest = line.sub(/_recv\z/, "").split("_")
|
|
61
|
+
short = rest.join("_")
|
|
62
|
+
next if short.empty?
|
|
63
|
+
devices[prefix][short] = Parameter.new(
|
|
64
|
+
pd_name: line, device_prefix: prefix, short_name: short
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Result.new(
|
|
69
|
+
devices: devices, matrix_params: matrix_params,
|
|
70
|
+
system_params: system_params, reset_verbs: reset_verbs
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|