Posts Tagged macruby
How to detect Cylons with MacRuby
Over the weekend, MacRuby’s trunk became version 0.6 and the bug fixing is currently done in both the 0.5 branch and trunk. Based on MacRuby’s usual release cycle I would expect a 0.5 beta3 or 0.5 final to be released soon so most of the work can be focused on trunk.
I’ll let you check on the TODO list to see what was done in 0.5 and what is in the plan for 0.6.
However, there is one feature in 0.6 that I know lots of you will just love! The good news is that Laurent already committed a very early version of his work so I figured, I should share the good news with you:
Introducing MacRuby’s debugger!
If you were expecting to read: “a Cylon detector!”, keep on reading.
Again, this feature is in really early development since it’s scheduled for 0.6 and 0.5 final is not out yet. But if you install MacRuby using the nightly builds or build from trunk, you can already play with the debugger.
Let me give you a really quick tour of the debugger.

Let’s imagine that we were given the task to debug the cylon detector written by Gaius Baltar which looks like that:
characters = %w{Adama Apollo Baltar Roslin StarBuck Six} def cylon?(character) false end characters.each do |character| if cylon?(character) puts "#{character} is a Cylon!" else puts "#{character} is not a cylon." end end |
Here is what happens when I execute the script:
$ macruby cylon_detector.rb Adama is not a cylon. Apollo is not a cylon. Baltar is not a cylon. Roslin is not a cylon. StarBuck is not a cylon. Six is not a cylon. |
The only problem is that we all know that Six is a Cylon, the detector isn’t working right so let’s debug it:
$ macrubyd cylon_detector.rb
Starting program.
cylon_detector.rb:1> b cylon_detector.rb:8 if character == 'Six'
Added breakpoint 1.
cylon_detector.rb:1> c
Adama is not a cylon.
Apollo is not a cylon.
Baltar is not a cylon.
Roslin is not a cylon.
StarBuck is not a cylon.
cylon_detector.rb:8> p cylon?(character)
=> false
cylon_detector.rb:8> p "This detector is broken!"
=> "This detector is broken!"
cylon_detector.rb:8> p def cylon?(character); character == 'Six'; end
=> nil
cylon_detector.rb:8> p cylon?(character)
=> true
cylon_detector.rb:8> p cylon?('Matt')
=> false
cylon_detector.rb:8> c
Six is a Cylon!
Program exited. |
The first thing we do is to add a conditional breakpoint:
b cylon_detector.rb:8 if character == 'Six' |
Basically, the debugger will add a breakpoint at line 8 which will only be active when the value of ‘character’ is equal to ‘Six’.
Now that the breakpoint added, we can continue the program execution and just wait until we reach the defined condition.
cylon_detector.rb:1> c |
Once we reach the breakpoint, we evaluate the result of “cylon?(character)” by using the p command. We see that the result is “false” when we know for sure that it should be true since the value of the character variable is ‘Six’ and she is a cylon. At this point, you might have guessed that somewhat acted as a cylon agent and I pretended to fix the problem by overwriting the “cylon?” method:
cylon_detector.rb:8> p def cylon?(character); character == 'Six'; end |
Now that the method is overwritten, I can check that Six is recognized as being a cylon:
cylon_detector.rb:8> p cylon?(character) => true |
and also check that I am not detected a cylon:
cylon_detector.rb:8> p cylon?('Matt')
=> false |
I can now continue the execution of the program and see that Six is detected as a Cylon!
Of course this is just a very early version of the debugger and we will see lots of improvement in the next few weeks. Who knows someone might even create a GUI for the debugger and/or a Xcode integration.
Anyway, the point being that MacRuby developers should expect a lot of awesome stuff coming up their way soon. (also be careful about the skin jobs around you, cylon detectors can’t be trusted!)
Controlling iTunes with MacRuby
Since Mac OS X v10.5, Apple added a technology called Scripting Bridge which allows to control and communicate with scriptable applications such as Mail, iChat or iTunes.
A few weeks back, I showed how to control iChat with MacRuby. This time I’m going to show you how to control iTunes.
Here is a small script that I wrote to wake me up in music every morning.
#!/usr/local/bin/macruby framework 'Foundation' framework 'ScriptingBridge' itunes = SBApplication.applicationWithBundleIdentifier("com.apple.itunes") load_bridge_support_file 'iTunes.bridgesupport' itunes.run class SBElementArray def [](value) self.objectWithName(value) end end itunes.stop playlist = itunes.sources["Library"].userPlaylists["morning"] playlist.playOnce(false) if playlist |
The idea is that I have a Mac Mini streaming music through speakers connected to an AirportExpress in my bedroom.
Let’s go through the script quickly.
We start by loading two frameworks, Foundation and ScriptingBridge.
Now that we have ScriptingBridge loaded, we can control iTunes. To do that, we use:
SBApplication.applicationWithBundleIdentifier(“com.apple.itunes”)
We then load a bridgesupport file that contains the enumerated constants from the iTunes scriptable dictionary.
We make sure iTunes is running by calling #run on the application object.
Before using iTune scriptable interface, we are making the API a bit nicer, it’s totally unnecessary but it makes our code look better.
itunes.sources returns an instance of SBElementArray which is not really an array nor a hash.
The rest of the code is pretty simple, we find the library, find the playlist called ‘morning’ and play it if found.
So you might wonder two things:
- What is the iTunes.bridgesupport file?
- How do you know what methods are available to control iTunes?
bridgesupport file
The bridgesupport file is important since it defines the required constants.
Apple provides a metadata generator called gen_bridge_metadata which generates a bridgesupport file.
Here is what the documentation says:
$ man gen_bridge_metadata |
NAME gen_bridge_metadata -- Objective-C Bridges Metadata Generator SYNOPSIS gen_bridge_metadata [options...] headers... DESCRIPTION gen_bridge_metadata is a tool that generates bridging metadata information for a given framework or set of head- ers. The Objective-C bridges supported in Mac OS X, such as RubyCocoa (Ruby) and PyObjC (Python), read this information at runtime. Metadata files describe the parts of an Objective-C framework that the bridges cannot automatically handle. These are primarily the ANSI C elements of the framework -- functions, constants, enumerations, and so on -- but also include special cases such as functions or methods that accept pointer-like arguments. These special cases must be manually specified in separate files called exceptions. The gen_bridge_metadata tool can then read in the exceptions file when it generates the framework metadata. The file extension used for metadata files should be .bridgesupport. Certain elements, such as inline functions, cannot be described in the metadata files. It is therefore required to generate a dynamic library in order to make the bridges use them. The gen_bridge_metadata tool can take care of that for you. The file extension for the dynamic libraries should be .dylib. You should install metadata files in one of three filesystem locations. For example, for a framework named MyFramework that is installed as /Library/Frameworks/MyFramework.framework, you can install the MyFramework.bridgesupport and MyFramework.dylib files in one of the following possible locations, in order of priority: o /Library/Frameworks/MyFramework/Resources/BridgeSupport o /Library/BridgeSupport o ~/Library/BridgeSupport |
The problem is that we don’t have a framework or header file to generate a bridgesupport file for.
So, what we need a header file for iTunes, turns out we have a tool to do that:
$ sdef /Applications/iTunes.app | sdp -fh --basename iTunes |
I won’t go in the details of what sdef and sdp do, just check their manual page.
Running the command above will create a iTunes.h which we can use to create a bridgesupport file.
Here is the generated header file: http://gist.github.com/279657
Now, let’s create a bidgesupport file:
$ gen_bridge_metadata -c '-I.' iTunes.h > iTunes.bridgesupport |
An that’s how we get the bridgesupport file. (see file: http://gist.github.com/279698)
iTunes Documentation
The easiest way to understand what’s available to you is to open iTunes’ dictionary in the AppleScript Editor.

Otherwise you can study the iTunes.h file.
I wrote a very dumb parser to give you an idea of the methods and properties available when controlling iTunes via ScriptingBridge, here is the output:
Class: iTunesPrintSettings Properties: copies (the number of copies of a document to be printed) collating (Should printed copies be collated?) startingPage (the first page of the document to be printed) endingPage (the last page of the document to be printed) pagesAcross (number of logical pages laid across a physical page) pagesDown (number of logical pages laid out down a physical page) errorHandling (how errors are handled) requestedPrintTime (the time at which the desktop printer should print the document) printerFeatures (printer specific options) faxNumber (for fax number) targetPrinter (for target printer) Method: printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme Returned: void Print the specified object(s) ---- Method: close Returned: void Close an object ---- Method: delete Returned: void Delete an element from an object ---- Method: duplicateTo:(SBObject *)to Returned: SBObject Duplicate one or more object(s) ---- Method: exists Returned: BOOL Verify if an object exists ---- Method: open Returned: void open the specified object(s) ---- Method: playOnce:(BOOL)once Returned: void play the current track or the specified track or file. ---- Class: iTunesApplication Properties: currentEncoder (the currently selected encoder (MP3, AIFF, WAV, etc.)) currentEQPreset (the currently selected equalizer preset) currentPlaylist (the playlist containing the currently targeted track) currentStreamTitle (the name of the current song in the playing stream (provided by streaming server)) currentStreamURL (the URL of the playing stream or streaming web site (provided by streaming server)) currentTrack (the current targeted track) currentVisual (the currently selected visual plug-in) EQEnabled (is the equalizer enabled?) fixedIndexing (true if all AppleScript track indices should be independent of the play order of the owning playlist.) frontmost (is iTunes the frontmost application?) fullScreen (are visuals displayed using the entire screen?) name (the name of the application) mute (has the sound output been muted?) playerPosition (the player’s position within the currently playing track in seconds.) playerState (is iTunes stopped, paused, or playing?) selection (the selection visible to the user) soundVolume (the sound output volume (0 = minimum, 100 = maximum)) version (the version of iTunes) visualsEnabled (are visuals currently being displayed?) visualSize (the size of the displayed visual) Method: browserWindows Returned: SBElementArray ---- Method: encoders Returned: SBElementArray ---- Method: EQPresets Returned: SBElementArray ---- Method: EQWindows Returned: SBElementArray ---- Method: playlistWindows Returned: SBElementArray ---- Method: sources Returned: SBElementArray ---- Method: visuals Returned: SBElementArray ---- Method: windows Returned: SBElementArray ---- Method: printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme Returned: void Print the specified object(s) ---- Method: run Returned: void run iTunes ---- Method: quit Returned: void quit iTunes ---- Method: add:(NSArray *)x to:(SBObject *)to Returned: iTunesTrack add one or more files to a playlist ---- Method: backTrack Returned: void reposition to beginning of current track or go to previous track if already at start of current track ---- Method: convert:(NSArray *)x Returned: iTunesTrack convert one or more files or tracks ---- Method: fastForward Returned: void skip forward in a playing track ---- Method: nextTrack Returned: void advance to the next track in the current playlist ---- Method: pause Returned: void pause playback ---- Method: playOnce:(BOOL)once Returned: void play the current track or the specified track or file. ---- Method: playpause Returned: void toggle the playing/paused state of the current track ---- Method: previousTrack Returned: void return to the previous track in the current playlist ---- Method: resume Returned: void disable fast forward/rewind and resume playback, if playing. ---- Method: rewind Returned: void skip backwards in a playing track ---- Method: stop Returned: void stop playback ---- Method: update Returned: void update the specified iPod ---- Method: eject Returned: void eject the specified iPod ---- Method: subscribe:(NSString *)x Returned: void subscribe to a podcast feed ---- Method: updateAllPodcasts Returned: void update all subscribed podcast feeds ---- Method: updatePodcast Returned: void update podcast feed ---- Method: openLocation:(NSString *)x Returned: void Opens a Music Store or audio stream URL ---- Class: iTunesItem Properties: container (the container of the item) index (The index of the item in internal application order.) name (the name of the item) persistentID (the id of the item as a hexidecimal string. This id does not change over time.) Method: id Returned: NSInteger the id of the item ---- Method: printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme Returned: void Print the specified object(s) ---- Method: close Returned: void Close an object ---- Method: delete Returned: void Delete an element from an object ---- Method: duplicateTo:(SBObject *)to Returned: SBObject Duplicate one or more object(s) ---- Method: exists Returned: BOOL Verify if an object exists ---- Method: open Returned: void open the specified object(s) ---- Method: playOnce:(BOOL)once Returned: void play the current track or the specified track or file. ---- Method: reveal Returned: void reveal and select a track or playlist ---- Class: iTunesPlaylist Properties: duration (the total length of all songs (in seconds)) name (the name of the playlist) parent (folder which contains this playlist (if any)) shuffle (play the songs in this playlist in random order?) size (the total size of all songs (in bytes)) songRepeat (playback repeat mode) specialKind (special playlist kind) time (the length of all songs in MM:SS format) visible (is this playlist visible in the Source list?) Method: tracks Returned: SBElementArray ---- Method: moveTo:(SBObject *)to Returned: void Move playlist(s) to a new location ---- Method: searchFor:(NSString *)for_ only:(iTunesESrA)only Returned: iTunesTrack search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes. ---- Class: iTunesAudioCDPlaylist Properties: artist (the artist of the CD) compilation (is this CD a compilation album?) composer (the composer of the CD) discCount (the total number of discs in this CD’s album) discNumber (the index of this CD disc in the source album) genre (the genre of the CD) year (the year the album was recorded/released) Method: audioCDTracks Returned: SBElementArray ---- Class: iTunesDevicePlaylist Method: deviceTracks Returned: SBElementArray ---- Class: iTunesLibraryPlaylist Method: fileTracks Returned: SBElementArray ---- Method: URLTracks Returned: SBElementArray ---- Method: sharedTracks Returned: SBElementArray ---- Class: iTunesRadioTunerPlaylist Method: URLTracks Returned: SBElementArray ---- Class: iTunesSource Properties: capacity (the total size of the source if it has a fixed size) freeSpace (the free space on the source if it has a fixed size) kind () Method: audioCDPlaylists Returned: SBElementArray ---- Method: devicePlaylists Returned: SBElementArray ---- Method: libraryPlaylists Returned: SBElementArray ---- Method: playlists Returned: SBElementArray ---- Method: radioTunerPlaylists Returned: SBElementArray ---- Method: userPlaylists Returned: SBElementArray ---- Method: update Returned: void update the specified iPod ---- Method: eject Returned: void eject the specified iPod ---- Class: iTunesTrack Properties: album (the album name of the track) albumArtist (the album artist of the track) albumRating (the rating of the album for this track (0 to 100)) albumRatingKind (the rating kind of the album rating for this track) artist (the artist/source of the track) bitRate (the bit rate of the track (in kbps)) bookmark (the bookmark time of the track in seconds) bookmarkable (is the playback position for this track remembered?) bpm (the tempo of this track in beats per minute) category (the category of the track) comment (freeform notes about the track) compilation (is this track from a compilation album?) composer (the composer of the track) databaseID (the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data.) dateAdded (the date the track was added to the playlist) objectDescription (the description of the track) discCount (the total number of discs in the source album) discNumber (the index of the disc containing this track on the source album) duration (the length of the track in seconds) enabled (is this track checked for playback?) episodeID (the episode ID of the track) episodeNumber (the episode number of the track) EQ (the name of the EQ preset of the track) finish (the stop time of the track in seconds) gapless (is this track from a gapless album?) genre (the music/audio genre (category) of the track) grouping (the grouping (piece) of the track. Generally used to denote movements within a classical work.) kind (a text description of the track) longDescription () lyrics (the lyrics of the track) modificationDate (the modification date of the content of this track) playedCount (number of times this track has been played) playedDate (the date and time this track was last played) podcast (is this track a podcast episode?) rating (the rating of this track (0 to 100)) ratingKind (the rating kind of this track) releaseDate (the release date of this track) sampleRate (the sample rate of the track (in Hz)) seasonNumber (the season number of the track) shufflable (is this track included when shuffling?) skippedCount (number of times this track has been skipped) skippedDate (the date and time this track was last skipped) show (the show name of the track) sortAlbum (override string to use for the track when sorting by album) sortArtist (override string to use for the track when sorting by artist) sortAlbumArtist (override string to use for the track when sorting by album artist) sortName (override string to use for the track when sorting by name) sortComposer (override string to use for the track when sorting by composer) sortShow (override string to use for the track when sorting by show name) size (the size of the track (in bytes)) start (the start time of the track in seconds) time (the length of the track in MM:SS format) trackCount (the total number of tracks on the source album) trackNumber (the index of the track on the source album) unplayed (is this track unplayed?) videoKind (kind of video track) volumeAdjustment (relative volume adjustment of the track (-100% to 100%)) year (the year the track was recorded/released) Method: artworks Returned: SBElementArray ---- Class: iTunesFileTrack Properties: location (the location of the file represented by this track) Method: refresh Returned: void update file track information from the current information in the track’s file ---- Class: iTunesURLTrack Properties: address (the URL for this track) Method: download Returned: void download podcast episode ---- Class: iTunesUserPlaylist Properties: shared (is this playlist shared?) smart (is this a Smart Playlist?) Method: fileTracks Returned: SBElementArray ---- Method: URLTracks Returned: SBElementArray ---- Method: sharedTracks Returned: SBElementArray ---- |
IM new year countdown with MacRuby
Here is the geekiest way I found to wish Happy New Year to my IM contacts:

framework 'ScriptingBridge' app = SBApplication.applicationWithBundleIdentifier("com.apple.iChat") original_status = app.statusMessage new_year = Time.mktime(2010, 1, 1, 0, 0) loop do now = Time.now time_left = (new_year - now).ceil if time_left > 0 app.statusMessage = "#{time_left} seconds left until 2010 (EST)" else app.statusMessage = "Happy New Year 2010!" exit end sleep(1) end |
If you are alone at home playing WOW, you can also trigger iTunes to play a mp3 file with crowd noise and people shouting ‘Happy New Year 2010‘!

Fun with MacRuby
To be ready for 2010, I’m taking some time off relaxing and spending time with my family in Florida.
During my free time, I’ve been reading, catching up on movies and TV shows and worked on the MacRuby book that I am writing for O’Reilly.
I wrote a bunch of small apps, played with various APIs and every single time I was amazed by all the goodies Apple makes available to developers. My most recent discovery is very simple but I wanted to share it with you.
I often type text in English, French and Spanish and I even mix the languages from time to time. SnowLeopard comes with a great spellchecker that auto detects the language I’m typing in and is most of the time correct. It’s a very impressive feature and I was wondering if, as a MacRuby developer, I could use one of Apple’s lib to detect what language is being used. I dug through the documentation but didn’t find anything. I started looking at some header files and found the API to use
framework 'Foundation' class String def language CFStringTokenizerCopyBestStringLanguage(self, CFRangeMake(0, self.size)) end end puts "Bonne année!".language # => "fr" puts "Happy new year!".language # => "en" puts "¡Feliz año nuevo!".language # => "es" puts "Felice anno nuovo!".language # => "it" puts "أعياد سعيدة".language # => "ar" puts "明けましておめでとうございます。".language # => "ja" |
The documentation says that the result is not guaranteed to be accurate and that typically 200-400 characters are required to reliably guess the language of a string. (CFStringTokenizer Doc)
Probably not the most useful piece of code, but really cool none the less
Happy new year!
MacRuby tips: browse for folder or file dialog
This is yet another pretty simple tip.
Use case: let say you want your applications users to choose one or multiple files or folder on their file system. A good example would be that you want the user to choose a file to process or a folder where to save some data.
![]()
In the example above, I added a browse button and a text field.
I would like my users to click on the browse button, locate a folder and display it in the text field.
In your MacRuby controller, use a simple action method as well as an accessor to the text field:
1 2 3 4 | attr_accessor :destination_path def browse(sender) end |
Now, in Interface builder bind the destination_path outlet to the text field you want to use to display the path and bind the button to the browse action.
Let’s go back to our action method and let’s create a dialog panel, set some options and handle the user selection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def browse(sender) # Create the File Open Dialog class. dialog = NSOpenPanel.openPanel # Disable the selection of files in the dialog. dialog.canChooseFiles = false # Enable the selection of directories in the dialog. dialog.canChooseDirectories = true # Disable the selection of multiple items in the dialog. dialog.allowsMultipleSelection = false # Display the dialog and process the selected folder if dialog.runModalForDirectory(nil, file:nil) == NSOKButton # if we had a allowed for the selection of multiple items # we would have want to loop through the selection destination_path.stringValue = dialog.filenames.first end end |
That’s it, your user can now browse for a folder and the selection will be displayed in the text field. Look at the NSOpenPanel documentation for more details on the Cocoa API.
MacRuby 0.5 beta 1 and Textorize
Good news everyone!
MacRuby beta 1 has been released! Official announcement here.
Note that the download is only for SnowLeopard, intel machines.
Lots of great stuff in this new release, the first one based on LLVM. Check the Laurent’s post to learn more about the work done on compilation, optimization, concurrency, compatibility and Cocoa interaction. And a big thank you to Laurent Sansonetti who is putting so much effort in this project!
However, don’t forget it’s still a beta release and you might encounter bugs. Feel free to report them in the bug tracker or ask on the mailing list.
On a different topic, the other day, John Gruber from Daring Fireball wrote a quick note about Thomas Fuchs’ textorize script which since got its own place on the internet http://textorize.org/.
Textorize is a Ruby-based font rasterizer command line utility for Mac OS X. It generates PNG files from an input string and options, using Mac OS X’s pristine typography facilities. As John said, it’s a case where a few lines of Ruby code beat Photoshop.
Thomas version is based on RubyCocoa which is great… but not MacRuby.
To celebrate MacRuby 0.5 beta1, I ported the gem over and pushed it to the excellent gemcutter.org facility.
After installing MacRuby beta, follow these directives:
$ macgem sources -a http://gemcutter.org $ sudo macgem install textorize-mr $ textorize -f"Didot" -s200 "MacRuby 0.5b1" $ open output.png
And you will get a subpixel antialiased fancy bitmap like that:

Check http://textorize.org/ for more examples and http://github.com/mattetti/textorize for the source code.
MacRuby tips: how to play an audio file
Let’s say you would like to play an audio file in your MacRuby app/script, how would you do?
It’s actually pretty simple, you just need to use NSSound. If we wanted to use a system sound we could do:
NSSound.soundNamed('Basso').play |
But let’s look at a more advanced example with some Cocoa patterns. We will loop through all the audio files in a folder and we will play them one after the other.
#!/usr/bin/env macruby framework 'Cocoa' # Cocoa documentation reference: # http://developer.apple.com/mac/library/documentation/Cocoa/Reference/ApplicationKit/Classes/NSSound_Class/Reference/Reference.html def play_sound if @sounds.empty? NSApplication.sharedApplication.terminate(nil) else sound_file = @sounds.shift s = NSSound.alloc.initWithContentsOfFile(sound_file, byReference: false) puts "previewing #{sound_file}" s.delegate = self s.play end end # This is a delegate method called by the sound object def sound(sound, didFinishPlaying: state) play_sound if state end # Delegate method called when the app finished loading def applicationDidFinishLaunching(notification) @sounds = Dir.glob("/System/Library/Sounds/*.aiff") play_sound end # We are delegating the application to self so the script will know when # it finished loading NSApplication.sharedApplication.delegate = self NSApplication.sharedApplication.run |
On my machine, I get the following output:
$ macruby macrubysound.rb previewing /System/Library/Sounds/Basso.aiff previewing /System/Library/Sounds/Blow.aiff previewing /System/Library/Sounds/Bottle.aiff previewing /System/Library/Sounds/Frog.aiff previewing /System/Library/Sounds/Funk.aiff previewing /System/Library/Sounds/Glass.aiff previewing /System/Library/Sounds/Hero.aiff previewing /System/Library/Sounds/Morse.aiff previewing /System/Library/Sounds/Ping.aiff previewing /System/Library/Sounds/Pop.aiff previewing /System/Library/Sounds/Purr.aiff previewing /System/Library/Sounds/Sosumi.aiff previewing /System/Library/Sounds/Submarine.aiff previewing /System/Library/Sounds/Tink.aiff |
Cocoa delegation might seem a bit strange at first when you come from Ruby. In Ruby we rarely do any type of async delegation so let’s quickly look at what’s going on in this script.
The first thing we do is loading the cocoa framework which makes total sense, then we define a bunch of methods and finally we get to:
NSApplication.sharedApplication.delegate = self NSApplication.sharedApplication.run |
We are setting the run loop to delegate to set, which means that when an event is triggered, the delegation method will be called on self (our script).
Once that done, we are starting out run loop.
Once the application is loaded the applicationDidFinishLaunching delegate is being triggered (line 24). At this point, we are looking for all the sound files in the sound system folder and storing them in an instance variable (line 25). Finally, we are calling the play_sound method (line 26).
The play_sound method checks that we have some audio files left to play (line 7), otherwise it quits the app. (line 8 ) If we still have some files in the queue, we get the first one (line 10) and use it to create an instance of NSSound (line 11).
Before playing the NSSound instance, we are setting its delegate to our script (self) (line 13). If you read NSSound Cocoa documentation for #play, you will notice that #play “initiates playback asynchronously and returns control to your application. Therefore, your application can continue doing work while the audio is playing.” In our case, we don’t want to do that, otherwise all the sounds will play at the same time.
The documentation also mentions a delegation you can use to avoid that. This is exactly what we did when we implemented: def sound(sound, didFinishPlaying: state) (line 19)
Rubyists might be surprised by the method signature. MacRuby extended Ruby to support Objective-C selectors.
When the sound is playing, this delegate gets called, we check on the state of the sound, if it finished playing then we call play_sound again.
That’s it! It’s a very elegant implementation giving us the benefits of both Ruby and Cocoa.

