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 +4 -4
- data/ChangeLog.md +9 -0
- data/Gemfile.lock +1 -1
- data/RubySketch.podspec +1 -1
- data/VERSION +1 -1
- data/lib/rubysketch/all.rb +7 -3
- data/lib/rubysketch/mml.rb +233 -0
- data/lib/rubysketch/sound.rb +8 -0
- data/lib/rubysketch/sprite.rb +23 -5
- data/rubysketch.gemspec +6 -6
- data/test/test_mml.rb +217 -0
- metadata +17 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21a167597b95be3b08c1a2ed54d74b82f7d8d3a14cd2a2ca4ea2273cf0738322
|
|
4
|
+
data.tar.gz: 291bbd92fa9c8377290a230e635da8515eea6bbf5637ffed73594a1cd9bc74ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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 = "
|
|
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.
|
|
1
|
+
0.7.15
|
data/lib/rubysketch/all.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'strscan'
|
|
2
2
|
require 'beeps'
|
|
3
|
+
require 'processing/all'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
module RubySketch
|
|
6
7
|
|
|
7
|
-
Vector
|
|
8
|
-
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
|
data/lib/rubysketch/sound.rb
CHANGED
|
@@ -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
|
data/lib/rubysketch/sprite.rb
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
29
|
-
s.add_dependency 'rucy', '~> 0.3.
|
|
30
|
-
s.add_dependency 'beeps', '~> 0.3.
|
|
31
|
-
s.add_dependency 'rays', '~> 0.3.
|
|
32
|
-
s.add_dependency 'reflexion', '~> 0.3.
|
|
33
|
-
s.add_dependency 'processing', '~> 1.1
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|