Crate sorceress[][src]

A Rust environment for sound synthesis and algorithmic composition, powered by SuperCollider.

Introduction

This crate provides Rust bindings to the SuperCollider audio synthesis and algorithmic composition platform.

There is no better overview of SuperCollider than the summary from its home page:

“SuperCollider features three major components:

  • scsynth, a real-time audio server, forms the core of the platform. It features 400+ unit generators (“UGens”) for analysis, synthesis, and processing. Its granularity allows the fluid combination of many known and unknown audio techniques, moving between additive and subtractive synthesis, FM, granular synthesis, FFT, and physical modeling. You can write your own UGens in C++, and users have already contributed several hundred more to the sc3-plugins repository.

  • sclang, an interpreted programming language. It is focused on sound, but not limited to any specific domain. sclang controls scsynth via Open Sound Control. You can use it for algorithmic composition and sequencing, finding new sound synthesis methods, connecting your app to external hardware including MIDI controllers, network music, writing GUIs and visual displays, or for your daily programming experiments. It has a stock of user-contributed extensions called Quarks.

  • scide is an editor for sclang with an integrated help system.“

Sorceress is a Rust alternative to sclang. It provides features for interacting with SuperCollider similar to those found in sclang. It currently contains:

  • synthdef - Core types for designing new synthesizers by creating “synth definitions”. Synth definitions are graphs of connected unit generators.

  • ugen - A library of unit generators or “UGens”. scsynth provides hundreds of unit generators and sorceress aims to provide interfaces for all of them some day, but its going to take a while. Please raise an issue on the source repository if there are UGens that you’d like to see implemented so I know what to prioritize.

  • pattern - A module for defining patterns. Patterns are collections of events with timing information. Patterns are created by combining collections of events in sequence or in parallel. Patterns are the primary way of composing music with sorceress. The scheduler module can be used in combination with the pattern::player submodule to play patterns.

  • scheduler - A module for running code on a recurring schedule. The scheduler is responsible handling the timing details of executing patterns.

  • server - A Server type that manages communication with scsynth and Rust definitions of all of the OSC commands understood by the server. Similar to the library of UGens, that are many commands to implement, around 65, so please create an issue if you would like to see certain command implemented sooner.

Examples

This example plays the opening melody Stravinsky’s The Rite of Spring.

use sorceress::{
    pattern::{
        player::{Player, Tempo},
        sequence, Pattern,
    },
    scheduler::Scheduler,
    server::{self, Bundle, Control, Server},
    synthdef::{encoder::encode_synth_defs, Input, SynthDef},
    ugen,
};

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

fn main() -> Result<()> {
    // Create a new SuperCollider server client. A SuperCollider server must be started
    // outside of this program and be configured to listen on this UDP port.
    let server = Server::connect("127.0.0.1:57110")?;

    // Create a synth definition.
    let sine_wave = {
        // Create and name the synth definition. We will use the name given here to refer
        // to this synth definition in all server commands.
        SynthDef::new("sine_wave", |params| {
            // Parameters allow synths created by this definition to be configured with
            // different values when they are created, or be changed while they are playing.
            let freq = params.named("freq", 0.0);

            // Creates a sine wave that oscillates between -7 and 7, 5 times per second.
            let vibrato = ugen::SinOsc::ar().freq(5).mul(7);

            // Sends the audio signal to bus 0, which represents your speakers.
            ugen::OffsetOut::ar()
                .bus(0)
                // Pan2 turns a mono signal into a stereo signal.
                .channels(
                    ugen::Pan2::ar()
                        // SuperCollider lets us plug UGens into the inputs of other UGens
                        // to create complex signal processing graphs. Here we are using
                        // the vibrato SinOsc UGen defined above to slightly oscillate the
                        // pitch of the sine wav we will actually hear.
                        .input(freq.mul(vibrato)),
                )
        })
    };

    // Send the synth definition to the SuperCollider server so that we can make synths
    // using it.
    let encoded_synthdef = encode_synth_defs(vec![sine_wave]);
    server.send_sync(server::SynthDefRecv::new(&encoded_synthdef))?;

    // Play a sequence of notes. The first number is the duration in beats and the second
    // number is the midi note number.
    let pattern: Pattern<Event> = sequence(|s| {
        s.play(1.2, Event::StartNote(72.0));
        s.play(1.5, Event::SetPitch(71.0));
        s.play(1.5, Event::SetPitch(72.0));
        s.play(3.0, Event::SetPitch(71.0));
        s.play(3.0, Event::SetPitch(67.0));
        s.play(3.0, Event::SetPitch(64.0));
        s.play(3.0, Event::SetPitch(71.0));
        s.play(6.0, Event::SetPitch(69.0));
        s.play(4.0, Event::SetPitch(72.0));
        s.play(2.0, Event::SetPitch(71.0));
        s.play(2.0, Event::SetPitch(72.0));
        s.play(4.0, Event::SetPitch(71.0));
        s.play(4.0, Event::SetPitch(67.0));
        s.play(4.0, Event::SetPitch(64.0));
        s.play(4.0, Event::SetPitch(71.0));
        s.play(6.0, Event::SetPitch(69.0));
    });

    let event_handler = |time, event| {
        // We launch a single synth and set its pitch multiple times to create a melody. Synth
        // IDs are numbers that let us to refer to synths after we crate them.
        let synth_id: i32 = 1000;

        let result = match event {
            Event::StartNote(midi_note) => {
                // Create a new synth from the synth definition we registered earlier.
                let command = server::SynthNew::new("sine_wave", 1)
                    // Convert the midi note into hertz. The "freq" string refers to the string
                    // given to the `Parameter` in the synth definition above.
                    .controls(vec![Control::new("freq", midi_to_hz(midi_note))])
                    // Explicitly set the synth ID so that we can control it later.
                    .synth_id(synth_id);

                server
                    // Ask SuperCollider to schedule this event at exactly this time.
                    .send(Bundle::new(time, vec![command]))
            }
            Event::SetPitch(midi_note) => {
                // Convert the midi note into hertz.
                let controls = vec![Control::new("freq", midi_to_hz(midi_note))];

                // Changes the pitch of the actively playing synth.
                let command = server::NodeSet::new(synth_id, controls);
                server.send(Bundle::new(time, vec![command]))
            }
        };
        if let Err(err) = result {
            println!("error: sending event to server: {}", err);
        };
    };

    // Play the pattern using the scheduler.
    Scheduler::new().run(Player::new(pattern, event_handler).tempo(Tempo::from_bpm(60.0)))?;

    // Stop all sounds on the server.
    server.reset()?;

    Ok(())
}

// Our event type.
enum Event {
    StartNote(f32),
    SetPitch(f32),
}

// Convert a midi note number into a frequency in hertz.
fn midi_to_hz(note: f32) -> f32 {
    let exp = (note - 69.0) / 12.0;
    440.0 * 2f32.powf(exp)
}

Learning SuperCollider

SuperCollider is a powerful platform and there are many concepts to learn on the path to proficiency. Currently, the documentation in this crate is not thorough enough to let people to get started using SuperCollider for the first time with sorceress. However, the Supercollider project provides excellent documentation and much of it carries over to using sorceress. If this is your first time learning about SuperCollider I recommend going through the following items before diving into sorceress:

  • Read the Client vs Server documentation to understand the different roles that SuperCollider and sorceress play. Remember that sorceress is a replacement for the sclang, SuperCollider’s interpreted object-oriented language.

  • Go through the Getting Started With SuperCollider tutorial series. This will teach you the all of the fundementals of SuperCollider and how to boot scsynth on your computer.

  • Poke around the Server Command Reference to understand all of the OSC commands that SuperCollider accepts. The server module does not yet contain all of these commands, so this document is useful for seeing what is available and what is currently missing.

Planned Features

  • Declarative resource management - sclang allows arbitrary sections of code in a source code file to be executed in any order using scide. This makes it very easy to run setup code that creates server-side resources such as buffers and synthdefs once before executing the code that creates synths and makes sounds. Instead of replicating this in Rust through condition execution of parts of code, we can create a declarative interface for managing server side resources. Code using this crate will declare what resources it needs, and resource management code provided by this crate will find the difference between what is currently provisioned and what is desired and automatically reconcile the two.

  • A live reloading development workflow - Dynamic interpreted languages like sclang provide an excellent environment for creative coding. The nature of how code is parsed and executed in those languages makes it easy to replace portions of code on the fly, at runtime, which is excellent for iterating on musical pieces. Rust presents a unique challenge in this area, as code cannot as easily be replaced during runtime. A similar workflow can still be achieved in Rust using a preemptible scheduler that can be paused and resumed when a new version of the code successfully compiles. If we add compilation triggered by filesystem notifications, we can create a streamlined creative workflow for creating music with Rust.

Modules

pattern

A module for defining patterns.

scheduler

An ahead-of-time scheduler for executing recurring jobs.

server

An OSC interface to SuperCollider.

synthdef

Core types for creating synth definitions.

ugen

The library of UGens.