Keeping it Small and Simple

2008.04.05

An mpd clone in Ruby

Filed under: Ruby Programming, Rubygame — Tags: , , , , , , , — Lorenzo E. Danielsson @ 18:02

I was bored, so I decided to write a clone of my music player of choice, the excellent mpd. I also put together a little client, similar to my favorite mpd client, the highly usable mpc.

These are of course watered-down clones that cannot match the original mpd. I wasn’t so much interested in writing an alternative to mdp as just writing something for the fun of it. Feel free to improve upon it.

You will need to have Rubygame installed on the machine that is running the server. There are alternatives that you could look into, but I happen to know Rubygame so I am using it. It is likely that I will at some point experiment with other libraries.

Server/client communication is done via XML-RPC. This makes everything very simple. Again, if you want to practice, you change to some other method for client/server communication (SOAP, your own protocol, or whatever).

Other things you could do include writing a GUI client (I recommend Tk, but Qt4 is also nice, Gtk less so, but at least its Ruby bindings are decent to work with). Or, if you think everything should run in the browser, why not a web client? Of course, you can also add more features. In that case I recommend looking into what mpd supports and try to implement as much of it as possible.

First the server, rmpd:

  1 #! /usr/bin/ruby
  2
  3 # A music playing daemon written in Ruby.
  4
  5 begin
  6   require rubygame
  7   require xmlrpc/server
  8   require optparse
  9 rescue LoadError => e
 10   msg, lib = e.to_s.split /\s+–\s+/
 11   abort "Unable to find #{lib}, cannot continue."
 12 end
 13
 14 class PlayerService
 15   def initialize(player)
 16     @player = player
 17   end
 18
 19   def add(song)
 20     @player.add song.to_i
 21   end
 22
 23   def lsc
 24     @player.lsc
 25   end
 26
 27   def lsq
 28     @player.lsq
 29   end
 30
 31   def play
 32     @player.play
 33   end
 34
 35   def next
 36     @player.next
 37   end
 38
 39   def stop
 40     @player.stop
 41   end
 42 end
 43
 44 class ControllerService
 45   def initialize(player)
 46     @player = player
 47   end
 48
 49   def shutdown
 50     @player.shutdown
 51   end
 52 end
 53
 54 class Player
 55   def initialize(path, port=8080, addr=*)
 56     @col = []
 57     @queue = []
 58     @current = nil
 59     @control = nil
 60
 61     parsedir(path)
 62
 63     @server = XMLRPC::Server.new port, addr
 64     @server.add_handler "player", PlayerService.new(self)
 65     @server.add_handler "controller", ControllerService.new(self)
 66     @server.set_default_handler do |name, *args|
 67       obj, meth = name.split /\./
 68       "No such command: #{meth}"
 69     end
 70
 71     Rubygame.init
 72     Rubygame::Mixer.open_audio
 73   end
 74
 75   def run
 76     @server.serve
 77   end
 78
 79   def add(song)
 80     @queue << @col[song]
 81     "Added #{song}: #{File.basename(@col[song])}"
 82   end
 83
 84   def lsc
 85     return "Collection is empty." if @col.empty?
 86
 87     str = ""
 88     @col.each_index do |idx|
 89       str += "#{idx.to_s.rjust(3)}: #{File.basename(@col[idx])}\n"
 90     end
 91
 92     return str
 93   end
 94
 95   def lsq
 96     return "Queue is empty." if @queue.empty?
 97
 98     str = ""
 99     @queue.each_index do |idx|
100       str += "#{idx.to_s.rjust(3)}: #{File.basename(@queue[idx])}\n"
101     end
102
103     return str
104   end
105
106   def play
107     return "Already playing" if !@current.nil? && @current.playing?
108     return "Queue is empty" if @queue.empty?
109
110     begin
111       song = @queue.shift
112       @current = Rubygame::Mixer::Music.load_audio song
113       @current.play
114       control
115       "Playing #{File.basename(song)}"
116     rescue 
117       "Something went wrong"
118     end
119   end
120
121   def next
122     return "Not playing." if @current.nil? || !@current.playing?
123     @control.exit unless @control.nil?
124     @current.stop
125     sleep 1 while @current.playing?
126     play
127   end
128
129   def stop
130     return "Not playing." if @current.nil? || !@current.playing?
131
132     @control.exit unless @control.nil?
133     @current.stop
134     "Stopped."
135   end
136
137   def control
138     @control = Thread.new {
139       sleep 1 while @current.playing?
140       play unless @queue.empty?
141     }
142
143     @control.run
144   end
145
146   def shutdown
147     @server.shutdown
148     Rubygame::Mixer.close_audio
149     Rubygame.quit
150     "Shutting down server."
151   end
152
153   def parsedir(path)
154     Dir.new(path).each do |file|
155       next if file =~ /^\./
156
157       if File.directory? "#{path}/#{file}"
158         parsedir "#{path}/#{file}"
159       else
160         @col << "#{path}/#{file}"
161       end
162     end
163   end
164 end
165
166 options = {
167   :addr => 127.0.0.1,
168   :port => 8080,
169   :dir => "#{ENV[HOME]}/music"    
170 }
171
172 begin
173   OptionParser.new do |opts|
174     opts.banner = "usage: #{$0} [options]"
175
176     opts.on("-a", "–address=ADDR", String, "Address to listen on.") do |addr|
177       options[:addr] = addr
178     end
179
180     opts.on("-p", "–port=PORT", Integer, "Port to listen on.") do |port|
181       options[:port] = port
182     end
183
184     opts.on("-d", "–dir=DIR", String, "Collection directory.") do |dir|
185       options[:dir] = dir
186     end
187   end.parse!
188 rescue
189   abort "Command line foo!"
190 end
191
192 mpd = Player.new options[:dir], options[:port], options[:addr]
193 mpd.run

Here is the client, rmpc:

 1 #! /usr/bin/ruby
 2
 3 # A client for rmpd.
 4
 5 require xmlrpc/client
 6 require optparse
 7
 8 options = {
 9   :port => 8080,
10   :addr => "127.0.0.1"
11 }
12
13 begin
14   OptionParser.new do |opts|
15     opts.banner = "usage: #{$0} [options] command"
16
17     opts.on("-a", "–address=ADDR", String, "Address of server") do |addr|
18       options[:addr] = addr
19     end
20
21     opts.on("-p", "–port=PORT", Integer, "Port of server") do |port|
22       options[:port] = port
23     end
24   end.parse!
25 rescue
26   abort "Command line foo!"
27 end
28
29 abort "No command." if ARGV.empty?
30
31 begin
32   server = XMLRPC::Client.new options[:addr], "/RPC2", options[:port]
33 rescue
34   abort "No contact with server."
35 end
36
37 player = server.proxy "player"
38 control = server.proxy "controller"
39
40 command = ARGV.shift
41 if command == shutdown
42   puts control.shutdown
43 else
44   puts player.send(command, *ARGV)
45 end

Advertisements

2007.12.27

Rubygame 2.2.0 released

Filed under: Rubygame — Tags: , — Lorenzo E. Danielsson @ 13:15

It bit of old news perhaps, but I’ll blame it on the season. 😉

Rubygame 2.2.0 was released on 2007.12.19. Among the changes you will notice that Surface#set_at is back. This means you’ll no longer need the kludge to add your own set_at method.

For those of you who are following my Rubygame tutorial, once you’ve installed Rubygame 2.2.0, you may want to update your programs. You can get rid of the Surface#put_pixel method. You will also need to change the call to put_pixel to instead call set_at.
Hm.. Maybe I’ll just have to give an example in my next tutorial.

2007.12.20

Rubygame tutorial #2: pixels

Filed under: Rubygame, Rubygame Tutorial — Tags: , , , , — Lorenzo E. Danielsson @ 13:10

Welcome to my second rubygame tutorial. This time we will look at pixels, which are the smallest of the building blocks available to you. We will also have a short introduction to timing. At the end of this tutorial you will have a framework for most of the applications that we will be writing over the next few tutorials.

Pixels

A pixel is the smallest possible unit that can be displayed on the screen. There are three attributes of a pixel that you always need to keep in mind: its x-coordinate, its y-coordinate and its color.

In contrast to pygame and just about every single game development library on the planet, Rubygame does not have a method to plot individual pixels. It used to but it was removed. Its not really a problem, because we can easily create our own, but it is a little annoying that something that you need so frequently is not there. Oh, well, let us not worry about such details now.

I normally learn best by looking at examples, so here we go. The following program plots random pixels to the screen.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24
25     @screen.fill [0, 0, 0]
26     @screen.update
27   end
28
29   def event_loop
30     loop do
31       @events.each { |event|
32         case event
33         when QuitEvent
34           return
35         end
36       }
37
38       draw
39       @screen.update
40     end
41   end
42
43   def draw
44     point = [rand(Width), rand(Height)]
45     color = [rand(255), rand(255), rand(255)]
46     @screen.put_pixel point, color
47   end
48 end
49
50 Rubygame.init
51 RandomPixels.new.event_loop
52 Rubygame.quit

The first thing we do is inject a little extra functionality into Rubygame’s Surface class. I chose to call it put_pixel (reminds me of Turbo Pascal’s graph library), but feel free to call it anything you want. The put_pixel method simple draws a box which is a single pixel high and a single pixel wide. The result is that I’ve plotted a single pixel.

Most of the rest of the program should be familiar to you, at least if you have already read tutorial #1. What is new is that there is a call to the draw method inside the event loop. The draw method creates a random point (x, y) and a random color (red, green, blue). Then it calls our Surface#putpixel method, passing in the point and color.

Notice that since the draw method is called inside the event loop, this program will continuously plot randomly colored pixels and random coordinates. There is also a call to Surface#update in the event loop. More about what that does below.

Exercises

  1. Modify the above program to only plot pixels that are random shades of blue.
  2. Modify the above program to only plot pixels of a single color, such as orange.
  3. Modify the above program to keep count of how many pixels have been plotted.
  4. Modify the program to only plot pixels in the top half of the screen.

Timing

Try running the above program. While it is running, open up a terminal and start ‘top’. You can also use any other utility that lets you see your system’s resource usage (such as Gnome system monitor). You may notice that CPU usage is quite high. What is happening?

Our program is actually very busy all the time, it is plotting points and checking if any event have occured, and its doing this constantly. Because of that, the program signals the kernel that “Hey, I’ve got a heavy workload over here. Better help me out!”

The fact is that the program isn’t really that busy. There is time for it to just relax a bit once in a while. If we can let our program take a break, it means there is more time available for the kernel to help out other tasks that are running on your system, and the whole system ends up being happier.

That is obvioulsy grossly over-simplified, but i think you get the point. We need to let our program “go to sleep” every so often. This can be achieved with the help of a clock. The following program looks like the previous one, but we have added a clock to it.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24     @clock = Clock.new
25     @clock.target_framerate = 120
26
27     @screen.fill [0, 0, 0]
28     @screen.update
29   end
30
31   def event_loop
32     loop do
33       @events.each { |event|
34         case event
35         when QuitEvent
36           return
37         end
38       }
39
40       draw
41       @clock.tick
42       @screen.update
43     end
44   end
45
46   def draw
47     point = [rand(Width), rand(Height)]
48     color = [rand(255), rand(255), rand(255)]
49     @screen.put_pixel point, color
50   end
51 end
52
53 Rubygame.init
54 RandomPixels.new.event_loop
55 Rubygame.quit

There are two things that are important here. The first happens on lines 24 and 25, where we initialize a clock and set a desired framerate for it. I’m going to let you experiment a little with the target_framerate. Set it to different values, high as well as low ones. Each time you do, run the program and see how “fast” the program itself seems to run. Also check the CPU usage. What do you notice?

The second thing that you should notice is that in the event loop, we added an extra line. On line 41 we call Clock#tick. This is the line that puts our program to rest for a short while. This call is inside the event loop so it gets called frequently.

Timing the program has other uses than just to lower its CPU usage, but we will look at that more in a later tutorial.

Exercises

  1. Set Clock#target_framerate to 10. What happens to your program? What happens to CPU usage while the program is running?
  2. Set Clock#target_framerate to 1000. What happens to your program? What happens to CPU usage while the program is running?

Doing more than one thing at once

Our program plots a single pixel every iteration of the event loop. Plotting just a single pixel is an expensive operation since @clock.tick and @screen.update take significant time (relatively speaking). In order to speed things up, it is better that we perform more than a single drawing operation at a time. In the following program we plot one hundred pixels each cycle of the event loop.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels2
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24     @clock = Clock.new
25     @clock.target_framerate = 120
26
27     @screen.fill [0, 0, 0]
28     @screen.update
29   end
30
31   def event_loop
32     loop do
33       @events.each { |event|
34         case event
35         when QuitEvent
36           return
37         end
38       }
39
40       draw
41       @clock.tick
42       @screen.update
43     end
44   end
45
46   def draw
47     100.times do
48       point = [rand(Width), rand(Height)]
49       color = [rand(255), rand(255), rand(255)]
50       @screen.put_pixel point, color
51     end
52   end
53 end
54
55 Rubygame.init
56 RandomPixels2.new.event_loop
57 Rubygame.quit

Normally we want to assemble our whole frame before displaying it. If we were to draw directly to the screen, we would get a lot of flicker. So instead we perform all our drawing on a hidden buffer. When we have drawn the entire frame, we call Surface#update to make the new frame visible.

This happens at a very high speed, so our eyes don’t see that what we are doing is switching buffers all the time. But updating the screen is relatively time-consuming. At a later stage we will see how we can update only those parts of our surface that have changed since the previous frame.

Since we only call Surface#update once our whole frame is assembled, it also means we have a limited time to perform all our drawing. If it takes a minute to draw a frame then our framerate will be 1 frame per minute. Any gamer who plays your game for more than a minute deserves a reward, for patience or stupidity, whichever comes first.

Exercises

  1. In the last program, experiement with the number of pixels to plot in the draw method. Try a relatively low number such as 10. Also try higher number such as 1000 or 10000. What do you think would happen to the program if you choose a very large number?
  2. In the Surface#set_pixel method we are calling Surface#draw_box with two points that are the same. Modify the method so that the second point is 5×5 points away from the first. What happens to the program.

Conclusion

Now you are able to create a Rubygame screen and plot pixels onto it. There is actually quite a bit we can do armed with only that knowledge. We will continue plotting pixels for a little longer, while we look at some interesting events that we can let our program respond to.

2007.12.09

Rubygame tutorial #1: getting started

Filed under: Rubygame, Rubygame Tutorial — Lorenzo E. Danielsson @ 21:58

I while ago I posted on Ruby/SDL, which allows you to program SDL applications in Ruby. It works, but I think many Ruby programmers would like to have a higher-level API, something similar to pygame.

Enter rubygame. The first rubygame site states that:

Rubygame is a cross-platform multimedia library for ruby, the most beautiful programming language in the world. It embraces the ruby spirit to provide developers with a library that is clean and easy to use so you can get things done painlessly, and yet powerful and flexible so you can get them done right

I have written a few pygame tutorials to date, but since I happen to also love Ruby I thought that I would try to put together some similar tutorials for Ruby programmers as well. So, let’s get started.

Getting Rubygame

Before you can start developing using rubygame, you obviously need to get it installed. It doesn’t exist in the Debian repositories (at least not yet), but I downloaded it by hand, and it wasn’t difficult at all to install. Just make sure you have all the dependencies installed. Then just follow the instructions in the README.

What you need to know already

I assume that you have some familiarity with Ruby, but you don’t have to be an expert at all (it would be difficult for me to demand that out of you since I am by no means one myself!).

Displaying a window

Let’s get started with a simple program that displays a small window. Type this in as you see it and save it as something like window.rb. You should have some familiarity with how to use a computer and how to type commands in a terminal window.


1 #! /usr/bin/ruby -w
2
3 require rubygame
4
5 Rubygame.init
6 screen = Rubygame::Screen.new [640, 400]
7 loop {}
8

You can run it directly by typing ruby window.rb (assuming that’s what you called it) or you can chmod +x window.rb in which case you can run it with ./window.rb.

You should see an empty, black window. The window is 640 pixels wide and 400 pixels high. You will also notice that clicking on the window’s close button doesn’t do a thing. In order to close the window you will have to activate your terminal window and press CTRL+C.

In order to use rubygame you first have to require it. Then you should call Rubygame.init to allow rubygame to initialize itself. Create a Rubygame::Screen, passing in the window’s width and height as an Array. The next line just jumps the program into an indefite loop, which is why we have to press CTRL+C to stop the program.

Exercises

1. Modify the program to use other values than 640 and 400 for the width and the height of the window. Run the program to confirm that the window size changes. For instance, create a window that is 320×200.

2. Modify the program to accept the width and the height of the window as command-line arguments.

3. Create a program that upon start up prompts the user for the width and the height and then creates a window of those dimensions.

4. Create two instances of Rubygame::Screen. You can call them screen1 and screen2 if you wish. What do you think should happen? Run the program. What actually happens? Can you explain why?

Adding an event loop

It would probably be a good idea to create a program that behaves properly, at the very least when the user tries to close the window. The following does just that:


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 Rubygame.init
 6 screen = Rubygame::Screen.new [640, 400]
 7 events = Rubygame::EventQueue.new
 8
 9 loop do
10   events.each { |event|
11      if event.class == Rubygame::QuitEvent
12        Rubygame.quit
13        exit
14      end
15   }
16 end

The first new thing is that we create and EventQueue. Events are things that happen to the window, like the mouse moving over it, a key on the keyboard being pressed while the window is active and so on. Another event is the Quit event, which is triggered when the close window button is clicked.

In order to be able to respond to events that happen during the life time of the application, we need an event loop. This is basically just a loop that checks if there are any new events in the event queue. If there are we can deal with them. In this case all we do is check if the event is the QuitEvent, and if so, close down Rubygame and quit the application.

Exercises

1. Rewrite all the exercises you wrote in the previous section to include an event loop.

2. See if you can re-write the application to break out of the outer loop when the application receives the Quit event. There are several ways to do this.

Adding some color

So far so good. But let’s change the background color. Try this:


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 Rubygame.init
 6 screen = Rubygame::Screen.new [640, 400]
 7 events = Rubygame::EventQueue.new
 8 screen.fill [255, 255, 0]
 9 screen.update
10
11 loop do
12   events.each { |event|
13      if event.class == Rubygame::QuitEvent
14        Rubygame.quit
15        exit
16      end
17   }
18 end

What is new is that we fill the screen with a color. The color comes in a triplet consisting of red, green and blue (RGB). Each can be a value between 0 and 255.

Exercises

1. Create a window with a white background color.

2. Create a window with a red background color.

3. Experiment with setting different background colors. If you are not familiar with RGB values then spend a little extra time to figure out how to get colors like yellow, brown, cyan etc.

4. Create a program that asks the user to specify the values for red, green and blue. Check that the values are in the valid range (0-255) and then use these for the background color.

5. Create a program that upon start-up checks the time of the day and sets the brightness of the background color accordingly. Use a blue scale. If it is midnight, the screen should be black. If it is midday, the screen should be bright blue. Any other time the background should be something in between corresponding to the brightness outside.

But wait

I was having so much fun that I almost forgot: Ruby is an object-oriented programming language. Below is a modified version of the previous program that sticks everything into a class and adds a few LOC at the same time. 😉


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 class Simple
 6   include Rubygame
 7
 8   def initialize
 9     @screen = Screen.new [640, 400]
10     @events = EventQueue.new
11     @screen.fill [255, 240, 0]
12     @screen.update
13   end
14
15   def event_loop
16     loop do
17       @events.each { |event|
18         case event
19         when QuitEvent
20           return
21         end
22       }
23     end
24   end
25 end
26
27 Rubygame.init
28 Simple.new.event_loop
29 Rubygame.quit
30

This example can be seen as a form of template for the next few programs that we will be writing.

Exercises

1. Modify the last version of the program so that it has the attributes width and height. The constructor should take a width and height as well. If you like, you can let the constructor have default values for these two.

Conclusion

Well, with that, the first Rubygame tutorial is at an end. In the next tutorial we will start drawing some things on the screen and looking at a few more events that our program can deal with. Until then, practice, practice, practice. And, remember to always have fun when you program in Ruby.

Create a free website or blog at WordPress.com.