DEV Community

loading...
Cover image for Glimmer Metronome & Hello, Canvas Animation Data Binding!

Glimmer Metronome & Hello, Canvas Animation Data Binding!

andyobtiva profile image Andy Maleh Updated on ・6 min read

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
Enter fullscreen mode Exit fullscreen mode

Initial Glimmer Metronome

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
Enter fullscreen mode Exit fullscreen mode

Glimmer Metronome

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
Enter fullscreen mode Exit fullscreen mode

Animation Data Binding

Here is an animated screenshot of it.

Happy Glimmering!

Discussion (0)

pic
Editor guide