Andy Maleh
Posted on March 5, 2021
While going through drum pad practice yesterday, I noticed that my iPhone metronome app was broken after the latest update as it was ticking up on the second beat, not the first anymore. It was a small thing, but quite annoying, so I deleted the app and wrote my own Metronome app in Glimmer DSL for SWT in under 10 minutes for the initial working 4/4 rhythm version.
# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#metronome
require 'glimmer-dsl-swt'
class Metronome
class Beat
attr_accessor :on
def off!
self.on = false
end
def on!
self.on = true
end
end
class Rhythm
attr_accessor :beats, :signature_top, :signature_bottom
def initialize(signature_top, signature_bottom)
@signature_top = signature_top
@signature_bottom = signature_bottom
@beats = @signature_top.times.map {Beat.new}
end
end
include Glimmer::UI::CustomShell
attr_reader :beats
before_body {
@rhythm = Rhythm.new(4, 4)
@beats = @rhythm.beats
}
body {
shell {
grid_layout 4, true
text 'Glimmer Metronome'
minimum_size 200, 200
4.times { |n|
canvas {
layout_data {
width_hint 50
height_hint 50
}
oval(0, 0, 50, 50) {
background bind(self, "beats[#{n}].on") {|on| on ? :red : :yellow}
}
}
}
on_swt_show {
@thread ||= Thread.new {
4.times.cycle { |n|
sleep(0.25)
beats.each(&:off!)
beats[n].on!
}
}
}
on_widget_disposed {
@thread.kill # safe since no stored data is involved
}
}
}
end
Metronome.launch
Afterwards, I decided to add more rhythm/bpm variations and include this app as a Glimmer DSL for SWT "Elaborate Sample". It is in fact the first Glimmer DSL for SWT internal sample to demonstrate use of the cross-platform Java Sound API via JRuby, a very practical and useful feature to have (though there is a Glimmer "External Sample" that makes use of the Java Sound API called Timer).
# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#metronome
require 'glimmer-dsl-swt'
class Metronome
class Beat
attr_accessor :on
def off!
self.on = false
end
def on!
self.on = true
end
end
class Rhythm
attr_reader :beat_count
attr_accessor :beats, :bpm
def initialize(beat_count)
self.beat_count = beat_count
@bpm = 120
end
def beat_count=(value)
@beat_count = value
reset_beats!
end
def reset_beats!
@beats = beat_count.times.map {Beat.new}
@beats.first.on!
end
end
include Glimmer::UI::CustomShell
import 'javax.sound.sampled'
GEM_ROOT = File.expand_path(File.join('..', '..'), __dir__)
FILE_SOUND_METRONOME_UP = File.join(GEM_ROOT, 'sounds', 'metronome-up.wav')
FILE_SOUND_METRONOME_DOWN = File.join(GEM_ROOT, 'sounds', 'metronome-down.wav')
attr_accessor :rhythm
before_body {
@rhythm = Rhythm.new(4)
}
body {
shell {
row_layout(:vertical) {
center true
}
text 'Glimmer Metronome'
label {
text 'Beat Count'
font height: 30, style: :bold
}
spinner {
minimum 1
maximum 64
selection bind(self, 'rhythm.beat_count', after_write: ->(v) {restart_metronome})
font height: 30
}
label {
text 'BPM'
font height: 30, style: :bold
}
spinner {
minimum 30
maximum 1000
selection bind(self, 'rhythm.bpm')
font height: 30
}
@beat_container = beat_container
on_swt_show {
start_metronome
}
on_widget_disposed {
stop_metronome
}
}
}
def beat_container
composite {
grid_layout(@rhythm.beat_count, true) {
margin_left 10
}
@rhythm.beat_count.times { |n|
canvas {
rectangle(0, 0, 50, 50, 36, 36) {
background bind(self, "rhythm.beats[#{n}].on") {|on| on ? :red : :yellow}
}
}
}
}
end
def start_metronome
@thread ||= Thread.new {
@rhythm.beat_count.times.cycle { |n|
sleep(60.0/@rhythm.bpm.to_f)
@rhythm.beats.each(&:off!)
@rhythm.beats[n].on!
sound_file = n == 0 ? FILE_SOUND_METRONOME_UP : FILE_SOUND_METRONOME_DOWN
play_sound(sound_file)
}
}
if @beat_container.nil?
body_root.content {
@beat_container = beat_container
}
body_root.layout(true, true)
body_root.pack(true)
end
end
def stop_metronome
@thread&.kill # safe since no stored data is involved
@thread = nil
@beat_container&.dispose
@beat_container = nil
end
def restart_metronome
stop_metronome
start_metronome
end
# Play sound with the Java Sound library
def play_sound(sound_file)
begin
file_or_stream = java.io.File.new(sound_file)
audio_stream = AudioSystem.get_audio_input_stream(file_or_stream)
clip = AudioSystem.clip
clip.open(audio_stream)
clip.start
rescue => e
puts e.full_message
end
end
end
Metronome.launch
Here is a video of the Glimmer Metronome app with sound (I increment spinners with the arrows and with Page Up and Page Down keyboard button presses, which increment/decrement 10 at a time).
https://github.com/AndyObtiva/glimmer-dsl-swt/raw/master/videos/glimmer-metronome.mp4
The code relied mainly on a separate Thread
loop that checked what the bpm and beat count were and ticked sound accordingly using wav
files for the metronome up and down sounds played by the Java Sound API. The Thread
loop used an implicit sync_exec
call in causing changes for the GUI to ensure that the lighting of on and off for both changed beats are rendered in the same go by SWT (not as separate rendering events to avoid a slight delay, which might not be perceptible anyways, but I chose to use sync_exec just in case). As for laying out the beats, I used a hybrid composite layout/canvas approach to avoid having to calculate the locations of every beat on the screen while still taking advantage of the Canvas Shape DSL. One last thing I wanted to ensure is that if I decrease or increase the number of beats (beat count), the window resizes accordingly to keep all beats on one line horizontally. This was accomplished by relying on the app body root (shell
widget representing window) layout and pack methods, which refresh the layout and packed sizing of the window completely if you pass true for their arguments.
I did not do any crazy assurances of perfect real time in this very quick version of the Metronome. It is good enough for my needs though it could have been written in a handful of ways (using the new animation
keyword, using data-binding based animation, heavy use of canvas math instead of grid layout, etc...). That said, I am sure that Metronome apps traditionally took months to build. Just imagine what you could do with all this extra time if you've built it in less than a day like I did. How many more features would you be able to add thanks to the productivity of Glimmer DSL for SWT, how many more client projects would you be able to handle, what else would you be able to do with your extra time or remaining months once you've delivered an app this quickly?
Glimmer DSL for SWT is the most productive cross-platform desktop development framework, bar none! I don't know anything as productive as Glimmer no matter the programming language or technology stack. All other solutions are either too cumbersome and imperative (as opposed to Glimmer's Domain Specific Language for SWT), too bureaucratic and ritualistic (as opposed to smart defaults and convention over configuration in Glimmer DSL for SWT), mix confusing or unproductive paradigms like XML/HTML (as opposed to the one-language approach of Glimmer DSL for SWT), or are based on a statically typed programming language not optimally productive for GUI (as opposed to dynamically typed Ruby, which is perfect for dynamic GUI authoring)
Otherwise, I added yet another sample demonstrating data-binding of the animation "every" property called Hello, Canvas Animation Data Binding!
# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#hello-canvas-animation-data-binding
require 'glimmer-dsl-swt'
require 'bigdecimal'
class HelloAnimationDataBinding
include Glimmer::UI::CustomShell
attr_accessor :delay_time
before_body {
@delay_time = 0.050
}
body {
shell {
text 'Hello, Canvas Animation Data Binding!'
minimum_size 320, 320
canvas {
grid_layout
spinner {
layout_data(:center, :center, true, true) {
minimum_width 75
}
digits 3
minimum 1
maximum 100
selection bind(self, :delay_time, on_read: ->(v) {(BigDecimal(v.to_s)*1000).to_f}, on_write: ->(v) {(BigDecimal(v.to_s)/1000).to_f})
}
animation {
every bind(self, :delay_time)
frame { |index|
background rgb(index%100, index%100 + 100, index%55 + 200)
oval(index*3%300, index*3%300, 20, 20) {
background :yellow
}
}
}
}
}
}
end
HelloAnimationDataBinding.launch
Here is an animated screenshot of it.
Happy Glimmering!
p.s. For hardcore music buffs out there who are shocked at my reversal or wrong usage of Up beat and Down beat, here are a few explanations to keep in mind. First of all, I am a drumkit (Rock) drummer, not a melodic musician. As such, drumkit drummers call every beat on the rhythm count a DOWNBEAT (i.e. 1, 2, 3, 4). Next, I am a Punk Rocker and I've been known to do some Punk drumming. What do I mean by that!? Punk drummers have a complete lack of respect for music rules. In fact, they write their own rules every day, and OFTEN IN TOTAL OPPOSITION TO MUSIC RULES. So, I just came up with the idea of calling the first beat the UPBEAT because the Metronome makes this HIGHER note shrill sound when it clicks on the first beat. Thank you for your understanding and have an awesome Punk Rocking day while at it!
Posted on March 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.