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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 765b62c41161f9939eccd6a53a5e0d4b32dc3cd3ec83584231d8dd4172655d5e
4
- data.tar.gz: 1a0c1b59f0c9cc746fbb97f19c11d66c14d5dd535d5618c6542c16c8c69fce63
3
+ metadata.gz: 78c8f609fa645c1807ac00bf970bf5d05fc78015c185a50d55410a81faa888a4
4
+ data.tar.gz: aab9adb5b304bbcedc94aee84ca95ddd4bf70307d5f807fcbca93944c79c109f
5
5
  SHA512:
6
- metadata.gz: 8a1986e575b290efe80774910bb1fa2c52f1be4f16d9aa9e3888711de7f29252c03fd46fab7a7a454314e7fbd9abbd9408c2229c93d3307fda0059987b94972a
7
- data.tar.gz: c312388efff73d5953984dd7803e3b6313de634549ef9538606def060cdd191c04ef295876cfb8d61d443693f40057206584bec82bba239ad97b6aa37b4f2a08
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
- * `#bend` followed by a note with any length, including ties, will bend for that note's duration. Continue the note with a non-collapsing tie `^^`. Octave changes, `l` commands, and other note modifiers are allowed here. This will compile to a correct `$DD` command.
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 `#bend` and `^^`:
338
+ * Manual pitch bends are fairly straightforward in ramekin, using `&` and `^^`:
339
339
 
340
340
  ```elisp
341
- ; b plays for an 8th note, then bends up to c for an eighth note,
342
- ; then continues on c for a quarter note.
343
- b8 #bend >c8 ^^ c4
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
@@ -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
- yield LegatoLastNote.new(last)
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 NoteEvent === el
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 NoteEvent === el
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
- when '0' then 192 * 2
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
@@ -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.1.9
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-06 00:00:00.000000000 Z
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