ramekin 0.1.9 → 0.2.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 +4 -4
- data/README.md +15 -5
- data/lib/ramekin/bends.rb +172 -0
- data/lib/ramekin/cli.rb +10 -0
- data/lib/ramekin/legato.rb +23 -11
- data/lib/ramekin/note_aggregator.rb +31 -1
- data/lib/ramekin/renderer.rb +17 -18
- data/lib/ramekin.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78c8f609fa645c1807ac00bf970bf5d05fc78015c185a50d55410a81faa888a4
|
4
|
+
data.tar.gz: aab9adb5b304bbcedc94aee84ca95ddd4bf70307d5f807fcbca93944c79c109f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42986dbf7c0e96bf7e1ecff4552b052cc99fa662f7e648f8aa16e5e5f668e76759b753f563a11a315c530d2d6ef86867a1f7cb0d1080a1a6d7cfdb0a3c8ac908
|
7
|
+
data.tar.gz: b7e71fecb8146fbd9b2a8391a827db80c95bff25deff7bc15a50619f8d9f8264346894dd4653629cfe36dc905b4b21c0ddf96c3054a4a2e610d5fe11759c11a6
|
data/README.md
CHANGED
@@ -108,7 +108,7 @@ A list of some ramekin preprocessor features (see `spec/rmk` for some examples!)
|
|
108
108
|
* `#adsr:A,D,S,RR` syntax for ADSR. Also usable on an `#instrument` declaration, with the same syntax. A, D, and R are flipped so that higher numbers are longer times.
|
109
109
|
* `v+X` and `v-X` are volumes relative to the last `vXXX` command. Useful for separating expression from mixing.
|
110
110
|
* `yLX` and `yRX` commands for panning left and right. `yC` to set back to center. Way easier for me to remember.
|
111
|
-
*
|
111
|
+
* The implementation of `&` is more robust, and it compiles to a proper `$dd` command. It can also much more flexibly chain multiple bends together, using a non-breaking tie `^^X` to establish where the boundaries of the bend should be, and handles quite a few idiosyncracies of `$dd` automatically behind the scenes. You should be able to use `&` and `^^` for all pitch bending needs. See "Pitch Expression" below for details.
|
112
112
|
```elisp
|
113
113
|
; bends to c two octaves up for two 16th notes, then continues for a quarter note
|
114
114
|
b8 #bend >>c16^16 ^^4
|
@@ -335,12 +335,22 @@ Volume can be set with `vXXX`, where `XXX` is a number between 0 and 255. This c
|
|
335
335
|
Pitch expression can be accomplished in two ways:
|
336
336
|
|
337
337
|
* `pA,B,C`, where A,B, and C are numbers, can be used to set vibrato - a slight pitch wiggle that starts sometime after a note is started. `A` specifies the delay - how long to wait before wiggling the pitch, `B` specifies the speed - how fast to wiggle the pitch, and `C` specifies the amplitude - how much to wiggle the pitch. Turn off vibrato with `p0,0`.
|
338
|
-
* Manual pitch bends are fairly straightforward in ramekin, using
|
338
|
+
* Manual pitch bends are fairly straightforward in ramekin, using `&` and `^^`:
|
339
339
|
|
340
340
|
```elisp
|
341
|
-
;
|
342
|
-
|
343
|
-
|
341
|
+
; bend b8 and then hold on c for the rest of the bar
|
342
|
+
b8 & >c4.^2
|
343
|
+
|
344
|
+
; multiple bends can be safely strung together
|
345
|
+
b8 & >c8 & d2
|
346
|
+
|
347
|
+
; to hold on a note and then do further bends, use a non-breaking tie ^^.
|
348
|
+
; this will hold steady on c, and then bend for an eighth note to the e.
|
349
|
+
b8 & c8^4 ^^8 & e8
|
350
|
+
|
351
|
+
; to bend at the very end, use a zero-length note.
|
352
|
+
; this falls to g and then stops.
|
353
|
+
c4 & <g0
|
344
354
|
```
|
345
355
|
|
346
356
|
#### Panning Expression
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Bend < NoteEvent
|
3
|
+
attr_reader :notes
|
4
|
+
def initialize(*notes)
|
5
|
+
@notes = notes
|
6
|
+
end
|
7
|
+
|
8
|
+
def ticks
|
9
|
+
@notes.map(&:ticks).sum
|
10
|
+
end
|
11
|
+
|
12
|
+
# special handling for this in legato.rb
|
13
|
+
# since this doesn't play nice with LegatoLastNote
|
14
|
+
def end_legato!
|
15
|
+
@end_legato = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"#bend(#{@notes.map(&:inspect).join('&')})"
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@notes.first.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def fin
|
27
|
+
@notes.last.fin
|
28
|
+
end
|
29
|
+
|
30
|
+
def rest?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def tie?
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def ending_octave
|
39
|
+
@notes.first.octave
|
40
|
+
end
|
41
|
+
|
42
|
+
# in the event of a tie-free bend exactly at a note out where we *also*
|
43
|
+
# have to trigger legato mid-way, we need to use separate splitting logic
|
44
|
+
# to make sure we toggle legato at least 2 ticks from the note end.
|
45
|
+
#
|
46
|
+
# this is complicated by the fact that $dd is sensitive to ties.
|
47
|
+
#
|
48
|
+
# this means there is a possibility we will have to make the bend 2 ticks
|
49
|
+
# shorter than what was actually specified - not a huge difference in sound,
|
50
|
+
# but it has to be handled carefully.
|
51
|
+
def render_massive_special_case(oct, div)
|
52
|
+
from = @notes[0]
|
53
|
+
to = @notes[1]
|
54
|
+
total_duration = (from.ticks + to.ticks) / div
|
55
|
+
bend_duration = from.ticks / div
|
56
|
+
|
57
|
+
leftover = total_duration - bend_duration
|
58
|
+
|
59
|
+
# ensure there is some leftover so we can toggle legato
|
60
|
+
# before the end
|
61
|
+
if leftover < 2
|
62
|
+
adjust = 2 - leftover
|
63
|
+
bend_duration -= adjust
|
64
|
+
leftover += adjust
|
65
|
+
if bend_duration < 0
|
66
|
+
return error! 'bend duration too small to insert legato-off toggle', el: self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
out = StringIO.new
|
71
|
+
|
72
|
+
out << from.octave_amk(oct)
|
73
|
+
out << from.note_name
|
74
|
+
out << "=#{bend_duration}"
|
75
|
+
out << dd(bend_duration, to)
|
76
|
+
out << "$f4$01^=#{leftover}"
|
77
|
+
|
78
|
+
out.string
|
79
|
+
end
|
80
|
+
|
81
|
+
def massive_special_case?(divisor)
|
82
|
+
# special case only matters if this is the last note in a legato chain
|
83
|
+
return false unless @end_legato
|
84
|
+
|
85
|
+
# if there's more than one note to bend, the normal handling will
|
86
|
+
# toggle in between them
|
87
|
+
return false unless @notes.size == 2
|
88
|
+
|
89
|
+
# if the bend is being split up anyways (with a leftover of at least 2
|
90
|
+
# ticks) then normal handling will toggle in between the split
|
91
|
+
return false unless (@notes[1].ticks+@notes[0].ticks) / divisor <= 0x62
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_amk(octave, divisor)
|
96
|
+
if massive_special_case?(divisor)
|
97
|
+
return render_massive_special_case(octave, divisor)
|
98
|
+
end
|
99
|
+
|
100
|
+
out = StringIO.new
|
101
|
+
|
102
|
+
(0...@notes.length-1).each do |i|
|
103
|
+
note = @notes[i]
|
104
|
+
to_note = @notes[i+1]
|
105
|
+
|
106
|
+
if i == 0
|
107
|
+
ticks = note.ticks + to_note.ticks
|
108
|
+
out << note.octave_amk(octave)
|
109
|
+
out << note.note_name
|
110
|
+
else
|
111
|
+
ticks = to_note.ticks
|
112
|
+
out << '^'
|
113
|
+
end
|
114
|
+
|
115
|
+
ticks /= divisor
|
116
|
+
|
117
|
+
# $dd doesn't work properly if the note
|
118
|
+
# beforehand is more than a half note, so we have to
|
119
|
+
# split it up
|
120
|
+
clamped_ticks = [0x60, ticks].min
|
121
|
+
leftover_ticks = [0, ticks-0x60].max
|
122
|
+
|
123
|
+
# always use tick count notation here because
|
124
|
+
# amk can insert ties in some cases which will break $dd
|
125
|
+
out << "=#{clamped_ticks}"
|
126
|
+
out << dd(note.ticks, to_note)
|
127
|
+
out << '$f4$01' if @end_legato && i == 0
|
128
|
+
out << "^=#{leftover_ticks}" if leftover_ticks > 0
|
129
|
+
end
|
130
|
+
|
131
|
+
out.string
|
132
|
+
end
|
133
|
+
|
134
|
+
def dd(dur, note)
|
135
|
+
sprintf '$dd$00$%02x$%02x', dur, note.note_hex
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class Bends < Processor
|
140
|
+
def flush!
|
141
|
+
yield @last_note if @last_note
|
142
|
+
@last_note = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def call(&b)
|
146
|
+
each do |el|
|
147
|
+
if NoteEvent === el && !el.rest?
|
148
|
+
flush!(&b)
|
149
|
+
@last_note = el
|
150
|
+
el.meta[:bends] = []
|
151
|
+
elsif Token === el && el.type == :amp
|
152
|
+
next error! 'bend must be followed by a note' unless NoteEvent === peek
|
153
|
+
next error! 'cannot bend to a rest' if peek.rest?
|
154
|
+
next error! 'bend must come after a note' if @last_note.nil?
|
155
|
+
|
156
|
+
to_note = next!
|
157
|
+
|
158
|
+
if Bend === @last_note
|
159
|
+
@last_note.notes << to_note
|
160
|
+
else
|
161
|
+
@last_note = Bend.new(@last_note, to_note)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
flush!(&b)
|
165
|
+
yield el
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
flush!(&b)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/lib/ramekin/cli.rb
CHANGED
@@ -97,7 +97,14 @@ module Ramekin
|
|
97
97
|
exit 1
|
98
98
|
end
|
99
99
|
|
100
|
+
def warn_unless_setup!
|
101
|
+
unless AMKSetup.setup_ok?
|
102
|
+
$stderr.puts "WARNING: ramekin is not set up. Have you run `ramekin setup`?"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
100
106
|
def compile(*argv)
|
107
|
+
warn_unless_setup!
|
101
108
|
@force = false
|
102
109
|
@play = false
|
103
110
|
|
@@ -178,6 +185,7 @@ module Ramekin
|
|
178
185
|
|
179
186
|
inner_chain = Processor.compose(
|
180
187
|
NoteAggregator,
|
188
|
+
Bends,
|
181
189
|
RestAggregator,
|
182
190
|
Legato,
|
183
191
|
Inspector,
|
@@ -258,6 +266,8 @@ module Ramekin
|
|
258
266
|
end
|
259
267
|
|
260
268
|
def package(*argv)
|
269
|
+
warn_unless_setup!
|
270
|
+
|
261
271
|
@mode = nil
|
262
272
|
@update = false
|
263
273
|
@search = nil
|
data/lib/ramekin/legato.rb
CHANGED
@@ -22,6 +22,10 @@ module Ramekin
|
|
22
22
|
@note.fin
|
23
23
|
end
|
24
24
|
|
25
|
+
def tie?
|
26
|
+
@note.tie?
|
27
|
+
end
|
28
|
+
|
25
29
|
def rest?
|
26
30
|
@note.rest?
|
27
31
|
end
|
@@ -78,14 +82,24 @@ module Ramekin
|
|
78
82
|
last = buffer.pop
|
79
83
|
error! "legato marks must follow a note", el: last unless NoteEvent === last
|
80
84
|
buffer.each(&b)
|
81
|
-
|
85
|
+
|
86
|
+
# ending legato on a bend is complicated - let Bend handle it
|
87
|
+
case last
|
88
|
+
when Bend then last.end_legato!; yield last
|
89
|
+
else yield LegatoLastNote.new(last)
|
90
|
+
end
|
82
91
|
buffer.clear
|
83
92
|
ensure
|
84
93
|
non_notes.reverse_each(&b)
|
85
94
|
end
|
86
95
|
|
96
|
+
def tieable?(t)
|
97
|
+
return true if NoteEvent === t && t.tie?
|
98
|
+
return true if Token === t && TIEABLE_EVENTS.include?(t.type)
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
87
102
|
TIEABLE_EVENTS = %i(
|
88
|
-
amp
|
89
103
|
y
|
90
104
|
rely
|
91
105
|
v
|
@@ -97,13 +111,16 @@ module Ramekin
|
|
97
111
|
transpose
|
98
112
|
adsr
|
99
113
|
bpm
|
114
|
+
native_tie
|
100
115
|
)
|
101
116
|
def call(&b)
|
102
117
|
return enum_for(:process) unless block_given?
|
103
118
|
|
104
119
|
each do |el|
|
105
120
|
if @in_legato
|
106
|
-
if
|
121
|
+
if tieable?(el)
|
122
|
+
buffer << el
|
123
|
+
elsif NoteEvent === el
|
107
124
|
if @seen_legato
|
108
125
|
@seen_legato = false
|
109
126
|
buffer << el
|
@@ -113,13 +130,8 @@ module Ramekin
|
|
113
130
|
flush_legato!(&b)
|
114
131
|
buffer!(el, &b)
|
115
132
|
end
|
116
|
-
elsif Token === el && el.type == :amp
|
117
|
-
@seen_legato = true
|
118
|
-
buffer << el
|
119
133
|
elsif Token === el && el.type == :legato_tie
|
120
134
|
@seen_legato = true
|
121
|
-
elsif Token === el && TIEABLE_EVENTS.include?(el.type)
|
122
|
-
buffer << el
|
123
135
|
else
|
124
136
|
@in_legato = false
|
125
137
|
@seen_legato = false
|
@@ -127,11 +139,11 @@ module Ramekin
|
|
127
139
|
yield el
|
128
140
|
end
|
129
141
|
else
|
130
|
-
if
|
142
|
+
if tieable?(el)
|
143
|
+
buffer << el
|
144
|
+
elsif NoteEvent === el
|
131
145
|
flush!(&b)
|
132
146
|
buffer!(el, &b)
|
133
|
-
elsif Token === el && TIEABLE_EVENTS.include?(el.type)
|
134
|
-
buffer << el
|
135
147
|
elsif Token === el && el.type == :legato_tie
|
136
148
|
@seen_legato = true
|
137
149
|
@in_legato = true
|
@@ -13,6 +13,10 @@ module Ramekin
|
|
13
13
|
@extensions = extensions
|
14
14
|
end
|
15
15
|
|
16
|
+
def meta
|
17
|
+
@note.meta
|
18
|
+
end
|
19
|
+
|
16
20
|
def start
|
17
21
|
@note.start
|
18
22
|
end
|
@@ -58,6 +62,8 @@ module Ramekin
|
|
58
62
|
end
|
59
63
|
|
60
64
|
def length_amk(div)
|
65
|
+
return '=0' if ticks == 0
|
66
|
+
|
61
67
|
if ticks % div != 0
|
62
68
|
divisible = div == 2 ? 'even' : "divisible by #{div}"
|
63
69
|
error!("too fast! tick count must be #{divisible} at this tempo", el: self)
|
@@ -69,6 +75,21 @@ module Ramekin
|
|
69
75
|
KNOWN_LENGTHS.fetch(t) { "=#{t}" }
|
70
76
|
end
|
71
77
|
|
78
|
+
NOTES = { 'c' => 0, 'd' => 2, 'e' => 4, 'f' => 5, 'g' => 7, 'a' => 9, 'b' => 11 }
|
79
|
+
def note_hex
|
80
|
+
@note_hex ||= begin
|
81
|
+
out = 0x80
|
82
|
+
out += 12 * (octave - 1)
|
83
|
+
@note.value =~ /(\w)([+-])?/
|
84
|
+
out += NOTES.fetch($1) # trust the tokenizer
|
85
|
+
|
86
|
+
out += 1 if $2 == '+'
|
87
|
+
out -= 1 if $2 == '-'
|
88
|
+
|
89
|
+
out
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
72
93
|
def octave_amk(current_octave=nil)
|
73
94
|
return '' if rest? || tie?
|
74
95
|
|
@@ -109,7 +130,8 @@ module Ramekin
|
|
109
130
|
|
110
131
|
case ext
|
111
132
|
when /\A=(\d+)\z/ then $1.to_i
|
112
|
-
|
133
|
+
# special case for a 0-length note, useful for bends
|
134
|
+
when '0' then 0
|
113
135
|
when /\A(\d+)\z/ then 192 / $1.to_i
|
114
136
|
when /\A(\d+)[.]\z/ then 192 * 3 / ($1.to_i * 2)
|
115
137
|
else
|
@@ -134,6 +156,14 @@ module Ramekin
|
|
134
156
|
error! "empty CombinedRestEvent" if rests.empty?
|
135
157
|
end
|
136
158
|
|
159
|
+
def tie?
|
160
|
+
false
|
161
|
+
end
|
162
|
+
|
163
|
+
def meta
|
164
|
+
@rests.first.meta
|
165
|
+
end
|
166
|
+
|
137
167
|
def start
|
138
168
|
@rests.first.start
|
139
169
|
end
|
data/lib/ramekin/renderer.rb
CHANGED
@@ -34,6 +34,12 @@ module Ramekin
|
|
34
34
|
def render(&b)
|
35
35
|
return render_string unless block_given?
|
36
36
|
|
37
|
+
if ENV['RAMEKIN_DEBUG'] == '1'
|
38
|
+
old_b = b
|
39
|
+
@so_far = StringIO.new
|
40
|
+
b = lambda { |chunk| @so_far << chunk; old_b.call(chunk) }
|
41
|
+
end
|
42
|
+
|
37
43
|
render_preamble(&b)
|
38
44
|
|
39
45
|
@track.channels.compact.each { |c| render_channel(c, &b) }
|
@@ -115,6 +121,16 @@ module Ramekin
|
|
115
121
|
@current = el = next!
|
116
122
|
|
117
123
|
case el
|
124
|
+
when Bend
|
125
|
+
old_tick = @tick
|
126
|
+
@tick += el.ticks
|
127
|
+
if (old_tick-1) / 192 != (@tick-1) / 192
|
128
|
+
yield "\n"
|
129
|
+
end
|
130
|
+
|
131
|
+
yield el.to_amk(@octave, @track.meta.divisor)
|
132
|
+
|
133
|
+
@octave = el.ending_octave
|
118
134
|
when NoteEvent
|
119
135
|
old_tick = @tick
|
120
136
|
@tick += el.ticks
|
@@ -124,7 +140,7 @@ module Ramekin
|
|
124
140
|
divisor = @track.meta.divisor
|
125
141
|
|
126
142
|
yield el.to_amk(@octave, @track.meta.divisor)
|
127
|
-
@octave = el.octave unless el.rest?
|
143
|
+
@octave = el.octave unless el.tie? || el.rest?
|
128
144
|
when MacroDefinition
|
129
145
|
# pass
|
130
146
|
when LegatoStart, LegatoLastNote
|
@@ -267,23 +283,6 @@ module Ramekin
|
|
267
283
|
case token.value
|
268
284
|
when 'SPC'
|
269
285
|
# pass
|
270
|
-
when 'bend'
|
271
|
-
note = next!
|
272
|
-
|
273
|
-
error! '#bend must be followed by a note' unless NoteEvent === note
|
274
|
-
error! 'cannot #bend to a rest' if note.rest?
|
275
|
-
|
276
|
-
ticks = sprintf("%02x", note.ticks)
|
277
|
-
yield "$dd$#{ticks}$#{ticks}#{note.octave_amk}#{note.note.value}"
|
278
|
-
|
279
|
-
case peek
|
280
|
-
when NoteEvent
|
281
|
-
peek.extensions.concat(note.extensions)
|
282
|
-
else
|
283
|
-
yield "^=#{note.ticks}"
|
284
|
-
end
|
285
|
-
|
286
|
-
@octave = note.octave
|
287
286
|
when 'legato'
|
288
287
|
yield '$f4$01'
|
289
288
|
when 'echo/toggle'
|
data/lib/ramekin.rb
CHANGED
@@ -11,6 +11,7 @@ require_relative 'ramekin/tokenizer'
|
|
11
11
|
require_relative 'ramekin/processor'
|
12
12
|
require_relative 'ramekin/macros'
|
13
13
|
require_relative 'ramekin/note_aggregator'
|
14
|
+
require_relative 'ramekin/bends'
|
14
15
|
require_relative 'ramekin/legato'
|
15
16
|
require_relative 'ramekin/loop_allocator'
|
16
17
|
require_relative 'ramekin/meta'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ramekin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jneen
|
8
8
|
bindir: gembin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-07 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: strscan
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- lib/ramekin/amk_runner.rb
|
78
78
|
- lib/ramekin/amk_runner/sample_groups.rb
|
79
79
|
- lib/ramekin/amk_setup.rb
|
80
|
+
- lib/ramekin/bends.rb
|
80
81
|
- lib/ramekin/channel_separator.rb
|
81
82
|
- lib/ramekin/cli.rb
|
82
83
|
- lib/ramekin/config.rb
|