Glimmer Metronome & Hello, Canvas Animation Data Binding!
Tags: ruby, desktop, gui, music
Published: 2021-03-05 14:10:46

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](https://github.com/AndyObtiva/glimmer-dsl-swt) in under 10 minutes for the initial working 4/4 rhythm version.

```ruby
# 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
```

![Initial Glimmer Metronome](https://1.bp.blogspot.com/-B8Fjsr1O114/YELUjOngRAI/AAAAAAAABqQ/2t1c41R-9PcNrW8-aL6N8sTAzI8AcxuYQCLcBGAsYHQ/s320/Screen%2BShot%2B2021-03-05%2Bat%2B8.00.36%2BPM.png)

Afterwards, I decided to add more rhythm/bpm variations and include this app as a [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) "[Elaborate Sample](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#metronome)". It is in fact the first [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) internal sample to demonstrate use of the cross-platform [Java Sound API](https://www.oracle.com/java/technologies/java-sound-api.html) 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](https://www.oracle.com/java/technologies/java-sound-api.html) called [Timer](https://github.com/AndyObtiva/glimmer-cs-timer)).

```ruby
# 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
```
![Glimmer Metronome](https://github.com/AndyObtiva/glimmer-dsl-swt/raw/master/images/glimmer-metronome.gif)

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](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#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`](https://help.eclipse.org/2020-12/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Shell.html) widget representing window) [layout](https://help.eclipse.org/2020-12/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Composite.html#layout(boolean,boolean)) and [pack](https://help.eclipse.org/2020-12/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Control.html#pack(boolean)) 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`](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#canvas-animation-dsl) keyword, using [data-binding based animation](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#animation-via-data-binding), heavy use of [canvas](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#canvas-shape-dsl) 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](https://github.com/AndyObtiva/glimmer-dsl-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](https://github.com/AndyObtiva/glimmer-dsl-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](https://github.com/AndyObtiva/glimmer-dsl-swt)), mix confusing or unproductive paradigms like XML/HTML (as opposed to the one-language approach of [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-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!](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#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.

![Animation Data Binding](https://github.com/AndyObtiva/glimmer-dsl-swt/raw/master/images/glimmer-hello-canvas-animation-data-binding.gif)

Happy [Glimmering](https://github.com/AndyObtiva/glimmer-dsl-swt)!

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!