Engine Yard Blog RSS Feed

One of the most powerful features of JRuby (and one that's unique to JRuby) is the fact that you can call out to any Java library as it if were just another piece of Ruby code. In fact, other than a few small details, you might not even realize you're using Java libraries. Today I'll walk through a quick example of "scripting" the MIDI support built into the Java platform.

Accessing Java

Because JRuby always strives to be a solid Ruby implementation first, we don't normally expose nonstandard features like Java integration. That said, it's easy to pull it in... just require the 'java' library:


  require 'java'

Alternatively, you can -rjava on the command line. And yes. That's all there is to it. You might want to consider taking a break at this point, so your manager doesn't realize just how simple it was to make this happen. Grab a snack, play some video games, and once you're done with all that hard work, we'll be ready to pull in some Java APIs!

Importing Java Classes

Once you've loaded Java support, all the Java classes that the JVM can see are available to you as though they were normal Ruby classes. You can access them directly:


  java.lang.System.out.println "hello, world"

This "package" access works for any Java classes in packages starting with "java", "javax", "org", or "com." In order to avoid polluting the top-level namespace (we actually define Kernel methods for "java", "javax", "org", and "com" to provide this feature), other packages need to be scoped under the Java module:


  reader = Java::jline.ConsoleReader.new
  obj = Java::SomeClassWithNoPackage.new

You can actually "import" these classes in a couple different ways. First, you can simply assign them to a constant:


  System = java.lang.System

You can also import them with the "java_import" Kernel method (also available as "import", but that frequently conflicts with libraries like Rake):


  # This is basically the same as "JButton = javax.swing.JButton"
  java_import javax.swing.JButton

Calling Methods

Once you have access to the Java classes, there's really nothing tricky about using them. Call the methods you want to call, and you'll get back the results you'd expect to get:


  # display current system ns-granularity timer value
  puts System.nanos

  # Java constructors are just Class#new, as in Ruby
  frame = JFrame.new "My window!"

  # notice we allow setSize to work as set_size
  frame.set_size 300, 300

  # set* and get* properties work like Ruby attributes
  frame.always_on_top = true
  frame.visible = true

  button = JButton.new "Press me!"
  frame.add button

A MIDI Keyboard

Ok, let's put it all together using a more entertaining library: Java's MIDI API. The MIDI API is located under the javax.sound.midi package, which contains all the classes you'd need to load, construct, and play MIDI sequences.

In this first MIDI example, we'll listen for a line of text at a time and convert that text into a playable MIDI sequence. First we'll load Java support and import the classes we need:


  require 'java'

  midi = javax.sound.midi
  import midi.MidiSystem
  import midi.Sequence
  import midi.MidiEvent
  import midi.ShortMessage

Notice I assigned the javax.sound.midi package to the 'midi' local variable in order to shorten the import lines a bit. It's all just objects, after all...

Next up we'll want a simple gets loop with an exit clause:


  while (line = gets) !~ /exit/

Ok, now we get to the meat of our app: building a sequence. For every line, we'll construct a new sequence. It's not the most efficient way, but it's nice and simple for now. Then for each character in the sequence we'll add a NOTE_ON event using the character code as the note. Finally we'll add a STOP event to stop playing the sequence.

# Construct a new sequence using 2 beats per quarter note
seq = Sequence.new Sequence::PPQ, 2
track = seq.create_track

# add each character into track as a note
for i in 0...line.length do
msg = ShortMessage.new
# args here are (message, data1, and data2 according to MIDI spec)
# NOTE_ON, character code, and a mid-range note velocity of 64
msg.set_message(ShortMessage::NOTE_ON, line \[i\], 64)
track.add MidiEvent.new(msg, i)
end

# add a STOP to end the track
msg = ShortMessage.new
msg.message = ShortMessage::STOP
track.add MidiEvent.new msg, i + 1

Finally we'll play the sequence and end the loop. There's also a hard JVM System.exit call here, to shut down the internal event threads that MIDI support starts.

# open the sequencer and play the sequence
seqer = MidiSystem.sequencer
seqer.open
seqer.sequence = seq
seqer.start
end
java.lang.System.exit(0)

Give it a try! Here's the full source to our first MIDI script.

Making it More Interactive

Given what we've learned, perhaps we can make our MIDI keyboard app a little more interactive. Ideally we'd like it to respond to keystrokes live,turning the note on when the key is pressed and turning it off when the key is released. For that, we'll launch a GUI window to receive keystroke events.

Our preamble this time only imports three classes, since we won't be constructing sequences this time.

# MIDI keyboard take two!
require 'java'

import javax.sound.midi.MidiSystem
import javax.swing.JFrame
import java.awt.event.KeyListener

In order to make this interactive, we're going to access the system synthesizer directly. Here we retrieve the default synthesizer, open it, and grab its channel number 0:

# Prepare the synth, get channel 0
synth = MidiSystem.synthesizer
synth.open
channel = synth.channels \[0\]

There are a few ways to receive keystroke events directly at the command line, but a lot of them are system-specific or require special libraries. For simplicity, we'll launch a GUI frame and just set it up to receive keystroke events.

# Prepare a frame to receive keystrokes
frame = JFrame.new("Music Frame")
frame.set_size 300, 300
frame.default_close_operation = JFrame::EXIT_ON_CLOSE

Here we're going to use another feature of JRuby: implementing an interface using a block of code. Every Java interface has a class method "impl" that receives a block and produces an object that implements that interface using the given block. The block itself is called for every interface method, receiving the name of the method and the original arguments as parameters.

In this case, we're going to handle the keyPressed and keyReleased methods on the KeyListener interface by turning notes on and off, again using the character code as the note:

# Listen for keystrokes, play notes
frame.add_key_listener KeyListener.impl { |name, event|
case name
when :keyPressed
channel.note_on event.key_char, 64
when :keyReleased
channel.note_off event.key_char
end
}

And finally, we'll show the frame to get the whole thing started:

# Show the frame
frame.visible = true

Here's the source for our MIDI keyboard.

Using JRuby, a whole new world of libraries are at your fingertips, and you never have to write a line of Java code. This is just one use case, but there really are an unlimited number of great ways JRuby allows you to leverage technologies that are otherwise out of the picture for Rubyists.

In the coming weeks and months we'll be working on a series of posts about how you can use JRuby, why we love it, and what you can look forward to in the future. If you've got a hankering for something specific, leave a comment—we aim to please :D


Tagged:

comments powered by Disqus