Ruby is more!
Posted: Wed Feb 15, 2017 5:03 pm
I once made a series of articles going into details about classes in Ruby, telling you that they are the heart of OOP. I think I was to quick with that. Most people will not really have an idea of object orientation in its entirety.
Of course, you can use Ruby as a one-time solution for a one-time task. That's totally fine:
This code will output 50% of the value that just came in. Nothing more and nothing less. And if you are only looking for such easy and quick solutions, you don't need to read further. These are valid tasks for scripting with Ruby.
But Ruby is so much more. Unfortunately you can't access its real power unless you understand the underlying idea of OOP. This time I will concentrate on this abstract layer and will not explain semantics or syntax. For those you can always go to ruby-doc.org. My examples in the past may have not been "audio" enough. So I will try to make you understand the idea of OOP by using a synthesizer as an image.
So, let us have a look at a synth's oscillators. There may be a sine oscillator, a square osc, a sawtooth and a triangle osc.
You might think of them as 4 objects. And in the analog world they indeed are. But the digital world is different. Let us have a closer look. What does such an oscillator provide? A phase setting, a frequency setting and in our case a sync setting.
If you consider this than there is only one difference to the oscillators: the produced waveform. In the digital world, therefore those oscillators (no matter how many of them you will have) are two objects: One object that has all the settings, and another object that builds on the first one and defines the waveform.
In Ruby it would look similar to the following code.
Note that I only cared about the sine oscillator. This is to keep the code small. A real code would of course have an option to define a waveform and not just hardcode "@waveform = "sine". And the class would not be called "Sine_osc", but probably "Waveform_osc".
Ruby itself works in the very same way. A number like 42 is an instance of the class "Fixnum". But Fixnum is based on "Numeric", a superclass that holds all informations that all numbers share. So, while 42.0 is of class "Float", Float is inherited from "Numeric", just as Fixnum was. If you ever wonder about the class relations, you can check them with the methods .class and .superclass:
There is one class that all the others are inherited from, and that class is "Object". It is the root of a tree, so to speak.
But back to our synth. Can I further inherit? Yes, of course. For example, you might have a class "Osc_section", inherited from "Waveform_osc" and adding tuning (octave, halfnotes, fine). Or adding envelopes. Or... You get the point.
Leaves one question: Why should I use such a tree structure, whith dozen of classes. Well, besides a well readable code, there's one mega-advantage. You only define those things once, and they are true for all instances you use of those classes. Now imagine you have created a synth with 100 oscillators, each featuring its own waveform. Now you encounter an error.
Without the OOP approach you have no choice than finding the error and correcting it in all 100 oscillators.
With the OOP approach you just have to find the error and correct it in the affected class - and all 100 oscillators are bug-free again! Is it cool, or is it cool?
Of course, you can use Ruby as a one-time solution for a one-time task. That's totally fine:
- Code: Select all
output(0, @ins[0] * 0.5)
This code will output 50% of the value that just came in. Nothing more and nothing less. And if you are only looking for such easy and quick solutions, you don't need to read further. These are valid tasks for scripting with Ruby.
But Ruby is so much more. Unfortunately you can't access its real power unless you understand the underlying idea of OOP. This time I will concentrate on this abstract layer and will not explain semantics or syntax. For those you can always go to ruby-doc.org. My examples in the past may have not been "audio" enough. So I will try to make you understand the idea of OOP by using a synthesizer as an image.
So, let us have a look at a synth's oscillators. There may be a sine oscillator, a square osc, a sawtooth and a triangle osc.
You might think of them as 4 objects. And in the analog world they indeed are. But the digital world is different. Let us have a closer look. What does such an oscillator provide? A phase setting, a frequency setting and in our case a sync setting.
If you consider this than there is only one difference to the oscillators: the produced waveform. In the digital world, therefore those oscillators (no matter how many of them you will have) are two objects: One object that has all the settings, and another object that builds on the first one and defines the waveform.
In Ruby it would look similar to the following code.
- Code: Select all
class Oscillator
def initialize
@frequency = 440.0
@phase = 0.0
@sync = false
end
attr_accessor :frequency, :phase, :sync
end
class Sine_osc < Oscillator ## "<" here indicates "inherit from"
def initialize
super ##calls the method with the same name from the class we're inheriting from
##you only need super, if you DON'T want to override the same method of the superclass
@waveform = "sine"
end
attr_accessor :waveform
end
s = Sine_osc.new
s.frequency ## returns 440.0
s.waveform ## returns "sine"
Note that I only cared about the sine oscillator. This is to keep the code small. A real code would of course have an option to define a waveform and not just hardcode "@waveform = "sine". And the class would not be called "Sine_osc", but probably "Waveform_osc".
Ruby itself works in the very same way. A number like 42 is an instance of the class "Fixnum". But Fixnum is based on "Numeric", a superclass that holds all informations that all numbers share. So, while 42.0 is of class "Float", Float is inherited from "Numeric", just as Fixnum was. If you ever wonder about the class relations, you can check them with the methods .class and .superclass:
- Code: Select all
42.0.class ## returns "Float"
42.0.class.superclass ## returns "Numeric"
42.0.class.superclass.superclass ## returns "Object"
There is one class that all the others are inherited from, and that class is "Object". It is the root of a tree, so to speak.
But back to our synth. Can I further inherit? Yes, of course. For example, you might have a class "Osc_section", inherited from "Waveform_osc" and adding tuning (octave, halfnotes, fine). Or adding envelopes. Or... You get the point.
Leaves one question: Why should I use such a tree structure, whith dozen of classes. Well, besides a well readable code, there's one mega-advantage. You only define those things once, and they are true for all instances you use of those classes. Now imagine you have created a synth with 100 oscillators, each featuring its own waveform. Now you encounter an error.
Without the OOP approach you have no choice than finding the error and correcting it in all 100 oscillators.
With the OOP approach you just have to find the error and correct it in the affected class - and all 100 oscillators are bug-free again! Is it cool, or is it cool?