rubysketch 0.7.14 → 0.7.15

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: 7fb4742ea051ed33c17f3bb8020d207523359025d8df86de5a5e4d09aa81dafc
4
- data.tar.gz: 31cfb5c9be061229e6cfd0385afae6a0fd9ba1e76908d4adb1aec4126beb7242
3
+ metadata.gz: 21a167597b95be3b08c1a2ed54d74b82f7d8d3a14cd2a2ca4ea2273cf0738322
4
+ data.tar.gz: 291bbd92fa9c8377290a230e635da8515eea6bbf5637ffed73594a1cd9bc74ff
5
5
  SHA512:
6
- metadata.gz: 68509d9831c127e4dd0671f847920935e0255e31973380fe0bafc823e772698d777f431ea94202a59890c859cf241cb369ddddb31fa3eac7fcc74c45668c0bab
7
- data.tar.gz: 3273c1129127a0399be849b0487b98c52ac6597aebf32913d3f8efb3d085da90a0cba8f065d33453c76be0707df5c56856e15f4df0bdb16f4f3dacd544c07229
6
+ metadata.gz: 85cc04f6637996f41f86ab1b20ef8488bc27da3e948cc91245243dd654176c118fa3575a867914225968f2db7a28f08564b3193e29ae18a46c1c4c1d9419a702
7
+ data.tar.gz: dc97d8c679832469f1577790858a075a347c5a2f9a2da87b58711cf53ff28cb9e021616f3838f44aaa324f79b84c9bfb80e69fcd1a81b5f33f8078864bdb34f7
data/ChangeLog.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # rubysketch ChangeLog
2
2
 
3
3
 
4
+ ## [v0.7.15] - 2026-04-09
5
+
6
+ - Add MML class
7
+ - Add Sprite#mouseWheel
8
+ - Update dependencies
9
+
10
+ - Fix mouseClicked? crash when view is detached from window
11
+
12
+
4
13
  ## [v0.7.14] - 2025-07-06
5
14
 
6
15
  - Add RtMidi to podspec
data/Gemfile.lock CHANGED
@@ -3,7 +3,7 @@ GEM
3
3
  specs:
4
4
  power_assert (2.0.3)
5
5
  rake (13.1.0)
6
- rexml (3.4.1)
6
+ rexml (3.4.4)
7
7
  test-unit (3.6.1)
8
8
  power_assert
9
9
  yard (0.9.34)
data/RubySketch.podspec CHANGED
@@ -12,7 +12,7 @@ Pod::Spec.new do |s|
12
12
  s.homepage = "https://github.com/xord/rubysketch"
13
13
 
14
14
  s.osx.deployment_target = "10.10"
15
- s.ios.deployment_target = "10.0"
15
+ s.ios.deployment_target = "12.0"
16
16
 
17
17
  root = "${PODS_ROOT}/#{s.name}"
18
18
  exts = File.read(File.expand_path 'Rakefile', __dir__)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.14
1
+ 0.7.15
@@ -1,11 +1,13 @@
1
- require 'processing/all'
1
+ require 'strscan'
2
2
  require 'beeps'
3
+ require 'processing/all'
3
4
 
4
5
 
5
6
  module RubySketch
6
7
 
7
- Vector = Processing::Vector
8
- Image = Processing::Image
8
+ Vector = Processing::Vector
9
+ Image = Processing::Image
10
+ WheelEvent = Processing::WheelEvent
9
11
 
10
12
  end# RubySketch
11
13
 
@@ -19,3 +21,5 @@ require 'rubysketch/sound'
19
21
  require 'rubysketch/easings'
20
22
  require 'rubysketch/graphics_context'
21
23
  require 'rubysketch/context'
24
+
25
+ require 'rubysketch/mml'
@@ -0,0 +1,233 @@
1
+ module RubySketch
2
+
3
+
4
+ class MML
5
+
6
+ TONES = %i[
7
+ sine triangle square sawtooth pulse12_5 pulse25 noise
8
+ ].freeze
9
+
10
+ class << self
11
+
12
+ def compile(str, streaming = false)
13
+ seq, duration = compile! str
14
+ Sound.new Beeps::Sound.new(seq, streaming ? 0 : duration)
15
+ end
16
+
17
+ def compile!(str)
18
+ scanner = StringScanner.new expandLoops__ str.gsub(/[;%].*(?:\n|$)/, '')
19
+ seq = Beeps::Sequencer.new
20
+ note = Note__.new
21
+ pending = nil
22
+ prevOsc = nil
23
+
24
+ scanner.skip(/\s*/)
25
+ until scanner.eos?
26
+ case
27
+ when scanner.scan(/T\s*(\d+)/i)
28
+ note.bpm = scanner[1].to_i
29
+ when scanner.scan(/O\s*(\d+)/i)
30
+ note.octave = scanner[1].to_i
31
+ when scanner.scan(/([<>])/)
32
+ note.octave +=
33
+ case scanner[1]
34
+ when '<' then -1
35
+ when '>' then +1
36
+ else 0
37
+ end
38
+ when scanner.scan(/@\s*(\d+)/)
39
+ note.tone = scanner[1].to_i
40
+ when scanner.scan(/L\s*(\d+)/i)
41
+ note.length = scanner[1].to_i
42
+ when scanner.scan(/V\s*(\d+)/i)
43
+ note.velocity = scanner[1].to_i
44
+ when scanner.scan(/Q\s*(\d+)/i)
45
+ note.quantize = scanner[1].to_i
46
+ when scanner.scan(/K\s*([+-]?\d+)/i)
47
+ note.transpose = scanner[1].to_i
48
+ when scanner.scan(/Y\s*([+-]?\d+)/i)
49
+ note.detune = scanner[1].to_i
50
+ when scanner.scan(/\&\s*(\d+)?\s*(\.+)?/)
51
+ len, dots = [1, 2].map {scanner[_1]}
52
+ if len || dots
53
+ pending.seconds += seconds__ note, len&.to_i, dots&.size if pending
54
+ else
55
+ note.tie = true
56
+ end
57
+ when scanner.scan(/\^\s*(\d+)?\s*(\.+)?/)
58
+ len, dots = [1, 2].map {scanner[_1]}
59
+ pending.seconds += seconds__ note, len&.to_i, dots&.size if pending
60
+ when scanner.scan(/_/)
61
+ note.portamento = true
62
+ when scanner.scan(/R\s*(\d+)?\s*(\.+)?/i)
63
+ len, dots = [1, 2].map {scanner[_1]}
64
+ note.clearLegatoFlags
65
+ addNote__ seq, pending, note, prevOsc if pending
66
+ pending = nil
67
+ prevOsc = nil
68
+ note.time += seconds__ note, len&.to_i, dots&.size
69
+ when scanner.scan(/([CDEFGAB])\s*([#+-]+)?\s*(\d+)?\s*(\.+)?/i)&.chomp
70
+ char, offset, len, dots = [1, 2, 3, 4].map {scanner[_1]}
71
+
72
+ note.frequency = frequency__ note, char, offset
73
+ prevOsc = addNote__ seq, pending, note, prevOsc if pending
74
+
75
+ pending = note.dup
76
+ pending.seconds += seconds__ note, len&.to_i, dots&.size
77
+
78
+ note.clearLegatoFlags
79
+ else
80
+ raise "Unknown input: #{scanner.rest[..10]}"
81
+ end
82
+
83
+ scanner.skip(/\s*/)
84
+ end
85
+
86
+ addNote__ seq, pending, note, prevOsc if pending
87
+ return seq, note.time
88
+ end
89
+
90
+ def play(str)
91
+ compile(str).play
92
+ end
93
+
94
+ private
95
+
96
+ V_MAX__ = 127.0
97
+ Q_MAX__ = 100.0
98
+
99
+ # @private
100
+ Note__ = Struct.new(
101
+ :time, :frequency, :seconds,
102
+ :bpm, :octave, :tone, :length, :velocity, :quantize, :transpose, :detune,
103
+ :tie, :portamento) do
104
+
105
+ def initialize()
106
+ super(
107
+ 0, 1, 0,
108
+ 120, 4, 0, 4, V_MAX__, Q_MAX__, 0, 0,
109
+ false, false)
110
+ end
111
+
112
+ def legato?()
113
+ self.tie || self.portamento
114
+ end
115
+
116
+ def clearLegatoFlags()
117
+ self.tie = self.portamento = false
118
+ end
119
+ end
120
+
121
+ # @private
122
+ def expandLoops__(str)
123
+ nil while str.gsub!(/\[([^\[\]]*)\]\s*(\d+)?/) {$1 * ($2&.to_i || 2)}
124
+ str
125
+ end
126
+
127
+ # @private
128
+ def seconds__(note, length, dots)
129
+ length ||= note.length
130
+ dots ||= 0
131
+ 60.0 * 4 / note.bpm / length * (1 + dots.times.map {0.5 ** (_1 + 1)}.sum)
132
+ end
133
+
134
+ # @private
135
+ DISTANCES__ = -> {
136
+ notes = 'c_d_ef_g_a_b'.each_char.with_index.reject {|c,| c == '_'}.to_a
137
+ octaves = (0..11).to_a
138
+ octaves.product(notes)
139
+ .map.with_object({}) {|(octave, (note, index)), hash|
140
+ hash[[note, octave]] = octave * 12 + index - 57
141
+ }
142
+ }.call
143
+
144
+ # @private
145
+ def frequency__(note, char, offset)
146
+ distance = DISTANCES__[[char.downcase, note.octave.to_i]] ||
147
+ (raise ArgumentError, "char:'#{char}' octave:'#{note.octave}'")
148
+ distance += (offset || '').each_char.reduce(0) {|value, char|
149
+ case char
150
+ when '+', '#' then value + 1
151
+ when '-' then value - 1
152
+ else value
153
+ end
154
+ }
155
+ distance += note.transpose + (note.detune / 100.0)
156
+ 440 * (2 ** (distance.to_f / 12))
157
+ end
158
+
159
+ # @private
160
+ def addNote__(seq, note, nextNote, prevOsc)
161
+ processor, sec = createProcessor__ note, nextNote
162
+ osc = findInput__(processor) {_1.class == Beeps::Oscillator}
163
+ syncPhase__ osc, prevOsc if prevOsc
164
+ seq.add processor, note.time, sec
165
+ nextNote.time += note.seconds
166
+ return osc
167
+ end
168
+
169
+ # @private
170
+ def createProcessor__(note, nextNote)
171
+ tone = TONES[note.tone] || (raise ArgumentError, "tone:'#{note.tone}'")
172
+ freq = nextNote.portamento ? Beeps::Value.new(note.frequency).tap {
173
+ _1.insert nextNote.frequency, note.seconds if nextNote.frequency != note.frequency
174
+ } : note.frequency
175
+ gate = note.quantize.clamp(0, Q_MAX__) / Q_MAX__ * note.seconds
176
+ vel = note.velocity.clamp(0, V_MAX__) / V_MAX__
177
+ adsr = {
178
+ attack_time: ( note.legato? ? 0 : nil),
179
+ release_time: (nextNote.legato? ? 0 : nil)
180
+ }.compact
181
+
182
+ osc = createOscillator__ tone, 32, freq: freq
183
+ env = Beeps::Envelope.new(**adsr) {
184
+ note_on
185
+ note_off nextNote.legato? ? note.seconds : (gate - release).clamp(0..)
186
+ }
187
+ gain = Beeps::Gain.new gain: vel
188
+ return (osc >> env >> gain), (nextNote.legato? ? note.seconds : gate)
189
+ end
190
+
191
+ # @private
192
+ def createOscillator__(type, size, **kwargs)
193
+ case type
194
+ when :noise then Beeps::Oscillator.new type
195
+ else
196
+ samples = (@samples ||= {})[type] ||= createSamples__ type, size
197
+ Beeps::Oscillator.new samples: samples, **kwargs
198
+ end
199
+ end
200
+
201
+ # @private
202
+ def createSamples__(type, size)
203
+ input = size.times.map {_1.to_f / size}
204
+ duty = {pulse12_5: 0.125, pulse25: 0.25, pulse75: 0.75}[type] || 0.5
205
+ case type
206
+ when :sine then input.map {Math.sin _1 * Math::PI * 2}
207
+ when :triangle then input.map {_1 < 0.5 ? _1 * 4 - 1 : 3 - _1 * 4}
208
+ when :sawtooth then input.map {_1 * 2 - 1}
209
+ else input.map {_1 < duty ? 1 : -1}
210
+ end
211
+ end
212
+
213
+ # @private
214
+ def syncPhase__(osc, prev)
215
+ osc.on(:start) {osc.phase = prev.phase}
216
+ end
217
+
218
+ # @private
219
+ def findInput__(processor, &block)
220
+ p = processor
221
+ while p
222
+ return p if block.call p
223
+ p = p.input
224
+ end
225
+ nil
226
+ end
227
+
228
+ end# <<self
229
+
230
+ end# MML
231
+
232
+
233
+ end# RubySketch
@@ -42,6 +42,14 @@ module RubySketch
42
42
  not @players.empty?
43
43
  end
44
44
 
45
+ # Returns the duration in seconds
46
+ #
47
+ # @return [Numeric] duration in seconds
48
+ #
49
+ def seconds()
50
+ @sound.seconds
51
+ end
52
+
45
53
  # Load a sound file.
46
54
  #
47
55
  # @param [String] path file path
@@ -931,6 +931,20 @@ module RubySketch
931
931
  nil
932
932
  end
933
933
 
934
+ # Defines mouseWheel block.
935
+ #
936
+ # @example Print wheel states on mouse wheel
937
+ # sprite.mouseWheel do |event|
938
+ # p event.getCount
939
+ # end
940
+ #
941
+ # @return [nil] nil
942
+ #
943
+ def mouseWheel(&block)
944
+ @view__.mouseWheel = block if block
945
+ nil
946
+ end
947
+
934
948
  # Defines touchStarted block.
935
949
  #
936
950
  # @example Print touches on touch start
@@ -1182,7 +1196,7 @@ module RubySketch
1182
1196
  #
1183
1197
  def offset()
1184
1198
  s, z = @view.scroll, zoom
1185
- Vector.new -s.x / z, -s.y / z, -s.z / z
1199
+ Vector.new(-s.x / z, -s.y / z, -s.z / z)
1186
1200
  end
1187
1201
 
1188
1202
  # Sets the offset of the sprite world.
@@ -1204,7 +1218,7 @@ module RubySketch
1204
1218
  when nil then [0, 0, 0]
1205
1219
  else raise ArgumentError
1206
1220
  end
1207
- @view.scroll_to -x * zoom_, -y * zoom_, -z * zoom_
1221
+ @view.scroll_to(-x * zoom_, -y * zoom_, -z * zoom_)
1208
1222
  offset
1209
1223
  end
1210
1224
 
@@ -1303,8 +1317,8 @@ module RubySketch
1303
1317
  end
1304
1318
 
1305
1319
  attr_accessor :update,
1306
- :mousePressed, :mouseReleased, :mouseMoved, :mouseDragged, :mouseClicked,
1307
- :touchStarted, :touchEnded, :touchMoved,
1320
+ :mousePressed, :mouseReleased, :mouseMoved, :mouseDragged,
1321
+ :mouseClicked, :mouseWheel, :touchStarted, :touchEnded, :touchMoved,
1308
1322
  :keyPressed, :keyReleased, :keyTyped,
1309
1323
  :contact, :contactEnd, :willContact
1310
1324
 
@@ -1383,6 +1397,10 @@ module RubySketch
1383
1397
  on_pointer_up e
1384
1398
  end
1385
1399
 
1400
+ def on_wheel(e)
1401
+ callBlock @mouseWheel, WheelEvent.new(e)
1402
+ end
1403
+
1386
1404
  def on_key_down(e)
1387
1405
  updateKeyStates e, true
1388
1406
  callBlock @keyPressed
@@ -1452,7 +1470,7 @@ module RubySketch
1452
1470
  end
1453
1471
 
1454
1472
  def mouseClicked?()
1455
- return false unless parent && @pointer && @pointerDownStartPos
1473
+ return false unless @pointer && @pointerDownStartPos && window
1456
1474
  [to_screen(@pointer.pos), @pointerDownStartPos]
1457
1475
  .map {|pos| Rays::Point.new pos.x, pos.y, 0}
1458
1476
  .then {|pos, startPos| (pos - startPos).length < 3}
data/rubysketch.gemspec CHANGED
@@ -25,12 +25,12 @@ Gem::Specification.new do |s|
25
25
  s.platform = Gem::Platform::RUBY
26
26
  s.required_ruby_version = '>= 3.0.0'
27
27
 
28
- s.add_dependency 'xot', '~> 0.3.9', '>= 0.3.9'
29
- s.add_dependency 'rucy', '~> 0.3.9', '>= 0.3.9'
30
- s.add_dependency 'beeps', '~> 0.3.9', '>= 0.3.9'
31
- s.add_dependency 'rays', '~> 0.3.9', '>= 0.3.9'
32
- s.add_dependency 'reflexion', '~> 0.3.10', '>= 0.3.10'
33
- s.add_dependency 'processing', '~> 1.1', '>= 1.1.13'
28
+ s.add_dependency 'xot', '~> 0.3.10'
29
+ s.add_dependency 'rucy', '~> 0.3.10'
30
+ s.add_dependency 'beeps', '~> 0.3.10'
31
+ s.add_dependency 'rays', '~> 0.3.10'
32
+ s.add_dependency 'reflexion', '~> 0.3.11'
33
+ s.add_dependency 'processing', '~> 1.1.14'
34
34
 
35
35
  s.files = `git ls-files`.split $/
36
36
  s.test_files = s.files.grep %r{^(test|spec|features)/}
data/test/test_mml.rb ADDED
@@ -0,0 +1,217 @@
1
+ require_relative 'helper'
2
+
3
+
4
+ class TestMML < Test::Unit::TestCase
5
+
6
+ def compile(str)
7
+ RubySketch::MML.compile! str
8
+ end
9
+
10
+ def procs(str, klass = nil)
11
+ compile(str)[0].each.with_object([]) do |(processor, *), array|
12
+ processor = find_input(processor) {_1.class == klass} if klass
13
+ array << processor
14
+ end
15
+ end
16
+
17
+ def times(str)
18
+ compile(str)[0].each.with_object([]) do |(_, offset, duration), array|
19
+ array << [offset, duration]
20
+ end
21
+ end
22
+
23
+ def duration(str)
24
+ compile(str)[1]
25
+ end
26
+
27
+ def oscillators(str) = procs(str, Beeps::Oscillator)
28
+
29
+ def gains(str) = procs(str, Beeps::Gain)
30
+
31
+ def find_input(processor, &block)
32
+ p = processor
33
+ while p
34
+ return p if block.call p
35
+ p = p.input
36
+ end
37
+ nil
38
+ end
39
+
40
+ def test_note_frequency()
41
+ assert_in_delta 440, oscillators('A') .first.freq
42
+
43
+ assert_in_delta 246.942, oscillators('O3 B') .first.freq
44
+ assert_in_delta 261.626, oscillators('O4 C') .first.freq
45
+ assert_in_delta 277.183, oscillators('O4 C+').first.freq
46
+ assert_in_delta 277.183, oscillators('O4 D-').first.freq
47
+ assert_in_delta 293.665, oscillators('O4 D') .first.freq
48
+ assert_in_delta 311.127, oscillators('O4 D+').first.freq
49
+ assert_in_delta 311.127, oscillators('O4 E-').first.freq
50
+ assert_in_delta 329.628, oscillators('O4 E') .first.freq
51
+ assert_in_delta 349.228, oscillators('O4 F') .first.freq
52
+ assert_in_delta 369.994, oscillators('O4 F+').first.freq
53
+ assert_in_delta 369.994, oscillators('O4 G-').first.freq
54
+ assert_in_delta 391.995, oscillators('O4 G') .first.freq
55
+ assert_in_delta 415.305, oscillators('O4 G+').first.freq
56
+ assert_in_delta 415.305, oscillators('O4 A-').first.freq
57
+ assert_in_delta 440, oscillators('O4 A') .first.freq
58
+ assert_in_delta 466.164, oscillators('O4 A+').first.freq
59
+ assert_in_delta 466.164, oscillators('O4 B-').first.freq
60
+ assert_in_delta 493.883, oscillators('O4 B') .first.freq
61
+ assert_in_delta 523.251, oscillators('O5 C') .first.freq
62
+ end
63
+
64
+ def test_note_duration()
65
+ assert_in_delta 0.5, duration('C')
66
+
67
+ assert_in_delta 4, duration('T60 C1')
68
+ assert_in_delta 6, duration('T60 C1.')
69
+ assert_in_delta 7, duration('T60 C1..')
70
+ assert_in_delta 7.5, duration('T60 C1...')
71
+ assert_in_delta 2, duration('T60 C2')
72
+ assert_in_delta 3, duration('T60 C2.')
73
+ assert_in_delta 3.5, duration('T60 C2..')
74
+ assert_in_delta 3.75, duration('T60 C2...')
75
+ assert_in_delta 1, duration('T60 C4')
76
+ assert_in_delta 1.5, duration('T60 C4.')
77
+ assert_in_delta 1.75, duration('T60 C4..')
78
+ assert_in_delta 1.875, duration('T60 C4...')
79
+ assert_in_delta 0.5, duration('T60 C8')
80
+ assert_in_delta 0.75, duration('T60 C8.')
81
+ assert_in_delta 0.875, duration('T60 C8..')
82
+ assert_in_delta 0.9375, duration('T60 C8...')
83
+ assert_in_delta 0.25, duration('T60 C16')
84
+ assert_in_delta 0.375, duration('T60 C16.')
85
+ assert_in_delta 0.4375, duration('T60 C16..')
86
+ assert_in_delta 0.46875, duration('T60 C16...')
87
+
88
+ assert_in_delta 2, duration('T120 C1')
89
+ assert_in_delta 1, duration('T120 C2')
90
+ assert_in_delta 0.5, duration('T120 C4')
91
+ assert_in_delta 0.25, duration('T120 C8')
92
+ assert_in_delta 0.125, duration('T120 C16')
93
+
94
+ assert_in_delta 1, duration('T60 C-4')
95
+ assert_in_delta 1, duration('T60 C+4')
96
+ assert_in_delta 1, duration('T60 C#4')
97
+ assert_in_delta 2, duration('T60 L4 C C')
98
+
99
+ assert_equal [[0, 1], [1, 1]], times('T60 C C')
100
+ end
101
+
102
+ def test_rest()
103
+ assert_equal [[0, 1], [2, 1]], times('T60 C R C')
104
+ assert_equal [[0, 1], [3, 1]], times('T60 C R2 C')
105
+ assert_equal [[0, 1], [4, 1]], times('T60 C R2. C')
106
+ assert_equal [[0, 1], [3, 1]], times('T60 C L2 R L4 C')
107
+ end
108
+
109
+ def test_tempo()
110
+ assert_equal 0.5, duration( 'C')
111
+ assert_equal 0.5, duration('T120 C')
112
+ assert_equal 1, duration('T60 C')
113
+ end
114
+
115
+ def test_octave()
116
+ assert_in_delta 440, oscillators('O4 A').first.freq
117
+ assert_in_delta 440 / 2, oscillators('O3 A').first.freq
118
+ assert_in_delta 440 * 2, oscillators('O5 A').first.freq
119
+ assert_in_delta 440 / 2, oscillators('O4 < A').first.freq
120
+ assert_in_delta 440 * 2, oscillators('O4 > A').first.freq
121
+ end
122
+
123
+ def test_tone()
124
+ assert_equal oscillators('@0 C').first.samples, oscillators( 'C').first.samples
125
+ assert_not_equal oscillators('@0 C').first.samples, oscillators('@1 C').first.samples
126
+ end
127
+
128
+ def test_length()
129
+ assert_equal [[0, 1], [1, 1]], times('T60 C C')
130
+ assert_equal [[0, 1], [1, 1]], times('T60 L4 C C')
131
+ assert_equal [[0, 2], [2, 2]], times('T60 L2 C C')
132
+ assert_equal [[0, 2], [2, 1]], times('T60 L2 C C4')
133
+ end
134
+
135
+ def test_velocity()
136
+ assert_in_delta 1, gains( 'C').first.gain
137
+ assert_in_delta 0, gains('V0 C').first.gain
138
+ assert_in_delta 0.5, gains('V63 C').first.gain, 0.01
139
+ end
140
+
141
+ def test_tie_and()
142
+ assert_equal 2.5, duration('T60 C&.')
143
+ assert_equal 1.5, duration('T60 C&8')
144
+ assert_equal 1.75, duration('T60 C&8.')
145
+ assert_equal 3.25, duration('T60 C&8.&.')
146
+
147
+ assert_equal [[0, 1], [1, 1]], times('T60 C & C')
148
+ assert_equal [[0, 1.5], [1.5, 1]], times('T60 C. & C')
149
+ assert_equal [[0, 1], [1, 1], [2, 1]], times('T60 C & C E')
150
+
151
+ assert_equal [[0, 1], [1, 0.5]], times('T60 Q50 C & C')
152
+ assert_equal [[0, 1], [2, 1]], times('T60 C & R C')
153
+ end
154
+
155
+ def test_tie_caret()
156
+ assert_equal 2, duration('T60 C^')
157
+ assert_equal 1.5, duration('T60 C^8')
158
+ assert_equal 1.75, duration('T60 C^8.')
159
+ assert_equal 2.75, duration('T60 C^8.^')
160
+ assert_equal 3.25, duration('T60 C^8.^.')
161
+ end
162
+
163
+ def test_portamento()
164
+ assert_equal [[0, 1], [1, 1], [2, 1]], times('T60 L4 C_D_E')
165
+ end
166
+
167
+ def test_quantize()
168
+ assert_equal [[0, 1], [1, 1]], times('T60 L4 C C')
169
+ assert_equal [[0, 1], [1, 1]], times('T60 L4 Q100 C C')
170
+ assert_equal [[0, 0.5], [1, 0.5]], times('T60 L4 Q50 C C')
171
+ assert_equal [[0, 0], [1, 0]], times('T60 L4 Q0 C C')
172
+
173
+ assert_in_delta 2, duration('T60 L4 C C')
174
+ assert_in_delta 2, duration('T60 L4 Q50 C C')
175
+ end
176
+
177
+ def test_transpose()
178
+ assert_in_delta 466.164, oscillators('K 1 A').first.freq
179
+ assert_in_delta 466.164, oscillators('K+1 A').first.freq
180
+ assert_in_delta 415.305, oscillators('K-1 A').first.freq
181
+ assert_in_delta 493.883, oscillators('K 2 A').first.freq
182
+ assert_in_delta 880, oscillators('K 12 A').first.freq
183
+ assert_in_delta 220, oscillators('K-12 A').first.freq
184
+ end
185
+
186
+ def test_detune()
187
+ assert_in_delta 442.549, oscillators('Y 10 A').first.freq
188
+ assert_in_delta 466.164, oscillators('Y 100 A').first.freq
189
+ assert_in_delta 466.164, oscillators('Y+100 A').first.freq
190
+ assert_in_delta 415.305, oscillators('Y-100 A').first.freq
191
+ assert_in_delta 493.883, oscillators('Y 200 A').first.freq
192
+ assert_in_delta 880, oscillators('Y 1200 A').first.freq
193
+ assert_in_delta 220, oscillators('Y-1200 A').first.freq
194
+ end
195
+
196
+ def test_loop()
197
+ assert_equal 6, procs('T60 [CDE]') .size
198
+ assert_equal 6, procs('T60 [CDE]2') .size
199
+ assert_equal 9, procs('T60 [CDE]3') .size
200
+ assert_equal 3, procs('T60 [CDE]1') .size
201
+ assert_equal 0, procs('T60 [CDE]0') .size
202
+ assert_equal 36, procs('T60 [[CDE]3]4').size
203
+ assert_equal 12, procs('T60 [[CDE]]') .size
204
+ end
205
+
206
+ def test_comment()
207
+ assert_in_delta 1, duration(<<~EOS)
208
+ ; comment
209
+ T60 L4 C ; C
210
+ EOS
211
+ assert_in_delta 1, duration(<<~EOS)
212
+ % comment
213
+ T60 L4 C % C
214
+ EOS
215
+ end
216
+
217
+ end# TestMMLCompiler
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubysketch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.14
4
+ version: 0.7.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - xordog
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-05 00:00:00.000000000 Z
11
+ date: 2026-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xot
@@ -16,120 +16,84 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.9
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 0.3.9
19
+ version: 0.3.10
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 0.3.9
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 0.3.9
26
+ version: 0.3.10
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: rucy
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
31
  - - "~>"
38
32
  - !ruby/object:Gem::Version
39
- version: 0.3.9
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: 0.3.9
33
+ version: 0.3.10
43
34
  type: :runtime
44
35
  prerelease: false
45
36
  version_requirements: !ruby/object:Gem::Requirement
46
37
  requirements:
47
38
  - - "~>"
48
39
  - !ruby/object:Gem::Version
49
- version: 0.3.9
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: 0.3.9
40
+ version: 0.3.10
53
41
  - !ruby/object:Gem::Dependency
54
42
  name: beeps
55
43
  requirement: !ruby/object:Gem::Requirement
56
44
  requirements:
57
45
  - - "~>"
58
46
  - !ruby/object:Gem::Version
59
- version: 0.3.9
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 0.3.9
47
+ version: 0.3.10
63
48
  type: :runtime
64
49
  prerelease: false
65
50
  version_requirements: !ruby/object:Gem::Requirement
66
51
  requirements:
67
52
  - - "~>"
68
53
  - !ruby/object:Gem::Version
69
- version: 0.3.9
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: 0.3.9
54
+ version: 0.3.10
73
55
  - !ruby/object:Gem::Dependency
74
56
  name: rays
75
57
  requirement: !ruby/object:Gem::Requirement
76
58
  requirements:
77
59
  - - "~>"
78
60
  - !ruby/object:Gem::Version
79
- version: 0.3.9
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: 0.3.9
61
+ version: 0.3.10
83
62
  type: :runtime
84
63
  prerelease: false
85
64
  version_requirements: !ruby/object:Gem::Requirement
86
65
  requirements:
87
66
  - - "~>"
88
67
  - !ruby/object:Gem::Version
89
- version: 0.3.9
90
- - - ">="
91
- - !ruby/object:Gem::Version
92
- version: 0.3.9
68
+ version: 0.3.10
93
69
  - !ruby/object:Gem::Dependency
94
70
  name: reflexion
95
71
  requirement: !ruby/object:Gem::Requirement
96
72
  requirements:
97
73
  - - "~>"
98
74
  - !ruby/object:Gem::Version
99
- version: 0.3.10
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: 0.3.10
75
+ version: 0.3.11
103
76
  type: :runtime
104
77
  prerelease: false
105
78
  version_requirements: !ruby/object:Gem::Requirement
106
79
  requirements:
107
80
  - - "~>"
108
81
  - !ruby/object:Gem::Version
109
- version: 0.3.10
110
- - - ">="
111
- - !ruby/object:Gem::Version
112
- version: 0.3.10
82
+ version: 0.3.11
113
83
  - !ruby/object:Gem::Dependency
114
84
  name: processing
115
85
  requirement: !ruby/object:Gem::Requirement
116
86
  requirements:
117
87
  - - "~>"
118
88
  - !ruby/object:Gem::Version
119
- version: '1.1'
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: 1.1.13
89
+ version: 1.1.14
123
90
  type: :runtime
124
91
  prerelease: false
125
92
  version_requirements: !ruby/object:Gem::Requirement
126
93
  requirements:
127
94
  - - "~>"
128
95
  - !ruby/object:Gem::Version
129
- version: '1.1'
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: 1.1.13
96
+ version: 1.1.14
133
97
  description: A game engine based on the Processing API.
134
98
  email: xordog@gmail.com
135
99
  executables: []
@@ -163,6 +127,7 @@ files:
163
127
  - lib/rubysketch/extension.rb
164
128
  - lib/rubysketch/graphics_context.rb
165
129
  - lib/rubysketch/helper.rb
130
+ - lib/rubysketch/mml.rb
166
131
  - lib/rubysketch/shape.rb
167
132
  - lib/rubysketch/sound.rb
168
133
  - lib/rubysketch/sprite.rb
@@ -172,6 +137,7 @@ files:
172
137
  - src/RubySketch.mm
173
138
  - test/helper.rb
174
139
  - test/test_context.rb
140
+ - test/test_mml.rb
175
141
  - test/test_sound.rb
176
142
  - test/test_sprite.rb
177
143
  homepage: https://github.com/xord/rubysketch
@@ -200,5 +166,6 @@ summary: A game engine based on the Processing API.
200
166
  test_files:
201
167
  - test/helper.rb
202
168
  - test/test_context.rb
169
+ - test/test_mml.rb
203
170
  - test/test_sound.rb
204
171
  - test/test_sprite.rb