Cshellsynth is library and a ruby module for software synthesis. It utilizes the jack api for interaction with the sound card and other utilities. It also has one module---so far---that utilizes libsndfile.
Cshellsynth is designed to be mathematically precise, yet with an incredibly simple interface. It is my hope that with the included Ruby module, it will be useful for livecoding, but that is certainly not its only use.
The source code for Cshellsynth can be downloaded here. Please refer to the enclosed documentation for compilation instructions.
To file a bug report, or for more information, see the github page: http://github.com/ebuswell/Cshellsynth
To get help, offer feedback of any sort, share what you've done, or especially to help with the development process, please join in at email@example.com. To join this list, send an email to firstname.lastname@example.org. Public archives are located at: http://groups.google.com/group/cshellsynth-dev
When installed to
/usr/local, you must set
LD_LIBRARY_PATH=/usr/local/lib when running ruby, e.g.
If you're doing something heavily polyphonic, Jack may run out of clients or ports. You can use
jackd -p [maximum number of ports]
to raise the maximum number of ports. Unfortunately, the only way to raise the maximum number of clients is to recompile jack2. Run waf with
./waf [other options] --clients=512
Since Ubuntu and most flavors of Debian have almost all of their audio stuff compiled against jack1, you'll need to build a .deb of jack2 that claims to be jack1. There's a patch against the jack2 source that should help you do that---as well as set up options suitable for cshellsynth---located in the attachment to this message. Obviously, this is a hack, and although no problems have been reported, it may be unstable in certain situations.
This assumes you have already compiled and installed Cshellsynth. If not, refer to the README and INSTALL file for details. This mainly pertains to using Cshellsynth through the Ruby module, but it should be useful even to someone who only wishes to use C.
Cshellsynth is a modular synthesizer. That means that every aspect of synthesis here is conceptually separate, and synthesis takes part in stages. Every module is (at lease one) Jack client, but you probably don't need to think about that fact unless you're having resource troubles. What's important to know is that every module has one or more out ports, and most modules have one or more in ports. You connect them together with jack_connect, or in Ruby, like:
m1.port1 = m2.port2
Note that although it is possible to have the left hand value be an out port, it is preferable to use in ports as the lvalue as slightly more magic happens in that case. This should be fixed at some point.
To create a synthesizer, hook up all the modules you want, finally hooking into
$mixer.in[N] or whatever other external jack port you wish to connect to. To get a list of ports on your system, type "@c c.ports," where
c is any Jack client.
In Cshellsynth, there are three main categories of modules:
Controllers generate control data, like "do this," or "stop doing this." A
Controllers::Sequencer (sequencer.h) is the most useful controller, but Cshellsynth also supplies a
Controllers::Instrument (instrument.h) to test things out. All controllers output data and control data. The control data consists of
0 (no change),
1 (begin), and
-1 (end). The output data can be whatever you want, but usually this consists of note numbers.
Synths generate sound. See below for the full list of synths. Synths usually have an input frequency port,
freq, which takes frequency as cycles per sample, and an output port,
out. Some synths have other parameters as well. Only one synth doesn't have any input:
Filters transform input to output. These don't necessarily involve frequency, as per the more usual usage of the term "filter." If a filter based on frequency is meant, I will specify "frequency filter". A filter isn't strictly necessary, but most things sound better with at least one filter.
Filters have at least an
in and an
out port, sometimes others.
In addition to these, there are a few important modules that aren't categorized.
Mixer (mixer.h) takes an arbitrary number of inputs (
in[0..x]) and adds them all to produce one output. Note that Jack will automatically mix together multiple incoming connectoins to a given port, but
Mixer allows for more explicit control.
Modulator (modulator.h) takes two inputs and creates an output equal to the multiplication of one input by the other input.
Pan (pan.h) takes one input and provides a left and right output, controlled by a pan control.
FullMixer is a combination of mixers and pan controls to make something like a studio mixer. By default, cshellsynth creates a FullMixer "@c $mixer," with outputs connected to your sound card out.
EnvelopeGenerator (envelope_generator.h) does something useful with control data, generating an envelope for your sound. This is usually used in conjunction with a
Modulator (modulator.h) to control the amplitude envelope of a synth.
Key (key.h) transforms note data into frequency data. You could just hand-code the frequency in, but
Key is a lot easier and more easily changeable. For example, you can change from minor to major or modulate keys with a single line of code.
Every module is configurable with parameters and ports. In most cases, ports and parameters are interchangable, e.g.
synth.freq = key.freq will connect the
synth object's frequency input port to the
key object's frequency output port, and
synth.freq = 440.0 will set the
synth object's frequency parameter to a constant 440. There are, however, some ports with no corresponding parameter, and some parameters with no corresponding port. However, this should only be the case with parameters for which a dynamic setting is useless, and ports for which a static setting is useless. Otherwise, it's a bug and should be reported; please include the use case.
At the moment, this is implemented by use of
NaN (Not a Number) values. If a parameter is set to a number, the port is not used; otherwise values are read from the port. In most cases, you won't have to think about this, as normally, when you connect a port the parameter will automatically be set to NaN. However, as Jack is somewhat independent, many things can happen whereby a port will be connected but a parameter will not be set to NaN. In that case, you can manually set the parameter to NaN (via something like
synth.param = 0.0/0.0), and the port will start working. This works fine, but is considered a low-priority bug.
The default units for parameters and ports allow for maximum control. Wherever possible, they are also intuitive, but control is always a priority over ease. With data sent over the ports, time units are always in samples, frequency units are always cycles per sample; when setting parameters, however, time units are always in seconds, frequency units are always in Hz, EXCEPT for values < 1 Hz, which are per sample. The reason for this is so that functions like
Key#note2freq (cs_key_note2freq) can be used for setting static parameters in a way that is identical to the ports. Unless a
Synth has a
scale parameter, amplitude is normalized so that the root frequency of a given wave is 1. This means that by default most synths have a maximum amplitude of more than 1 and should be mixed accordingly; e.g. a sawtooth wave (ideally) has a maximum amplitude of π/2, and a parabolic wave has a maximum amplitude of π2/6. The actual maximums are usually even greater from the effects of bandlimiting.
All modules (except
Mixer) have a
polyphony parameter, that controls the maximum number of simultaneous notes that can be played. All modules in a given chain should have the same value for polyphony.
Sequencers will sequence round-robin over each polyphonic node of a module. Currently, this has limitations. Polyphony is implemented purely by multiplexing sequencer commands using multiple
Sequencers, and so certain types of dynamic changes to the sequencing will end up screwing with the round-robin order.
Sequencer is probably the most volatile object at this point and is subject to change in future versions. This description is only valid for the time being.
Sequencers can be sequenced using two forms. The first is to use a time range and a list of triples, like:
seq.sequence(start, stop, [start_time1, stop_time1, value1], [start_time2, stop_time2, value2], [start_time3, stop_time3, value3], ...)
The second form uses a mini-language. You pass the sequencer a string where every token takes up a fixed amount of time, a space indicates a silence, a number indicates a value, and a tilde indicates the continuation of a note, e.g.:
seq << "0~2~4~5~7 5 4 2 0~4~7 4 0~~~~~ "
will play a simple warm-up excercise. The length of time that a given token takes up is given by
Sequencer#granularity, where a beat (quarter note) is 1, an eighth note is 0.5, etc.
Since the target use is livecoding, by default both of these methods will repeat once they are finished. If you don't want this, use
A sequencer will only start to do anything once a
Clock is connected. All
Sequencers should (probably) be connected to the same
Clock. There is a default global clock created as
Clock runs from 0 to
Clock#rate is set via port, it is always in beats per sample. If the
rate parameter is set instead, values greater than 1 are interpreted as beats per minute, less than one as beats per sample.
Clock#meter only has an effect on the offset parameter for newly added sequences. A given sequence need not be an integer multiple of
Clock#meter in length; this allows for certain complex rhythms to be set up, but also allows for mistakes that sound really bad, so be careful.
Cshellsynth comes bundled with a class
MiniSynth that is pre-configured to turn your computer into a less-than-totally modular synth. Play with it, then look at the source code in
ruby/cshellsynth/extra.rb to get an idea of how it works.
Start up ruby with
irb, then type:
require 'cshellsynth' s = MiniSynth.new
To play a note, type:
s.inst.play [note number]
[note number] is some number, positive or negative.
0 is middle C.
This won't sound very good yet. You need to mess with stuff to get it to sound like anything. Try
s.envg first, then maybe
s.fenvg. Here's a full list:
MiniSynth#seq A sequencer to control it instead, if you prefer.
MiniSynth#clock Clock for the sequencer
MiniSynth#envg Envelope generator
MiniSynth#synth The synth currently in use
MiniSynth#synth= To use a different synth
MiniSynth#lfo The low-frequency sine wave oscillator, which modulates the amplitude of the synth.
MiniSynth#fenvg Drives the frequency of the filter, in terms of number of octaves above the frequency playing.
MiniSynth#filter The filter currently in use
MiniSynth#filter= To set a new filter