Sardine is a free/open-source software for Python 3.10+. Sardine turns Python into a musical instrument. It is a framework for live coding music and controlling musical hardware from Python. You can play alone or synchronize with other audio softwares and instances of Sardine. With Sardine you can:

  • Play synthesizers and audio samples using SuperCollider.
  • Control synthesizers through MIDI and OSC messages.
  • Interconnect audio/video softwares and/or sound engines.
  • Play collaboratively with your friends or other musicians.
  • Extend your musical environment using any Python package.
  • Explore multiple algorithmic pattern languages for improvisation.

img

Sardine is designed to allow the exploration of algorithmic improvisation using a friendly syntax/environment. It transforms Python into a time-aware environment capable of sequencing any event with precision. Code can be updated anytime while the program still runs, a technique called live coding!

What is Sardine?

img

Sardine is a Python based musical instrument. While it may look like a typical Python library, it is best described as a modular live coding environment. You won’t just import it and forget it, you will play it like a guitar or a trombone. Sardine will allow you to sequence synthesizers, audio samples and various messages used by software or hardware audio/video equipment. Sardine is leveraging Python as a way to build powerful and terse algorithmic sequences that you can type fast while on stage. Sardine is also embedding multiple small languages (the more the merrier!) allowing you to do that even faster, and with style!

Sardine is using Python for its flexibility. Python is a very popular programming language for a good reason. It has a lot of different packages and it is very well supported pretty much everywhere :) You won’t have any problem finding a good text editor or finding the right tools for the job.

A small community of musicians and artists in France (and even elsewhere) are using Sardine. You can find more by looking at the showcase section or by getting in touch with other Sardine users on social medias: Discord, etc…

img

What is Live Coding?

img

Sardine offers a different approach to musical creativity by allowing you to control the instrument through live programming. Your keyboard becomes your primary musical interface, a refreshing departure from traditional musical practices. Sardine is designed to explore various live coding techniques and seamlessly integrate with other live coding software and environments.

For those new to the concept, live coding embodies three distinct aspects:

  1. A programming technique: Live coding involves manipulating, redefining, or altering software while it's running. This approach explores hot swapping, reloading, or re-compiling parts of a software stack without interruption, as an integral part of the software's intended usage.
  2. An artistic practice: Live coding encourages artistic expression through the use of computer code. It transforms programming into a gestural and expressive act, typically shared with an audience to create a more immersive experience.
  3. A subculture: Live coding represents a niche within the broader landscape of computer and electronic music. Its roots can be traced back to the 1970s, and today, it is primarily championed by algoravers and the TOPLAP collective.

The world of live coding is a fascinating realm of passionate individuals hacking and sharing software, typically with a focus on free and open-source resources. The objective is to find the right tool for your specific creative goals and build a personalized artistic environment on your computer.

Try to read about it. It's a fascinating world of people happily hacking and sharing software, usually free and open source. The goal is to find the right tool for the nail you want to hammer, and build from there, turning your computer into an environment for personal artistic expression. For our case, it means that Sardine is meant to be extended, modified, specialised for what you need to do :)

Community

img

There are multiple pages on the internet to share your new-found passion about Sardine:

You can also find some Sardine related content by checking the Cookie Collective website. There are some users documenting their art practice and their love/hate relationship with Sardine:

  • BuboBubo (Raphaël Maurice Forment): I created the Sardine live coding environment and I am live coding as well.
  • Rémi Georges: the infamous Rémi Georges, the first Sardine user. A lover of old/ancient digital machines and emerit live-coder.

Installation

This section will guide you through the installation of Sardine. Sardine is a multi-layered system:

  • Sardine: the Python parts of the system.
  • SuperCollider (for sound only): optional audio backend.
  • Dependencies: C++ dependencies for MIDI and Ableton Link.
  • Sardine Web: the optional Sardine text editor written in TypeScript.

There is nothing more I can do to help if you are not reading these lines! Please read the instructions and install all the necessary. Sardine will break if you have not carefully prepared your environment. I insist, Please, read the instructions:

  • you probably don't know how your Python is installed.
  • you will most likely miss a dependency if you don't read.
  • you will report broken behavior when really.. it's not!

Installing Sardine can be very easy by following the guide or very hard if you don't take time. Moreover, the nature of this software makes it very hard to package as a single executable. There is no shortcut. Just RTFM.

Once you get it to work, you won't have any trouble at all configuring it and tweaking it to your liking.

Preliminary words

Sardine will allow you to fly. Before flying, please make sure that you have wings. Otherwise, you are just building a rocket and you will crash into a wall. Read this page carefully, prepare your computer for installation. In particular we will make sure that:

  1. You know how Python is installed.
  2. You downloaded all the required software.
  3. You understand what Sardine is.

1) The proper way to install Python

xkcd

It is extremely important to know how your current version of Python is installed.

  • You can have multiple versions of Python running on the same system.
  • Always prefer a version of Python that you installed yourself.
  • Be careful with aliases. On Windows, people often have python and py living side by side.
  • They are not the same installation of Python.

You will now install Pyenv, the only Python version manager that you can trust. For Windows, please install Pyenv for Windows. The installation process is well explained by the Pyenv team. Once it is done, please run the following commands:

  • pyenv install python3.11: install Python 3.11 with all its bells and whistles.
  • pyenv global python3.11: make this version the base install on your system.

Now, kill your command line and restart a fresh terminal. The output of python --version should look like you have installed a Python 3.11 version:

❯ python --version
Python 3.11.3

2) Other versions of Sardine

  • As funny as it may sound, I am not the owner of the sardine package on Pypi. Sardine is named sardine-system. Some people sometimes end up installing a totally unrelated tool!
  • sardine-system is very outdated. Please install from source.

3) The modular architecture of Sardine

  • Sardine is a very flexible software. It can be hard to install for that reason.
  • You probably don't need everything but you need to understand the architecture:
    • Sardine web is an optional text editor for Sardine written in TypeScript.
    • Sardine is an asynchronous Python interpreter firing up the Sardine library.
    • Sardine Core is the Python library that is responsible for all that livecoding.
      • it defines a temporal engine allowing you to live code in Python.
      • it communicates through the OSC protocol or through MIDI ports.
      • it allows you to create musical code using powerful patterning engines.

Windows

Installing Sardine on Windows is more difficult than installing it on Linux/MacOS. Don't blame me, blame Windows for not providing the right tools. Microsoft are doing everything they can to prevent you from having fun. Why aren't you running Excel or Word already?

  • PLEASE READ THE PRE-INSTALLATION PAGE BEFORE GOING FURTHER.
  • Install the Windows Terminal, the modern terminal for Windows, made by Microsoft.
  • Install loopMIDI for MIDI connectivity. This will allow you to play with any synthesizer.

Preparing your environment

The installation of Sardine takes place in several steps and has some prerequisites. You will have to install all the development environment that will allow you to live code comfortably. You will of course have to install Python but also make sure you have SuperCollider, the audio engine used by the application. You will also have to install some tools that will allow you to compile the application.

  1. Install the latest Python version for your OS (currently 3.11).

    • Sardine will not work with a Python older than 3.10.
    • Be careful with distribution provided Python versions! They might be incomplete!
    • Install Pyenv or use virtual environments to keep everything nice and tidy!
  2. Install SuperCollider, the default audio backend used by Sardine.

    • Once this step is over, open SCIDE (or click on the SuperCollider icon) and type: Quarks.install("SuperDirt")
    • Press Shift + Enter and wait for the installation to be done! Close SuperCollider when done.
    • Optional: You can also install sc3plugins to get more audio effects and synthesizers!

Installing Sardine

We will now proceed to the installation of Sardine. Sardine is a Python library which is composed of two modules:

  • Sardine Core: the Python library for live coding. Contains all the goodies.
  • Sardine: an asynchronous Python interpreter AND integrated text editor.
python -m pip install sardine-system

If you want to install the development version, which may have more features than the package on PyPI:

python -m pip install git+https://github.com/Bubobubobubobubo/sardine

Note: If you get an error when trying to install python-rtmidi, you can install the package manually using one of the following commands:

  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git@eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba#egg=python-rtmidi
    
  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git#eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba
    

Installing Sardine Web

After Sardine is installed, you may choose to install the sardine web editor with the following command:

python -m pip install sardine-web

More details are provided in the Sardine Web section.

MacOS

The installation of Sardine on MacOS is pretty straightforward. You shouldn't encounter any particular issue to make it work properly. I am developing Sardine on MacOS Ventura 13.0.1.

  • [PLEASE READ THE PRE-INSTALLATION PAGE BEFORE GOING FURTHER]

Preparing your environment

The installation of Sardine takes place in several steps and has some prerequisites. You will have to install all the development environment that will allow you to live code comfortably. You will of course have to install Python but also make sure you have SuperCollider, the audio engine used by the application. You will also have to install some tools that will allow you to compile the application.

  1. Install the latest Python version for your OS (currently 3.11).

    • Sardine will not work with a Python older than 3.10.
    • Be careful with distribution provided Python versions! They might be incomplete!
    • Install Pyenv or use virtual environments to keep everything nice and tidy!
  2. Install SuperCollider, the default audio backend used by Sardine.

    • Once this step is over, open SCIDE (or click on the SuperCollider icon) and type: Quarks.install("SuperDirt")
    • Press Shift + Enter and wait for the installation to be done! Close SuperCollider when done.
    • Optional: You can also install sc3plugins to get more audio effects and synthesizers!
    • You might have to add sclang to your path. To do so, copy alias sclang="/Applications/SuperCollider.app/Contents/MacOS/sclang" into your .bashrc or .zshrc in the $HOME directory.

Installing Sardine

We will now proceed to the installation of Sardine. Sardine is a Python library which is composed of two modules:

  • Sardine Core: the Python library for live coding. Contains all the goodies.
  • Sardine: an asynchronous Python interpreter AND integrated text editor.
python -m pip install sardine-system

If you want to install the development version, which may have more features than the package on PyPI:

python -m pip install git+https://github.com/Bubobubobubobubo/sardine

Note: If you get an error when trying to install python-rtmidi, you can install the package manually using one of the following commands:

  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git@eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba#egg=python-rtmidi
    
  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git#eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba
    

Installing Sardine Web

After Sardine is installed, you may choose to install the sardine web editor with the following command:

python -m pip install sardine-web

More details are provided in the Sardine Web section.

Linux

Sardine feels right at home on Linux. Its modular architecture is like a fish in water on a Unix system. Sardine makes extensive use of the terminal, another of the strengths of Unix systems.

  • [PLEASE READ THE PRE-INSTALLATION PAGE BEFORE GOING FURTHER]

Preparing your environment

The installation of Sardine takes place in several steps and has some prerequisites. You will have to install all the development environment that will allow you to live code comfortably. You will of course have to install Python but also make sure you have SuperCollider, the audio engine used by the application. You will also have to install some tools that will allow you to compile the application.

  1. Install the latest Python version for your OS (currently 3.11).

    • Sardine will not work with a Python older than 3.10.
    • Be careful with distribution provided Python versions! They might be incomplete!
    • Install Pyenv or use virtual environments to keep everything nice and tidy!
  2. Install SuperCollider, the default audio backend used by Sardine.

    • Once this step is over, open SCIDE (or click on the SuperCollider icon) and type: Quarks.install("SuperDirt")
    • Press Shift + Enter and wait for the installation to be done! Close SuperCollider when done.
    • Optional: You can also install sc3plugins to get more audio effects and synthesizers!

Installing Sardine

We will now proceed to the installation of Sardine. Sardine is a Python library which is composed of two modules:

  • Sardine Core: the Python library for live coding. Contains all the goodies.
  • Sardine: an asynchronous Python interpreter AND integrated text editor.
python -m pip install sardine-system

If you want to install the development version, which may have more features than the package on PyPI:

python -m pip install git+https://github.com/Bubobubobubobubo/sardine

Note: If you get an error when trying to install python-rtmidi, you can install the package manually using one of the following commands:

  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git@eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba#egg=python-rtmidi
    
  • python -m pip install git+https://github.com/SpotlightKid/python-rtmidi.git#eb16ab3268b29b94cd2baa6bfc777f5cf5f908ba
    

Installing Sardine Web

After Sardine is installed, you may choose to install the sardine web editor with the following command:

python -m pip install sardine-web

More details are provided in the Sardine Web section.

Post installation

You just installed Sardine. Congratulations. Now let's check if everything is alright. Read and answer each and every of these questions. You will get familiar with the system doing so!

Do you see an error involving Alsa and MIDI?

Two months ago, an error started to appear for Linux users: rtmidi._rtmidi.SystemError: MidiInAlsa::initialize: error creating ALSA sequencer client object.. To fix this error, you will have to manually copy some files to a different location:

  • copy the /usr/lib/alsa-lib/ into usr/lib/x86_64-linux-gnu/alsa-lib and/or usr/lib64/alsa-lib.
  • if this path does not exist, try searching into usr/lib/x86 or similar.

The source of this error is currently unknown and is not Sardine related. Hopefully, it will resolve automatically with future updates of the packages Sardine uses.

Are you running Windows?

  • Windows users: check if SuperCollider is properly detected.

    • if not, boot SuperCollider and Sardine side by side. Check the configuration section and the troubleshot section to learn more about this process.
  • Windows users: do you want to play virtual MIDI instruments (VSTs, etc)?

    • install a tool to create virtual midi ports. Create at least one virtual port.
    • configure Sardine to use it in the MIDI section of sardine config.
    • Linux and MacOS users don't need to do this. The Sardine port is automatically created.

Can you run sardine and sardine web?

  • in your terminal, run sardine first! If you see a splashscreen, everything is fine! :)
  • if you can run sardine web, you have the sardine-web package installed! if not, you must install that package before you can use the command (docs).

Can you run sardine config?

  • in your terminal, run sardine config? If you see the configuration tool appearing, perfect!
  • take some time to familiarise yourself with all the available options! The configuration step is important.

Do you want to turn on SuperCollider?

  • If you want to play audio directly with Sardine, check out the configuration section. Head to the SuperCollider section and turn everything on!

You need a code editor now!

  • Check out the Editor section to choose your favorite editor!

Updating / Uninstalling

This section is about:

  • Repairing or fixing issues with your Sardine installation.
  • Updating Sardine to keep on track with latest modifications.
  • Uninstalling Sardine from your system.

Update / Uninstall

Sardine is distributed as a Python package. As such, installing, updating or deleting is similar to doing so with a regular Python package.

Deleting Sardine

  • In your terminal, run pip uninstall sardine.
  • Delete your sardine directory if you cloned it using Git.
  • You will have to get rid of the configuration files manually.

Note that you will still have an installation of SuperCollider and SuperDirt if you followed the full install. Refer to their respective documentation if needed.

Updating Sardine

I recommend installing Sardine using a freshly cloned version using Git. This will allow you to get updates much faster by just running git pull from your terminal inside of the Sardine folder. For the updates to be instantly applied to your version, note that you need to have installed Sardine using the --editable flag. Please refer to the installation section to learn more about this. If you followed the tutorial, you must have it installed in editable mode already.

The --editable mode means that your Sardine installation that Python refers to is folder you just cloned and not a copy of it. Any modification made to it will be immediately mirrored to the application you have installed.

Errors during installation

Sardine is unable to locate SuperCollider

If Sardine is unable to locate SuperCollider, a message will be printed whenever you start sardine. Don't worry! Unfortunately, this error is fairly common on Windows. You should be able to play with Sardine by turning off the autoboot feature from sardine config:

  • run sardine config.
  • enter the SuperCollider section.
  • toggle the first option, untoggle the rest.
  • exit the configuration tool, save your changes.

Once done, use the following steps to start Sardine:

  • start SuperCollider, write SuperDirt.start and press Shift+Enter.
  • start sardine in your terminal.

SuperCollider and Sardine will be able to communicate but you will have to manage the two applications separately!

ModuleNotFoundError: No module named 'setuptools.command.build'

This error is fairly common if you install Sardine using the default Python installation that comes with your system. This is likely the case for Linux and MacOS users. This error happens because these pre-installed versions of Python do not come with the setuptools module. You'll have to install it yourself:

  • install setuptools using pip: python -m pip install setuptools
  • install Pyenv to get a clean install of Python.
    • Pyenv is the recommended method to use. It makes managing Python versions easier.

No sound, what should I do?

Sometimes, SuperDirt will refuse to boot. You won’t hear anything and Sardine will appear to be working perfectly. There are some steps I recommend to follow while trying to debug that issue:

  • Check if SuperDirt is configured to boot in your sardine config
  • Check that your audio output and microphones are running at the audio sample rate (44100 or 48000hz) on both sides (audio output / input). You can check this using your operating system usual configuration tools. Note that pluging in and out a microphone can change the sampling rate automatically. This is annoying, but so is life!

I still can't hear anything!

  • Sometimes, when you play around with booting and quiting Sardine repeatedly, your computer might start to get confused about who is using some of the network connections or not. You now have zombie connexions blocking the I/O process from running normally. This can also happen simply by opening multiple instances of Sardine on the same computer!
    • kill every instance of Sardine and SuperCollider and the code editors that hosted them.
    • run Server.killAll in a brand-new SuperCollider window.

This should solve the issue. If not, it might be something more serious and is less likely to be an error arising from Sardine itself. As crazy as it might sound, I'm not responsible of all the computer errors :)

  • Use sardine config and tell it not to boot SuperCollider automatically by itself.
  • Open SuperCollider and Sardine side by side. From there:
    • type SuperDirt.start in your SuperCollider window and press Shift + Enter to manually start SuperDirt.
    • boot Sardine as usual, and try to play some sounds using it.

If you are stil unable to play sound then you have a broken install. Join us on the Discord server to get some help fixing the issue.

Sardine

This section is about fixing errors when running the sardine command. sardine is Sardine's playing environment that you fire up just like any other Python REPL (Read Eval Print Loop).

UVLoop is not installed

UVLoop is not available on Windows. If you are running Windows, this message will pop-up everytime you start Sardine. Don't worry too much about it!

The d or D objects are not found

  • Make sure that you turned on SuperDirt Handlers in sardine config.

Configuration

The configuration of Sardine is simple using the Configuration tool: sardine config. In this section, you will learn:

  • how to use sardine config for setting up the system you'd like to play with.
  • how to setup OSC and MIDI connexions to your hardware and or software instruments.
  • how to configure SuperDirt for audio playback.

Configuration tool

img

Sardine is shipping its own configuration tool, named sardine config. Typing sardine config in your terminal will open a configuration helper tool :) Using it, you can finetune your Sardine experience. Please note that Sardine is writing configuration files to a specific location depending on the OS you are using:

  • Windows: in the %APPDATA%/Local/Bubobubobubobubo/Sardine folder.
    • to get there: Win+R -> type %appdata% and press Enter
  • MacOS: ~/Library/Application Support/Sardine
  • Linux: ~/.local/share/Sardine/

The path leading to the configuration folder can be printed out by typing print_config() from inside your typical Sardine session. How convenient :) You can also manage to print the PATH to your configuration folder directly from the configuration tool.

There are three main files you can tweak to configure Sardine:

  • config.json: the main configuration file.
  • default_superdirt.scd: the default configuration for the audio engine.
  • user_configuration.py: a file that will be runned automatically everytime you start Sardine.

You may (or may not) also see a couple of other folders:

  • synths/ folder (to store SuperCollider synthesizers as .scd files)
  • buffers/ folder (used by the web editor, storing .py files).

Configuration tour

Let's break down what the options in the configuration tool are. To start the configuration tool, please type sardine config in your terminal. A splashscreen will appear and some options will pop up as well!

Show Config

  • Print the configuration file.

Reset

  • Reset the configuration file.
    • other files such as the SuperDirt configuration are unnafected.
    • this won't delete your Python buffers or SuperCollider files.

MIDI

The MIDI menu will allow you to select the default MIDI port used by Sardine. This port will be used to automatically create some targets for you to play with when first starting a session. More ports can be configured manually later on.

  • Automatic: Sardine will try to create the... Sardine virtual port.
    • Only works for MacOS and Linux.
  • Manual: Select a MIDI port from the list.
    • The list is made out of all the MIDI hardware or software ports currently available on your system.
  • Custom (advanced): write the name of your MIDI port directly. Do not use this except for very good reasons!

Clock

This menu will allow you to configure the default clock used by Sardine at the start of a session. You can always switch clock later (even when playing!) but you will usually stick to one clock only for the duration of a session.

  • No (internal clock): use the system clock.
    • unsuitable for synchronisation with other users.
  • Yes (external clock): use the external Ableton Link clock.
    • Automatic synchronisation with other players on the local network.
  • You will also be prompted to enter a new default tempo and a default number of beats per measure.

SuperCollider

  • Add SuperDirt Handler: do you want to interact with SuperDirt at all?!
    • This is different from booting SuperCollider. SuperDirt is a more specialised engine for audio sampling and managing synthesizers. For newcomers, yes, you want to play with SuperDirt!
  • Boot a SuperCollider instance: should Sardine try to manage SuperCollider by itself?
    • This is a safe option to use for people using MacOS or Linux but can result in problems later on for those using Windows. You will have to boot SuperCollider and SuperDirt manually if you untoggle this option!
  • Use Sardine boot file: should we load our default boot file?
  • Turn on verbose output: Debug printing.
    • Can prove very useful for MacOS and Linux users playing with their configuration.
    • Please turn it off for playing! Printing is expensive in Python!
  • Enter your SuperDirt booth path: leave blank if you don't know what you are doing.

Editor

  • This menu will allow you to toggle the web editor by default or not. See the section concerning text editors to know if this is an option you want to consider. Note that this option is untoggled by default.

More

  • Don't touch it: it's for devs only :)

MIDI

Receiving MIDI

MIDI Input is supported through the use of a special object, the MidiInHandler object. This object will open a connexion listening to incoming MIDI messages on any valid port. There are only a few types of messages you should be able to listen to:

  • MIDI notes using the get_note(channel) method or its sibling, get_note_last(channel).
  • MIDI control changes using the get_control(control, channel) method or its sibling, get_control_last(control, channel).

MIDI messages are stored in a dictionary of lists, meaning that you can store the last n values received for each valid route. The memory is 20 events long by default. The get_control and get_control_last methods are calling the same internal method, named _get. The difference is only that one flips the optional last boolean that will return the latest value received in the list.

Using Sardine to receive MIDI messages, thus, is fairly straightforward. Declare your MidiInHandler:

midi_in = MidiInHandler(port_name=config.midi)
bowl.add_handler(midi_in)

You can now safely start to peek at messages coming in using the methods described above:

# Listen to control n°50 on channel n°1, always get latest message
midi_in.get_control(control=50, channel=0, last=True)
midi_in.get_control_last(control=50, channel=0) # idem

Sending MIDI

By default, Sardine will connect to a MIDI port. There is no such thing as a Sardine instance without a link to MIDI. Having only one port means that you will be limited to 16 channels. While this may already be a lot for some, other users will want to do something with their collection of 123 synthesizers. You can manually open up new MIDI ports by tweaking your Sardine session from the Python side:

# Add a new MidiHandler focusing on a specific port
your_midi_port: str = "exact_name_of_midi_port"
your_midi = MidiHandler(port_name=your_midi_port)

# Add the MIDI port to the session fishbowl
bowl.add_handler(your_midi)

Done! You now have a new MIDI port. The tricky part is now to add new objects to play with! Here is how to do so:

# If Ziffers is imported, grab a reference to its parser!

if ziffers_imported:
    midi._ziffers_parser = z2

N2 = your_midi.send  # For sending MIDI Notes
PC2 = your_midi.send_program  # For MIDI Program changes
CC2 = your_midi.send_control  # For MIDI Control Change messages
SY2 = your_midi.send_sysex  # For MIDI Sysex messages

if ziffers_imported:
    ZN2 = midi.send_ziffers  # Connecting the new Ziffers parser

You now have access to an interface to play notes, control changes, program changes and sysex messages. If you want to use the shorthand notation, you will have to do one extra step:

# Boilerplate for using the newly creating MIDI port with the shorthand
# syntax for swimming functions

def sy2(*args, **kwargs):
    return _play_factory(your_midi, your_midi.send_sysex, *args, **kwargs)

def n2(*args, **kwargs):
    return _play_factory(your_midi, your_midi.send, *args, **kwargs)

def zn2(*args, **kwargs):
    return _play_factory(your_midi, your_midi.send_ziffers, *args, **kwargs)

def cc2(*args, **kwargs):
    return _play_factory(your_midi, your_midi.send_control, *args, **kwargs)

def pc2(*args, **kwargs):
    return _play_factory(your_midi, your_midi.send_program, *args, **kwargs)

The _play_factory() method is not a function you are supposed to use directly. This function is mapping a sender (d(), n()) to a function that can be understood by a player (Pa, Pb).

This is everything you need to open new MIDI ports and replicate the normal behavior of the Sardine MIDI port. If you want to go even further, feel free to deep dive into the midi object itself. It might contain some sweet methods that you want to use!

MIDI Instrument

You can use MIDIInstrument to create a new sender that will allow you to send MIDI notes and MIDI control changes at the same time. Creating your own MIDI instruments can be advantageous, especially if you are used to playing with external instruments or physical hardware:

  • you can name every control change you are using instead of relying on numbers and variable names.
  • you only have one interface to control both notes and control changes instead of two separate patterns.
  • you can build yourself a library of MIDI instrument so that you don't need to reconfigure your studio everytime.

To create a new MIDIInstrument, you will need two things:

  • a mapping describing the MIDI configuration of your instrument.
  • a call to MIDIInstrument to create both the sender and the player for your instrument

The mapping is a simple Python dictionary:

plaits_channel=4
plaits = {
   'model': { 'control': 50, 'channel': plaits_channel, },
   'timbre': { 'control': 51, 'channel': plaits_channel, },
   'morph': { 'control': 52, 'channel': plaits_channel, },
   'harmo': { 'control': 53, 'channel': plaits_channel, },
}

Note that we provide both a channel and a control number for the CC we want to map. Once this is done, you can call the MIDIInstrument function to create your new instrument. Please note that we are capturing two new names. By convention, the uppercase version is for senders while the lowercase version is for players.

Plaits, plaits = MIDIInstrument(midi, channel=plaits_channel, instrument_map=plaits)

You can now use this new MIDI instrument in each of the playing modes:

Pc * plaits('C..F.G..B.', model='rand*12', morph='rand*127', p='0.5 1 0.25!4')

@swim
def demonstration(p=1/2, i=0):
    Plaits('C3 G4 F3 F4', i=i, d=2)
    again(demonstration, p=1/2, i=i+1)

panic()

You can use this system to map your hardware instruments or some software you previously configured for CC. The biggest advantage of this method is that it allows you to name all the CC messages you use automatically and to pattern them freely.

See the complete example to get a better understanding of this system.

Mapping example: Prok Drums

img

In this example, we are mapping the Prok drum modules from VCVRack into Sardine. We have previously defined a MIDI mapping that we are going to use.

# Definition of controls
bass_drum = {
   'x': { 'control': 0, 'channel': 0, },
   'y': { 'control': 1, 'channel': 0, },
   't': { 'control': 2, 'channel': 0, },
   'len': { 'control': 3, 'channel': 0, },
   'quality': { 'control': 4, 'channel': 0, },
}
snare_drum = {
   'x': { 'control': 5, 'channel': 0, },
   'y': { 'control': 6, 'channel': 0, },
   't': { 'control': 7, 'channel': 0, },
   'len': { 'control': 8, 'channel': 0, },
   'quality': { 'control': 9, 'channel': 0, },
}
klonk_drum = {
   'x': { 'control': 10, 'channel': 0, },
   'y': { 'control': 11, 'channel': 0, },
   't': { 'control': 12, 'channel': 0, },
   'len': { 'control': 13, 'channel': 0, },
   'quality': { 'control': 14, 'channel': 0, },
}
clap_drum = {
   'x': { 'control': 16, 'channel': 0, },
   'y': { 'control': 17, 'channel': 0, },
   't': { 'control': 18, 'channel': 0, },
   'len': { 'control': 19, 'channel': 0, },
   'quality': { 'control': 20, 'channel': 0, },
}
hat_drum = {
   'x': { 'control': 21, 'channel': 0, },
   'y': { 'control': 22, 'channel': 0, },
   't': { 'control': 23, 'channel': 0, },
   'len': { 'control': 24, 'channel': 0, },
   'quality': { 'control': 25, 'channel': 0, },
}
# Adding the new drum modules
Kick, kick = MIDIInstrument(midi, channel=0,  instrument_map=bass_drum)
Snare, snare = MIDIInstrument(midi, channel=0, instrument_map=snare_drum)
Klonk, klonk = MIDIInstrument(midi, channel=0, instrument_map=klonk_drum)
Clap, clap = MIDIInstrument(midi, channel=0,  instrument_map=clap_drum)
Hat, hat = MIDIInstrument(midi, channel=0,   instrument_map=hat_drum)

We can now start playing with our drums:

Pa * kick('(eu 48 7 8)', p=1, t='10', len=1, dur=0.1, x=0, y=0, quality=0)

Pb * hat('(eu 51 5 8)', p=.25, x='rand*120', y=120, quality='0', len='1')

Mapping example: Faust Synthesizer

Here is a new example we have been cooking for a workshop. This time, we are going to link a synthesizer developed using the Faust programming language to Sardine. It uses a simple substractive architecture and we added a small reverb for demonstration purposes. In the FaustIDE, run the following lines:

import("stdfaust.lib");

reverb_amount = hslider("reverb_wet",0,0.01,0.9,0.01);
dry_wet_mono(c, x0,x1) = y0, y1 
with {
    y1 = (1-c) * x0;
    y0 = c * x1;
};

attack = hslider("attack[midi:ctrl 32]",0.1, 0.01, 2, 0.1):si.smoo;
release = hslider("release[midi:ctrl 33]",0.1, 0.01, 2, 0.1):si.smoo;
cutoff = hslider("cutoff[midi:ctrl 34]",2000,200,44100/4.0,0.5):si.smoo;
resonance = hslider("resonance[midi:ctrl 35]",0.5,0.01,0.9,0.01);
filter_gain = hslider("filter_gain[midi:ctrl 36]",0.9,0.01,0.9,0.01);
fx = hslider("fx[midi:ctrl 37]",0.5,0.01,0.9,0.01);
freq = hslider("freq",200,50,1000,0.01);
gain = hslider("gain",0.5,0,1,0.01);
gate = button("gate");
process = os.sawtooth(freq)*gain*en.ar(attack, release, gate) : fi.resonlp(cutoff, resonance, filter_gain) 
<: dry_wet_mono(fx, _, _:re.mono_freeverb(0.75, 0.85, 1.0, 8000)):>_<:_,_;

Make sure that you are listening to MIDI inputs coming from the right port and that you have switched on the polyphonic mode. By evaluating the following Sardine code, you should be able to start playing with your synth immediately:

faust_synth = {
        'attack': {'channel': 0, 'control': 32},
        'release': {'channel': 0, 'control': 33},
        'cutoff': {'channel': 0, 'control': 34},
        'resonance': {'channel': 0, 'control': 35},
        'filter_gain': {'channel': 0, 'control': 36},
        'fx': {'channel': 0, 'control': 37},
}
Faust, faust = MIDIInstrument(
        midi, channel=0, 
        instrument_map=faust_synth
)

MIDI Controller

You can use MIDIController to create a new sender that will allow you to send multiple MIDI control messages at the same time, much alike a regular MIDI controller. This feature is very similar to the MIDIInstrument approach described earlier. This is an interesting approach for hardware or external software base approaches to playing Sardine:

  • you can name every control change you are using instead of relying on numbers and variable names.
  • you only have one interface to control multiple related messages.
  • you can build yourself a library of MIDI controllers so that you don't have to do it again.

To create a new MIDIController, you will need two things:

  • a mapping describing the MIDI configuration of your controller.
  • a call to MIDIController to create both the sender and the player for your controller.
cmap = {
    'reverb': {'channel': 0, 'control': 20},
    'delay': {'channel': 0, 'control': 21},
}

Note that we provide both a channel and a control number for the CC we want to map. Once this is done, you can call the MIDIController function to create your new instrument. Please note that we are capturing two new names. By convention, the uppercase version is for senders while the lowercase version is for players.

Controller, controller = MIDIController(midi, channel=0, controller_map=cmap)

You can now use this new MIDI controller in each of the playing modes:

You can now use it:

Pa * controller(reverb='rand*120', delay='rand*120')

OSC

Sardine is capable of receiving and sending custom OSC messages. Obviously, this should be configured manually on your side. I am only providing the basic tools do to so without encountering any hurdle! Configuring OSC is prone to errors and has always been a very painful activity that computer musicians like to do for some reason.

Sending OSC

output_one = OSCHandler(
    ip="127.0.0.1", port=12345,
    name="A first test connexion",
    ahead_amount=0.0, loop=osc_loop, # The default OSC loop, don't ask why!
)
bowl.add_handler(output_one)

# Look who's here, the send functions as usual
one = output_one.send
two = output_two.send

You can now use the methods one and two as OSC senders just like D() or N().

@swim
def one_two_test(p=0.5, i=0):
    """This is a dummy swimming function sending OSC."""
    one('random/address', value='1 2 3')
    again(one_two_test, p=0.5, i=i+1)

If you'd like, you can also make a Player out of it by using the following technique:

def osc_player(*args, **kwargs):
    """Partial function to add a new OSC player :)"""
    return play(
        output_one,
        output_one.send,
        *args, **kwargs
    )
Pa >> osc_player('random/address', value='1 2 3')

You are now able to send OSC messages just like if they were patterns. It means that you can use the Sardine pattern syntax to compose complex algorithmic sequences of OSC messages. Note that you can also pattern the address, making it a super fun/powerful way to explore your OSC bindings.

Receiving OSC

You can receive and track incoming OSC values coming from your controllers or devices. In fact, you can even attach callbacks to incoming OSC messages and turn Sardine into a soundbox so let’s do it!

# Making a new OSC-In Handler
listener = OSCInHandler(
    ip="127.0.0.1",
    port=44444,
    name="Listener",
    loop=osc_loop
)

# Adding the listener to the bowl
bowl.add_handler(listener)

def funny_sound():
    D('bip', shape=0.9, room=0.9)

listener.attach('/bip/', funny_sound)

That's everything you need! In the above example, we are declaring a new OSCInHandler object that maps to a given port on the given IP address (with 127.0.0.1 being localhost). All we have to do next is to map a function to every message being received at that address and poof. We now have a working soundbox. Let’s break this down and take a look at all the features you can do when receiving OSC.

There are three methods you can call on your OSCInHandler object:

  • .attach(address: str, function: Callable, watch: bool) : attach a callback to a given address. It must be a function. Additionally, you can set watch to True (False by default) to also run the .watch method automatically afterhands.
  • .watch(address: str) : give an address. The object will track the last received value on that address. If nothing has been received yet, it will return None instead of crashing \o/.
  • .get(address) : retrieve the last received value to that address. You must have used .watch() before to register this address to be watched. Otherwise, you will get nothing.

SuperCollider / SuperDirt

The default_superdirt.scd is your default SuperDirt configuration. SuperDirt is the nickname of a very powerful audio engine used by some live coding libraries like TidalCycles or Sardine. By default, this file will specify where to look for audio samples or how many inputs and outputs your system must use. You can open it from the terminal by typing sardine config-superdirt.

You must edit it manually if you are willing to change anything to it. This is outside of the reach of Sardine and it is preferable to let the user decide for the most suitable configuration. The SuperDirt repository is a good place to start, especially the hacks/ folder. It will teach you how to edit and configure SuperDirt to your liking. SuperDirt was initially conceived for TidalCycles. You will find a great amount of customization options on their website too!

Loading audio samples

Here is an example showing of how to load more audio samples to play with:

(
s.reboot {
    s.options.numBuffers = 1024 * 256;
    s.options.memSize = 8192 * 32;
    s.options.numWireBufs = 128;
    s.options.maxNodes = 1024 * 32;
    s.options.numOutputBusChannels = 2;
    s.options.numInputBusChannels = 2;
    s.waitForBoot {
        ~dirt = SuperDirt(2, s);
        ~dirt.loadSoundFiles;
        ~dirt.loadSoundFiles("/Users/bubo/Dropbox/MUSIQUE/LIVE_SMC/DRUMS/*");
        s.sync;
        ~dirt.start(57120, 0 ! 12);
        (
            ~d1 = ~dirt.orbits[0]; ~d2 = ~dirt.orbits[1]; ~d3 = ~dirt.orbits[2];
            ~d4 = ~dirt.orbits[3]; ~d5 = ~dirt.orbits[4]; ~d6 = ~dirt.orbits[5];
            ~d7 = ~dirt.orbits[6]; ~d8 = ~dirt.orbits[7]; ~d9 = ~dirt.orbits[8];
            ~d10 = ~dirt.orbits[9]; ~d11 = ~dirt.orbits[10]; ~d12 = ~dirt.orbits[11];
        );
    };
    s.latency = 0.3;
};
)

SuperDirt treats a wildcard (*) at the end of the path to mean that there are named subdirectories. If you want to load just one sample directory, omit the wildcard.

Text Editors

Text editors are particularly important to get the most out of Sardine. Remember that this is a tool for live coding and that you will need to setup everything to feel comfortable. Sardine can support most text editors and IDEs. I have been using Sardine using Vim, Neovim, Emacs, VSCode and of course… the integrated text editor. This editor is provided mostly for workshops and demos. If you are planning for a gig or anything serious, please consider using and learning a real text editor.

Sardine Web

img

Introduction

There is a separate sardine-web package you can install to have a specialized Sardine editor. The stable version is provided on PyPI and can be installed with pip:

pip install sardine-web

Afterwards you can run the sardine web command to start the editor. Our text editor is a web application, and all your code files are stored on your web browser!

If you are interested in installing the development/latest version of the package, see the instructions provided on their GitHub repository.

Usage

Everything you need to learn how to use the editor is already included. You will also find many Sardine tutorials directly included with the editor.

Flok

img

Sardine has been integrated to Flok, a collaborative text editor for live coding. Using Flok, you can easily share a Sardine session with other musicians and visualists. Flok is not an online version of Sardine. You will still need to install it locally to have sound. However, if you are playing together with some friends in a single room, one only needs to have Sardine installed and active! To use Flok, follow the following instructions:

  • Install Flok on your computer if you want to use the audio backend (Sardine itself)!
  • Go to flok.cc or sardine.doesnotexist and create a new session by following the prompt.
  • Share the session link with your friends. Use the command to connect your REPL to the session.
  • Have fun!

To be 100% sure that everything will work perfectly, please use Firefox. Chrome has been reported not to work well with MacOS.

VSCode

img

VSCode is a powerful and all-devouring code editor developed by Microsoft. It is the most widely spread code editor out there with millions of users, thousands of plugins and corporate support. VSCode is more than capable of handling Sardine sessions and there are multiple ways to configure everything for it.

You can find a package called sardine in the Extensions marketplace for VSCode and VSCodium:

  • click on the Extensions Tab on the left panel
  • Search for Sardine (currently v.0.0.2)
  • install and read the instructions :)

Vim / Neovim

img

NeoVim (and by extension Vim) is the editor I currently use both on stage, for development, writing my PhD thesis and also to write the docs. Its target audience is mostly developers, old Unix gurus and command-line users. Vim is a modal text editor with multiple modes for editing and jumping around in the source code. It can be extended using plugins and tweaked to your liking. Quite powerful, but it requires some learning to be proficient.

Using vim-slime

The process for working with Sardine from Neovim is pretty straightforward:

  1. install the slime plugin.
    • note that the technique to do so might vary depending on your configuration. I am using Lua to write my configuration. In the past, I had previously used Plug for years without encountering any issue!
  2. split your workspace in two vertical (:vs) or horizontal (:sp) panes.
  3. open up a :terminal in one of them and run sardine.
  4. work in the other one and use C-c C-c (Control+C twice) to send code from one side to the other.
    • slime will probably ask you which job to target, just press enter!

Usin iron.nvim

iron.nvim is a modern plugin for handling REPLs from Neovim. It took me some time to configure but it works well too.

  1. install by following the instructions on the repository.
  2. copy/paste all the necessary configuration files and customize it to your liking.
  3. take note of the details below.

For sending code blocks, you will have to customize your REPL a little more. Note that I am using Lua, the modern Neovim replacement to VimScript. Please look at the following amendments to the base configuration:

local iron = require("iron.core")
iron.setup({

  repl_open_cmd = "vertical botright 50 split", -- better split
  config = {
    -- Whether a repl should be discarded or not
    scratch_repl = false,
    close_window_on_exit = true,
    -- Your repl definitions come here
    repl_definition = {
      sh = {
        -- Can be a table or a function that
        -- returns a table (see below)
        command = { "zsh" },
      },
      python = {
        command = { "sardine" },
        format = require("iron.fts.common").bracketed_paste, -- super important (!!!)
      },
    },
    -- How the repl window will be displayed
    -- See below for more information
    repl_open_cmd = require("iron.view").split.vertical.botright(50),
  },
  -- ... etc ... your typical config
})

This will allow you to send the following events to the Sardine REPL:

  • sending a single line: <space>sl with your cursor on the line you want to send.
  • sending a code block: <space>sc with a selected block of text for multi-line.

(Doom) Emacs

doom

I am using Doom Emacs for many things in my life:

  • writing this documentation
  • writing manuscripts or papers
  • playing some music with Sardine.

The venerable Emacs is of course able to manage Sardine! Please use the python.el plugin. This mode will allow you to pipe easily your code from a text buffer to a running interpeter. The plugin is adding quality-of-life features for working with Python in general but also makes working with a REPL much easier and much more convenient. If you are new to the vast world of Emacs, it is probably worthwhile to take a look at Doom Emacs  or Spacemacs, both being equally great. I will not dive into more details. If you are able to configure Emacs, you will be able to configure your editor for Sardine :).

The following code is the one I use for running Sardine using Doom Emacs. It is not great and I should probably make something cleaner or even create a dedicated package for Sardine but life is short, and nobody is writing the docs while I finetune my Emacs config.

;; =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
;; SARDINE MODE
;; =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
;; Customize the python-mode to run Sardine code using the terminal.

(setq
 python-shell-interpreter "sardine"
 python-shell-interpreter-args "")

(defun sardine/start-sardine ()
  "Start a new interactive Sardine Session"
  (interactive)
  (run-python))

(defun sardine/eval-block ()
  "Evaluate a sardine code block"
  (interactive)
  (mark-paragraph)
  (if (and transient-mark-mode mark-active)
      (python-shell-send-region (point) (mark))
    (python-shell-send-region (point-at-bol) (point-at-eol)))
  (forward-paragraph))

(defun sardine/stop-code ()
  "Stop all the Sardine code currently running"
  (interactive)
  (python-shell-send-string "panic()"))

; Unmapping keys from the Python mode
(add-hook 'python-mode-hook
          (lambda() (local-unset-key (kbd "C-c C-c"))))
(add-hook 'python-mode-hook
          (lambda() (local-unset-key (kbd "C-c C-s"))))

; Remapping keys
(global-set-key (kbd "C-c C-c") #'sardine/eval-block)
(global-set-key (kbd "C-c C-s") #'sardine/stop-code)

Others

In the past, people have been running Sardine code on many different text editors and platforms:

  • Jupyter Notebook: a very popular framework for using Python in data sciences, machine learning, etc.
  • Atom (depracated) / Pulsar: killed by Microsoft, was once a very nice but slow text editor/IDE.

We know this is working, but there is no documentation about it or the one we have is outdated!

Getting started

img

Starting Sardine

Before starting Sardine, you need understand what Sardine is and where it sits on your system:

  • The sardine command is opening the Sardine interpreter. To play with Sardine, you need to start sardine.
  • Sardine is the main Python library that you will be interacting with.
  • Behind the scene, everything will be translated to SuperCollider, MIDI or OSC messages
    • text editor -> sardine -> SuperCollider/MIDI/OSC)

Do not try to import sardine in a regular Python interpreter! It won't work, you will be disappointed. Whenever you start sardine in your terminal, the following splashscreen will appear:

╭──────────────────────────────────────────────────────╮
│                                                      │
│ ░██████╗░█████╗░██████╗░██████╗░██╗███╗░░██╗███████╗ │
│ ██╔════╝██╔══██╗██╔══██╗██╔══██╗██║████╗░██║██╔════╝ │
│ ╚█████╗░███████║██████╔╝██║░░██║██║██╔██╗██║█████╗░░ │
│ ░╚═══██╗██╔══██║██╔══██╗██║░░██║██║██║╚████║██╔══╝░░ │
│ ██████╔╝██║░░██║██║░░██║██████╔╝██║██║░╚███║███████╗ │
│ ╚═════╝░╚═╝░░╚═╝╚═╝░░╚═╝╚═════╝░╚═╝╚═╝░░╚══╝╚══════╝ │
│                                                      │
│ Sardine is a MIDI/OSC sequencer made for live-coding │
│ Play music, read the docs, contribute, and have fun! │
│ WEBSITE: https://sardine.raphaelforment.fr           │
│ GITHUB: https://github.com/Bubobubobubobubo/sardine  │
│                                                      │
╰──────────────────────────────────────────────────────╯
BPM: 120.0,BEATS: 4 SC: [X], DEFER: [X] MIDI: Sardine
>>>

Only then will you know that Sardine has started and that everything is working! Some additional messages are likely to appear shortly after, warning you that the audio engine was hooked correctly or that an error has happened somewhere.

  • You can write code directly in the interpreter. However, this is not a recommended practice!
    • You will soon begin to see that the system will print some useful information, preventing you from writing easily in the interpreter window. You should jump to your text editor!
  • For the duration of this tutorial, I will make the assumption that you are using sardine web, our internal text editor and environment!

Code evaluation

Evaluating code

To live code, you need two things :

  • a document where you write your code.
  • a running interpreter that will receive code.

One action is the base of everything, sending new code for evaluation :

  • Your main document is your playing interface. Write / edit / change code.
  • Send code for evaluation by pressing a key or keystroke combination.
  • This combination of text / code editor with a code enterpreter is called REPL (read-eval-print-loop).

On the included web text editor, select the code you want to execute and press Shift + Enter or Ctrl+E. This will cause the code to evaluated. If an error occurs, the code execution will not stop but will report the error and continue running using the existing code. The interpreter will warn you if something goes wrong! Every text / code editor that functions as REPL will have its own keystroke combination for code evaluation.

Evaluating code

Write the following line and evaluate it:

Pa >> d('bd cp')

At the beginning of the next bar, a musical sequence will start to play with a kickdrum and a clapping sound in quick succession. This is a pattern. This pattern will repeat indefinitely until you stop it.

To stop a pattern, use one of the following functions:

silence() #  gentle shutdown
panic() #  hard stop (will be detailed later)

You can also be more precise about your intentions by giving the name of the pattern you want to stop:

silence(Pa)
Pa.stop()

The code below repeats the kickdrum two times. Change the code and press Shift+Enter again:

Pa >> d('bd!2 cp')
silence(Pa)

You can live code anything. The system will jump to the new version of your code as soon as you submit it. This is how you live code.

Coding like this takes practice. There are some pitfalls to avoid:

  • keep your code statements simple at first.
  • sometimes code lines must be evaluated in a specific order.
  • make sure that ALL your needed code is evaluated.
  • sending code can fail for cryptic reasons (invisible characters or missing characters).
  • pay attention to error messages, but sometimes they won't be helpful!
    • when your code fails - study it closely for syntax, and try simpler statements

Synths and samples

The sounds from Sardine are generated from the SuperDirt audio engine, which runs in SuperColider. SuperDirt comes with a standard sample set and a group of synthesizers. SuperDirt receives information about audio samples and synthesizers from a string ('bd, 'cp', 'tech:2').

SuperDirt default samples:

(Numbers show how many samples in each bank.)
808 (6) 808bd (25) 808cy (25) 808hc (5) 808ht (5) 808lc (5) 808lt (5) 808mc (5)
808mt (5) 808oh (5) 808sd (25) 909 (1) ab (12) ade (10) ades2 (9) ades3 (7)
ades4 (6) alex (2) alphabet (26) amencutup (32) armora (7) arp (2) arpy (11)
auto (11) baa (7) baa2 (7) bass (4) bass0 (3) bass1 (30) bass2 (5) bass3 (11)
bassdm (24) bassfoo (3) battles (2) bd (24) bend (4) bev (2) bin (2) birds (10)
birds3 (19) bleep (13) blip (2) blue (2) bottle (13) breaks125 (2) breaks152 (1)
breaks157 (1) breaks165 (1) breath (1) bubble (8) can (14) casio (3) cb (1) cc (6)
chin (4) circus (3) clak (2) click (4) clubkick (5) co (4) coins (1) control (2)
cosmicg (15) cp (2) cr (6) crow (4) d (4) db (13) diphone (38) diphone2 (12) 
dist (16) dork2 (4) dorkbot (2) dr (42) dr2 (6) dr55 (4) dr_few (8) drum (6)
drumtraks (13) e (8) east (9) electro1 (13) em2 (6) erk (1) f (1) feel (7)
feelfx (8) fest (1) fire (1) flick (17) fm (17) foo (27) future (17) gab (10)
gabba (4) gabbaloud (4) gabbalouder (4) glasstap (3) glitch (8) glitch2 (8)
gretsch (24) gtr (3) h (7) hand (17) hardcore (12) hardkick (6) haw (6) hc (6)
hh (13) hh27 (13) hit (6) hmm (1) ho (6) hoover (6) house (8) ht (16) if (5)
ifdrums (3) incoming (8) industrial (32) insect (3) invaders (18) jazz (8)
jungbass (20) jungle (13) juno (12) jvbass (13) kicklinn (1) koy (2) kurt (7)
latibro (8) led (1) less (4) lighter (33) linnhats (6) lt (16) made (7) made2 (1) 
mash (2) mash2 (4) metal (10) miniyeah (4) monsterb (6) moog (7) mouth (15) 
mp3 (4) msg (9) mt (16) mute (28) newnotes (15) noise (1) noise2 (8) notes (15)
numbers (9) oc (4) odx (15) off (1) outdoor (6) pad (3) padlong (1) pebbles (1)
perc (6) peri (15) pluck (17) popkick (10) print (11) proc (2) procshort (8)
psr (30) rave (8) rave2 (4) ravemono (2) realclaps (4) reverbkick (1)
rm (2) rs (1) sax (22) sd (2) seawolf (3) sequential (8) sf (18) sheffield (1)
short (5) sid (12) sine (6) sitar (8) sn (52) space (18) speakspell (12)
speech (7) speechless (10) speedupdown (9) stab (23) stomp (10) subroc3d (11)
sugar (2) sundance (6) tabla (26) tabla2 (46) tablex (3) tacscan (22) tech (13)
techno (7) tink (5) tok (4) toys (13) trump (11) ul (10) ulgab (5) uxay (3)
v (6) voodoo (5) wind (10) wobble (1) world (3) xmas (1) yeah (31)

Each sample bank is a folder with individual sample files. A colon after the name designates the individual sample in that folder. Without a colon, SuperDirt will use the first file ('bd' = 'bd:0').

Pa * d('voodoo:0 voodoo:1 voodoo:2')
silence(Pa)

This pattern plays the first, second and third sample from the voodoo folder. The sample counter starts at 0 and wraps back to 0 when it reaches the last sample number, so higher numbers will work.

The same syntax is used for SuperDirt synthesizers:

# Requires sc3_plugins
Pa * d('supersaw superpiano')
Pb * d('supersaw superpiano', n='60 62 63 67')
    
silence()

Summary:

  • You call synthesizers and samples using "strings".
  • You call a specific sample by using the colon syntax bd:4.
  • "names" can refer to a synth or a sample.

Playing SuperDirt samples

Use the Players below to listen to each sample bank. Execute one line at a time and Sardine will play through all of the samples in that bank.

For example, this one will play each sample in the alphabet bank. The period value p=0.5 is set to 1/2 second. For samples with a longer duration, you will need to set this to a higher value.

Pa * d('alphabet:[0:25,1]', p=0.5, room=1,size=0.5,dry=0.4)
clock.tempo = 60
silence()

Pa * d('808bd:[0:24,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808cy:[0:24,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808hc:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808ht:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808lc:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808lt:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808mc:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808mt:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808oh:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('808sd:[0:24,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('909', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ab:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ade:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ades2:[0:8,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ades3:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ades4:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('alex:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('alphabet:[0:25,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('amencutup:[0:31,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('armora:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('arp:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('arpy:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('auto:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('baa:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('baa2:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bass:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bass0:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bass1:[0:29,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bass2:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bass3:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bassdm:[0:23,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bassfoo:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('battles:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bd:[0:23,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bend:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bev:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bin:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('birds:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('birds3:[0:18,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bleep:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('blip:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('blue:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bottle:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('breaks125:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('breaks152', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('breaks157', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('breaks165', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('breath', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('bubble:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('can:[0:13,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('casio:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('cb', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('cc:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('chin:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('circus:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('clak:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('click:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('clubkick:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('co:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('coins', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('control:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('cosmicg:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('cp:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('cr:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('crow:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('d:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('db:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('diphone:[0:37,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('diphone2:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dist:[0:15,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dork2:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dorkbot:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dr:[0:41,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dr2:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dr55:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('dr_few:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('drum:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('drumtraks:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('e:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('east:[0:8,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('electro1:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('em2:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('erk', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('f', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('feel:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('feelfx:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('fest', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('fire', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('flick:[0:16,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('fm:[0:16,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('foo:[0:26,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('future:[0:16,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gab:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gabba:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gabbaloud:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gabbalouder:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('glasstap:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('glitch:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('glitch2:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gretsch:[0:23,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('gtr:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('h:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hand:[0:16,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hardcore:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hardkick:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('haw:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hc:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hh:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hh27:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hit:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hmm', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ho:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('hoover:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('house:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ht:[0:15,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('if:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ifdrums:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('incoming:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('industrial:[0:31,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('insect:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('invaders:[0:17,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('jazz:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('jungbass:[0:19,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('jungle:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('juno:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('jvbass:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('kicklinn', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('koy:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('kurt:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('latibro:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('led', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('less:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('lighter:[0:32,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('linnhats:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('lt:[0:15,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('made:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('made2', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mash:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mash2:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('metal:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('miniyeah:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('monsterb:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('moog:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mouth:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mp3:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('msg:[0:8,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mt:[0:15,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('mute:[0:27,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('newnotes:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('noise', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('noise2:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('notes:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('numbers:[0:8,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('oc:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('odx:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('off', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('outdoor:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('pad:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('padlong', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('pebbles', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('perc:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('peri:[0:14,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('pluck:[0:16,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('popkick:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('print:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('proc:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('procshort:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('psr:[0:29,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('rave:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('rave2:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ravemono:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('realclaps:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('reverbkick', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('rm:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('rs', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sax:[0:21,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sd:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('seawolf:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sequential:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sf:[0:17,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sheffield', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('short:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sid:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sine:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sitar:[0:7,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sn:[0:51,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('space:[0:17,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('speakspell:[0:11,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('speech:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('speechless:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('speedupdown:[0:8,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('stab:[0:22,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('stomp:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('subroc3d:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sugar:[0:1,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('sundance:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tabla:[0:25,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tabla2:[0:45,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tablex:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tacscan:[0:21,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tech:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('techno:[0:6,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tink:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('tok:[0:3,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('toys:[0:12,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('trump:[0:10,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ul:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('ulgab:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('uxay:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('v:[0:5,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('voodoo:[0:4,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('wind:[0:9,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('wobble', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('world:[0:2,1]', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('xmas', p=0.5, room=1,size=0.5,dry=0.4)
Pa * d('yeah:[0:30,1]', p=0.5, room=1,size=0.5,dry=0.4)

silence()

Patterns

Creating, extending, and modifying patterns is the most essential part of using Sardine. Here we illustrate different patterns and simple ways to modify and extend them. Explanations, references, and usage details will come in later sections of the docs (Basics, Pattern Languages, Diving Deeper). If you want to try different sounds, go back to the samples page and substitute different sample names and index numbers: (drum:2, hh27:4, stomp:0, noise:7, etc.)

These examples all use a Player (Pa) which is the simplest way to start making sound with Sardine. Here is a very simple musical pattern alternating between two audio samples (1 per beat):

Pa * d('bd cp')

You can change the period of a pattern by using the keyword p. The period relates to beats, so when p=0.5 there are two notes per beat, when p=2 there are 2 beats per note.

  • Change the first string bd cp to play different samples, to repeat them, etc...
  • The character "." in a pattern is a rest (no sound).
  • 'bd!2 cp' will play the bd sample twice.
  • Pattern elements are delimited by whitespace (no comma) and are enclosed in quotes.
Pa * d('bd cp', p=0.5) # sets period to 0.5, yields 2 notes per beat
Pa * d('bd cp hh:2', p=0.5) # adds the hh:2 sample, meter is now felt in 3
Pa * d('bd cp hh:2', p=0.25) # period is 0.25, 4 notes per beat, rhythm is twice as fast
Pa * d('bd!2 cp hh:2 .', p=0.25) # adds bd repeat and a rest "." - meter is felt in 5.

silence(Pa)

Notice how the basic pattern we started with has expanded, step by step. This is how you start creating patterns, this is livecoding.

Adding parameters

Let's try new sounds. Each sender (functions similar to d) can accept many arguments. Below, we are using speed and shape to change the audio playback parameters for our samples. There are many parameters you can tweak. See the reference section for an overview.

  • In the 2nd version of the Player, we switch the p=0.5 argument from a single value to a pattern p='0.5 0.25', and similarly for the speed parameter speed='1.2 2'. Note how this adds rhythmic variety shifts the pitch.
  • In the 3rd version, we add an additional sample cr and vary the rhythm with a less regular period pattern p='0.5!2 0.25!5'. The music now shifts around more.
Pa * d('reverbkick east:4 yeah:2 mt', speed=1.2, shape=0.5, p=0.5) 
Pa * d('reverbkick east:4 yeah:2 mt', speed='1.2 2', shape=0.5, p='0.5 0.25')
Pa * d('reverbkick east:4 yeah:2 mt cr', speed='1.2 2.2', shape=0.7, p='0.5!2 0.25!5')

silence(Pa)

Multiple players

Let's use multiple players! Select all the players and evaluate. You are now playing four different patterns at the same time. They will all start on the first beat of the bar and be synchronised with each other.


Pa * d('bd cp', p=1) # plays every beat, alternating bd and cp
Pb * d('hh27:2', p=0.5) # plays every 1/2 beat
Pc * d('. east:4!2 .', p=0.25) # plays 2nd and 3rd of the 1/4 beat
Pd * d('. . . bleep:0', p=0.25) # plays every 4th of the 1/4 beat

silence()

Tempo

The tempo is set using a beats per minute value in your Sardine configuration. You can also change it on the fly. Learn more about time and tempo in the appropriate section:

clock.tempo # prints the current clock tempo value
#
clock.tempo = 100 # sets a new tempo - try different values
Pa * d('bd cp', p=1)
Pb * d('hh27:2', p=0.5)
Pc * d('. east:4!2 .', p=0.25)
Pd * d('. . . bleep:0', p=0.25)

silence()

Summary

This concludes the Getting Started section. The Basics section will go into more detail for the components covered here. Before moving on, spend time experimenting with what you have learned.

  • Explore the sample library, subsitute samples in the examples above.
  • Use different period values to control the speed of rhythmic changes.
  • Add and change parameters to alter pitch and timbre.
  • Try different pattern values to get an understanding of what patterns are.
  • Have fun, you are livecoding now!

Basics

This is a reference for the main components used with Sardine. When you are learning, you won't need to understand everything in each page. But later, these pages will be the go-to source for how things works and what syntax to use.

Some sections cover concepts like temporal recursion. We try to keep the technical jargon to a minimum but sometimes it's important to understand more about how Sardine works.

Components and features covered in Basics:

  • Senders: syntax options for samples, MIDI, Ziffers, etc in both Play and @swim functions.
  • Players: shorthand syntax and the most basic way to create patterns.
  • @swim functions: the fundamental mechanism for organizing patterns.
  • Iterators: used in @swim functions to manage changing values.
  • Period, Divisor, Rate: used for controlling rhythm and repetition.
  • Tempo and playback control

Senders

Senders are required building blocks used in all ways of making sound with Sardine.

d('bd') is a Sender. It is a special function that sends messages to musical applications and audio tools. It also provides an interface to the pattern languages (Sardine, Ziffers). Basically, senders provide a gate outside of Sardine.

Default Senders

The Case signifies whether the sender is used in a Player (lower), or @swim function (UPPER). See Player vs @swim. Using the Ziffers pattern language requires different senders.

Player@swimPurpose and description
SardineSenderssenders used with Sardine patterns
d( )D( )SuperDirt - samples and synthesizers
n( )N( )MIDI notes
cc( )CC( )MIDI control messages
pc( )PC( )MIDI program changes
sy( )SY( )MIDI sysex message
ZiffersSenderssenders used with Ziffers patterns
zd( )ZD()Ziffers SuperDirt - samples /synths with Ziffers patterns
zn( )ZN()Ziffers MIDI notes with Ziffers patterns

There is also a standalone zplay() sender associated with Ziffers documented in the Ziffers section.

Arguments and syntax

Senders require arguments and keyword arguments. These provide the values that represent notes, samples, patterns, parameters, etc. Here are the syntax rules:

  • Patterns are given as strings with double or single quote. No comma is tolerated in the pattern.
    • Using d('bd, cp') will error. There are some exceptions to the comma rule, covered in the Pattern Language section.
  • Patterns use a distinct syntax from Python. Learn about it in the Pattern Languages section.
  • Keyword parameters are regular keyword arguments: parameter=value. Arguments are separated with commas!

Sender example with arguments

## Sender syntax: This statement evaluates correctly but will not produce any sound.
d('bd cp', speed='1 2', shape=0.5, p='0.5!4 0.25!2') # sender syntax

## It needs to be called from a Player:
Pa * d('bd cp', speed='1 2', shape=0.5, p='0.5!4 0.25!2') 
  • 'bd cp': initial argument to SuperDirt. Specifies the pattern of samples and synthesizers.
  • speed='1 2': speed is a SuperDirt sampling parameter, set here with a pattern that alternates between 1 and 2 (doubles speed = raise pitch an octave).
  • shape=0.5: shape is an effects parameter from SuperDirt, single value.
  • p='0.5!4 0.25!2': the letter "p" is shorthand for "period". It is set to a more complex pattern that controls the playback rhythm.

More sender examples

Here are some additional patterns using the Sardine senders. If your patterns are getting longer, you can indent them on multiple lines. This is just normal Python code after all!

Pa * n('C5 E5 G5', p=0.25) # playing a chord, one note every 1/4 of a beat.
Pb * cc(ctrl=20, chan=0, value='rand*127') # sending a random MIDI control on ctrl 20, channel 0
Pc * d('tabla tabla:2') # Playing audio samples of an indian tabla

Player vs @swim function

The are two ways to generate patterns with Sardine:

  1. @swim functions: the fundamental mechanism.
  2. Player: a shorthand syntax to use it.

They share common features, leverage the same pattern languages, but have a few key differences. It is important to understand both, as well as when and how to use each. You will find that using one syntax or the other is more appropriate depending on the what you are trying to do.

This example will produce the same musical output with Player and @swim:

# Player version
Pa * d('bd!2 cp hh27:2 . ', speed='1 2', 
       room=0.6, dry=0.2, size=0.5, p='0.5') 

# Swim function version
@swim
def inFive(p=0.5, i=0):
    D('bd!2 cp hh27:2 . ', speed='1 2',
      room=0.6, dry=0.2, size=0.5, i=i)
    again(inFive, p=0.5, i=i+1)

The Player version has a more compact syntax, referred to as shorthand. Both have Senders with most of the same parameters and patterns as arguments. The @swim function is essentially a Python function, invoked with the @swim decorator. It can contain multiple Senders, like playing multiple instruments or tracks together. @swim functions are unique with the iterator value i and have the period and iterator initiallized in the function arguments.

Feature / Syntax comparison

Here is a short comparison of the Player syntax against the @swim syntax:

FeaturePlayer@swimnotes
Operator>> or *@swimFunctions take a decorator, Players use an operator.
Senderd(), n(), ...D(), N(), ...Uppercase against lowercase.
Multiple SendersnoyesPlayers can only play one pattern at the same time.
periodp=1 or period=1p=1period not valid in @swim
iteratorImplicitExplicitFor @swim, iterator control is manual. Initialize it in function args: i=i, increment it in the final recursive call: again(i=i+1)
spanspan=1.5n/aSee Diving Deeper > High Level Patterns
snapsnap=-0.5snap=-0.5See Diving Deeper > High Level Patterns

Benefits and Uses

Each technique has some benefits and some downsides.

Players:

  • Simplified friendly syntax: often one line statements.
  • Good for quick development, experimentation, learning.
  • Multiple players can be used simultaneously.
  • Each player can start and stop independantly.
  • Similar to the well-known syntax of FoxDot.
  • Complex patterns can be harder to understand on a single line.

@swim functions:

  • Containers for more complex constructions.
  • One @swim function can have multiple senders, Python statements, etc...
  • Manual control over looping through iteration.
    • With different values or iteration methods, more creative patterns can be found.
  • One @swim function can control a complete session.
  • More suited for adding custom python code.

Demo

Complex swimming function

Here is a more advanced @swim function. Notice how it spans over multiple lines and contains a large group of independent senders. This function is playing many synths at the same time and also controls drumming!

@swim
def tran(p=0.5, i=0):
    D('(eu braids 5 8)', n='C2', model=9, lpf=500, i=i, timbre='0.5')
    D('(eu braids 7 8)', n='C4|C3', model=9, lpf='rand*4000', i=i, timbre='0.5')
    ZD('braids', '<0 7> (2,8)', 
       scale='minor', model=9, lpf='((sin $)/2)*2000', i=i, timbre='0.5', leg=1.5)
    ZD('braids', '_ <0 7> (2,8) [0 ^5]', 
       scale='minor', model=9, lpf='((sin $)/2)*4000', i=i, timbre='0.5', leg=1.5)
    ZD('braids', '_ <0 3> (2,8) [0 ^7]', 
       scale='minor', model=12, lpf='((sin $)/2)*5000', i=i, timbre='0.5', leg=1.5)
    D('hkick ...', i=i)
    D('..  hsnare:8 .', r=0.75, i=i, lpf=4000, amp=0.1)
    again(tran, p=0.5, i=i+1)

Complex player function

Players can also be used to generate complex patterns but notice how you quickly loose in readability! It is better to limit the usage of Players to single tasks (one instrument, one musical part). They are easier to control individually.


Pc >> n("vanish(bass(if(every(5) [{[D F|G A|Bb]} [D|G A {D D'}]] {D A}) do=beat(2)) \
     20 do=beat(1))", p='[1 0.5 1 0.25!4]*2', dur=0.1, vel='50~100', d=1, r=1)


Player

The Player is the most basic way to create patterns - it uses a shorthand syntax, and uses patterns and arguments to make musical output.

Pa * d('bd cp', p=0.5)
  • Pa is a player - it acts on a pattern.
  • d() is the sender and provides the pattern. It takes any number of arguments.
  • * is the operator that assigns the pattern to the player.
    • * is equivalent to >>.
  • p=0.5 is an argument where p is shorthand for period.

There are 48 players by default:

# List of all players
all_players = [Pa, Pb, Pc ...Pz, PA, PB, PC, ... PZ]

Pattern arguments control the rhythm, pitch and timbre of the pattern. A Sardine pattern is a sandwich of values. Here is a player with a more detailed pattern:

Pa * d('bd cp', speed='1 2', shape=0.5, room=0.5, dry=0.25, size=0.1, p='0.5!4  0.25!2')
    
# This is easier to read
Pa * d('bd cp',
        speed='1 2',
        shape=0.5,
        room=0.5,
        dry=0.25,
        size=0.1,
        period='0.5!4 0.25!2'
)
  • p or period: the rhythm of each step in beats. It can be a number (single value) or string (pattern).
    • Period is always relative to the tempo.
    • Here the period is a string, which makes this a pattern. 0.5 and 0.25 divide the beat, and 0.5!4 means to repeat that step division 4 times.
    • The note values bd cp are applied to the step divisions of the period.
  • shape, room, etc.: these are parameters of the audio sampler, to shape the sound.

There are two types of arguments you can give to a pattern:

  • pattern-relative: these arguments determine how the pattern unfolds in time. These include: period, divisor, rate.
  • instrument-relative: these arguments control parameters specific to SuperDirt / MIDI / OSC. For the SuperDirt sampler parameters, refer to the Audio Engine Reference in the sidebar.

Numbers vs Strings: Numbers are just numbers and can be integers or floats (decimals). Strings are interpreted as patterns that are evaluated based on the syntax used. They move in time with each step. In the example above the pattern "0.5!4, 0.25!2" as interpreted becomes: [0.5 0.5 0.5 0.5 0.25 0.25].

Summary:

  • Players (Pa) are used together with an operator (>>) and pattern sender.
  • Patterns are a complete description of an algorithmic musical expression: pitch, timbre, rhythm, etc.
  • Patterns are a collection of values or other patterns and can take an indefinite amount of arguments.
  • Patterns use a special syntax as 'strings'.

Player arguments

Every sender is specialised for a task. Every sender will have its own special arguments. Some of them can also be aliased (e.g vel for velocity). Learning these arguments is part of learning the Sardine instrument. There is no way around it!

MIDI Arguments

There are multiple senders for MIDI because there are different MIDI messages you can write. To each messsage its sender.

  • N("pattern", velocity, channel, duration): the sender for MIDI notes.

    • velocity or vel: how hard the note is played, from 0 to 127.
    • channel or chan: on which channel to play the note (from 0 to 15).
    • duration or dur: for how long to play the note (in beats).
  • CC(control, channel, value): the sender for MIDI control changes.

    • control or ctrl: number of the control to target (0 to 127).
    • channel or chan: on which channel to send the control.
    • value or val: value of that control (0 to 127).
  • PC(program, channel): the sender for MIDI program changes.

    • program or prog: program number to send.
    • channel or chan: on which channel to send the control.

There is also a special SY sender that is very experimental and is used to control some very specific gear. I have currently no plan to open it for others to play but you can still send Sysex messages by using private methods of the midi object. The basic senders should cover 99% of your needs. If ever you were to miss one, it is easy to add them. Just contact me!

SuperDirt Arguments

There is only one sender for SuperDirt: D() or d(). This sender is a basic interface to SuperDirt, allowing you to play sounds or synthesizers and to add effects to them. You will notice that the SuperDirt sender can take any number of arguments. It all depends on how much arguments your synthesizers can take and on how precise you want to be in the description of a specific musical event.

  • D("pattern", orbit=0): the sender for SuperDirt.
    • orbit : channel the sound will played on (mono or stereo).

The concept of orbit is just a way to precise on which channel of the audio console some specific effects should be applied. Assigning an orbit to an event guarantees that the sound you want to play (reverb amount, low-pass filter, etc…) will only be local and not global to every other pattern currently playing. This concept of orbit is an important concept specific to SuperDirt.

@swim function

The @swim function is the fundamental mechanism for managing output and changing values and parameters dynamically. Strictly speaking, a @swim function is an example of a temporally recursive function. This is a programming construct that supports repetition in time with dynamic parameter values. Temporal recursion is common in livecoding languages as it is a very fundamental way to think about time in the context of the execution of a program. In Sardine, the @swim function has unique ways and syntax to manage repeats. The Sardine scheduling engine will manage repeats as well as parameter and pattern value changes strictly following the tempo and beat patterns we tell Sardine to use.

To illustrate, let's start with a basic Python function:

def hello_world():
    print('Hello, World!')

# Call this function with this command.
hello_world()

Now let's convert this into a @swim function which adds temporal recursion with looping at the beat. Execute the code below, then make changes to the tempo value. Watch how the output of the python print statement comes in the exact tempo you specify. The again() statement is what provides the temporal recursion (looping in tempo). You could also just say that again() tells the @swim function to repeat!

clock.tempo=60 
@swim
def hello_world():
    print('Hello, World!')
    again(hello_world)

You can stop a swimming function by changing the Python "decorator" from @swim to @die.

@die
def hello_world():
    print('Hello, World!')
    again(hello_world)

# You can also stop a @swim function using these commands: 
silence(hello_world)
hello_world.stop()
silence() # stops everything

Iterator: "i" is the essential driver

Let's translate this into musical output. The swimming function below uses a SuperDirt Sender D() with a sample pattern. Importantly, we now have an iterator - which by Sardine convention uses the character i. It appears in the function arguments, in the recursion again(), and in the Sender.

  • function definition: inFive(p=1, i=0) the period (p) and iterator (i) are initialized.
  • sender argument: i=i is added. This tells Sardine to iterate over the sample and speed patterns on every recursion (repeat).
  • again argument: i=i+1 This expression increments the iterator. We will see later how this expression can be changed to create interesting musical results.
@swim
def inFive(p=1, i=0):
    print(f"Value of i: {i}")
    D('bd!2 cp hh27:2 cr:2', speed='1 2', i=i)
    again(inFive, p=0.5, i=i+1)

@swim function syntax

  • @swim: this is the "decorator" in python terms. It always has its own line.
  • def inFive(p=1, i=0): the "def" keyword in python creates a custom function.
    • Format: def <name>(arguments with default values)
    • Function names can contain alpha-numeric but no spaces or special characters.
    • Function names must match the name provided in the again() statement.
    • Iterator and period values are initialized.
  • D() is the SuperDirt Sender.
    • Any of the valid @swim Senders can be used - N(), ZD(), etc.
    • Multiple senders can be used.
    • Sender arguments are separated by comma.
    • Senders need the iterator value: i=i.
  • Python functions can be added.
    • In the example above, a simple python print statement is called with the value of "i" changing every beat.
    • Functions will be called on each iteration.
  • again(<functionName>, p=<value>, i=<expression>)
    • The again() statement requires the exact function name.
    • The period assignment is optional, and will default to 1. It is common to include it to be able to set or easily change the rhythm per beat value.
    • iterator expression i=i+1 is the most common, but other values and patterns are valid and can yield interesting musical results.

More @swim function examples

clock.tempo=100

@swim
def sequence(p=0.5, i=0):
    D('bd!2 cp:rand*20 tabla',
        speed='1 2 3 4 5 6',
        room=0.5, size=0.4, dry=0.5,
        lpf="200+rand*2000", i=i)
    D('. hh:7 hh:10', r=0.5, i=i)
    again(sequence, p=P('0.5!4 0.25!2', i), i=i+1)
clock.tempo=64

@swim 
def test(p=1, i=0):
    D('jvbass:0~5', 
         room=0.9, gain=0.8, size=0.4, dry=0.5,
         midinote='C4 G3 E3 D6 .', i=i)
    again(test, p=1/8, i=i+1)

Period - Divisor - Rate

Period, Divisor and Rate all impact how rhythm is expressed in Sardine. Period is the most important and has the most flexibility. Divisor and rate have a more narrow focus, but can be used effectively particularly in combination with period.

Period

Period determines the number of steps per beat. (Beat is set by the clock.tempo value in BPM.) Period has a default value of 1 and often set to values less than one to subdivide the beat. Setting p=0.5 sets the period to half of the beat (notes will be twice as fast). p=2 sets the period to twice the length of the beat (notes will be twice as slow).

It is common to use regular values of p, such as 0.5, 0.25. But period can be set to any value, including fractions and decimals. This can result in rhythms that don't line up on the beats, polyrhythms, or granular synthesis like effects when very low period values (0.05 or less) are used.

Period with Players

To understand how period works, consider the set of Players below. The clock is set to 60 BPM to give a slower tempo and make everything clear. Keep the first pattern playing to serve as a metronome. Add each player individually and notice what the different period values do.

clock.tempo=60
Pa * d('hh27:2', p=1)   # one note per beat - use as a metronome

Pb * d('hh:10',  p=0.5) # subdivides beat
Pc * d('hh:4',   p=2)   # every other beat
Pd * d('hh:9',   p=1/3) # triplet subdivision
Pe * d('hh:2 . hh:2', p=0.268) #irregular subdivision 
Pf * d('hh:5',   p='1!2 0.5!2 0.25!4') # period pattern

Notice the use of a pattern value for period in the Pf player p='1!2 0.5!2 0.25!4'. The pattern means that Sardine will change the period value on each repeat. This creates rhythm. Try changing the pattern to see how the rhythm changes.

Period in @swim functions

Period also works in @swim functions with some differences. Most importantly, the period value is set for the whole @swim function and is not set in each Sender. Also, to use a pattern with the period value the Pattern Object is required.

The example below translates the Players above into @swim functions. Notice that we can't contain all 5 players in a single @swim function because we can't set a unique period value in each Sender. The period needs to be set as an argument in the again() statement.

clock.tempo=60
@swim
def demo1(p=1, i=0):
    D('hh27:2', i=i)
    D('hh:4', d=2, i=i) # divisor set to 2
    again(demo1, p=1, i=i+1)

@swim
def demo2(p=1, i=0):
    D('hh:9', i=i) 
    again(demo2, p=1/3, i=i+1)

@swim
def demo3(p=1, i=0):
    D('hh:5!2 hh:11', i=i) 
    again(demo3, p=P('1!2 0.5!2 0.25!4', i), i=i+1) # P() is the Pattern Object

Pattern Object

The demo3 function above introduces the Pattern Object.

  • p=P('<pattern value sequence>', i) p=P('1!2 0.5!2 0.25!4', i)
  • Within the quotes, any valid pattern can be used.

The Pattern Object provides an iterator value within the again() statement. If you leave off the P('') and just put a pattern, Sardine will throw an error. For more information see Diving Deeper > Patterning everything.

Period with random values

Periods can be randomized but will behave differently in Player and @swim. Note that you first need to import the random functions in python. In the Pa player below, a random decimal value between 0 and 2 will be generated the first time it is called. But in a Player, the value assigned to the period will stay the same until you re-execute the Player code line.

In @swim, the random call will happen automatically on every iteration, making the period value change on every beat. This has an unsual effect musically where the rhythm becomes irregular.

from random import *
Pa * d('hh27:2', p=random()*2)

@swim
def demoR(p=1, i=0):
    D('hh27:2', i=i)
    D('hh:4', d=2, i=i) 
    again(demoR, p=random()*2, i=i+1)

Divisor

Divisor is a way of controlling the number of beats per note. Think of it as dividing the step so that multiple beats occur before the next step. Divisors are expressed as integer values. These will slow the pattern down and stretch it out. It can be helpful to use this in combination with faster period values.

Syntax

Player@swimNotes
d=2 or divisor=2d=2"d" is most common
argument in sender(same)in @swim divisor can be set independantly in each sender
  • use integers: 1, 2, 3, 4 ...

Adding a divisor together with a period in a single player may seem like they would cancel each other out. But higher divisor values can be used to create offbeat rhythms. With irregular or patterned periods, a divisor is an effective way to preserve the period but stretch it out.

clock.tempo=60
Pa * d('hh27:2', p=1) # play as metronome

# try the different values for d and p
Pb * d('hh:10', p=0.5, d=1) # start here d=1
Pb * d('hh:10', p=0.5, d=2) # changes divisor to 2
Pb * d('hh:10', p=0.25, d=2) # lowers period to 0.25 - notice the offbeat rhythm

Divisors can also be used to preserve an irregular period value but extend it and slow it down.

clock.tempo=60
Pa * d('hh27:2', p=1) # play as metronome

# try the different values for d and p
Pe * d('hh:2 . hh:2', p=0.268, d=1)
Pe * d('hh:2 . hh:2', p=0.268, d=3) # preserves the period value but slows it down

Pf * d('hh:5', p='1!2 0.5!2 0.25!4', d=1)
Pf * d('hh:5', p='1!2 0.5!2 0.25!4', d=2) # slows the pattern down

Divisor in @swim

When using multiple senders, divisors let you set and change the rhythmic pace independantly for each sender. This can be useful for having different patterns execute at different rates. Note how each sender below has a different divisor value. This works well with a low period setting p=0.25.

@swim
def demoDiv(p=1, i=0):
    D('hh27:2', d=2, i=i)
    D('hh:4', d=3, i=i) 
    D('hh:9', d=4, i=i) 
    D('hh:3 . hh:3!2', d=1, i=i) 
    D('hh:2 . . hh:2', d=5, i=i) 
    D('. . alphabet:6', d=6, i=i) 
    again(demoDiv, p=0.25, i=i+1)

Rate

The rate argument is used in Senders and works the same for Players and @swim. The rate value is applied to a pattern and controls the rate at which the iterator cycles through pattern changes. With values less than 1, the rate of change is slower, resulting in more repetition within the pattern. For example, with rate=0.5, each value in a pattern will repeat twice.

Try the different values on the same players below, and see how different rate values change the output.

clock.tempo=120
Pa * d('hh27:5 hh27:2 hh27:1 hh27:6 hh:9', p=0.5, rate=1)
Pa * d('hh27:5 hh27:2 hh27:1 hh27:6 hh:9', p=0.5, rate=0.5)
Pa * d('hh27:5 hh27:2 hh27:1 hh27:6 hh:9', p=0.38, rate=0.85)

Pb * d('hh:0~12', p=0.5, rate=1)
Pb * d('hh:0~12', p=0.5, rate=0.5)
Pb * d('hh:0~12', p=0.5, rate=0.25)

@swim
def demoDiv(p=1, i=0):
    D('hh27:2 hh27:9 hh27:1', d=2, rate=0.5, i=i)
    D('hh:4 hh:9', d=3, rate=0.2, i=i) 
    D('. reverbkick', d=5, i=i)
    again(demoDiv, p=0.5, i=i+1)

Here is an example of period, divisor, and rate working in combination to create an unusual result.

TBD - Bubo, can you add something here? 

Tempo and playback

Sardine executes code based on timing given by its clock. The clock starts with Sardine and runs until Sardine exits. There are two clocks:

  • internal clock: Your regular system clock.
  • external clock: A special clock for synchronisation on the network. (See Diving Deeper)

Clock commands:

  • clock.tempo reports the current tempo in bpm.
  • clock.tempo = 140 sets tempo to 140 bpm.

Position in time:

  • clock.phase Position in phase (0.0 - 1.0).
  • clock.beat Cumulative number of beats (note: doesn’t reset at each bar.)
  • clock.bar Cumulative number of bars.
  • clock.time time elapsed since start in seconds (monotonic time)
  • clock shows elapsed time, tempo, beats per bar

Bowl commands can be used to start/stop the clock, which will also impact any Player output.

  • bowl.pause() / bowl.resume() : pause and resume runnnig code and stops/resumes the clock.
  • bowl.stop() / bowl.start(): stop and play. (Start does not restart the clock.)

Tempo change

Tempo changes can be achieved in @swim by using the Pattern Object on the clock.tempo value. For details on the Pattern Object, see Diving Deeper > Patterning everything.

In the first function below, the Pattern Object uses the ramp operator to increment the tempo by 2 BPM between 90 and 180, then hold the tempo, and then decrease back faster to 90 (decrements by 4 BPM). To see the tempo values print out, uncomment the print line.

  • Pattern changes will come at the rate set by the function's period value. Setting the period to 1 (p=1) will make changes happen every beat. Set a higher value for more gradual tempo changes.
  • It is best to have a separate function just for clock.tempo changes. That keeps the clock patterns independant from musical functions.
  • Tips
    • Set the clock.tempo to your initial value every time you run the function. When you reload the function the clock will be at it's last value.
    • Reset your clock to your default when you are done!
# @swim to manage clock.tempo changes
# tempo increases from 90 -> 180, holds there, then decreases back to 90

clock.tempo=90
@swim
def clockPat(p=1, i=0):
    #print(f"clock.tempo: ", clock.tempo)
    clock.tempo=P('[90:180,2] [180:181,0.1] [180:90,4]', i)
    again(clockPat, p=1, i=i+1)

@swim
def hh(p=1, i=0):
    D('hh27:2 hh27:9 hh27:1', i=i)
    D('hh:4 . hh:9', i=i) 
    again(hh, p=0.5, i=i+1)

# reset your clock value when you are done!
clock.tempo=120

Pattern Languages

Sardine comes out of the box with three pattern languages for you to play with :

  • Sardine Pattern Language: the default pattern language used everywhere.
  • Ziffers: a pattern language specialised in writing chords and melodies.
  • Vortex: a Python port of the TidalCycles patterning language.

It is up to you to know if you'd prefer to use one or the other. They are both very capable but one of them might be more suitable for certain tasks. Ziffers is generally more terse and easier to use for melodic writing. The SPL is good at generating data patterns and interesting pattern transformations.

Sardine Pattern Language

What and where are patterns?

You will find the pattern language pretty much everywhere in Sardine. Everytime you use any of the senders like D(), N(), P() and their lowercase variants, you are likely using the pattern language already. Sardine automatically turns every string argument it receives into a pattern using the Sardine Pattern Language:

D('bd', speed=1, legato=2) # speed and legato are using regular Python types
D('b', speed='1|2', legato='1~4') # speed and legato are now patterns (string)

Think of it as having a second programming language inside your main programming language. This language is a welcomed addition:

  • It is an efficient way to create variety in the musical output of your code.
  • It saves space and makes it easier to express complex pattern transformations.
  • It gives you access to new operators that Python doesn't provide by default.
  • It makes writing lists way easier and less verbose compared to Python.

Note that pattern languages are one of the basic tools used in most live coding environments. Each environment comes with its own flavour, and Sardine comes with multiple pattern languages!

Pattern Object

Sardine also has a pattern object, named P(). This object is very useful when you start exploring on your own and start building your own abstractions. The most typical usage of the P() is to pattern the again call in a swimming function like so:

@swim
def donothing(p=1, i=0):
    print('I do nothing really interesting!')
    again(donothing, p=P('1 2 0.5!4', i), i=i+1)

If you want to learn more about it, I encourage you to read: Diving Deeper > Pattern Object.

Numbers

Integers and floating point numbers

The Sardine Pattern Language is supporting the same number types as Python:

  • integers: 1, 5, 50012.
  • floating point numbers: 10.182, 0.18, 123.91239.

All the common mathematical operators are also available and have their usual behavior:

@swim
def number(p=0.5, i=0):
    print(P('1 1+1 1*2 1/3 1%4 1+(2+(5/2))', i))
    again(number, p=0.5, i=i+1)

Parentheses can be used for greater precision in the sequence of operations and to specify priority. The mathematical operators apply to numbers as well as to lists. You can for instance write an addition between a number and a list, between two lists, between a number and a note, between a chord and a list, etc..

@swim
def number(p=0.5, i=0):
    print(P('C5 + 12', i))
    print(P('[10 20 30] + 2', i))
    again(number, p=0.5, i=i+1)

Many things can be safely considered as numbers such as notes. Internally, a note is also a number, whether it is a MIDI note number, a note inside a scale or any other mathematical representation.

Random numbers

You can write random numbers by using the word rand or using the custom operators.

  • rand will return a floating point number between 0.0 and 1.0.
  • rand will be casted to an Integer depending on the context (e.g sample:r*8).
  • rand and 0.0~1.0 yield a similar result. Two ways to express the same idea.

There are multiple ways to generate randomness using Sardine. Even though rand is useful, using time functions will yield unpredictible results that are perceived by humans as randomness.

Notes

There are multiple ways to write notes in the Sardine Pattern Language. Most of the time, the way you will write notes will depend on context. You can play absolute notes, notes in the context of a scale, frequencies instead of notes, etc...

Note using the custom syntax

There is a special representation to write notes using the english name of notes: ABCDEFG. Notes can also be flat (b) or sharp (#) and receive an octave number like so: C5b.

  • Notes will be converted to a numerical MIDI note value (e.g C3 is 60).
    • MIDI Notes start at 0 and end at 127. You can use numbers as well if you prefer.
@swim
def notes(p=0.5, i=0):
    D('pluck', midinote='C5 D5 E5 F5 G5', i=i)
    again(notes, p=0.5, i=i+1)

The syntax to write notes is the following:

  • A capital letter indicating the note name: C D E F G A B.
    • Optionally, you can write Do Ré Mi Fa Sol La Si (french note system).
  • An optional flat or sharp: #, b.
  • An optional octave number: 0..9.

Math with notes

Notes are just numbers. If you want to go up an octave, you can just write an addition: C5 + 12. You can also add a note to an other note, etc... You can make use of that to generate weirdly shaped chords, for transposing or inverting things: {([0 4 7 9 10 11]+50)^1}.

Note qualifiers

You can use the @ operator to qualify a note. Qualyfing a note means that you are turning it into a specific collection of notes (a collection of intervals, a collection of scale notes, etc).

  • A note will become a collection of notes based on the initial note.
    • C@penta will yield a major pentatonic scale based on C4: [60 62 64 67 69].
@swim
def notes(p=0.5, i=0):
    D('pluck', n='C5@penta', i=i)
    again(notes, p=0.5, i=i+1)

Be careful with this rule! This will instantly turn a single token into a list of x tokens. Filter part of your newly generated collection for better control. Note that this can become really explosive if you play carelessly with collections and polyphony!

To put it bluntly, the collection mechanism if very crude and raw. It was introduced in the alpha version of Sardine as a way to play notes but some people still rely on it to play their music! To play chords and harmonies, prefer the Ziffers patterning language that is included with Sardine or use the scale system for better precision.

qualifiers = {

    ##########
    # Chords #
    ##########

    "dim": [0, 3, 6, 12],
    "dim9": [0, 3, 6, 9, 14],
    "hdim7": [0, 3, 6, 10],
    "hdim9": [0, 3, 6, 10, 14],
    "hdimb9": [0, 3, 6, 10, 13],
    "dim7": [0, 3, 6, 9],
    "aug": [0, 4, 8, 12],
    "augMaj7": [0, 4, 8, 11],
    "aug7": [0, 4, 8, 10],
    "aug9": [0, 4, 10, 14],
    "maj": [0, 4, 7, 12],
    "maj7": [0, 4, 7, 11],
    "maj9": [0, 4, 11, 14],
    "minmaj7": [0, 3, 7, 11],
    "five": [0, 7, 12],
    "six": [0, 4, 7, 9],
    "seven": [0, 4, 7, 10],
    "nine": [0, 4, 10, 14],
    "b9": [0, 4, 10, 13],
    "mM9": [0, 3, 11, 14],
    "min": [0, 3, 7, 12],
    "min7": [0, 3, 7, 10],
    "min9": [0, 3, 10, 14],
    "sus4": [0, 5, 7, 12],
    "sus2": [0, 2, 7, 12],
    "b5": [0, 4, 6, 12],
    "mb5": [0, 3, 6, 12],

    ##########
    # Scales #
    ##########

    "major": [0, 2, 4, 5, 7, 9, 11],
    "minor": [0, 2, 3, 5, 7, 8, 10],
    "hminor": [0, 2, 3, 5, 7, 8, 11],
    "vminor": [0, 2, 3, 5, 7, 8, 10],
    "penta": [0, 2, 4, 7, 9],
    "acoustic": [0, 2, 4, 6, 7, 9, 10],
    "aeolian": [0, 2, 3, 5, 7, 8, 10],
    "algerian": [0, 2, 3, 6, 7, 9, 11, 12, 14, 15, 17],
    "superlocrian": [0, 1, 3, 4, 6, 8, 10],
    "augmented": [0, 3, 4, 7, 8, 11],
    "bebop": [0, 2, 4, 5, 7, 9, 10, 11],
    "blues": [0, 3, 5, 6, 7, 10],
    "chromatic": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
    "dorian": [0, 2, 3, 5, 7, 9, 10],
    "doubleharmonic": [0, 1, 4, 5, 8, 11],
    "enigmatic": [0, 1, 4, 6, 8, 10, 11],
    "flamenco": [0, 1, 4, 5, 7, 8, 11],
    "gypsy": [0, 2, 3, 6, 7, 8, 10],
    "halfdim": [0, 2, 3, 5, 6, 8, 10],
    "harmmajor": [0, 2, 4, 5, 7, 8, 11],
    "harmminor": [0, 2, 3, 5, 7, 8, 11],
    "hirajoshi": [0, 4, 6, 7, 11],
    "hungarianminor": [0, 2, 3, 6, 7, 8, 11],
    "hungarianmajor": [0, 3, 4, 6, 7, 9, 10],
    "in": [0, 1, 5, 7, 8],
    "insen": [0, 1, 5, 7, 10],
    "ionian": [0, 2, 4, 5, 7, 9, 11],
    "istrian": [0, 1, 3, 4, 6, 7],
    "iwato": [0, 1, 5, 6, 10],
    "locrian": [0, 1, 3, 5, 6, 8, 10],
    "lydianaug": [0, 2, 4, 6, 8, 9, 11],
    "lydian": [0, 2, 4, 5, 7, 8, 9, 11],
    "majorlocrian": [0, 2, 4, 5, 6, 8, 10],
    "majorpenta": [0, 2, 4, 7, 9],
    "minorpenta": [0, 3, 5, 7, 10],
    "melominup": [0, 2, 3, 5, 7, 9, 11],
    "melomindown": [0, 2, 3, 5, 7, 8, 10],
    "mixolydian": [0, 2, 4, 5, 7, 9, 10],
    "neapolitan": [0, 1, 3, 5, 7, 8, 11],
    "octatonic": [0, 2, 3, 5, 6, 8, 9, 11],
    "octatonic2": [0, 1, 3, 4, 6, 7, 9, 10],
    "persian": [0, 1, 4, 5, 6, 8, 11],
    "phrygian": [0, 1, 4, 5, 7, 8, 10],
    "prometheus": [0, 2, 4, 6, 9, 10],
    "harmonics": [0, 3, 4, 5, 7, 9],
    "tritone": [0, 1, 4, 6, 7, 10],
    "ukrainian": [0, 2, 3, 6, 7, 9, 10],
    "whole": [0, 2, 4, 6, 8, 10],
    "yo": [0, 3, 5, 7, 10],
    "symetrical": [0, 1, 2, 6, 7, 10],
    "symetrical2": [0, 2, 3, 6, 8, 10],
    "messiaen1": [0, 2, 4, 6, 8, 10],
    "messiaen2": [0, 1, 3, 4, 6, 7, 9, 10],
    "messiaen3": [0, 2, 3, 4, 6, 7, 8, 10, 11],
    "messiaen4": [0, 1, 2, 4, 6, 7, 8, 11],
    "messiaen5": [0, 1, 5, 6, 7, 11],
    "messiaen6": [0, 2, 4, 5, 6, 8],
    "messiaen7": [0, 1, 2, 3, 5, 6, 7, 8, 9, 11],

    ##############
    # Structures #
    ##############

    "fourths": [0, 4, 10, 15, 20],
    "fifths": [0, 7, 14, 21, 28],
    "sixths": [0, 9, 17, 26, 35],
    "thirds": [0, 4, 8, 12],
    "octaves": [0, 12, 24, 36, 48],
}

Chord inversions

You can write chord/sequence inversions using the ^ syntax:

  • The right hand side of the expression accept any number: ^(1~5).
    • You can also feed negative numbers for inverting a chord downwards.
  • Chord inversions work on anything that is a list. Write custom chords!
@swim
def notes(p=0.5, i=0):
    D('pluck', midinote='C5@maj7^(0~4)', i=i)
    again(notes, p=0.5, i=i+1)

Polyphony

Note polyphony

  • You can use the { and } delimiters to turn any pattern into a polyphonic pattern. Sardine makes a distinction between two types of polyphony:
    • Note polyphony: multiple notes played together.
    • Parametric polyphony: same event played multiple times with different synthesis parameters.
@swim
def poly(p=0.5, i=0):
    D('superpiano', 
        cutoff=500,
        midinote='{D@maj9} {G@maj7^0} {D@maj9} {G@dim7^1}',
        i=i, d=2, r=0.25
    )
    again(poly, p=P('0.5!4 0.25!2', i), i=i+1)

There a some rules to understand with polyphony and polyphonic messages. These rules can sound quite counter-intuitive if you understand polyphony coming from a classical music or score music background.

  • The size of a polyphonic event – meaning the number of messages sent for one occurence of an event - is equal to the length of the largest polyphonic pattern you declared.

In the first example, we have a 4-5 note polyphony. Every polyphonic element from our pattern is a major 9 or 7 chord (e.g [62, 66, 69, 73, 76]). It means that if you have a polyphony of 2 somewhere and a polyphony of 4 elsewhere, your first polyphony will be distributed over the second one:

PATTERN:

1.  [1 2 3 4]
2.  [0 1]

RESULT (POLYPHONY):

1.  [1 2 3 4]
2.  [0 1 0 1]

To illustrate the preceding rule we just talked about, here is a truly bizarre example. Half of our chord is played by a tuned bassdrum, the other half by a piano. Even though this may look odd, this is fully compliant with how parameters are handled by Sardine.

@swim
def poly(p=0.5, i=0):
    D('{[bd  superpiano]}', cutoff=500, midinote='{D@maj9} {G@maj7^0} {D@maj9} {G@dim7^1}', i=i, d=2, r=0.25)
    again(poly, p=Pat('0.5!4 0.25!2', i), i=i+1)

We have two clear alternations, one between the superpiano and bd sound sets, the other between the four or five values that form our chords. It is then natural that half of our polyphony will be composed from a tuned bassdrum and the remaining half from a tuned piano.

Once you get use to this novel way of thinking about polyphonic patterns, you will see that it opens up some space for interesting polyphonic interactions between sounds :) It is currently not possible to limit the number of voices generated by an event. Be careful! It is quite easy to get overrun and to kill your computer playing with polyphony!

Parametric polyphony

Everything can become polyphonic. Just wrap anything between { and } and you will return the same event piled up multiple times with different parameters. It allows you to be very creative with patterns.

@swim
def poly(p=0.5, i=0):
    D('drum:[1 6]', speed='{[1 rand]} {[2 1.9]}', i=i, d=3)
    D('drum:2', cutoff='{[[500:2000,100] 500]}', i=i, d=2)
    D('bd', shape=0.5, i=i, d=4)
    again(poly, p=0.5/2, i=i+1)

Names

The Sardine Pattern Language is using names for multiple things:

  • audio sample and synthesizer names like kick or sawsynth.
  • variable naming in the context of amphibian variables or internal variables.
  • addresses, a special type of composed name used for OSC addressing.
@swim
def names(p=0.5, i=0):
    D('bd pluck bd pluck:(2+4)', i=i)
    again(names, p=0.5, i=i+1)

A single letter (if it's not already a note name or a known symbol) can be considered as a name. Be careful! There are a few hidden rules for names:

  • Names can be one letter long but some letters are already taken.
  • Names cannot begin with a number.
  • Names can't contain any special symbol like _ or ^.

Sample association

In the context of audio samples, appending a number to a sample using the association operator (:) will complete that name and refer to the nth sample in the folder you are designating by the name. For a synthesizer, the behavior is a little different. It will pitch the synth in semitones.

dada:0 # first sound in the dada folder
dada:1 # second sound in the dada folder

Note: when indexing on sample names, the number can be infinitely high. It does not matter. The indexing will simply loop around and select a valid sample number!

Synthesizer name and numbers

The same operator (:) used on synthesizers can allow you to directly play a note in a pattern:

Pa * d('superpiano:[0 7 0 5]')
  • These are not MIDI note values but absolute values around C4.
    • Use the octave or oct parameter to change the base octave.
  • Silences are also supported (.)

This mechanism can be used as a fast way to write simple looping melodies.

Addresses

Addresses are not really common until you play a lot with custom OSC messages.

  • Addresses are names containing one or multiple / separators just like any hierarchical address on the web, UNIX systems, etc...
  • If using the send_raw function in conjunction with OSC, the syntax differs:
    • prepend your address with an additional /.
O("an/address another/address", value=1, other_value=2)

Operators

Python is limited to a set of operators and you can't even create your own. Operators are great for typing speed Sardine is a live coding library. It is faster to write !! than to use a function named repeat_and_copy. That's why having a pattern language is important:

  • new operators to work on lists and note collections, very common in a musical context.
  • new behavior for well known operators such as the addition, the multiplication, etc...
  • new operators for working on custom objects : sample names, addresses, etc...

Silence

Everyone loves silence, especially when you spent the last two hours patterning a duck sample.

Event-based silence

  • You can use a dot (.) inside any pattern to denote a silence.
@swim
def silence_demo(p=0.5, i=0):
    D('bd ...', i=i, d=1)
    D('hh . hh ..', i=i, d=1)
    again(silence_demo, p=1/8, i=i+1)

Be careful, the concept of silence in Sardine can be pretty confusing. Silence is a very important and complex topic. Adding silences is a great way to generate interesting patterns. Silences are different for each sender because silence doesn’t have the same meaning for a sampler, a MIDI output or an OSC output (D(), N(), etc.).

  • D(): a silence is the absence of a sample. The event will be skipped.
  • N(): a silence is the absence of a note. The event will be skipped.
  • any OSC based Sender: a silence is the absence of an address. The event will be skipped.

Parametric silence

What about the other patterns that live alongside the main pattern? A silence in an auxilliary pattern will cause a parametric silence to happen. Take a look at the following example:

@swim
def silence_demo(p=0.5, i=0):
    D('sitar', legato='0.5', speed='[1:4] .!8', i=i, d=1)
    again(silence_demo, p=1/8, i=i+1)

In this example, we always include the sample name in our pattern: sitar. There is no real silence, only a parametric silence used in the pattern used for the speed parameter.

A parametric silence will cause the pattern to search its last value and hold it. Sardine will backtrack and search the last value that could have been generated by the pattern. The result of the speed parameter will then be [1 2 3 4 8 8 8 8 8 8 8 8].

For people familiar with modular synthesizers and analog circuits, this is pretty much the same thing as a sample & hold mechanism. Note that it is impossible to write a parametric silence composed only of silences. A pattern of nothing is not a pattern. It will crash, and rightfully so.

Choice

The pipe operator | can be used on to generate a 50/50% choice operation between two tokens. You can also chain them: 1|2|3|4. The behavior of chaining multiple choice operators has not been clearly defined. The distribution might not be the one you expect.

@swim
def choosing stuff(p=0.5, i=0):
    D('bd|pluck', speed='1|2', i=i)
    again(choosing_stuff, p=0.5, i=i+1)

Range

If you want to generate a number in the range x to y included, you can use the ~ operator. This operator will adapt to context (integer or floating point number). It can be used as an alternative to rand for scaled randomisation.

@swim
def ranges(p=0.5, i=0):
    D('pluck|jvbass', speed='1~5', i=i)
    again(ranges, p=0.5, i=i+1)

People often forget about this one even though it is way shorter than rand.

Ramp

This operator is analogous to Python's range() function, only better.

  • Generate a ramp using the [1:10] syntax: [1 2 3 4 5 6 7 8 9 10].
  • Ramps can go up and down, and even go both directions (pal [1:10]).
  • You can also specify a step amount: [1:10,2]: [1 3 5 7 9].
    • also works with floating point numbers: [1:10,0.5].
  • Or alternatively a fixed number of steps: [0:10;5]: [0 2.5 5 7.5 10].
@swim
def ramps(p=0.5, i=0):
    D('amencutup:[0:10]',
      room='[0:1,0.1]',
      cutoff='[1:10]*100', i=i)
    again(ramps, p=0.5, i=i+1)

Repeat

The ! operator inspired by TidalCycles is used to denote the repetition of a value. You need to add a number or a list to its right side.

@swim
def repeat_stuff(p=0.5, i=0):
    D('pluck|jvbass', speed='1:2', n='C4!4 E4!3 E5 G4!4', i=i)
    again(repeat_stuff, p=0.5, i=i+1)

Slicing

The Sardine pattern notation is built around the idea of having multiple ways to deal with linear lists and collections. The basic arithmetic syntax and most operators work on single tokens but will also work on lists. It means that you can write expressions such as:

[0 1 2 3]%8
[0 2 4 5]*[4 5]
[1:8 0.1]&[2 9]
[0 2 4 5 9 10 12 14]!2
[0 2 4 5 9 10 12 14]!!4

There are a few special operators that are only available when you deal with lists. This is something you will get familiar with by trying. You will see that most things work while some will not yield the result you expect.

Slicing and indexing

@swim
def test_slicing(p=0.5, i=0):
    pattern = P('[1 2 3]&[1]') # change me
    print(pattern)
    again(test_slicing, p=0.125, i=i+1)
  • You can get a slice or just one value from a list by using the special & operator.
  • It will work with any list on the right side of the operator but it will only take the first and second value of it no matter what to compose a slice.
  • The index value can be infinite because the index is looping on the list. You can feed a random number generator and get something out.

On the down side, slicing can become quite complex to write, so be careful with it:

@swim
def test_slice(p=0.5, i=0):
    D('pluck:19', legato=0.2,
      n='[60 62 63 67 69 71]^(1~5)&[1~4]', i=i)
    again(test_slice, p=0.125, i=i+1)

Extend

You can extend a list by calling the ! operator on it. This will repeat the list x times. Some people confuse this operator with its bigger brother, !!, called extend-repeat.

@swim
def test_extend(p=0.5, i=0):
    D('pluck:19', legato=0.2, midinote='[60 62]!2', i=i)
    again(test_extend, p=0.125, i=i+1)

Extend-Repeat

Use the !! operator to repeat each element of a list x times. Beware of the confusion with it's little brother, !. These two operators work hand in hand!

@swim
def test_extend_repeat(p=0.5, i=0):
    D('pluck:19', legato=0.2,
    midinote='[60 62 63]!!3', i=i) #note the repetition of values within the list
    again(test_extend_repeat, p=0.125, i=i+1)

Logic

AND (union)

Using &&, you can combine two lists using an element-wise AND. If one of the operand is shorter than the other, it is repeated until it reaches the length of the longest one. Works best with 1s, 0s, and rests. If two "falsy" or two "truthy" elements are being compared, the one from the left operand is kept, otherwise the "falsy" element among the two is kept.

Pa * d('(set pk [1 . . .]) * kick')
Pb * d('(set ps [.!4 1 .!2 1]) * sd')
Pd * d('(get pk) && (get ps) * bleep')  # outputs [1 ... 1 .. 1]

OR (intersection)

Using ||, you can combine two lists using an element-wise OR. If two "falsy" elements are being compared, the one from the left operand is kept, otherwise, the leftmost "truthy" is kept.

Pa * d('(set pk [1 . . .]) * kick')
Pb * d('(set ps [.!4 1 .!2 1]) * sd')
Pd * d('(get pk) || (get ps) * bleep')  # outputs [.... 1 ...]

OR (exclusive disjunction)

Using ^|, you can combine two lists using an element-wise OR. If two "truthy" or two "falsy" elements are being compared, a rest is returned, otherwise the truthy element is returned.

Pa * d('(set pk [1 . . .]) * kick')
Pb * d('(set ps [.!4 1 .!2 1]) * sd')
Pd * d('(get pk) ^| (get ps) * bleep')

Function Library

The Sardine Pattern Language is a real programming language. It supports function calls with arguments and keyword arguments just like Python and you can combine multiple functions to create complex patterns.

Calling a function is really simple given that you know the syntax to do so. The syntax functions as follows:

  • a function starts with a name wrapped in-between two parentheses: (sopr 1 2 3 4), (time), (bar).
  • Functions can take any amount of arguments. Some of them will have fixed positional arguments.
    • any amount: (func 1 2 3 4 5 6 ...)
    • positional: (func 1 2) where 1 and 2 are required and other arguments get discarded.
  • Some functions take keyword arguments. They are written using the ::keyword form: (disco C4 D4 E4 ::depth 2).

The documentation is the best place to learn about functions. Your code editor won't help you because the Sardine Pattern Language is written as strings and is not recognized as code by any autocompletion engine or plugin.

I will always provide indications on how to call the function, telling you the role of each argument.

  • ... means that the function can take any number of arguments.
  • otherwise, the function will take a fixed number of arguments whose name will be given.

Variable Functions

This set of functions is handling all the variables available in the Sardine Pattern Language. You will find functions used to set the value or get the value of amphibian variables, internal variables and also some other important variables such as the currently selected global scale.

set (s)

Assign a variable to a name in a global namespace shared by all the SPL expressions. The variable can be anything: a number, a name, a list, etc... The function also returns the value, allowing it to be re-used immediately for the current pattern. This function is of paramount importance. You can use it to write reactive patterns, where multiple patterns share the same data.

Arguments:

  • name: variable name. Can be any valid name.
  • value: a single value (int, float, name, list).

Example:

# Change (set a ...) to update multiple patterns at once
Pa * d('kick', speed='(set imp [1 3 4 9 2 4])') # Setting some numbers in the variable "imp"
Pb * d('hat:(get imp)', speed='(get imp)')      # Getting these numbers in many other patterns

get (g)

Get a variable associated to a name. This function is the second part of the get/set mechanism. You can retrieve any value currently associated to a name. If no name-value is found for that name, the value 0 will be returned in an attempt to prevent crashes.

Arguments:

  • name: variable name.

Example:

Pz * d('clap', room='(get roomy)', crush='(get globalcrush)')

setA (sa)

This function is part of the amphibian variables mechanism. In a similar fashion to set, this function can be used to set the value of the associated amphibian variable. There is currently 26 amphibian variables you can use, one for each letter of the latin alphabet in lowercase. Just like set, you can set a value and still return its value for later usage.

Arguments:

  • variable name: name of the amphibian variable to get.

Example:

(setA b 20)
(sa b 20)

getA (ga)

  • variable name: name of the amphibian variable to set. Arguments:

Example:

V.n = [52, randint(40, 60), 72, 35]
Pa * d('supersaw', n='(ga n)', p=0.75 )

setscl

The Sardine Pattern Language always remember the name of a global scale that the user can set. It defaults to major, taken from the list of possible qualifiers. You can choose any scale from that list as the default scale for all patterns using scl, thus the name of this very function.

Arguments:

  • scale_name: valid scale from the qualifiers list. If the name doesn't exist, will default to major.

Example:

P('(setscl minor)')
P('(setscl acoustic)')
P('(setscl min9)')

Time Functions

This type of functions is all about returning information about the current time. This timing information can either refer to the wall clock (the real world time) or to the Sardine Clock (internal representation of time). It will sometimes refer to musical time, sometimes to 'time as we know it'.

time (t)

This function returns wall clock time, spanning from the current year down to microseconds. It takes zero or one argument, the type of time you want to see returned.

Arguments:

  • data: year, month, day, minute, second, micro. If data is not given, will return the current internal time of the Sardine clock.

Example:

P('(time year)')

bar (b)

This function returns current the current bar (as integer).

Arguments:

  • None

Example:

P('(bar)')

phase (p)

This function returns current the current phase (as float).

Arguments:

  • None

Example:

P('(phase)')

beat (b)

This function returns current the current beat (as float).

Arguments:

  • None

Example:

P('(beat)')

unix (u)

This function returns current the current Unix Time (as integer) because why not! It can be used as a cool random number generator or as an incrementer.

Arguments:

  • None

Example:

P('(unix)')

Math Functions

Simple mathematical functions that can be applied on any numeric expression. They are very often the typical generic operations that you can find on digital calculators:

sin

Apply the sinus function to all the provided arguments.

Arguments:

  • ... (any number of arguments)

Example:

(sin 1 2 3)
(sin time)

usin

Unipolar version of the sin function.

Arguments:

  • ... (any number of arguments)

Example:

(usin 1 2 3)
(usin time)

cos

Apply the cosinus function to all the provided arguments.

Arguments:

  • ... (any number of arguments)

Example:

(cos 4 5 6)
(cos bar)

ucos

Unipolar version of the cos function.

Arguments:

  • ... (any number of arguments)

Example:

(cos 4 5 6)
(cos bar)

tan

Apply the tangent function to all the provided arguments.

Arguments:

  • ... (any number of arguments)

Example:

(tan (abs -0.25))
(tan (sin (time)))
(tan 2)

saw

Sawtooth wave generator, in a range from -1 to 1.

Arguments:

  • ... (any number of arguments)

Example:

(saw (time))
(saw (phase))/4

usaw

Unipolar variant (from 0 to 1) of the saw function.

Arguments:

  • ... (any number of arguments)

Example:

(usaw (time))
(usaw (phase))/4

rect

Square wave generator, in a range from -1 to 1. The pulse width can be modulated using a special argument.

Arguments:

  • ... (any number of arguments)
  • pwm: pulse width modulation (0 to 1).

Example:

(rect (time))
(rect (phase))/4

urect

Unipolar variant (from 0 to 1) of the rect function. The pulse width can be modulated using a special argument.

Arguments:

  • ... (any number of arguments)
  • pwm: pulse width modulation (0 to 1).

Example:

(urect (time))
(urect (phase))/8

abs

Returns the absolute value of all the provided arguments.

Arguments:

  • ... (any number of arguments)

Example:

(abs [1:-5, 1])
(abs -10)

max

Returns the maximum value in all the numbers provided as argument.

Arguments:

  • ... (any number of arguments)

Example:

(max 1 2 3)
(max [rand rand rand rand])

min

Returns the minimum value in all the numbers provided as argument.

Arguments:

  • ... (any number of arguments)

Example:

(min 1 2 3)
(min [rand rand rand rand])

mean

Returns the mean of all the numbers provided as argument.

Arguments:

  • ... (any number of arguments)

Example:

(mean 1.5 3 2 10.4)

scale

Scale a number z from the range x y to the range x1, y1.

Arguments:

  • None

Example:

(scale (bar) 0 4 0 10)

clamp

Clamp a value a in between b and c. This means that the number a will be limited and range and will never be able to be set lower than b or higher than c.

Arguments:

  • None

Example:

(clamp 1000 0 127) # -> returns 127

drunk

The drunk walk function will return +1 or -1 to any provided integer with a 50% chance for either of them. It is frequent to see this function used for writing generative melodies or parameter trajectories. This version of drunk also features a keyword argument to specify by how much a step should increment or decrement.

Arguments:

  • span: increment span, from 0 to x.

Example:

(drunk (get a) ::span 4) # -> returns 127

More examples and applications

@swim
def demo(p=1/4, i=0):
    D('moog:5', lpf='(sin (time)*2500)', res='(cos (time))/2', i=i, legato=0.1)
    D('cp', speed='0+(abs -rand*5)', d=8, i=i)
    again(demo, p=1/8, i=i+1)

These functions are the bread and butter of a good high-speed Sardine pattern. They will allow you to create signal-like value generators (e.g Low frequency oscillators). They are also very nice to use in conjunction with (time) or any time function. You will find many creative ways to use them (especially by combining with arithmetic operators).

Rhythm Functions

This set of functions is all about generating and manipulating rhythms. There are different ways to generate rhythms using Sardine. The system is quite permissive and will allow you to do a lot of things either by manipulating the period argument, by filtering some of your events, by adding silences, etc... This variety of approaches is also reflected in the different techniques for applying rhythm generation functions.

euclid (eu)

This function generates euclidian rhythms by filtering a list of events, replacing some of them by silences. If you take the basic sample hat and apply the function by running (eu hat 5 8), you will get the following pattern: 'hat . hat . hat hat . hat'. As you can see, some events have been removed to generate an euclidian rhythm on the event side. There are other algorithms such as numclid to generate real temporal euclidian rhythms. This implementation of the euclidian rhythms also supports the typical rotation parameter to shift the pattern around.

This function is the bread and butter of many musicians. Spend some time to learn how to use it!

Arguments:

  • element: element to apply the euclidian rhythm (list, sample name, etc.)
  • hits: how many steps to evenly distribute in the total number of steps.
  • steps: total number of steps.
  • rotation: pattern rotation.

Example:

Pa * d('(eu kick 5 8)', p=.5)
Pb * d('(eu snare 2 8)', p=.5)
Pc * d('(eu hat 7~8 8)', p=.5)
Pd * d('(eu tom 1~2 8 1)', p=.5)

neuclid (neu)

If euclidian rhythms are a thing, then you can do the opposite and generate the negative of any given euclidian rhythm. This is a super frequent operation that you can do to play on the silent steps of an euclidian rhythm.

Arguments:

  • element: element to apply the euclidian rhythm (list, sample name, etc.)
  • hits: how many steps to evenly distribute in the total number of steps.
  • steps: total number of steps.
  • rotation: pattern rotation.

Example:

Pa * d('(eu kick 5 8)', p=.5)
Pb * d('(neu snare 2 8)', p=.5)
Pc * d('(eu hat 7~8 8)', p=.5)
Pd * d('(neu tom 1~2 8 1)', p=.5)

numclid (e)

The algorithms described above (euclid and neuclid) works by filtering some items in a total number of items. This approach is highly unconventional, even for algorithmic musicians! This new implementation of euclidian rhythms is working differently. It will generate durations that you can use in your period argument instead of losing some events. This is what you typically want when you think about euclidian rhythms. The arguments are similar.

Arguments:

  • hits: how many steps to evenly distribute in the total number of steps.
  • steps: total number of steps.
  • rotation: pattern rotation.

Example:

# We are changing rhythms now!
Pa * d('kick', p='(e 5 8)/2')
Pb * d('snare', p='(e 2 8)/2')
Pc * d('hat', p='(e 7~8 8)/2')
Pd * d('tom', p='(e 1~2 8 1)/2')

binary rhythm (br)

This function is a binary rhythm generator algorithm. Give it any number and this number will be converted to its binary representation. Using this intermediate representation, the number will then be turned into a rhythm using a special algorithm (look at the source if you are interested in the finicky details). You can also rotate the binary number to get more rhythms from the same initial number.

Arguments:

  • number: a number to transform into a binary rhythm.
  • rotation: rotation applied to the binary intermediate representation.

Example:

# Let's take a simple number and generate rhythms from its rotation
Pa * d('kick', p='(br 20 0)/4')
Pb * d('snare', p='(br 20 1)/4')
Pc * d('hat', p='(br 20 2)/[4 2 8!2]!!8')
Pd * d('openhat', p='(br 20 3)/4')

notdot (rest inversion)

The notdot value is used to reverse the rhythm of a pattern you are currently using. In the example above, Pa is playing a rhythm by alternating between kick and silence (.). To get the opposite of that list, the notdot function will replace the 1 by silences and the silences by 1. It can be quite useful to get complementary and matching rhythms.

Arguments:

  • collection a collection/list containing silences and values
Pa * d('(set pk [1 . . .]) * kick')
Pd * d('(notdot (get pk))  * hat')  # outputs [. hat hat hat]

Filter Functions

There is a group of functions used for filtering: removing events, filtering values, etc... These functions are very low-level but they can be really interesting for specific use cases.

mask

Apply a boolean mask on values from the collection. True values will return the value itself, others will return a silence. This function is used by others for many different operations.

Arguments:

  • collection: list to be masked by the boolean pattern.
  • bool_pattern: boolean pattern to apply on the collection.

Example:

# Filtering the hihats using a boolean pattern
Pa * d('(mask [kick hat snare hat] [1 0 1 0])', p=.5)

vanish

Vanish will make x% of your pattern vanish magically. It is similar to other functions like degrade in TidalCycles. Every element from your pattern will be returned (or not) based on a probability.

Arguments:

  • collection: a list containing a pattern to vanish.
  • percentage: how much the pattern should be degraded

Example:

Pa * d('(vanish [kick hat snare hat] 60)', p=.5')

filt

This function can be used to filter some values from a pattern. Imagine generating a 50 elements long list but being stuck with values you would like to get rid of. Using this function, you can remove one or many elements at once.

Arguments:

  • collection: the collection you would like to filter.
  • filter: a list containing the elements to filter

Example:

# Removing 2 and 4 from the generated ramp
Pa * d('hat', p=.25, speed='(filter [0:10] [2 4])')

filtdot

This function can be used to filter out some values from a pattern, replacing them with silences (.).

Arguments:

  • collection: the collection you would like to filter.
  • filter: a list containing the elements to filter

Example:

# Replacing 2 and 4 By silences
Pa * d('hat', p=.25, speed='(filtdot [0:10] [2 4])')  # Equivalent to speed='0 1 . 3 . 5 6 7 8 9 10'

keepdot

This function does the opposite of filtdot: only the values that are in the given filter are kept.

Arguments:

  • collection: the collection you would like to filter.
  • filter: a list containing the elements to keep

Example:

# Replacing 2 and 4 By silences
Pa * d('hat', p=.25, speed='(keepdot [0:10] [2 4])')  # Equivalent to speed='. . 2 . 4 . . . . . .'

Melody Functions

scl

This function will map any number of numeric arguments to the global melodic scale (defaulting to major). You can use the setscl function to set a new global scale. This is the easiest and fastest way to generate pleasant melodies with Sardine without having to worry about scales/modes, etc.. This is similar to what most beginner friendly music environments are offering to their users.

Arguments:

  • ...: list of notes to put on the scale. If the number is higher than the number of notes in the scale, it wraps around and octaves up. Same thing happens if you use negative numbers.
  • octave: optional octave modifier where 0 returns the scale note without any octaviation, and up.

Example:

Pa * d('supersaw', n='C + (scl 0 2 4 0 3 5)', p='(e 5 8 2)/2')

disco

The disco function is a joke. It was initially supposed to mimick the typical disco cliché of playing every note in alternating octave patterns. The name of this function is so easy to remember that it is now used as the default function for explaining people how functions work in the Sardine Pattern Language.

Arguments:

  • ...: notes to disco.
  • depth: depth for the disco effect. Negative number will jump up octaves. Positive number will jump down octaves.

Example:

Pa * d('pluck', n='(disco C Eb G C)', p='.5')
Pa * d('pluck', n='(disco C Eb G C ::depth -2)', p='.5')

quant

Quantize a given list of arguments to the nearest value in the provided set. This set can be any list of numbers or can also be a scale or any other list object that you want to use as a quant reference. The quantized values are clamped in the range of MIDI note values (from 0 to 127).

Arguments:

  • collection: the collection/list that you want to quantize.
  • reference: the reference collection/list to quantize to.

Example:

(quant [1 2 3 4] maj7) # quantize on a major seventh chord

expand

Chance-based operation. Apply a random octave transposition process to every note in a given collection. There is an optional factor parameter that multiplies the octave transposition.

Arguments:

  • collection:
  • factor: multiplicator for the expansion amount.

Example:

Conditions

Every function in the Sardine Pattern Language can be applied conditionally. To do so, use the special cond keyword argument. This keyword is available for every function. There are many types of conditions you can apply on your pattern and you can even create your own types of conditions.

There is no True or False statement in SPL. Conditions are created by using binary values:

  • 1 represents truth: True.
  • 0 represents false: False.

The function above will turn the sequence into a palindrome but only on beats 1 and 3:

Pa * d('(pal F A C E ::cond (beat 0 2))')

Of course, the rabbit goes down and you go after it. Any condition can receive conditions, etc...

If condition

The if condition is a binary condition: it will execute something if true (1), something else if false (0). In the following example, the function will play a clap for every pair bar (2, 4, etc.) and another sample on every odd bar.

Pa * d('(if (every 2) cp sid')

The nif function can be used to reverse the logic (not if) just by typing one letter Saving 20 milliseconds is important because life is too short.

While condition

The while condition is an unary condition: it will execute something if true (1), nothing at all if false (0). In the following example, the function will sometimes play a clap:

Pa * d('(while rand*5>2 cp)')

In that specific case, as a demonstration, we craft our own condition by using the greater-than (>) operator. In the semblance of if and nif, there is also a nwhile condition to reverse the condition logic.

Special condition functions

Some functions from the library can be used to build more complex conditions in conjunction with if or while. Use them wisely:

Function name Arguments Description Result
beat ... numbers 1 or more beats numbers to match Boolean
phase low high Check if currently in-between range of phase (between 0.0 and 1.0) Boolean
every ... bar_numbers Similar to TidalCycles every function. Will be true every x bars. Boolean
proba probability Simple probability in % Boolean
modbar choice faces Simulation of a dice with n faces. Boolean
obar None Check if the current bar is odd Boolean
ebar None Check if the current bar is even Boolean
modbar modulo Modulo against current bar number Boolean

The beat function is a great to start with because it is also very simple. Let's play a clap on the start of the bar:

Pa * d('(if (beat 0) cp)')

Let's add another beat to the equation. It will now also play on the following beat of the bar:

Pa * d('(if (beat 0 1) cp)')

You get it. Other functions are working in a similar fashion. These functions are cool but you might have guessed already that you can craft your own functions yourself if you are clever enough :) If you think of some cool functions to add, I'll be more than happy to include them into Sardine :)

Combinatorics

This category of functions is containing many functions that perform many simple but important operations on lists.

rev

Reverse a list.

  • Arguments:
    • None

Example:

(rev 1 2 3 [1 2 3])

rot

Rotates a list. This will move the n first steps at the end of the list.

  • Arguments:
    • n: How many steps to rotate by

Example:

(rot [1 2 3] 1) # gives [2 3 1]

pal

Creates a palindrome. This will keep the list as is but will also append the list in reverse.

  • Arguments:
    • cut: whether to repeat the last/first value of reversed list or not.

Example:

(pal [1:10] ::cut 1) # or ::cut 0

shuf

Shuffle any list.

Description ...

  • Arguments:
    • None

Example:

(shuf 1 2 3 4 5)

leave

Braid multiple lists of uneven length.

  • Arguments:
    • ...: feed multiple lists to braid them together.

Example:

(leave [1 2 3] [3 4 5] [1 2] [4 8 6 4])

insertp

Insert a fixed element as pair element of each list.

  • Arguments:
    • None

Example:

insert

Insert a fixed element as odd element of each list.

  • Arguments:
    • None

Example:

insertprot

Description ...

  • Arguments:
    • None

Example:

insertrot

aspeed

Low Frequency Oscillators

These functions are implementing low-frequency oscillators whose period is based on a selectable number of clock beats. They offer another better flavor to the basic technique that works by manually calculating stuff using sin() or cos() functions.

lsin

This is a bipolar (-1 to 1) sinusoïdal low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.

Example:

(lsin 4)

ltri

This is a bipolar (-1 to 1) triangular low frequency oscillator that you can use for modulations in any pattern. Arguments:

  • period: frequency expressed in clock beats.

Example:

(ltri 4)

lsaw

This is a bipolar (-1 to 1) sawtooth low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.

Example:

(lsaw 4)

lrect

This is a bipolar (-1 to 1) rectangular low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.
  • pwm: pulse width modulation (number between 0 and 1).

Example:

(lrect 4 0.2)

ulsin

This is an unipolar (0 to 1) sinusoïdal low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.

Example:

(ulsin 3)

ultri

This is an unipolar (0 to 1) triangular low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.

Example:

(ultri 3)

ulsaw

This is an unipolar (0 to 1) sawtooth low frequency oscillator that you can use for modulations in any pattern.

Arguments:

  • period: frequency expressed in clock beats.

Example:

(ulsaw 3)

What to do with Low Frequency Oscillators?

Remember that you can do math operations on these oscillators such as clamping (clamp), scaling (scale), etc. You can also pattern the period or pwm for extra weirdness or for doing custom shapes. Folding and wrapping operations can be very useful to generate interesting shapes on a large time scale.

Modulations are extremely important to get dynamic sounding patterns.

Ziffers

Ziffers is a generative numbered musical notation (aka. Ziffersystem) and a live code golfing framework that makes composing melodies and rhythms easier and faster. It has been recently integrated to Sardine as an effort to make it a multi-paradigm live coding system.

Presentation

Pitch

Basic syntax

Ziffers is a numbered musical notation and live code golfing language. Melodies are written using numbers 0-9 or characters 'T' 'E' which can be used in chromatic scale (T=10, E=11).

By default melodies are played in C Major. Examples:

# Play 0 1 2 3 sequentially, in key of C its C D E F:
@swim
def z(p=0.5, i=0):
    ZD('superpiano','0 1 2 3', i=i)
    again(z, p=1, i=i+1)

# Play 0 and then chord 023:
@swim
def z(p=0.5, i=0):
    ZD('superpiano','0 023', i=i)
    again(z, p=1, i=i+1)

# Take pattern durations to account
@swim
def z(p=0.5, i=0):
    dur = ZD("superpiano","0.25 0 3 0.125 4 6 3 1", i = i) # Returns length of the pattern in beats
    again(z, p=dur, i=i+1)

# Loop the chromatic scale:
Pa * zd('superpiano', '0 1 2 3 4 5 6 7 8 9 T E', scale='chromatic') 

# Loop some notes and chords in D Minor:
Pa * zd('superpiano', '0 023 3 468', key='D', scale='minor')

Scales

Ziffers supports a great number of scales. The list of supported scales is superior to 1000:

...
"Aeradyllian": [1, 1, 1, 1, 1, 1, 1, 2, 2, 1],
"Ryptyllian": [1, 1, 1, 1, 1, 1, 2, 2, 1, 1],
"Loptyllian": [1, 1, 1, 1, 1, 2, 2, 1, 1, 1],
"Kataphyllian": [1, 1, 1, 1, 2, 2, 1, 1, 1, 1],
"Phradyllian": [1, 1, 1, 2, 2, 1, 1, 1, 1, 1],
"Dagyllian": [1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
"Katyllian": [1, 2, 2, 1, 1, 1, 1, 1, 1, 1],
"Gothyllian": [2, 1, 2, 1, 1, 1, 1, 1, 1, 1],
"Lythyllian": [1, 2, 1, 1, 1, 1, 1, 1, 1, 2],
"Bacryllian": [2, 1, 1, 1, 1, 1, 1, 1, 2, 1],
"Aerygyllian": [1, 1, 1, 1, 1, 1, 1, 2, 1, 2],
"Dathyllian": [1, 1, 1, 1, 1, 1, 2, 1, 2, 1],
"Boptyllian": [1, 1, 1, 1, 1, 2, 1, 2, 1, 1],
"Bagyllian": [1, 1, 1, 1, 2, 1, 2, 1, 1, 1],
"Mathyllian": [1, 1, 1, 2, 1, 2, 1, 1, 1, 1],
"Styptyllian": [1, 1, 2, 1, 2, 1, 1, 1, 1, 1],
"Zolyllian": [1, 2, 1, 2, 1, 1, 1, 1, 1, 1],
"Staptyllian": [2, 1, 1, 2, 1, 1, 1, 1, 1, 1],
"Danyllian": [1, 1, 2, 1, 1, 1, 1, 1, 1, 2],
"Goptyllian": [1, 2, 1, 1, 1, 1, 1, 1, 2, 1],
"Epocryllian": [2, 1, 1, 1, 1, 1, 1, 2, 1, 1],
...

To get a complete list of scales, see this file.

Examples:

Pa * zd('superpiano', 'q 0 1 2 3') # Defaults to 'major'
Pa * zd('superpiano', 'q 0 1 2 3', scale= 'minor') # minor scale
Pa * zd('superpiano', 'q 0 1 2 3', scale='rocitronic') Another one

Sharp and flat

  • b is flat
  • # is sharp

Use sharps or flats to go off scale. Sharps and flats are not sticky so you have to use it every time before the note number. For example in key of C: #0 = C#

@swim
def z(p=0.5, i=0):
    ZD('superpiano','0 4 #4 b0 2 #2 ##2 ###2', i=i)
    again(z, p=1, i=i+1)

Note lengths

Default note length is a whole note q, which means 1.0 beats of sleep after the note is played. Note lengths can be changed with characters or with decimal notation.

Most common note length are:

  • w = Whole (Semibreve) = 1.0 = 4.0 beats
  • h = Half (Minim) = 0.5 = 2.0 beats
  • q = Quarter (Crotchet) = 0.25 = 1.0 beats
  • e = Eight (Quaver) = 0.125 = 0.5 beats
  • s = Sixteenth (Semiquaver) = 0.0625 = 0.25 beats

Note lengths can be defined for all following notes or for single notes by grouping the note length characters. For example note lengths for Twinkle twinkle little star could be notated in various ways. Default length is q so in this case it is not required to define the length at the start. Note lengths can also be grouped, like h4, which means it only affects the given pitch. Alternatively decimals can be used, here in the middle 0.5 is used and then changed back using 0.25 for the next pitch. Alternatively decimal durations can be grouped using <0.5>1 notation:

# Twinke twinkle little star
Pa * zd('superpiano','0 0 4 4 5 5 h4 3 3 2 2 1 1 h0 4 4 3 3 2 2 0.5 1 0.25 4 4 3 3 2 2 <0.5>1 0 0 4 4 5 5 h4 3 3 2 2 1 1 h0')

More info about note lengths in Rhythms section.

Rhythm

Rest or silence

Use r to create rhythm with musical rest in the melodies. r can be combined with note length, meaning it will sleep the length of the r, for example:

# Play quarter note 1 (D) and then sleep half note and then play half note 2 (E)
@swim
def z(p=1, i=0):
    dur = ZD("superpiano","q 0 e3 qr e 2 4 r 1", i = i) # Returns length of the pattern in beats
    again(z, p=dur, i=i+1)

Dotted notes

. for dotted notes. First dot increases the duration of the basic note by half of its original value. Second dot half of the half, third dot half of the half of the half ... and so on. For example dots added to Whole note w will change the duration to 1.5, second dot w.. to 1.75, third dot to 1.875.

# Row row row your boat using dotted notes
@swim
def z(p=1, i=0):
    dur = ZD("superpiano","q. 0 0 | q0 e1 q.2 | q2 e1 q2 e3 | h.4 | e 7 7 7 4 4 4 2 2 2 0 0 0 | q4 e3 q2 e1 | h. 0 ", i = i) # Returns length of the pattern in beats
    again(z, p=dur, i=i+1)

Subdivision

Subdivision notation divides the previous note length to equal proportions and can be used to create complex patterns:

# Subdivided from 0.25 by default
Pa * zd('superpiano', '[4 2 4 2] [4 5 4 2] [3 1 3 1] [3 4 3 1] [4 2 4 2] [4 5 4 2] 4 [4 3 2 1] 0')
# Note length for the next subdivision can be defined using characters or decimals
Pa * zd('superpiano', 'w [1 2 3 4] 0.5 [1 2 3 4] q [1 2 3 4] w [1 2[3 4]] h [ 1 [ 2 [ 3 [ 4 ]]]]')

Triplets

Triplets can be defined using note characters or by list notation, for example:

# Triplets with note characters
Pa * zd('superpiano', 'q 2 6 a 1 3 2 q 5 1 a 4 3 2')

# Triplets with list notation
Pa * zd('superpiano', 'q 2 6 h [1 3 2] q 5 1 h [4 3 2]')

Ties

Ties can be created using multiple note length characters. Tied note lengths are summed up for the next degree, for example:

# q+e=0.375
Pa * zd('superpiano', 'q 0 qe2 3 4 qe 3 q 4')

List of all note length characters

CharacterNote lengthNote name (US)Note name (UK)Ticks
m8.0MaximaLarge15360
k5.333Triplet longTriplet large10240
l4.0LongLonga7680
p2.667Triplet wholeTriplet longa5120
d2.0Double whole noteBreve3840
c1.333Triplet wholeTriplet breve2560
w1.0Whole noteSemibreve1920
y0.667Triplet halfTriplet semibreve1280
h0.5Half note Minim960
n0.333Triplet quarterTriplet minim640
q0.25Quarter noteCrotchet480
a0.167Triplet 8thTriplet crochet 320
e0.1258th noteQuaver240
f0.083Triplet 16thTriplet quaver160
s0.062516th noteSemiquaver120
x0.042Triplet 32thTriplet semiquaver80
t0.03132th noteDemisemiquaver60
g0.021Triplet 32thTriplet demi-semiquaver40
u0.01664th noteHemidemisemiquaver30
j0.0078Triplet 128thTriplet Hemidemisemiquaver15
o0.00416128thSemihemidemisemiquaver8
z0.0No lengthNo length0

Note that i, v (chords), r (rest) and b (flat) are exceptions to a rule that all lower letters are note lengths!

Chords

Chords can be played using groups of numbers "024 357" or using roman numerals: i ii iii iv v vi vii.

"[: i vi v :]" # Play chords as a sequence using default chord length of 1

"q [: iv 1 2 3 iii 2 3 4 ii 4 3 2 i 1 2 3 :]"

In major scale (or any other diatonic) roman numerals would correspond to these pitch classes:

i = 024
ii = 135
iii = 246
iv = 357 or 35^0
v = 468 or 46^1
vi = 579 or 5^0^2
vii = 68{10} or 6^1^3

Chord key is assigned with the key keyword argument (defaults to major).

Chord names

Chord names are nice shorthands for similar group of notes. Define chord name using using ^ with the name or by using chord_name parameter. Chord names work for minor, major and chromatic scales. Other than that who knows? Not me. Some might work and others might sound a bit funny.

Examples using the chord names:

"i vi^dim"
"i vi"
"i vi^m11+"
"i^7"
"i^maj9" 

Chords can also be inverted using % char, for example %1 to invert all following chords up by one:

D('superpiano', 'vii%-2 iii%-1 vi%-1 ii v i%1 iv%2', key='D', scale='minor')

List of all chord names (Pitch classes in chromatic scale):

^major  = 047       # ^M
^min  	= 037       # ^minor or ^m
^major7 = 04711     # ^maj7 or ^M7
^7      = 047T      # ^dom7
^m7     = 037{10}   # ^minor7
^aug    = 048       # ^augmented or ^a
^dim    = 036       # ^diminished or ^i
^dim7   = 0369      # ^diminished7 or ^i7
^m7-5   = 036{10}   # ^m7b5 or ^halfdim or ^halfdiminished
^1    	= 0
^5    	= 07
^+5     = 048
^m+5  	= 038
^sus2   = 027
^sus4   = 057
^6    	= 0479
^m6     = 0379
^7sus2	= 027{10}
^7sus4	= 057{10}
^7-5  	= 046{10}
^7+5  	= 048{10}
^m7+5 	= 038{10}
^9    	= 047{10}{14}
^m9     = 037{10}{14}
^m7+9 	= 037{10}{14}
^maj9   = 04711{14}
^9sus4	= 057{10}{14}
^6*9  	= 0479{14}
^m6*9 	= 0379{14}
^7-9  	= 047{10}{13}
^m7-9 	= 037{10}{13}
^7-10 	= 047{10}{15}
^7-11 	= 047{10}{16}
^7-13 	= 047{10}{20}
^9+5  	= 0{10}{13}
^m9+5 	= 0{10}{14}
^7+5-9	= 048{10}{13}
^m7+5-9 = 038{10}{13}
^11   	= 047{10}{14}{17}
^m11    = 037{10}{14}{17}
^maj11  = 04711{14}{17}
^11+  	= 047{10}{14}{18}
^m11+ 	= 037{10}{14}{18}
^13   	= 047{10}{14}{17}{21}
^m13  	= 037{10}{14}{17}{21}
^add2   = 0247
^add4   = 0457
^add9   = 047{14}
^add11  = 047{17}
^add13	= 047{21}
^madd2  = 0237
^madd4  = 0357
^madd9  = 037{14}
^madd11 = 037{17}
^madd13 = 037{21}

Random values

Random values can be generated using ? or (n,n) syntax:

Pa * zd('superpiano', 'q 0 ? 3 ? 5') # Random within the scale (0-last scale degree)
Pa * zd('superpiano', 'q 0 (1,4) 5 (6,8)') # Random within a range

Lists and operations

Integers can be arranged to lists for applying different operations.

Pa * zd("superpiano","(0 1 2 3)-4") # Create a list and minus 4 from the items

Pa * zd("superpiano","(0 1 2 3)+<1 4 3>") # Create a list and cycle add operations

Pa * zd("superpiano","(0 1 2 3)+(1 2)") # Do a cartesian sum 0+1 0+2 1+1 1+2 2+1 2+2 3+1 3+2

Assingment

List operations can be assigned to variables and played as a pattern

Pa * zd("superpiano","A=(0 1 2) B=(4 2)+<4 2> A B A B B")

Ranges and random values from list

Pa * zd('superpiano', '(0..7)')  # Create a list of 7
Pa * zd('superpiano', '(0..7)?4') # Pick random 4
Pa * zd('superpiano', '(0..7)~4') # Pick unique 4 (Suffle and pick first n)

Euclidian

Euclidean rhythms can be used to create rhythms or complex rhythmic variation to melodies.

Euclidean syntax is (onbeat)<2,4>(offbeat) where (offbeat) is optional and defaults to r.

Pa * zd("superpiano","(0)<3,5>") # 0 r 0 0 r
Pa * zd("superpiano","(0 1 2)<3,5>") # 1 r 2 3 r
Pa * zd("superpiano","(0 1 2)<3,5>(6 5)") # 0 6 1 2 5
Pa * zd("superpiano","((q <0 3> e 2) (q 5 e 2))<3,5>") # Cycling groups 
Pa * zd("superpiano","((q <-3 3> s 8 2) (q 5 e 2))<5,8>(e 8 (s 9 6))") # Cycling onset and offset groups

Euclidean patterns with samples

TBD!

TidalCycles (Vortex)

Vortex is an experimental Python port of TidalCycles that was initially started by Alex McLean, Sylvain Le Beux, Silvani Damián and Raphaël Maurice Forment ( conference paper, other conference paper). It has been developed as an independant program for quite a while but the development stalled in favour of the new Tidal 2.0 and Tidal Strudel. The original repository can be found here.

A Vortex prototype is now available for Sardine, and with it, the deep and robust TidalCycles patterning language. Vortex is transparently added to your session as long as your superdirt_handler is turned on. You have nothing to do or install.

Some work remains to be done to be on par with the base implementation but I am doing what I can to save this great project. I have included and supported the most critical parts of it by embedding it into Sardine.

Players

Syntactic Sugar

In accordance with the TidalCycles model, there are a few players you can use to make your life easier: d1, d2, d3, d4, d5, d6, d7, d8 and d9. The players are just thin wrappers and syntactic sugar for the tidal function:

d1 = TidalD(name="d1", orbit_number=0)
d2 = TidalD(name="d2", orbit_number=1)
d3 = TidalD(name="d3", orbit_number=2)
d4 = TidalD(name="d4", orbit_number=3)
d5 = TidalD(name="d5", orbit_number=4)
d6 = TidalD(name="d6", orbit_number=5)
d7 = TidalD(name="d7", orbit_number=6)
d8 = TidalD(name="d8", orbit_number=7)
d9 = TidalD(name="d9", orbit_number=8)

Each player will be associated with an orbit number. This allows you to add effects to your players without having to think about the orbit you are currently targeting. Please note that the players are also slowed down a bit .slow(4) as patterns tend to be quite fast by default.

The Tidal function

A Tidal pattern can be created using the tidal function. The tidal function takes two arguments:

  • name: a name to give to the pattern.
  • pattern: a pattern or any combination of patterns.

You can use it that way:

tidal('my_pat', s("bd [hh hh:2] sn(2,3) <hh crow>")
                .slow("<4!4 2 0.125>")
                .striate("<2 8>")
                .jux(rev))

Stopping Tidal Patterns

You can stop all the Tidal patterns using the hush() function. You can also stop everything by running the base silence() or panic() functions. You can also stop individual Tidal patterns.

To do so, use the hush() function with a string or the player that holds the pattern you are willing to stop:

# Using hush on a Tidal Player
d1 * s('bd sn')
hush(d1)

# Using hush with the Tidal function
tidal('dada', s('bd sn'))
hush('dada')

Clock

There is no difference between the Sardine Clock and the Tidal Clock. There are a few functions you can use if you are more familiar with the Tidal model:

  • clock.cps: set the number of cycles per second for your patterns.
    • (135/60/4) to set the tempo at 135 beats per minute.
  • clock.get_cps(): get the cps from the current clock.tempo.

The cps attribute is both a setter and a getter, meaning that you can write:

clock.cps = 0.5 # set the value
clock.cps       # get the value

There is no difference between running a pattern using the Internal Clock or using the Link Clock.

Mini notation

The most basic pattern you can play using Tidal/Vortex is:

d1 * s('bd hh sn hh')

This will play a drum pattern using the d1 Tidal player. s is a shortcut notation for the sound function. This function allows you to make a pattern of SuperDirt audio samples/synthesizers in a similar fashion to the basic behavior of Sardine. Note the usage of a string to define an expression, similar to how SPL or Ziffers are working.

This expression is written using the Tidal mini-notation, a powerful language made to describe complex recurring cycles of events. The first step to master it is to learn the mini-notation.

Mini-notation

The bd hh sn hh part is using Tidal mini-notation, a custom patterning DSL (domain specific language) similar to other notations available in Sardine. Tidal inspired most of the constructs you will find both in SPL and Ziffers. You will feel familiar with it in no time because of the redundancies.

NameSymbolExample
Rest~d1 * s("~ hh")
Group[ ]d1 * s("[bd sd] hh")
Group.d1 * s("bd sd . hh hh hh")
Superpose,d1 * s("[bd sd, hh hh hh])"
Multiply*d1 $ s("bd*2 sd")
Slow/d1 * s("bd/2")
Choice|d1 * s("[bd &#124; cp &#124; hh]")
Alternate< >d1 * s("bd <sd hh cp>")
Replicate!d1 * s("bd!3 sd")
Elongate_d1 * s("bd _ _ ~ sd _")
Elongate@d1 * s("superpiano@3 superpiano")
Random?d1 * s("bd? sd")
Select:d1 * s("bd:3")
Euclid()d1 * s("bd(3,8)")
Polymetric{}d1 * s("{bd bd bd bd, cp cp hh}")
Polymetric subdivision{}%d1 * s("{bd cp hh}%8")

Here is a short description of each operation:

NameSymbolDescription
Rest~Rest / silence: no event
Group[ ]Grouping multiple values on the same step
Group.Grouping multiple values on the same step
Superpose,Superpose multiple values on same step
Multiply*Multiply the number of values on that step (subdivide)
Slow/The event will only play every /n cycles
Choice|Choose between one or more elements
Alternate< >Alternate between each of the values each cycle
Replicate!Replicate the event (but not on the same step)
Elongate_Elongate the duration of that event (multiple steps)
Elongate@Elongate the duration of that event (multiple steps)
Random?50/50 chance of returning the value (or rest)
Select:Specify sample in the SuperDirt folder (sound library)
Euclid()Euclidian pattern (e.g. bd(5,8))
Polymetric{}Polymetry using the grouping
Polymetric subdivision{}%Subdivides pattern to the measure

The mini-notation is only the start of learning Tidal/Vortex. Once you are comfortable using it, you can start to go further by chaining functions together and by diving into function composition.

For reference, see the Tidal Mini Notation documentation.

Functions

Vortex contains most of the basic TidalCycles functions. Here is a comprehensive list of all the functions currently defined in the Python codebase. Take some time to discover them all.

Concatenative operations

These functions typically take up to n patterns to form a single pattern that contains them all, making a sequence.

  • slowcat: make a single pattern out of multiple patterns without speeding up. The pattern will thus span over multiple cycles.
    d1 * slowcat(
      s('bd*2 sn'), s("jvbass*3"),
      s("drum*2"), s("ht mt")
    )
  • fastcat: make a single pattern out of multiple patterns but mashing all of them into a single cycle. Be careful with long / rich patterns :)
    d1 * fastcat(
      s('bd*2 sn'), s("jvbass*3"),
      s("drum*2"), s("ht mt")
    )
  • append: synonym of fastcat.

  • timecat: Like fastcat except that you provide proportionate sizes of the patterns to each other for when they're concatenated into one cycle. The larger the value in the list, the larger relative size the pattern takes in the final loop. If all values are equal then this is equivalent to fastcat.

    d1 * timecat((1, s("bd*4")), (1, s("hh27*8"))))
  • randcat: Pick patterns in a random order.
    d1 * randcat(
      s('bd*2 sn'), s("jvbass*3"),
      s("drum*2"), s("ht mt")
    )
  • struct: Restructure the pattern according to a binary pattern (false values are dropped).
    # add an example
  • stack: Pile up patterns or stack patterns. Useful to group some patterns together.
    # add an example

Superposition and layering

  • jux: a complex function. It takes your pattern, makes two versions of them, one playing on the left channel, the other on the right channel. It then applies a function but only on the right-hand channel.
    # On the right channel: reversed and filtered
    d1 * s('bd jvbass bd sn').jux(lambda p : p.rev().hpf(500))
  • superimpose: superpose a modified version of a pattern on the top of the original pattern. They are played at the same time, thus superimposed.
    d1 * s('hh cp hh cp').superimpose(lambda p: p.fast(2).rev())
  • layer: Layer up multiple functions on one pattern. For example, the following will play two versions of the pattern at the same time, one reversed and one at twice the speed.
    d1 * s("arpy [~ arpy:4]").layer(rev, lambda p: p.fast(2)])

If you want to include the original version of the pattern in the layering, use the id function:

    d1 * s("arpy [~ arpy:4]").layer(id, rev, lambda p: p.fast(2)])

Pattern degradation

  • degrade: randomly removes events from pattern, by 50% chance.
    d1 * s('[bd(5,8), hh(7,8), sn(2,8)]').degrade()
  • degrade_by: Simlar to degrade but you can control the percentage of events that are removed with by.
    d1 * s('[bd(5,8), hh(7,8), sn(2,8)]').degrade_by(0.2)
  • undegrade: Same as degrade, but random values represent percentage of events to keep, not remove, by 50% chance.
    d1 * s('[bd(5,8), hh(7,8), sn(2,8)]').undegrade()
  • undegrade_by: Similar to degrade but you can control the percentage of events that are removed with by.
    d1 * s('[bd(5,8), hh(7,8), sn(2,8)]').undegrade_by(0.2)

Sometimes family

The sometimes family of function can sometimes apply a function to a pattern... but only sometimes :) There is a large family of functions and helper functions to manipulate probabilities.

  • always: will always apply the function, similar to not using the function and applying directly.
    d1 * s('drum(5,8)').n('1 2 3 4').always(fast(2))
  • almostAlways: will apply the function 90% of the time, at random.
    d1 * s('drum(5,8)').n('1 2 3 4').almostAlways(fast(2))
  • often: will apply the function 75% of the time, at random.
    d1 * s('drum(5,8)').n('1 2 3 4').often(fast(2))
  • sometimes: Applies a function to pattern, around 50% of the time, at random.
    d1 * s('drum(5,8)').n('1 2 3 4').sometimes(fast(2))
  • sometimes_by: Applies a function to pattern sometimes based on specified by percentage, at random. by is a number between 0 and 1, representing 0% to 100% chance of applying function.

  • sometimes_pre: Similar to sometimes but applies a function to the pattern before filtering events 50% of the time, at random.

  • rarely: will apply the function 25% of the time, at random.

    d1 * s('drum(5,8)').n('1 2 3 4').rarely(fast(2))
  • almostNever: will apply the function 10% of the time, at random.
    d1 * s('drum(5,8)').n('1 2 3 4').almostNever(fast(2))
  • never: will apply the function 0% of the time, at random. The function will thus never be applied.
    d1 * s('drum(5,8)').n('1 2 3 4').never(fast(2))

The every family and friends

  • every: allows you to apply a function based on a condition. You need a function to determine when the function should be applied (e.g. a number n to apply the function every n cycles). You also need a transformation such as rev (reverse a pattern) or fast (make a pattern faster).
    d1 * s('[bd(5,8), jvbass(3,8)]').every(3, lambda p: p.superimpose(fast 2))
  • somecycles: Apply a function on certain cycles (e.g on cycle 3) ???
    # add an example
  • somecycles_by: Apply a function on certain cycles with a change factor ???
    # add an example

Operators

The Haskell version of TidalCycles uses a lot of overloaded operators. Python is not really flexible in that regard. You can't really define new operators such as $ or |+ but most of the base operators have been overloaded to denote a particular pattern transformation. They are used to denote a particular application of a pattern over another pattern.

  • +
  • -*
  • /
  • //
  • **
  • >>
  • <<

Time manipulation

  • rev: This function can be used to reverse a pattern. Very useful!
    # Normal
    d1 * s('bd hh sn hh')

    # Reversed
    d1 * s('bd hh sn hh').rev()
  • fast: fast is a function used to speed up a pattern. fast(2) will play the pattern twice as fast. fast(0.5) will play the pattern twice as slow. You can pattern the value as well.
    d1 * s('bd hh sn hh').fast(2)
  • slow: slow is a function used to slow down a pattern. It is a mirror of fast. slow(2) will play the pattern twice as slow, slow(0.5) will play a pattern twice as fast.
    d1 * s('bd hh sn hh').slow(2)
  • early: Equivalent of Tidal's <~ operator. This function can shift an event earlier in time, nudging it a little bit before the moment where it was initially supposed to play.
    d1 * s('[tabla drum] cp').early('0.2 0.1')
  • late: Equivalent of Tidal's ~> operator. This function can shift an event later in time, nudging it a little bit after the moment where it was initially supposed to play.
    d1 * s('[tabla drum] bd cp').late('0.2 0.1')
  • off: Combination of the stack function with early. Will play the same pattern twice, one version of it will be played slightly earlier.

  • compress: Squeeze pattern within the specified time span.

    # add an example
  • fastgap: Similar to fast but maintains its cyclic alignment. For example, p.fastgap(2) would squash the events in pattern p into the first half of each cycle (and the second halves would be empty). The factor should be at least 1.
    # add an example

Conditional application

  • when: Applies function func on each event of pattern if boolean_pat is true. You will have to feed a pattern of boolean values (1 or 0) using another function.
    # add an example
  • when_cycle: Applies function func to pattern only if test_func returns True on each cycle. Similar to when, but instead of working with a boolean pattern, this evaluates a boolean function with the cycle number and applies (or not) transformation on each cycle.
    # add an example

Iteration over patterns

  • iter: Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
    d1 * s("bd hh sn cp").iter(4)

reviter: Same as iter but in the other direction.

    d1 * s("bd hh sn cp").reviter(4)
  • striate

maskeuclid

Signals and Generators

Signal functions are functions generating streams of values to apply to a pattern. They are very useful to create low-frequency oscillators or continuously evolving stream of values. These functions should be manipulated to your liking using segment and range: scaling the stream of generated values, getting the desired granularity/precision of the stream).

Signal manipulation functions

  • range: Rescales values to the range [min, max]. Assumes pattern is numerical, containing unipolar values in the range [0, 1].
  • rangex: Rescales values to the range [min, max] following an exponential curve. Assumes pattern is numerical, containing unipolar values in the range [0, 1].
  • segment: Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
    sine().segment(8).range(4, 8)

Generators

  • sine2: Bipolar sinusoïdal oscillator: negative and positive values.
  • sine: Unipolar sinusoïdal oscillator.
  • cosine2: Similar to sine2 but the phase is shifted (e.g. cosinus function).
  • cosine: Similar to sine but the phase is shifted (e.g. cosinus function).
  • saw2: Bipolar sawtooth-like oscillator: negative and positive values.
  • saw: Unipolar sawtooth-like oscillator.
  • isaw2: Inverted Bipolar sawtooth-like function.
  • isaw: Inverted unipolar sawtooth-like function.
  • tri2: Bipolar triangular oscillator: negative and positive values.
  • tri: Unipolar triangular oscillator.
  • square2: Bipolar square wave oscillator: negative and positive values.
  • square: Unipolar square wave oscillator.
  • rand: Generate a continuous pattern of pseudo-random numbers between 0 and 1.
  • irand: Generate a pattern of pseudo-random whole numbers between 0 to n-1 inclusive.
    # generates a pattern of 8 events per cycle
    # with values ranging from 0 to 15 inclusive.
    irand(16).segment(8)
  • perlin: 1D Perlin (smooth) noise, works like rand but smoothly moves between random values each cycle.
    perlin().segment(8)

Combinators

  • run

  • scan

  • choosewithchoose

  • choose_cycles

  • wchoose

Query pattern values

We have already seen that you can play music using the TidalCycles pattern system and SuperDirt. Tidal can also be used as a value/pattern generator for other things: visuals, sending through OSC, etc. There is a tiny but very welcome mechanism to extract any value from a Tidal stream to be reused later in other patterns or for anything else.

Let's start by playing a simple pattern:

d1 * s('kick hat snare hat')

Now we can use the .stream.get('value_name', 0) method to extract any value name from the pattern being played:

@swim
def gui_loop(p=1/32, i=0):
    # ... doing whatever ...

    blip = d1.stream.get('cycle', 0)
    bloop = d1.stream.get('s', 0)

    my_super_func(blip=blip, bloop=bloop)
    again(gui_loop, p=1/32, i=i+1)

Have fun!

Diving Deeper

This section assumes a good understanding of Sardine. It will help you to push your playing skills to a new level. The functionalities covered here are advanced and can really bring a new level of expressivity with the system.

The FishBowl

The FishBowl is central to the Sardine system. As its name might suggest, it is what holds everything together. Properly speaking, it is the environment, the water, what makes the fishes swim :) The system is composed of some hard dependencies and many soft dependencies. Hard dependencies are important components like the clock or the parser. They are needed all the time. As such, you can’t really remove them or everything would fall apart. Soft dependencies are the various senders or I/O components that you use to perform your music. Some of them are installed based on the content of your configuration, some can be created on the fly later on.

Hard dependencies

Core components cannot be removed from the FishBowl. However, they can be swapped! It means that you can all of the sudden rip off the current clock and switch to a new one. The system might hiccup a bit but it will recover! To do so, note that you can use two important methods:

  • bowl.swap_clock(clock: "BaseClock"): swaps a clock. InternalClock() and LinkClock() are the two clocks currently implemented. The latter is used for synchronisation with every device capable of using the Ableton Link protocol.
  • bowl.swap_parser(parser: "BaseParser"): switch from a parser to another parser. There is no reason to do that because there is only one parser for the moment but it might be useful in the future.

Soft dependencies

This is where the fun begins. Pretty much everything in the Sardine system is a modular component that can be added or removed. Take a look at the run.py file if you want to see how the system is first initialized. By default, Sardine is proposing a small collection of handlers / senders that will allow you to send or receive MIDI, OSC or SuperDirt messages. Some other handlers are used for various internal functions that you might not care about. Take a look at the following code detailing how to add modular components:

# Adding a MIDI Out handler: sending MIDI notes
# control changes, program changes, etc...
midi = MidiHandler(port_name=str(config.midi)) # new instance of the Handler
bowl.add_handler(midi) # adding the handler to the FishBowl

# OSC Loop: internal component used for handling OSC messages
osc_loop = OSCLoop() # new instance of the Handler
bowl.add_handler(osc_loop)  # adding the handler to the FishBowl

# OSC Handler: dummy OSC handler
dummy_osc = OSCHandler(
    ip="127.0.0.1",
    port=12345,
    name="My OSC sender",
    ahead_amount=0.0,
    loop=osc_loop,
)

# Aliasing some methods from the handlers for later :)
M = midi.send
CC = midi.send_control_changes
PC = midi.send_program_changes
O = dummy_osc.send

Please take note of the bowl.add_handler method. If you don't add your component to the FishBowl, your component will inevitably crash! This is a fairly common mistake, especially if you are working in a hurry.

Messaging system

You might wonder what the FishBowl is actually doing behind the scene. Factually, it allows component to talk with each other by sharing a reference to the bowl. It means that any component can send a message to any other component. It also means that this same component can promptly react to any event dispatched through the FishBowl. Internal messages are sent using the bowl.dispatch(message_type: str, *args) method. This is how messages such as bowl.('pause'), bowl.('resume'), bowl.('stop') and bowl.('play') are able to stop and resume everything when needed. They are messages dispatched to the FishBowl making everyone aware that a major event occured.

  • Introduction to the concept.
  • Hard dependencies
  • Soft dependencies
  • Messaging system.

Nudging and time-alignment

Synchronising computers and audio software is rather difficult. Some people have built careers trying to tackle this problem. It is very unlikely that everything will be properly synchronised out of the box. It's not because Sardine is incapable of doing it, it's just that the topic is very complex and that there are lot of different places where things can go wrong.

You already know about the Ableton Link clock that you can take advantage of to synchronise code or sound with your friends. It does 90% of the job. However, you might have to flick Sardine just a little to get it properly on time. Think of it as putting your finger on a vinyl disk while it is playing, just like a DJ.

  • the midi.nudge attribute can add a little delay to your MIDI outputs: midi.nudge = 0.2.
  • the dirt.nudge attribute can add a little delay to your SuperDirt output: dirt.nudge = 0.3.
    • try to keep nudge always above zero to give some spare time to the system. Not having any nudge means that you are asking to your computer to play everything right now, something that it is very unlikely to do.

By preparing carefully before a session, you will be able to time-sync properly with your friends. The same also applies for recording! If you see ever see that Sardine is spitting out MIDI too soon or too late, you can even try an additional trick:

@swim(snap=0.5) # change this variable
def midifun():
    N('C5')
    again(midifun)

The snap argument will start the function before or after the first beat of the measure. A snap of 0.5 means that we want to start half a beat after the beginning of the bar. It can be a negative value too.

High-level patterns

Once you understand the basic Sardine model, you can start thinking about musical patterns at different scales. You can think of patterns in the context of the Sardine pattern language or Ziffers pattern language. You can also think about patterns of these patterns, and so on. In this section, you will learn how to generate musical structures by controlling how your functions are looping, when your functions are looping, etc… There are some functions that apply globally to patterns. These functions can be accessed using keyword arguments. They can help you to shape your improvisation, to master repetition and cyclical constructs.

Span argument (Players only)

The span argument can be used with Players in order to control the flow of time. The span argument will compress or extend the duration of your pattern. Think of it as a pattern level event-based time-strecthing method :).

The following patterns are identic. However, note the use of span. The first pattern is 1.5 times longer compared to the version without span. The second one, on the other hand, is 50% shorter:

Pa >> d('voodoo voodoo:4 linnhats:(rand*20)!4', span=1.5, p=0.5)
Pb >> d('voodoo voodoo:4 linnhats:(rand*20)!4', span=0.5, p=0.5, speed=2)

Try to add or remove the span argument and listen to the difference.

Play carefully with it as you might sometimes end up with weird rhythmical results, especially if you are already playing complex rhythms with p.

Snap argument (Swimming functions)

snap is an additional argument for the @swim decorator. snap will time-shift the beginning of your pattern around the first beat of the measure:

  • snap=1 will start the pattern one beat after the beginning of the bar.
  • snap=-0.5 will start the pattern half a beat before the beginning of the next bar.

It is a useful function for shifting things around and for synchronising Sardine in some scenarios (recording, etc..).

Until argument

The until argument can also be used in the @swim decorator for swimming functions and as a regular arguemnt for players. As its name suggests, this argument will loop the function n times and stop. This is a very useful trick to use for solos or one-shots events during your sessions:

It's bouncing ball time!

@swim(until=10)
def baba(p=0.5, i=0):
    D('sid:2',
      legato=0.9,
      speed=0.5,
      lpf='[5000:200,400]',
      i=i
    )
    print(i)
    again(baba, p=P('[1:0.1,0.1]', i), i=i+1)

You can also pair it with the snap argument if you wish to!

Here is the bouncing ball example from above written using the Players syntax:

Pa >> d('sid:2', until=10, legato=0.9, speed=0.5, lpf=['5000:200,400'], p='[1:0.1,0.1]')

Loaf and On

The loaf and on arguments can be used in your senders to determine if the pattern should play on a given bar or not. In the following diagram, each - represents one bar.:

--------------------------

The loaf argument will cut slices of time, just like if you were cutting with a knife in a loaf of bread:

-------------------------- (time)
---- (loaf=4)

Using the on argument, you can now select which of these slices your pattern is going to be played on:

-------------------------- (time)
---- (loaf=4)
- -  (on=(1, 3))

Note that the on argument can be given as an integer or as a integer-based tuple for targetting multiple bars in your loaf! In code, the above example would look like:

Pa >> d('bd', loaf=4, on=(1,3))

You can create extremely complex rhythmic structures using these two arguments in conjunction with complex patterns!

On

on can also be used alone!

Euclid and Neuclid

Just like in the Sardine Pattern Language, you can compose euclidian rhythms. This time, these euclidian rhythms will not be composed of single events but of entire bars! This is an extremely funny way to compose complex evolving structures spanning over multiple bars.

Pa >> d('(eu bd 5 8)', p=.5, euclid=(5, 8))
Pb >> d('linnhats', p=.25)
Pc >> d('(eu cp 3 8', p=.5, euclid=(4, 8))

euclid is the regular euclidian rhythm function while neuclid will play the opposite version of that pattern.

Binary

binary is another argument similar to loaf, on, euclid and neuclid. Using it, you can declare a boolean structure that will determine if a bar is to be played or not. Use it like so:

Pa >> d('voodoo:[1:10]', binary=[1, 0, 0, 1, 1, 0])

This pattern will be played on bars 1, 4 and 5. It will cycle every 6 bars and restart :)

Sometimes

This is a port of the TidalCycles sometimes family of functions:

  • "always": 100%
  • "almostAlways": 90%
  • "often": 75%
  • "sometimes": 50%
  • "rarely": 25%
  • "AlmostNever": 10%
  • "never": 0%

These functions represent a likelihood for an event to be played. In order to use these functions, please use the chance keyword in any of your patterns:

Pa >> d('linnhats', p=.25, chance="almostAlways")

Sleeping and oversleeping

Sardine has a few tricks up its sleeves. Swimming functions can mimick Sonic Pi sleep() method because why not! The sleep() method from Python (and from most programming languages) is very imprecise. It doesn’t offer any guarantee on the duration of the sleep. Your program will halt and come back after a certain time, but not always when you need it the most. It won’t be really helpful to write precise rhythms like we do in music. Sonic Pi, long time ago, acknowledged that issue. They did something about it.

Following its model, Sardine is overriding the default sleep() method. You can use the new version just like the old one. The interface is very similar. However, it will allow you to write precise rhythms:

@swim
def super_sleeping(p=2, i=0):
    D('bd')
    sleep(1)
    D('cp')
    again(super_sleeping, p=2, i=i+1)

Let me explain what I just wrote:

  • we are using a regular swimming function. The syntax is untouched.
  • we use a central sleep(1) statement to make a pause in our pattern.
  • we do the recursion after a period of two beats.

There is something a bit un-intuitive about this. Strictly speaking, the sleep method is not halting anything, it just shifts the events coming after it to some point in the future. Read this twice!

It means that you can sleep() for some time but the function will not end if the recursion is coming much later. If will just defer the execution of what comes after the sleep and wait until the function is done looping:

clock.tempo = 100
@swim
def super_sleeping(p=2, i=0):
    D('bd')
    sleep(0.25)
    D('{cp sn}')
    D('tabla')
    # ...
    # Nothing happens
    # ...
    again(super_sleeping, p=2, i=i+1)

You can also oversleep the duration of your function. You defer an event enough so that it occurs after swimming function loops:

clock.tempo = 100
@swim
def super_sleeping(p=2, i=0):
    D('cp', speed='[1:5,0.25]', i=i)
    sleep(0.75)
    D('linnhats', speed='[1:5,0.25]', i=i)
    again(super_sleeping, p=0.5, i=i+1)

In the example above, the linnhats sound is deferred to later, and later means on the next loop of our swimming function. Rhythms piling up on top of rhythms!

Patterning sleep()

This example shows how to add patterning to sleep(). Musically this can be used to vary the rhythm. Notice that this requires use of the Pattern Object. Don't forget to include the iterator (i)!

@swim
def super_sleeping(p=2, i=0):
    D('cp', speed='[1:5,0.25]', i=i)
    sleep( P('.85 1.25!3', i) ) # Pattern Object with iterator
    D('linnhats', speed='[1:5,0.25]', i=i)
    again(super_sleeping, p=0.5, i=i+1)

Patterning everything

You can use the P() object to get a generic interface to Sardine patterns. This object can be used just anywhere you would like to see a pattern. It means that you can contaminate your Python functions or anything in your code with them. Under the hood, Sardine patterns are spitting out valid Python integers, floats or strings.

@swim
def free(p=0.5, i=0):
    print(P('1 2 3 4', i))
    again(free, p=0.5, i=i+1)

In the example above, we are just using a swimming function to print the result of a pattern in the interpreter window. Note that P() can take your basic iterator arguments as always (P(pattern, iterator, divisor, rate).

We can do the same thing but using P() in a strategic location, replacing the static value for p:

@swim
def free(p=0.5, i=0):
    D('cp')
    again(free, p=P('0.5!4 0.25!2', i), i=i+1)

Using this technique, you can easily generate rhythms or pattern data that you send to any Python function. Using P() is a good way to study Sardine patterns. You can just take a few minutes and study some specific patterns if you have a hard time understanding them. That’s what I do sometimes when developping them :)

Amphibian Variables

from random import random
V.s = 60 # this is an amphibian variable

@swim
def fun():
    # Calling the variable from inside the Sardine Pattern Language
    N('(getA s)')
    if random() > 0.8:
        V.s = random() * 80 # setting a random value
    again(fun)
  • There are variables called amphibian variables. They are both valid inside and outside the pattern notation.
  • They are defined using the variable V followed by a letter from the alphabet (uppercase or lowercase) : V.a, V.A, V.Z, V.j. These variables can be freely manipulated from the Python side or from the pattern side. They are amphibian because they exist in the two languages.

Here is how you manipulate them:

  • The (getA letter) function can access an amphibian variable from inside the pattern language.
  • The (setA letter value) function can set an amphibian variable from inside the pattern language.
@swim
def fun(p=0.25):
    # Now having fun with it
    N('(setA s 5~80)') # setting a random value to the variable
    if random() > 0.8:
        v.s = 50
    again(fun, p=0.25)

Amphibian Variables also work in Players:


V.n = [52, randint(40, 60), 72, 35]
Pa * d('supersaw', n='(getA n)', p=0.75 )

You can use Amphibian Variables to leverage Python or the pattern syntax for what they do best: patterning or dealing with complex algorithmic transformations. Having them both available makes the pattern syntax even more expressive.

For more exploration of Amphibian Variables, see:

Using Python

Sardine is written and executes in python. You can use python code both inside and outside of Players and @swim functions. This is one of most powerful features of Sardine - once you understand how to use it. There are also times where you need to execute python commands.

Import modules

Importing modules is a standard way to access additional functionality in python. In Sardine, importing from the random module is needed whenever you want random values in your expressions or patterns. In the example there are two calls to random functions: randint(100 ,400) and random(random()*1.5)+0.4. random() generates a float between 0 and 1. Multipling this by 1.5 scales the random range out, and +0.4 moves it up, to avoid speeds that are too low. This yields random values between 0.4 and 1.9.

from random import * 
clock.tempo=90

@swim
def demo(p=1, i=0):
    D('electro1:2 electro1:4 electro1:3 feelfx:2',
    freq=randint(100,400),
    speed=(random() * 1.5) + 0.4,
    i=i)
    again(demo, p=0.5, i=i+1)

Aliases

Looking for a way to reduce keystrokes? Aliases are simple. This example uses a python assignment with a as an alias for "again."

a=again
@swim
def demo(p=1, i=0):
    D('electro1:2 electro1:4 electro1:3 feelfx:2', i=i)
    a(demo, p=0.5, i=i+1)

Calling python functions

You can call python functions from within a @swim. The @swim below uses a Pattern Object to gradually increase and decrease the clock.tempo value. A python print() statement shows the value of clock.tempo as it is changing.

clock.tempo=90
@swim
def clockPat(p=1, i=0):
    print(f"clock.tempo: ", clock.tempo) # python print function
    clock.tempo=P('[90:180,2][180:90,4]', i) # Sardine Pattern Object
    again(clockPat, p=1, i=i+1)

Generating values dynamically w custom functions

This shows a simple substitution. A variable is created with a string of note values.

seq = '40 51 62 72'
Pa * d('supersaw', n=seq)

Now instead of just setting fixed values, we write a short custom function to generate note values. Load the lowHigh() function first. Then start the Pa player. The same two notes will continue to play until you execute the Player again. This will cause it to make another function call to lowHigh().

def lowHigh():
    low = str(randint(30, 50))
    high = str(randint(50, 70))
    notes = low + ' ' + high
    return notes

Pa * d('supersaw', n=lowHigh()) # execute this line again to change note values

Custom functions can also be called within @swim, with an important difference. Here load the lowHigh() custom function then start the @swim. When referenced within the @swim, we get new note values every time!

def lowHigh():
    low = str(randint(30, 50))
    high = str(randint(50, 70))
    notes = low + ' ' + high
    return notes

@swim
def melody(p=1, i=0):
    D('supersaw', midinote=lowHigh(), i=i)
    again(melody, p=1, i=i+1)

Setting amphibian variables

A similar results can be achieved using Amphibian Variables.

Amphibian variables can have their values set outside of @swim with a python assignment/expression, or with a custom function.

The custom function below returns a list of midi note value with an argument that sets the number of notes in the list.

def randNotes(listLen):
    notes = []
    i = 0
    while i < listLen: 
        notes.append(randint(30,80))
        i += 1
    return notes

V.n = randNotes(5) # Execute again to change notes
Pa * d('supersaw', n='(v n)', p=1 )

For more info see Amphibian Variables.

Conditional logic

This example uses python if/elif/else conditional logic to switch between sample sets and change the tempo. Notice the use of the iterator and how resetting it to 0 at the end resets the conditional logic.

@swim
def demo(p=1, i=0):
    print(f"i = ", i)
    if (i < 8):
        clock.tempo = 60
        D('electro1:2 electro1:4 electro1:3 feelfx:2', i=i)
    elif (i < 16):
        D('east:0~8', i=i)
        clock.tempo = 120
    else:
        i = 0
    again(demo, p=0.5, i=i+1)

Going even deeper

See the next section - Advanced python: Sample Slicer, where custom functions generate 4 Amphibian Variables at once.

Advanced Python - Sample Slicer

Using custom python functions to multiple amphibian variables

This example uses custom functions to generate 4 amphibian variables at once. It also uses presets, initialized values, and sample lists. The Sample Slicer is a complete livecoding program, ready to use. You can also extend it, add more samples to slice, add more amphibian variables, add more parameters, etc.

Description

  • Custom functions randomize the selection of which sample file to use and then randomize start and end points in the sample file (sample slicing).
  • Output of the functions is assigned to Amphibian values for sampleName:index, begin, end, period.
  • Amphibian variables are referenced in the @swim function.
  • There is a simple command to "reset" the amphibian variables, based on which sampleList you choose.
  • During a running @swim, reseting will change the sample slice and @swim period -- which can dramatically alter the sound.
""" 
Sample Slicer: llustrates the use of custom python functions that generate Sardine amphibian variable values. 

Instructions
1. Load Presets, Initialize, and Functions.
2. Set the amphibian variables once before starting @swim. You will see the values print out. 
3. Start the @swim function.
4. Reset amph variables and change @swim parameters at will.
5. Try adding your own sampleList -> have fun! 
"""
########## PRESETS #########################
rev0 = {'room':1.5, 'size':0.8, 'dry':0.8}
verb0 = {'verbwet':0.8, 'verbtime':0.7, 'verbgain':0.8}
del0 = {'delay':0.5, 'delaytime':0.4, 'delayfeedback':0.6}

########## Initialize ######################
from random import * 
clock.tempo=60 # assumed for period calculations

# Sample lists - fm:3 is the ~dirt.samples format. Floats values are the sample length in secs.
# Any list of samples loaded in SuperDirt will work.
sampleListFm = [["fm:3", 4.197], ["fm:4", 1.92], ["fm:7", 1.97], ["fm:9", 4.42], ["fm:14", 1.73] ]
sampleListBirds = [["birds:0", 2.0], ["birds:1", 2.0], ["birds:2", 3.0], ["birds:3", 2.5], ["birds:4", 4.0], ["birds:5", 1.0], ["birds:8", 1.75], ["birds:9", 1.75] ]
sampleListDiphone = [["diphone:0", 0.9], ["diphone:1", 0.9], ["diphone:2", 0.9], ["diphone:3", 0.9], ["diphone:4", 0.9], ["diphone:5", 0.9], ["diphone:8", 0.9], ["diphone:9", 0.9], ["diphone:10", 0.9], ["diphone:11", 0.9] ]

########## FUNCTIONS #############################
def setSampleVals(sampleDurIn, directionIn):
# generates random start and end points and calculates the period duration
   endRand = random()
   beginRand = random() * endRand
   periodDur = (endRand - beginRand) * sampleDurIn # sets period length
   if (directionIn == -1):
       return(endRand, beginRand, periodDur) # switch end and begin for reverse play
   else:
       return(beginRand, endRand, periodDur)

def genSampSlice(sampListIn, directionIn):
# random selects the sample from the sample list
   sampListIndex = randint(0, len(sampListIn)-1) # rand pick a sample from list
   sampName = sampListIn[sampListIndex][0]
   beginN, endN, periodDurT = setSampleVals(sampListIn[sampListIndex][1], directionIn)
   print(sampListIn[sampListIndex], round(beginN,3), round(endN,3), round(periodDurT,4))
   return(sampName, beginN, endN, periodDurT)

######### LIVE CODING Section  #############################
### set / reset Amphibian Variables - execute one of the Amph Variables assignment statements
# Amph Variables: V.s = sampleName:index, V.b = begin, V.e = end, V.p = period
# execute one line to initialize all amphibian variables before starting the @swim 
# execute one line while @swim is playing to reset the amph vars - this will change the sound and rhythm, often radically
# change the direction argument: 1=forward, -1=reverse

# Execute any one of these statements before and then when @swim is playing.
V.s, V.b, V.e, V.p = genSampSlice(sampleListFm, 1) # drum beats
V.s, V.b, V.e, V.p = genSampSlice(sampleListDiphone, 1) # speech 
V.s, V.b, V.e, V.p = genSampSlice(sampleListBirds, 1) # birds

V.s, V.b, V.e, V.p = ['diphone:4', 0.662, 0.902, 0.21] # hard code the amph vars

# @swim function: start play, then execute one of the lines above to change parameters.
# Uncomment / comment lines to change parameter values. Adjust clock.tempo.

@swim
def sampleSlicer(p=1, i=0):
   D('(v s)', 
   begin='(v b)', end='(v e)', **rev0, 
   #**del0, # delay preset
   speed='1 0.5 1.5',
   #freq=randint(150,400), 
   #freq='[150:270,10] [270:240,4] [240:270,4] [272:150,15]',
   pan='0 1', amp=0.9, d=1, rate=1, i=i)
   again(sampleSlicer, p=P('(v p)'), i=i+1)

clock.tempo=60
silence(sampleSlicer)

External to Sardine

Sardine relies upon communication with external programs - most importantly SuperDirt and SuperCollider. Other external programs are optional but these can expand your environment significantly.

Included in this section are details on:

  • External clock: Sardine has an internal clock and an external clock. The external clock LinkClock lets you interface with Ableton Link.
  • SuperCollider interaction - commands and examples to help you get the most out of using SuperCollider
  • Sardine and your DAW

Also see the Configuration Section:

  • MIDI
  • OSC

SuperCollider interaction

If you are using SuperDirt, you are also somehow using SuperCollider. Most of the time, you can’t really know about this because the process handling SuperCollider is invisible. Everytime you start Sardine, you also incidentally start SuperCollider and SuperDirt with only one command. SuperCollider is a very exciting programming language and audio server. Since its first publication in 1996, this software has been adopted by many musicians worldwide because of its robustness and performance. Some people are live coding directly in SuperCollider because they can handle its verbosity and complexity. Most of the time, people are developing high-level layers to communicate more easily with SuperCollider just like we do.

To get a better understanding of the Sardine environment, think about it this way:

  • Sardine is an independant layer on top of everything. It can do MIDI and OSC alone.
  • SuperDirt is a specialised audio engine designed to make live coding on SuperCollider simpler.
  • SuperCollider is the fundamental audio server that receives all the information and processes audio.

We can't do the audio synthesis or the scheduling directly in Python. The language is not fast enough to handle most of it and we must rely on external software to make things work smoothly. However, we can collaborate with it very easily.

It means that Sardine has a few commands that will help you get the most out of SuperCollider:

  • SC.scope() will open an oscilloscope pop-up window to visualise all your audio channels.
  • SC.freqscope() will open a frequency analyzer pop-up window to visualise your audio output.
  • SC.info() will open a general pop-up window that can help you with a few things:
    • monitoring your CPU usage.
    • monitoring the audio volume.
    • recording sound on the server.

It does not end here. You can also execute arbitrary code written in SCLang, SuperCollider’s programming language. To do so, simply pass a string to the SC() object:

# Play a sinewave at 200hz
SC("a = play({SinOsc.ar(200) * 0.25});")

# Stop that sinewave
SC("s.freeAll;")

In the future, it is very likely that Sardine interaction with SuperCollider will be further refined. For the moment, it is only used as lightweight high-level layer on top of SuperCollider :)

External Clock

Sardine lets you chose between two clocks: the InternalClock and the LinkClock:

  • the InternalClock is a monotonic clock tied to your system time. It is the default time used by your system.
  • the LinkClock is an interface to the Ableton Link link protocol.

You can use sardine-config to change which clock your Sardine is using.

Because clocks share the same interface, there is no visible difference to using one VS the other. However, only the LinkClock will allow proper synchronisation with outside peers.

To synchronise, you simply need to share a local network with any other application (or Sardine instance) using the Ableton Link protocol. If nothing is blocking the signal (firewall, network protection), your Sardine will automatically try to match with the tempo of all the other applications around. The tempo given by Ableton Link is an average tempo that adjusts itself according to the tempo given by everyone. There is no master instance and no passive instance that just listens like MIDI. This means that anyone can submit a tempo change, and that this change will apply to all players.

Internally, the Link clock is implemented differently. In order for it to work with Sardine, a task is started in the background. This task will regularly (several times per second) fetch new information about the temporal position of the clock and propagate this information to all Sardine components that need it.

SuperCollider with DAWs

Writing presets

You can use Python dictionaries to write presets for your patterns. It is very simple to do so using the ** operator that allows you to map dictionaries as keywords. See for yourself:

padcc = { 'timbre': {'control' : 18, 'chan': 2}, 'time': {'control' : 19, 'chan': 2},
        'metal': {'control' : 16, 'chan': 2}, 'fx': {'control' : 17, 'chan': 2}}
basscc = { 'timbre': {'control' : 18, 'chan': 0}, 'time': {'control' : 19, 'chan': 0},
        'cutoff': {'control' : 16, 'chan': 0}, 'fx': {'control' : 17, 'chan': 0}}
jupcc = { 'decay': {'control' : 81, 'chan': 1}, 'time': {'control' : 19, 'chan': 1},
        'cutoff': {'control' : 74, 'chan': 1}, 'resonance': {'control' : 71, 'chan': 1}}

@swim
def structure(p=0.5, i=0):
    N("C2 C3", chan=2, vel=120, i=i)
    N("G5 G4", chan=2, vel=120, i=i, r=0.25/4)
    N("[G6]-[0:12]", chan=2, vel=120, i=i, r=0.25/2)
    CC(**jupcc['cutoff'], value=100) # Here, I am injecting stuff
    CC(**jupcc['decay'], value=80)
    N("[G6]-[0:12]", chan=1, vel=120, i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

Effects Presets

Presets are a simple way to pre-load effects with multiple parameters. With multiple versions of an effect (like reverb, delay, etc), you can easily switch between presets with a simple change (**rev1 -> **rev2). Start by "loading" (executing) all of the effects lines.

#effects presets
# reverb
rev0 = {'room':0.9, 'size':0.6, 'dry':0.2}
rev1 = {'room':1.5, 'size':0.8, 'dry':0.8}

#delay
del0 = {'delay':0.5, 'delaytime':0.3, 'delayfeedback':0.3}
del1 = {'delay':0.5, 'delaytime':0.4, 'delayfeedback':0.6}

#distortion
fx0 = {'comb':0.4, 'scram':0.4, 'shape':0.2}
fx1 = {'comb':0.7, 'scram':0.6, 'shape':0.4}

## Effects Presets used in Players
clock.tempo=100
Pb >> d('bd sd ht mt', p='0.5!4 0.25!2', **rev0)

Pb >> d('east:2 east:4 peri:3 east:5', p=0.5, **rev1, **del1)
Pb >> d('east:2 east:4 peri:3 east:5', p=0.25, **rev1, **del0, **fx0)

Extensions

Sardine also has an extension/plugin system that lets you write custom handlers and take advantage of the time scheduling engine for your own purposes.

The steps for making a Sardine extension are:

  • create a Python package containing your custom handlers
  • create a configuration file for your extension
  • reference this configuration file in Sardine's user configuration file.

The next paragraphs describe in more details how to do this with a simple example: the Doug extension.

Doug structure

The project structure for a Sardine extension is quite standard: all you need is a root folder containing your Python package, and inside that package the modules containing the handlers (of course the package's content is not restricted to Sardine handlers, it is a regular Python package).

You can also place your extension's configuration file in your project for convenience, but note that this is not mandatory as you can place it anywhere that is accessible on your file system.

Doug
|__ doug
|   |__ __init__.py
|   |__ DougHandler.py
|__ doug-config.json

Doug Handler

Here is an example of a Sardine extension handler:

from sardine_core.handlers.sender import Number, NumericElement, ParsableElement, Sender, StringElement
from sardine_core.utils import alias_param

from typing import Optional, List


class DougHandler(Sender):

    def __init__(self, params: dict):
        super().__init__()

        self._intro = params['intro']

    def _doug_print(self, message):
        word = message["words"]
        if word is not None:
            print(f'{self._intro}:: {word}')

    @alias_param(name="iterator", alias="i")
    @alias_param(name="divisor", alias="d")
    @alias_param(name="rate", alias="r")
    def send(
        self,
        words: Optional[StringElement | List[StringElement]],
        iterator: Number = 0,
        divisor: NumericElement = 1,
        rate: NumericElement = 1,
        **pattern: ParsableElement,
    ):
        if words is None:
            return

        if self.apply_conditional_mask_to_bars(pattern):
            return

        pattern["words"] = words
        deadline = self.env.clock.shifted_time
        for message in self.pattern_reduce(pattern, iterator, divisor, rate):
            self.call_timed(deadline, self._doug_print, message)

Let's decompose this and understand what Doug does.

The DougHandler class inherits from Sardine's Sender class: this allows us to use Sardine's native pattern parsing and time scheduling tools.

In the constructor method, we initialize our handler and provide it with a custom parameter called intro::. The value of such initialization parameters are specified in the extension's configuration file.

In the _doug_print method comes the core of what Doug does: it simply prints what it receives, starting all its lines with a little intro.

The send method is a simplified version the send method of Sardine's native sender (MidiHandler, SuperDirtHandler, etc.): it allows us to use Doug in swim functions and to use patterns.

Doug configuration

The extension's configuration file is a JSON file that describes the project structure:

{
  "root": "/path/to/Doug",
  "package": "doug",
  "handlers": [
    {
      "module": "DougHandler",
      "class": "DougHandler",
      "send_alias": "Doug",
      "params": {
        "intro": "sample"
      }
    }
  ]
}

The params field corresponds to the handler's constructor parameters.

The send_alias field corresponds to the alias that will be given to your handler's send method in Sardine's session (like the D for SuperDirt or N for Midi Notes): this alias will become a global variable so you have to make sure it doesn't conflict with anything else.

Using Doug in Sardine

Using extensions is achieved by referencing the extension configuration files in Sardine's user configuration file:

{
  "config": {
    ...
  },
  "extensions": [
    "/path/to/extension-config.json"
  ]
}

For now you have to edit the file manually by adding the path to the configuration files in the extensions list.

Resetting the user configuration file will clear all references to any extension you may have.

Doug in practice

Now that we are able to use Doug in a Sardine session, let's see an example of what we can do with it:

@swim
def hello_doug(p=1, i=0):
    pat = 'alphabet:[0:26]'
    D(pat, i=i)
    Doug(pat, i=i)
    again(hello_doug, p=1, i=i+1)

Calling Doug will actually call the DougHandler.send method (as it is the alias for it), which will parse the given pattern: hence in this example Doug will print the name of the sample SuperCollider is playing.

The visual output should look like:

sample:: alphabet:0
sample:: alphabet:1
sample:: alphabet:2
...

and the audio output should sound like: "A", "B", "C", ...

Audio Engine Reference

SuperDirt documentation is rather scarce and most of it needs to be inferred by looking at the source code. However, the behavior of most parameters is well known – usually from experience – by live coders. Moreover, SuperDirt can be customised freely to add custom effects and synthesizers. I’m working hard on gathering information about each and every parameter I can find :) Some of them are rather arcane. They are probably not meant to be used directly. Keep in mind that not all of them are useful and that you will likely find better options by building your own environment.

Sampler

Everytime you play an audio sample, this sampler will be invoked. It is pretty powerful and it is worth spending some time studying its behavior.

Parameter Brief description Typical range alias
amp Sound volume (linear scaling) 0->x  
gain Sound volume (exponential scaling) 0->1  
freq Pitch around given frequency 0->x  
midinote Pitch around given MIDI note 0-127  
note Pitch around given note ???  
octave Pitch up or down depending on octave number 0->x oct
sound Implicit (first argument of D())    
begin Start position of audio playback 0->1  
end End position of audio playback 0->1  
speed Sample playback, impacts pitch. Negative will play reverse -x->0->x  
accelerate Rising sample playback speed (pitch glissando) -x->0->x accel
cps Implicit (cycles per second, inherited from Tidal)    
loop ???    
delta Unused    
cut Cut other sounds playing on same orbit, start playing 0 or 1  
legato Play sample for the given duration (without cutting others) 0->x leg
pan Pan sound from left to right speaker (by default) 0->1  
orbit Play sound/synth on the given audio effect bus (0 - 11) 0->11  
latency Add a latency to audio playback (in seconds) 0->x  
lag Similar to latency/offset 0->x  
offset Similar to latency/lag 0->x  

Effects

Reverb

This is a pretty basic metallic sounding reverb. Not very usable but still.

Parameter Brief description Typical range
room Size of the room 0->x
size Size of the reverb - keep below 1 (inf) 0->1
dry Dry/Wet balance 0->1
@swim
def test_fx(p=0.25):
    D('hh', amp=1, room='(sin $.S)', dry=0.1, size='(sin $)')
    again(test_fx, p=0.25)

Delay

The delay effect is initially built for Tidal, which is based on a cyclical time representation. However, it has been pre-configured here to work properly with Sardine. Be careful with the feedback if you don’t want to see things explode!

Parameter Brief description Typical range
delay Wet / Dry 0->1
delaytime Delay time 0->x
delayfeedback Amount of reinjection of the dry signal 0->.99
@swim
def test_fx(p=0.25):
    D('hh',
        speep='1|2|4',
        delay=1/2, delaytime=1/(2/3),
        delayfeedback='0.5+(rand/4)',
        amp=1
    )
    again(test_fx, p=0.25)

Phaser

Parameter Brief description Typical range
phaserrate Speed of phaser 0->x
phaserdepth Depth of phaser 0->x
@swim
def test_fx(p=0.25):
    D('jvbass',
        midinote='C|Eb|G|Bb',
        phaserrate='1~10',
        phaserdepth='(sin $*2)', amp=1
    )
    again(test_fx, p=0.5)

Leslie

This is a simple emulation of a Leslie rotating speaker typically used in music for treating organ sounds, voices, and to add an eary tint to everything that goes through it. This is basically a way to play creatively with doppler effects.

Parameter Brief description Typical range
Leslie Dry / wet 0->1
lrate Rate 0->x
lsize Wooden cabinet size (in meters) 0->x
@swim
def test_fx(p=0.25):
    D('jvbass', amp=1, leslie=0.9,
        lrate=0.1, lsize='0.1+rand*2')
    again(test_fx, p=0.25)

Tremolo

A simple tremolo effect.

Parameter Brief description Typical range
tremolorate Tremolo speed 0->x
tremolodepth Depth of tremolo 0->x
@swim
def test_fx(p=0.25, i=0):
    D('amencutup:[1:20]',
        tremolorate='16|32',
        tremolodepth='[0.0~1.0 0.25]', i=i
    )
    again(test_fx, p=0.5, i=i+1)

Granular weird

This is a weird granular effect probably intended to serve as a building block for some other effect but you can use it as is nonetheless. It will slice your audio sample into tiny fragments of it while applying some amount of pitch-shifting on every sample.

Parameter Brief description Typical range
psrate Pitch-shift rate 0->x
psdisp Pitch-shift dispersion 0->x
scram Weird spectral effect 0->x
binshift Spectral bin shifter 0->x
@swim
def test_fx(p=0.25, i=0):
    D('amencutup:[1:20]',
            psrate='2',
            psdisp='[0:1,0.5]',
            i=i)
    again(test_fx, p=0.5, i=i+1)

Filtering

Parameter alias Brief description Typical range
cutoff lpf Low-pass filter: cutoff freq (hertz) 0 - > x
resonance lpq res Low-pass resonance Q 0.0 - 1.0
hcutoff hpf High-pass filter: cutoff freq (hertz) 0 -> x
hresonance hpq High-pass resonance Q 0.0 - 1.0
bandf bpf Bandpass filter - cutoff freq (hertz) 0 -> x
bandq bpq Bandpass resonance Q 1 - 100+
djf djf DJ Filter: Low pass: 0 - 0.5, High pass: 0.5 - 1.0 0.0 - 1.0
hbrick Spectral high pass Mads Kjeldgaard 0.0 - 1.0
lbrick Spectral low pass Mads Kjeldgaard 0.0 - 1.0

Filter resonance

  • Take caution with filter resonance for lpf and hpf. Values > 0.5 can be harsh!
  • Resonance for bandpass filter bandq is different and often needs higher values (over 10) to be perceived.

This example shows a band pass filter cycling through the harmonic series. Notice the high resonance setting. With bandq=1 the filtering effect is too small to be heard.

# fire is a SuperDirt sample with a spectrum like colored noise
Pa * d('fire', legato=1.5, bandf='100 200 400 600 700 800 900', bandq='100')

Other filter examples:

# low pass randomized
@swim
def test_fx(p=0.25):
    D('jvbass',
        midinote='C|C|Eb|G|Bb',
        cutoff='rand*7000', resonance='rand/2', amp=1
    )
    again(test_fx, p=0.5)

# djf
@swim
def djf(p=1, i=0):
    D('supersaw', n='40 52 64 52',
    djf=random(), i=i)
    again(djf, p=1, i=i+1)

Spectral comb filter

Included in Superdirt, engineered by Mads Kjeldgaard. Width and number of teeth are controlled by one floating point number. Note that as you increase the comb, more frequencies will be filtered out, resulting in reduced gain.

Parameter Brief description Typical range
comb Spectral comb filter 0.0 - 1.0
@swim
def test_fx(p=0.25):
    D('jvbass',
        midinote='C|C|Eb|G|Bb',
        cutoff='rand*7000', resonance='rand/2', amp=1
    )
    again(test_fx, p=0.5)

Distortion

Nobody knows why but SuperDirt is full of distortion effects. I suppose that saturation and distortion are good effects to apply to audio samples. Don't be afraid and try them all.

Squiz

Will distort your signal, combination of multiple effects put together. It works better if you input multiples of two as parameters.

Parameter Brief description Typical range
squiz amount 0.2->x
@swim
def test_fx(p=0.25):
    D('tabla:rand*200', cut=1,
            squiz='0|2|4|8',
            midinote='C|F|Bb|E5b', amp=1)
    again(test_fx, p=0.5)

Triode

Another type of distortion. Emulating a triode distortion unit.

Parameter Brief description Typical range
triode Distortion amount 0->x
@swim
def test_fx(p=0.25):
    D('tabla:rand*200', cut=1,
            triode='rand', # comment me
            midinote='C|F|Bb|Eb5', amp=1)
    again(test_fx, p=0.5)

Distort

Heavy distortion that will/can wildly change the spectrum of your sound.

Parameter Brief description Typical range
distort Distortion amount 0->x
@swim
def test_fx(p=0.25):
    D('sd:rand*200', cut=1,
            distort='0|0.5',
            midinote='C|G', amp=1)
    again(test_fx, p=0.5)

Shaping

One of my favorites. It adds some warness (and loudness) to any sound. It sounds more natural than just pushing gain or amp up. Use it if you want to be loud and gritty, not if you just want to play something louder.

Parameter Brief description Typical range
shape Amplification amount 0->x
@swim
def test_fx(p=0.25, i=0):
    D('amencutup:[1:20]', shape='[0:1,0.1]', i=i)
    again(test_fx, p=0.5, i=i+1)

Crush

Crush will.. crush your sound. You get it!

Parameter Brief description Typical range
crush Crushing Factor 0->x
@swim
def test_fx(p=0.25, i=0):
    D('bd sn hh sn', crush=4, i=i)
    again(test_fx, p=0.5, i=i+1)

Hidden Gems

There are hidden gems in the SuperDirt engine. Note that you can also customize it with your own effects. This documentation, thus, is only covering the tip of the iceberg.

Vowel

Vowel is installed with SuperDirt as a SuperCollider Quark. It uses a formant filter to emulate the sound of vowels applied to the sound.

Parameter Brief description Typical range
vowel formant filter to simulate vowels a e i o u
Pa * d('supersaw', n='40 52 64 52', vowel='e')

Showcase

The Showcase section has examples of live coding using Sardine. It includes Algoraves, Solstice streams, experimental electronic music, and Sardine with graphics.

Anyone using Sardine can submit their work to include in the Showcase. The best way is to submit a pull request, but you can also reach out to Bubo in the Sardine Discord channel for help.

Guidelines:

  • Made using Sardine or involves Sardine
  • Recording available to share (YouTube, Vimeo, Bandcamp, etc )
  • Code available to share (GitHub, or we can include it here)
  • Any styles, any genres, etc.
  • Doesn't have to be a fully polished performance

Join the fun! Share your screens, share your code!

Acousmatic Tekno

 ▄▄▄▄     ▄▄▄▄    ▄▄▄   ▄▄▄ ▄▄▄   ▄▄▄▄  ▄▄ ▄▄ ▄▄    ▄▄▄▄   ▄██▄  ▄▄▄    ▄▄▄▄     
▀▀ ▄██  ▄█   ▀▀ ▄█  ▀█▄  ██  ██  ██▄ ▀   ██ ██ ██  ▀▀ ▄██   ██    ██  ▄█   ▀▀    
▄█▀ ██  ██      ██   ██  ██  ██  ▄ ▀█▄▄  ██ ██ ██  ▄█▀ ██   ██    ██  ██         
▀█▄▄▀█▀  ▀█▄▄▄▀  ▀█▄▄█▀  ▀█▄▄▀█▄ █▀▄▄█▀ ▄██ ██ ██▄ ▀█▄▄▀█▀  ▀█▄▀ ▄██▄  ▀█▄▄▄▀    
                                                                                 
                                                                                 
█▀▀██▀▀█ ▀██▀▀▀▀█  ▀██▀  █▀  ▀█▄   ▀█▀  ▄▄█▀▀██      
   ██     ██  ▄     ██ ▄▀     █▀█   █  ▄█▀    ██     
   ██     ██▀▀█     ██▀█▄     █ ▀█▄ █  ██      ██    
   ██     ██        ██  ██    █   ███  ▀█▄     ██    
  ▄██▄   ▄██▄▄▄▄▄█ ▄██▄  ██▄ ▄█▄   ▀█   ▀▀█▄▄▄█▀     
                                                     
                                                     
clock.tempo=187

silence()

Pi * d("teratoma:[1~150]", leg=0.25, p=0.5, begin="rand")

Pi * d("spore:[1~150]", leg=0.25, p=0.5, begin="rand")

Pd * d('teratoma:[0:150]', amp=0.4, p=.5, leg=.05, pan='rand', speed='(if (beat 0 2) 1 0.5)')
Pe * d('.!8 spore:[0:50]', p=.25, leg=.1, pan='rand', on=2, speed=2)

PD * d('crunchorganic:[0:150]', amp=0.4, p=.5, leg=.05, pan='rand', speed='(if (beat 0 2) 1 0.5)')

PE * d('.!8 proximity:[0:50]', p=.25, leg=.1, pan='rand', on=2, speed=2)

Pk * d("kick:1 . laSNARE:[1~18] .", p=0.5, shape=0.45, room=0.15)

ph * d("leHIHAT:[8]", leg=0.25, p=0.25, orbit=2, shape=0.2)

PP * d("[proximity:[0:50] spore:[0:150]]", p=0.125, speed="(lsin 2)", 
        lpf="150*(lsaw 4)", leg=0.2, res=0.2)

Pk * d("kick:1 kick:[0~8] laSNARE:[1~18] .", p=0.5, shape=0.45,room=0.26)

ph * d("leHIHAT:[9:12]", leg=0.15, p=0.25, orbit=2, shape=0.2)

#double loud kicks c64 vibe

Pk * d("{kick:1 sid:2} {kick:[0~8] sid:2} laSNARE:[1~18] sid:2", p=0.5, room=0.26,shape=0.65)

#acousmatic break ?

silence()
Pk * d("{kick:1 sid:2} {kick:[0~8] sid:2} laSNARE:[1~18] sid:2", room=0.6,p="(rand)*0.5", shape=0.45,lpf=1000)
Pd * d('teratoma:[0:150]', amp=0.4, p=.5/4, leg=.1, pan='rand', speed='(if (beat 0 2) 1 0.5)')
Pe * d('.!8 spore:[0:50]', p=.25/4, leg=.3, pan='rand', on=2, speed="(lsaw 8)")
PD * d('crunchorganic:[0:150]', amp=0.4, p=.5/4, leg=.1, pan='rand', speed='(if (beat 1 3) 1 0.5)')
PE * d('.!8 proximity:[0:50]', p=.25/4, leg=.3, pan='rand', on=1, 
        speed="(lsaw 8)",lpf="2502*(lsin 8)")

silence(Pk,Pd,Pe,PD)
Pk * d("protokick", lpf="250*(lsaw 4)",res="(lsin 1)*0.5",p=0.25)

Pk * d("(eu protokick 3 5)", shape=0.9,lpf="250*(lsaw 4)",res="(lsin 1)*0.5",p=0.5)

Pd * d('teratoma:[0:150]', amp=0.4, p=.5, leg=.05, pan='rand', speed='(if (beat 0 2) 1 0.5)')
Pe * d('.!8 spore:[0:50]', p=.25, leg=.1, pan='rand', on=2, speed=2)
PD * d('crunchorganic:[0:150]', amp=0.4, p=.5, leg=.05, pan='rand', speed='(if (beat 0 2) 1 0.5)')
PE * d('.!8 proximity:[0:50]', p=.25, leg=.1, pan='rand', on=2, speed=2)
ph * d("leHIHAT:[9:12]", leg=0.25, p=0.25, orbit=2)
Pk * d("(eu protokick 3 5)", gain=0.93,shape=0.9,lpf="2500*(lsaw 4)",res="(lsin 1)*0.5",p=0.5)

#boom boom [o] [o]

solo(Pk)

Pk * d("(eu protokick [2~5] [6~9])", gain=0.93,shape=0.9,lpf="2500*(lsaw 4)",res="(lsin 1)*0.5",p=0.5)

silence()

Transport Variations

Transport Variations (by HighHarmonics) is a composed live coding work of experimental electronic music. It is loosely an hommage to the early French pioneers of musique concrète, Pierre Schaeffer and Pierre Henry.

  • Custom samples are from field recordings of public transportation - light rail, bus hydraulic lift, streetcars.
  • A set of performance presets have been composed and arranged based on each of the main samples.
  • Live coding is done in Sardine, with use of Sardine features:
  • Sardine, python, and MaxMSP code files in GitHub.

Organization

Presets are built up (composed) by:

  • Generating sample slice values (begin/end points) together with a pulse value ("period" in Sardine) - with a custom python function that uses contrained randomness.
  • Adding values or patterns for amplitude, sample speed, frequency, sample duration, pan.
  • Adding sample processing and signal processing parameters (fx).

Preset variations

  • Presets values are assigned to the Sardine Amphibian Variables which are used in the main @swim function.
  • Presets values are also saved in the performance code to support alternation and variations of presets. This is how the structure of the performance is built.
  • Liveness is maintained through constant experimentation with the preset variations and preset combinations.
  • Put simply - I first generate and compose the presets, then in a performance session, play and vary the presets.

Audio processing

  • Output from SuperCollider is routed to MaxMSP, which acts as a mixer.
  • An EQ type filter is added to each channel using FabFilter Pro-Q3.
  • EQ filters also have presets, which are changed with a Midi Controller.
  • Display of the filter output during performance shows how the filter changes impact the audio.

Transport Variations in Sardine

Code files available in HighHarmonics Github

  • Sardine Players (Pa, Pb, etc) are set up to play the transport sample files. Multiple players for each sound allow for variants with different parameter and pattern values. These work well for playback where the sound source is more recognizable.

    # rail car rattles
    Pa * d('trimet:0', p=2, legato=2, amp='[0.1:1,0.12] [1:0.1,0.12]', pan='[0:1,0.1] [1:0,0.15]', **rev1, orbit=4)
    Pa * d('trimet:0', p=2, legato=1.2, amp='.8', speed= 1, pan='[0:1,0.1]', **rev1, orbit=0)
    
    # air blast
    Pb * d('trimet:3', p='2 3 1', legato='0.8 2 0.4', amp='0.4~0.8', freq='260!2 280', pan='rand', comb='[.1~.4]', **rev1, orbit=5)
    
    # streetcar
    Pc * d('trimet:5', p=2, pan='[0:1,.2]', amp='0.5 0.9 0.2', legato=8, **rev0, orbit=6)
    
  • Sardine @swim functions are setup with a group of 9 Amphibian variables controlling parameter values of sample, begin, end, speed, frequency, amp, pan, legato, period. Presets for reverb and effects processing parameters are included in the @swim.

    @swim
        def slicerA1(p=1, i=0):
        D(V.s, chance='often',
        begin=V.b, end=V.e, speed=V.S, amp=V.A, pan=V.P, freq=V.F,
        legato=V.L, 
        **rev1, # del0,
        **fx, 
        d=1, rate=1, orbit=0, i=i)
        again(slicerA1, p=P('(v p)', i), i=i+1) 
    
  • The Amphibian Variable presets are grouped into a python dictionary (presetsTrimet). Calling a dictionary key is used to assign all of the values into the Amphibian Variables and FX parameters. Executing on this while the @swim is running will change all of the Amphibian Variables and FX values at once. It does this on the next @swim iteration and is not subject to the normal rule of change happens only at the next bar. Example:

    V.s, V.b, V.e, V.p, V.A, V.S, V.F, V.L, V.P, fx, desc = presetsTrimet['speed1']
    fx = {'accelerate': 0, 'comb':0.4, 'shape':0.5, 'vowel':'0 e a', 'hpf':'200 300 800 1200 300', 'hresonance':'0.4' }
    
  • Presets are also replicated in the execution code so that individual parameter changes can be easily accomplished. Example:

    V.s, V.b, V.e, V.p, V.A, V.S, V.F, V.L, V.P, fx, desc = ['trimet:0', 0.5397, 0.775, .4, 0.8, 1, 260, 0.5, '[0 1]', {'accelerate':1, 'comb':0.1, 'shape':0.2, 'vowel':'e a'}, 'speed1']
    
  • This results in a large amount of code that is essentially only for variable and preset assignment. But it makes the act of evolving the variations much easier.

  • The end result is that large or small changes can be made instantly building a musical structure or shaping the constantly evolving variations.

D'Auzon

Live-coding réalisé sous Sardine (sardine.raphaelforment.fr). 
Matière sonore synthétisée et manipulée au travers du live-coding dans Supercollider.

- Rémi

clock.tempo = 60

D("long:5", attack = 1,release =15, accelerate = (-1),gain = 0.5, room = 0.5)
D("long:5", attack = 1,release =15,speed = 0.5, accelerate = (-1),gain = 0.5, room = 0.5)
D("long:5", attack = 1,release =15,speed = 0.6, accelerate = (-1),gain = 0.5, room = 0.5)
D("long:5", attack = 1, release =15,speed = 0.8, accelerate = (-1),gain = 0.5, room = 0.5, sz = 0.6)

@swim
def downsweep(p=0.5, i=1):
    D("long:5", attack = 2, release =10,
            coarse = 6, pan = "rand",
            speed = 0.8, accelerate = "1,-1,2,-2",
            gain = "0.9,0.8,0.9,0.8", room = 0.5, sz = 0.6, i=i)
    again(downsweep,p=P("8,4,16",i), i=i+1)

@swim
def downsweep2(p=0.5, i=1):
    D("long:10", release =2.5,
           # coarse = 6, pan = "rand",
            speed = 0.8,
            #speed = "0.8,0.70,0.75,0.70",scram = "0!8,0.5!2",
            #speed = "0.73,0.75,0.70,0.75,0.75,0.85", scram = "0!2, 0.5!6",
            gain = "1.2,1.1,1.1,1.2", room = 0.2, sz = 0.6, 
            i=i)
    again(downsweep2,p=P("1!2,0.25!4,1!2,0.5!2,16",i), i=i+1)

@swim
def romble(p=0.5,i=1):
    D("long:[1~32]", lpf=100, legato = 2, i=i)
    again(romble,p=P("0.5!32,32",i),i=i+1)

@swim
def shark(p=0.5,i=1):
    D("pluck", scram = 0.2, binshift = 1,
            gain =0.6, i=i)
    D("long:9", legato = 4, 
            i=i)
    again(shark,p=P("4,2,4",i),i=i+1)


panic()


panic()

Dumpster Dive (Sardine Alpha)

dumpsterDive (HighHarmonics) is a short piece using composed live-coding practices. With the samples and code below, it can be performed using Sardine v0.1.0 - Alpha. It is intended to be freely shared, much like a musical score or sheet music can be used by anyone to perform and interpret. The Sardine @swim function uses the stacked samples model, organized into sections which function like different instruments. One sender in each section should be played alone or together with senders in others sections. Performance Video.

  • Custom samples: field recordings of a metal dumpster
  • Dumpster samples are available via the sardine-sounds repository.
  • A conversion / updated version to a current Sardine release is underway.
# Load audio effect and preset dictionaries first.
# Livecoding: Play one or both lines from each section (basic, reverse rhythms, melodic patterns, bass, scrape). Explore combinations.
c.bpm=60

@swim
def dumpsterDive(d=1, i=0):
## basic samples - cycle thru the sounds used by all layers
    S('dumpster:[0,1,4,2,5,3,.!2]', speed=1, amp=.5, **rev1, orbit=0).out(i, div=8)
    #S('dumpster:[2,0,1,4,3,5]', speed='2', pan=.7, amp=.5, **rev1, orbit=1).out(i, div=2) #**del1
## reverse rhythms
    #S('dumpster:1', begin=.065, end=.4, speed='-1', pan='[.14:.84,0.1],[.83:.15,0.1]', amp=.8, **rev0, orbit=2).out(i, div4)
    #S('dumpster:0!2,.', begin=0, end=.85, speed='1,-1', pan='[.9:.1,0.2],[.1:.9,0.2]', amp=.6, **rev1, orbit=3).out(i, rate=1, div=2) #**del1,
## melodic patterns
    #S('dumpster:[1!2,4!2,5!2,4]', begin=.052, end=.088, freq='[414,240,620,.,500,380,820,750]', timescale=1.4, pan='[.1,.9]', amp=.95, **rev2, orbit=4).out(i, rate=1, div=1)
    #S('dumpster:[0,1,0,3,4,]', begin='0', end='.2',speed='1', amp=.6, pan=.3, **rev1, orbit=5).out(i, rate=1, div=2)
    #S('dumpster:[6!2,8!3,7!2]', octave='7', cut=1, pan=.3, amp=.9, **rev2, orbit=6).out(i, div=4)
## bass - choose one or the other
    #S('dumpster:[2,1,0,.,4,1]', octave=4, amp=.5, **rev1, orbit=7).out(i, rate=1, div=4)
    #S('dumpster:2', octave='[4.8:5.1,.04],[4.6:4.8,.04]', amp=.95, **rev1, orbit=8).out(i, div=4)
## scrape
    #S('dumpster:[5,.,5]', octave='6', cut=1, amp=1.2, **del1, **rev1, orbit=8).out(i, rate=.5, div=4)
    #S('dumpster:[5,.,5]', octave='[6!3,6.62,5.4]', cut=1, amp=.9, **rev2, orbit=9).out(i, rate=1, div=1) #**del2
## presets
    #returnGroove['bass1'].out(i, rate=1, div=4)
    #returnGroove['melody2'].out(i, rate=1, div=2)
    #closing['basic2'].out(i, div=2)
    #closing['basic2a'].out(i, rate=2, div=2) ## div=1
    #closing['scrape2'].out(i, rate=1, div=2)
    #c.bpm = P('[60:90,.03]', i) #accelerate tempo at the end

    a(dumpsterDive, d=1/8, i=i+1)

######################################################
#hush(dumpsterDive)

######################## LOAD THESE FIRST - python dictionarys referenced in dumpsterDive function #################
c.bpm=60
## audio effects
rev0 = {'room':.8, 'size':0.5, 'dry':0.5}
rev1 = {'room':.9, 'size':0.6, 'dry':0.4}
rev2 = {'room':1.5, 'size':0.7, 'dry':0.4}
rev3 = {'room':2, 'size':0.8, 'dry':0.3}

del0 = {'delay':0.5, 'delaytime':0.3, 'delayfeedback':0.5, 'triode':0}
del1 = {'delay':0.5, 'delaytime':0.4, 'delayfeedback':0.6}
del2 = {'delay':0.5, 'delaytime':0.25, 'delayfeedback':0.8}

#Preset dictionaries
returnGroove = {'bass1':  S('dumpster:[2,1,0,.,4,1]', octave=4, amp=.5, **rev1, orbit=7),
    'melody2': S('dumpster:[0,1,0,3,4,]', begin='0', end='.2',speed='1', amp=.4, pan=.3, **rev1, orbit=5) }
closing = {'basic2': S('dumpster:[12,0,1,4,3,5]', speed='2', pan=.7, amp=.5, **del1, **rev1, orbit=1),
    'basic2a': S('dumpster:[0,5,3,4,3,0]', speed='[2.01:1.96,.01],[1.96:2.01,.01]', pan='[.99:.01,0.3],[.01:.99,0.3]', amp=.7, **rev1, **del2, orbit=1),
    'scrape2': S('dumpster:[5,.,5]', octave='[6!3,6.62,5.4]', cut=1, amp=.9, **del2, **rev2, orbit=9) }

Solstice (v.0.2.0)

This piece was submitted by BuboBubo. This is a 20 minutes performance that I did for the TidalCycles annual solstice stream. Three of the four tracks were rapidly composed before the stream. I’ve tried to highlight some of the new features we have worked on for the v.0.2.1. I am sometimes playing additional keyboard on top of the code. Performance Video.

padcc = { 'timbre': {'control' : 18, 'chan': 2},
        'time': {'control' : 19, 'chan': 2},
        'metal': {'control' : 16, 'chan': 2},
        'fx': {'control' : 17, 'chan': 2}}
basscc = { 'timbre': {'control' : 18, 'chan': 0},
        'time': {'control' : 19, 'chan': 0},
        'cutoff': {'control' : 16, 'chan': 0},
        'fx': {'control' : 17, 'chan': 0}}
jupcc = { 'decay': {'control' : 81, 'chan': 1},
        'time': {'control' : 19, 'chan': 1},
        'cutoff': {'control' : 74, 'chan': 1},
        'resonance': {'control' : 71, 'chan': 1}}
dirt._ahead_amount = 0.4

#######################################################################
# FRENCH TOUCH SAMBA
#######################################################################

PE >> d('long:3', cut=1, begin="[0.0:0.6,0.1]")

Pc >> d('ff:4!3, gg:12', cut=1, p=0.25, orbit=1, shape=0.5)

Pb >> d('f!7', cut=0, p=1, orbit=2, shape=0.5)

Pd >> d('g:10', p='.5, .5, .25', orbit=2, shape=0.5, speed='2,2,1!2,4')

Pb >> None
Pc >> None
Pc >> d('bip:rand*20', speed=2,
        cut=0, p=0.25, orbit=1, shape=0.5, hcutoff='[500:15000,1000]')

# ---

PE >> d('long:3', cut=1, begin="[0.0:0.6,0.1]")
Pc >> d('ff:4!3, gg:12', cut=1, p=0.25, orbit=1, shape=0.5)
Pb >> d('f!7', cut=0, p=1, orbit=2, shape=0.5)
Pd >> d('g:10', p='.5, .5, .25', orbit=2, shape=0.5, speed='2,2,1!2,4')
Pf >> d('bip:rand*50', speed=2, midinote='C5,C5,G5,A5',
        cut=1, p=0.25, orbit=1, shape=0.5)
Pg >> d('bip:rand*50', squiz=4, speed=1, midinote='C3,C4,G3,G4,A4,A5', shape=0.5,
        cut=1, p=0.25, orbit=1)

# ---

Pa >> None
Pb >> None
Pc >> None
Pd >> None

PE >> d('long:3', cut=1, begin="[0.0:0.6,0.1]")
Pc >> d('ff:4!3, gg:12', cut=1, p=0.25, orbit=1, shape=0.5)
Pb >> d('f!7', cut=0, p=1, orbit=2, shape=0.5)
Pd >> d('g:10', p='.5, .5, .25', orbit=2, shape=0.5, speed='2,2,1!2,4')
Pf >> d('bip:rand*50', speed=2, midinote='C5,C5,G5,G5',
        cut=1, p=0.25, orbit=1, shape=0.5)
Pg >> d('bip:rand*50', squiz=4, speed=1, midinote='C5@fifths', shape=0.5,
        cut=1, p=0.25, orbit=1)

Pa >> None
Pb >> None
Pc >> None
Pd >> None
PE >> d('long:3', cut=1, begin="[0.0:0.6,0.1]", speed='2!4,4!4')


PE >> d('long:3', cut=1, begin="[0.0:0.6,0.1]")
Pc >> d('ff:4!3, gg:12', cut=1, p=0.25, orbit=1, shape=0.5)
Pb >> d('f!7', cut=0, p=1, orbit=2, shape=0.5)
Pd >> d('g:10', p='.5, .5, .25', orbit=2, shape=0.5, speed='2,2,1!2,4')
Pf >> d('bip:rand*50', speed=2, midinote='C5,C5,G5,G5',
        cut=1, p=0.25, orbit=1, shape=0.5)
Pg >> d('bip:rand*50', squiz=4, speed=1, midinote='C5@fifths', shape=0.5,
        cut=1, p=0.25, orbit=1)

###################################################################
# CAROTTE INTERLUDE
###################################################################

panic()

@swim
def baba(p=0.5, i=0):
    D('juppad:54, juppad:55', cutoff=2000, begin=0.1,
      orbit=2, cut=0, legato=1.1, i=i, d=8, r=0.25)
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    D('juppad:54, juppad:55', cutoff=5000, begin=0.1,
      orbit=2, cut=0, legato=1.1, i=i, d=8, r=0.25)
    D('boop:rand*20', shape=0.4,
      midinote='G4|G5,Bb5,F6, G4|G5,Bb5,G6', i=i, r=0.25, d=2)
    D('boop:rand*40')
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    # D('f', shape=0.4, i=i, d=4)
    # D('f:3', amp='[0:0.4,0.05]', legato='0.01~0.2', i=i)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i, d=1)
    D('juppad:54, juppad:55', cutoff=5000, begin=0.1,
      orbit=2, cut=0, legato=1.1, i=i, d=8, r=0.25)
    D('.., p:6, ., .., p:3, ..', legato=0.5, shape=0.7, i=i)
    D('bip:rand*20', midinote='adisco((G|[G,G|Ab|G5])!2)', i=i, d=2)
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    D('f, f, ..', shape=0.4, i=i, d=4)
    D('f:4', amp='[0:0.4, 0.05]', legato='0.1~0.5', i=i)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i)
    D('juppad:54, juppad:55', cutoff=5000, begin=0.1,
      squiz=2, orbit=2, cut=0, legato=1.1, i=i, d=8, r=0.25)
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    D('f', shape=0.4, i=i, d=4)
    D('f:8~12', speed='4~8', amp='[0:0.4, 0.05]', legato='0.1~0.5', i=i)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i, d=1)
    D('laz:rand*20',
            speed="1, 2,4",  hcutoff='3000~6000',
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4, i=i, d=0.25)
    D('juppad:54, juppad:55', cutoff=5000, begin=0.1,
      squiz='0!4,2',
      orbit=2, cut=0, legato=1.1, i=i, d=8, r=1)
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    # D('f', shape=0.4, i=i, d=4)
    # D('f:3', speed=4, amp='[0:0.4, 0.05]', legato='0.1~0.5', i=i)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i)
    D('laz:rand*20',
            speed="1, 2,4",  hcutoff=6000,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4, i=i, d=1, r=0.25)
    D('juppad:54, juppad:55', cutoff=5000, begin=0.1,
      pan='r', speed='1|2|4', leslie=1, lesliespeed=8,
      orbit=2, cut=0, legato=1.1, i=i, d=8, r=0.25)
    again(baba, p=1/4, i=i+1)

@swim
def baba(p=0.5, i=0):
    D('f', shape=0.4, i=i, d=4)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i)
    D('conga:rand*20', speed="[1,2,4]/4", hcutoff='500~1000', shape=0.4,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.5, i=i, d=1, r=0.25)
    D('kit2:3', shape=0.5, i=i, d=8)
    D('., kit2:10, ., kit2:9!2', shape=0.5, i=i, d=2)
    again(baba, p=1/4, i=i+1)


@swim
def baba(p=0.5, i=0):
    D('f', shape=0.4, i=i, d=4)
    D('.., p:5, .', legato=0.5, shape=0.7, i=i)
    D('conga:rand*20', speed="[1,2,4]/4", hcutoff='500~1000', shape=0.4,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.5, i=i, d=1, r=0.25)
    D('conga:rand*20', speed="[1,2,4]/2", hcutoff='2000', shape=0.4,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.5, i=i, d=2, r=0.5)
    D('kit2:3', shape=0.5, i=i, d=8)
    D('., kit2:10, ., kit2:9!2', shape=0.5, i=i, d=2)
    again(baba, p=1/4, i=i+1)

# Ici on joue uniquement avec les percus et on lave les oreilles

@swim
def baba(p=0.5, i=0):
    D('f:3', amp='[0:0.2,0.01]', legato='0.1~0.5', i=i)
    D('.., p:(5|10), .', legato=0.5, i=i, d=1)
    D('m|c:[4:9]', legato=0.2, i=i, d='4!12, 3!12')
    D('jupbass:[1:100]', # -> lost into jupfx
            cutoff=3000, # ->
            shape=0.5,
            pan='sin($/40)', # -> X
            legato=0.2, # ->
            begin='r', i=i)
    again(baba, p=1/4, i=i+1)


@swim
def baba(p=0.5, i=0):
    D('a', shape=0.7, i=i, d=4)
    D('c', shape=0.7, i=i, d=3)
    D('d:7', orbit=3, room=0.2, size=0.8, dry=0.2, i=i, d=8)
    D('hhh:3', amp='[0:0.2, 0.01]', legato='0.1~0.5', i=i)
    D('f:3', amp='[0:0.2,0.01]', legato='0.1~0.5', i=i)
    D('.., p:(5|10), .', legato=0.5, i=i, d=1)
    D('m|c:[4:9]', legato=0.2, i=i, d='4!12, 3!12')
    D('jupbass:[1:100]', # -> lost into jupfx
            cutoff=3000, # ->
            shape=0.5,
            pan='sin($/40)', # -> X
            legato=0.2, # ->
            begin='r', i=i)
    again(baba, p=1/4, i=i+1)

panic()
D('girls:2')


#####################################################################
# COMPUTER LAMENTO
#####################################################################

@swim
def structure(p=0.5, i=0):
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("[G6]-[0:12]", chan=2, vel=120, i=i, r=0.25/2)
    CC(**jupcc['cutoff'], value=100)
    CC(**jupcc['decay'], value=80)
    N("[G6]-[0:12]", chan=1, vel=120, i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("[G6|D5]-[0:12]", chan=2, vel=120, i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    CC(**padcc['timbre'], value='50~120')
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("[G6]-[0:12]", chan=2, vel=120, i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("[G6]-[0:12]", chan=2, vel=120, i=i, r=0.25/2)
    N("[G7]-[0:12]", chan=2, vel=120, i=i, r=0.25/1)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    CC(**padcc['timbre'], value='(90~100)-10') # go down
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4) # middle voice
    N("Eb4, F4, G4", chan=2, vel='50~100', i=i, r=0.25/2)
    N("pal(C|C5|C6@minor)", d=2,
      chan=2, vel='50~100', i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    CC(**padcc['timbre'], value='(70~110)') # go down
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("Eb4, F4, G4", chan=2, vel='50~100', i=i, r=0.25/2)
    N("pal(C|C5|C6@minor)", d=2,
      chan=2, vel='50~100', i=i, r=0.25/2)
    CC(**basscc['timbre'], value='rand*127')
    CC(**basscc['fx'], value='80')
    CC(**basscc['cutoff'], value='[1:127,20]')
    N("disco(pal(C3|C5|C4@minor))", d=1,
      chan=0, vel='(50~100)-30', i=i, r=0.25)
    again(structure, p=0.5, i=i+1)

@swim
def structure(p=0.5, i=0):
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("Eb4, F4, G4", chan=2, vel='50~100', i=i, r=0.25/2)
    CC(**basscc['cutoff'], value=127, i=i)
    N("pal(C|C5|C6@minor)", d=2,
      chan=2, vel='50~100', i=i, r=0.25/2)
    N("disco(pal(C3|C5|C4@minor))", d=1,
      chan=0, vel='50~100', i=i, r=0.25)
    D('ff', d='3, 3, 2', i=i, cutoff=2500)
    D('s, u, n, d, o, w, n', d='3, 3, 2', i=i, p=0.5)
    D('kk:2~8, bb:1~9', legato=0.2, d='2, 3, 1!4', i=i,
      speed='0.25, 0.5!5, 1!8')
    again(structure, p=0.5, i=i+1)


@swim
def structure(p=0.5, i=0):
    N("C2,C3", chan=2, vel=120, i=i)
    N("G5,G4", chan=2, vel=120, i=i, r=0.25/4)
    N("Eb4, F4, G4", chan=2, vel='50~100', i=i, r=0.25/2)
    CC(**basscc[pick('timbre', 'cutoff')], value='20~120', i=i)
    CC(**basscc[pick('time')], value='20', i=i)
    N("pal(C|C5|C6@minor)", d=2,
      chan=2, vel='50~100', i=i, r=0.25/2)
    N("disco(pal(C4|C6|C5@minor))", d=1,
      chan=0, vel='50~100', i=i, r=0.25)
    D('ff', d='3, 3, 2', i=i, cutoff=2500)
    D('s, u, n, d, o, w, n', d='3, 3, 2', i=i, p=0.5)
    D('kk:2~8, bb:1~9', legato=0.2, d='2, 3, 1!4', i=i,
      speed='0.25, 0.5!5, 1!8')
    again(structure, p=0.5, i=i+1)

Pb >> d('g,o,o,d,b,y,e,t,r,a,c,k', d='1', p=0.5, orbit=2, cut=0)

@swim
def structure(p=0.5, i=0):
    N("C2,C3, F2, F3", chan=2, vel=120, i=i)
    N("G5,G4, Ab5, Ab4", chan=2, vel=120, i=i, r=0.25/4)
    N("Eb4, F4, G4, Eb4, Eb5, Eb4, Eb5", chan=2, vel='50~100', i=i, r=0.25/2)
    N("pal(F|F5|G6@minor)", d=2,
      chan=2, vel='50~100', i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)


Pc >> d('s, u, n, d, o, w, n', d='3, 3, 2', p='0.25!16, 0.5!4', orbit=3, cut=1, speed='2,4')

@swim
def structure(p=0.5, i=0):
    N("pal(F|F4|G3@minor)", d=2,
      chan=2, vel='100~120', i=i, r=0.25/2)
    N("pal(F|F5|G6@minor)", d=2,
      chan=2, vel='100~120', i=i, r=0.25/2)
    again(structure, p=0.5, i=i+1)

############################################################
# IDEE POUR UN TROISIEME MORCEAU
############################################################

silence(structure)
Pc >> None
@swim(snap=0)
def baba(p=0.5, i=0):
    D('ff', i=i, d=4, shape=0.5)
    D('s:[1:20]', i=i, d=3, speed='1|1|2|4', legato=0.4, pan='r')
    D('l:[1:20]', i=i, d=2, speed='1|1|2|4', legato=0.2, pan='r')
    D('jupfx:[0:20]', midinote='rev(C3, Eb3, G, Bb4|Bb5)',
      room=0.5, size=0.21, dry=0.12, orbit=3, amp=0.25,
      i=i, d=2, speed='1|1|2|4', legato=0.08, pan='r')
    again(baba, p=0.25, i=i+1)


Pb >> None
@swim(snap=0)
def baba(p=0.5, i=0):
    D('long', orbit=3, cut=1, begin='r', i=i)
    D('ff', i=i, d=4)
    D('kit2:[1,20]', legato=0.1, i=i, d='3!32, 4!16', speed='1,2')
    again(baba, p=0.25, i=i+1)


@swim(snap=0)
def baba(p=0.5, i=0):
    D('ulh:60', orbit=3, cut=1, begin='r', i=i)
    D('ff', i=i, d=4)
    D('ff:9', i=i, d=8, orbit=2)
    if sometimes():
        D('ff:rand*40', i=i, d=2, orbit=2, legato=0.1)
    else:
        D('bb|gg:rand*40', speed='<1,2>,4', i=i, d=1, orbit=2, legato='0.01~0.2')
    D('kit2:[1,20]', legato=0.1, i=i, d='3!32, 4!16', speed='1,2')
    again(baba, p=0.25, i=i+1)
# Change p to 2, I don't know why but it is working

panic()


##################################################################
# VOLAILLE DE BRESSE
##################################################################

Pa >> d('juppad:12|51', begin='r', amp=0.20, speed='1', legato=4,
        room=0.5, orbit=3, dry=0.2, size=0.8,
        midinote='Do,Fa,Ab3,Eb4', cutoff=4000)

Pb >> d('bip:rand*50', begin='0,0.2,0.5', amp=0.45, speed='2',
        room=0.5, orbit=3, dry=0.2, size=0.8,
        legato=0.18, midinote='adisco(Do,Fa,Ab3,Eb4)', cutoff=8000, p=0.5)

Pd >> d('ff:4', shape=0.5, speed=1, p=0.5, cutoff='[200:2000,100]', amp=0.5)


Pa >> d('juppad:12|51', begin='r', amp=0.20, speed='1', legato=4,
        room=0.5, orbit=3, dry=0.2, size=0.8,
        midinote='Do,Fa,Ab3,Eb4', cutoff=4000)
Pb >> d('bip:rand*50', begin='0,0.2,0.5', amp=0.45, speed='2',
        room=0.5, orbit=3, dry=0.2, size=0.8,
        legato=0.18, midinote='adisco(Do,Fa,Ab3,Eb4)', cutoff=8000, p=0.5)
Pc >> d('ff', shape=0.5, speed=1, p=1, cutoff='[2000:5000,100]')
Pc >> d('nn:4~8', legato=0.2,
        shape=0.5, speed='1,2', p=0.5, cutoff='[2000:5000,100]')
Pe >> d('ff', shape=0.5, speed=1, p=2, cutoff='[200:2000,100]')

Pc >> d('[f,i,s,h,e,s]:[1:20]', shape=0.5, p=0.5, legato=0.02, pan='r')
Pd >> d('euclid([gg:rand*20]!8, 5,8)', shape=0.5, speed=4,
        p=0.5, cutoff='5000', resonance='0.1,0.2')

Pb >> None # d('j, a, j, a', orbit=2, p='1,0.5')
Pc >> None # d('f, l, o, w, e, e:rand*4', shape=0.5)
Pd >> None # d('bb:5~6', p='0.25, 0.125', legato=0.05)

panic()

Zorba in Belleville

This code is taken from an algorave that took place at the Zorba (Belleville, Paris) in early november (2022). It is a very straightforward dance oriented performance that plays a lot with simple audio sample manipulations. As stated in the opening banner, this performance was meant to test the stability of Sardine after introducing new features and control mechanisms. Everything lives in the baba function, meaning that you only need to keep track one function during the whole performance. Performance video Sounds are extracted from a very heavy sound library, lazy-loaded when needed. This is how I like to make music, extracting a lof of raw audio files from my hard disk :)

# ██████████████████████████████████████████████████████████████████████████████
# █                                                                            █
# █  █   ▄▄   █▀█ ▄▀█ █▀▄▀█ █▀▀ █▀█   █▀ ▄▀█ █▄░█ █▀   █▀█ ▄▀█ █▀▄▀█ █▀▀       █
# █  █   ░░   █▀▄ █▀█ █░▀░█ ██▄ █▀▄   ▄█ █▀█ █░▀█ ▄█   █▀▄ █▀█ █░▀░█ ██▄       █
# █                                                                            █
# ██████████████████████████████████████████████████████████████████████████████

@swim
def baba(d=0.5, i=0):
    S('juppad:3, juppad:4', cutoff=5000, begin=0.1, orbit=2, cut=0, legato=1.1).out(i, 8, 0.25)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('juppad:3, juppad:4', cutoff=5000, begin=0.1, orbit=2, cut=0, legato=1.1).out(i, 8, 0.25) # up
    # S('bip:rand*20', shape=0.4, midinote='quant([0,3,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    S('boop:rand*40').out()
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    # S('f', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    # S('hhh:3', amp='[0:0.4,0.05]', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    # S('.., p:6, ., .., p:3, ..', legato=0.5, shape=0.7).out(i, 1)
    S('juppad:3, juppad:4', begin=0.1, orbit=2, cut=0, legato=1.1).out(i, 8, 0.25)
    # S('bip:rand*20', midinote='adisco((C|[C,F|Ab])!2)').out(i, 2) # petit surplus harmonique
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f, f, ..', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    S('hhh:3', amp='[0:0.4, 0.05]', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('juppad:3, juppad:4', begin=0.1, orbit=2, cut=0,
            legato=1.1, speed='1',
            crush=4).out(i, 8, 0.25) # -> ici il y a de la réduction
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    S('hhh:3', amp='[0:0.4, 0.05]', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('laz:rand*20',
            speed="1, 2,4",  hcutoff=6000,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    S('juppad:3, juppad:4', begin=0.1, orbit=2, cut=0,
            legato=1.1, speed='1, 2',
            crush=4).out(i, 8, 0.25) # -> ici il y a de la réduction
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    S('hhh:3', amp='[0:0.4, 0.05]', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('laz:rand*20',
            speed="1, 2,4",  hcutoff=6000,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    S('juppad:3, juppad:4', begin=0.1, orbit=2, cut=0,
            pan='r',
            legato=1.1, speed='1|2|4', leslie=1, lesliespeed=8,
            crush=12).out(i, 8, 0.25) # -> ici il y a de la réduction
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('., f', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    # S('juppad:3, juppad:4', orbit=2, cut=0, legato=1.1).out(i, 8, 0.25)
    S('laz:rand*20',
            speed="1, 2,4",  hcutoff=3000, legato=1,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    S('juppad:3, juppad:4',
            speed=0.75, squiz=2,
            orbit=2, cut=0,
            legato=1.1).out(i, 8, 0.25)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f', shape=0.7).out(i, 4)
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('conga:rand*20', speed="[1,2,4]/4", hcutoff=2000, shape=0.7,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    S('juppad:3, juppad:4',
            speed=0.75, squiz=2,
            orbit=2, cut=0,
            legato=1.1).out(i, 8, 0.25)
    S('kit2:3', shape=0.5).out(i, 8)
    S('., kit2:10, ., kit2:9!2', shape=0.5).out(i, 2)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f', shape=0.7).out(i, 4)
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('conga:rand*20', speed="[1,2,4]/4", hcutoff=2000, shape=0.7,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    S('conga:rand*20', speed="[1,2,2]/2", hcutoff=1000, shape=0.7,
              room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.5)
    # S('juppad:3, juppad:4', # commenter ce bloc
    #         speed=0.75, squiz=2,
    #         orbit=2, cut=0,
    #         legato=1.1).out(i, 8, 0.25)
    S('kit2:3', shape=0.5).out(i, 8)
    S('., kit2:10, ., kit2:9!2', shape=0.5).out(i, 2)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    # S('f', shape=0.7).out(i, 4)
    S('.., p:5, .', legato=0.5, shape=0.7).out(i, 1)
    S('conga:rand*20', speed="[1,2,4]/4", hcutoff=2000, shape=0.7,
            room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    # S('euclid(conga:rand*20, 12,16)', speed="[1,2,4]/2", hcutoff=1000, shape=0.7,
    #         room=0.5, size=0.2, dry=0.1, orbit=3, amp=0.4).out(i, 1, 0.25)
    # S('juppad:3, juppad:4', # commenter ce bloc
    #         speed=0.75, squiz=2,
    #         orbit=2, cut=0,
    #         legato=1.1).out(i, 8, 0.25)
    S('kit2:3', shape=0.5).out(i, 8)
    S('., kit2:10, ., kit2:9!2', shape=0.5).out(i, 2)
    a(baba, d=1/8, i=i+1)

# Remonter à la ligne 167 pour plus de fun

#############################################################################
## ICI RUPTURE VERS L'INCLUSION DES FOUND SOUNDS
#############################################################################

@swim
def baba(d=0.5, i=0):
    # S('f', shape=0.7, cutoff=100).out(i, 8)
    S('hhh:3', amp='[0:0.2,0.01]', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:(5|10), .', legato=0.5).out(i, 1)
    S('m|c:[4:9]', legato=0.2).out(i, P('4!12, 3!12', i))
    S('lost:[1:100]', # -> lost into jupfx
            cutoff=9000, # ->
            shape=0.5,
            pan='sin($/40)', # -> X
            legato=0.3, # ->
            begin='r').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)

# Inclure
@swim
def baba(d=0.5, i=0):
    S('a', shape=0.7).out(i, 4) # -> monter shape pour les harmoniques
    S('c', shape=0.7).out(i, 3) # -> monter shape pour les harmoniques
    S('d:7', orbit=3, room=0.2, size=0.8, dry=0.2).out(i, 8)
    S('hhh:3', amp='{0, 0.2, 0.01}', legato='0.1~0.5').out(i) # -> hhh ramp
    S('.., p:5, .', legato=0.5).out(i, 1) # -> refaire entrer ça
    S('m|c:[4:9]', legato=0.2).out(i, P('4!12, 3!12', i))
    S('lost:[1:100]', # -> lost into jupfx
            cutoff=9000, # ->
            shape=0.5,
            pan='sin($/40)', # -> X
            legato=0.9, # ->
            begin='r').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0): # potentiomètre du réel
    S('a', shape=0.7).out(i, P('4!12, 5!12', i)) # -> monter shape pour les harmoniques
    S('c', shape=0.7).out(i, 3) # -> monter shape pour les harmoniques
    # S('c', shape=0.7).out(i, P('3!12, 2!12, 5!12',i)) # -> monter shape pour les harmoniques
    # S('hhh', amp='{0, 0.2, 0.01}', legato='0.1~0.5').out(i) # -> hhh ramp
    S('d:4, d:5, .', legato=0.5).out(i, 3)
    S('m|g:[4:9]', legato=0.2).out(i, P('4!12, 1!24', i))
    S('long|(lost:rand*8)', # -> lost into jupfx
            midinote='C',
            cutoff=4000, # ->
            pan='[0:0.5, 0.1], [0.5:1, 0.1]', # -> X
            legato='0.1|0.2|0.7|0.1',
            cut=1, orbit=2, room=0.5, size=0.2, dry=0.1,
            begin='[0:1,0.01], [1:0,0.01]').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)

# Ici on peut explorer des choses plus ambient et se perdre un peu

@swim
def baba(d=0.5, i=0): # potentiomètre du réel
    S('a', cutoff=200, shape=0.7).out(i, P('4!12, 5!12', i))
    # S('c', cutoff=100, shape=0.7).out(i, 3)
    # S('c', shape=0.7).out(i, P('3!12, 2!12, 5!12',i))
    # S('hhh', amp='{0, 0.2, 0.01}', legato='0.1~0.5').out(i) # -> hhh ramp
    # S('d:4, d:5, .', legato=0.5).out(i, 3)
    S('m|g:[4:9]', legato=0.2).out(i, P('4!12, 1!24', i))
    S('long|(lost:rand*8)', # -> lost into jupfx
            midinote='C',
            cutoff=4000, # ->
            pan='[0:0.5, 0.1], [0.5:1, 0.1]', # -> X
            legato='[0.1|0.2|0.7|0.1]+0.6', # -> facteur de fun
            cut='1|0, 1|0, 1!4', orbit=2, room=0.5, size=0.2, dry=0.1,
            begin='[0:1,0.01], [1:0,0.01]').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    # S('f', shape=0.5).out(i, 4)
    # S('hhh', amp='{0, 0.2, 0.01}', legato='0.1~0.5').out(i) # -> hhh ramp
    # S('d:4, d:5, .', legato=0.5).out(i, 3)
    # S('d:{4,9}', legato=0.5).out(i, 4)
    # S('z', shape=0.8).out(i, 4)
    S('hhh:12', hcutoff=500, speed='[1:10]', shape=0.8).out(i, 1)
    # S('kit5:[6!4,7!2,5!5,4]', shape=0.8).out(i, 3)
    # S('q:rand*8', shape=0.4).out(i, P('1!12, 2!8', i))
    S('long:1', # -> lost into jupfx
            midinote='C',
            cutoff=4000, # ->
            pan='[0:0.5, 0.1], [0.5:1, 0.1]', # -> X
            legato='0.1|0.2|0.3|0.1',
            begin='[0:1,0.01], [1:0,0.01]').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)


@swim
def baba(d=0.5, i=0):
    # S('f', shape=0.5).out(i, 4)
    # S('hhh', amp='{0, 0.2, 0.01}', legato='0.1~0.5').out(i) # -> hhh ramp
    # S('d:4, d:5, .', legato=0.5).out(i, 3)
    # S('d:{4,9}', legato=0.5).out(i, 4)
    # S('z', shape=0.8).out(i, 4)
    S('hhh:12', hcutoff=500, speed='[1:10]', shape=0.8).out(i, 1)
    # S('kit5:[6!4,7!2,5!5,4]', shape=0.8).out(i, 3)
    # S('q:rand*8', shape=0.4).out(i, P('1!12, 2!8', i))
    S('long:1', # -> lost into jupfx
            midinote='C',
            cutoff=4000, # ->
            pan='[0:0.5, 0.1], [0.5: 1, 0.1]', # -> X
            legato='0.1|0.2|0.3|0.1',
            begin='[0:1,0.01], [1:0,0.01]').out(i) # -> begin r ou {0, 1, 0.1}
    a(baba, d=1/8, i=i+1)

panic()

S('lost').out()

S('lost:2').out()

# Fêter Halloween

S('lost:7', legato=7, speed=0.5, release=7).out()

S('lost:0', legato=7, speed=0.5, release=7).out()

S('lost:3', legato=7, speed=0.5, release=7).out()

panic()

# ██████████████████████████████████████████████████████████████████████████████
# █                                                                            █
# █     █ █   ▄▄   █░█ ▄▀█ █░█ ▄▀█ █░░ █ █▄░█ ▄▀█                              █
# █     █ █   ░░   █▀█ █▀█ ▀▄▀ █▀█ █▄▄ █ █░▀█ █▀█                              █
# █                                                                            █
# ██████████████████████████████████████████████████████████████████████████████


@swim
def baba(d=0.5, i=0):
    # S('bip:rand*20', shape=0.4, midinote='quant([0+12|24,3,6,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    # S('bip:rand*20+20', shape=0.4, midinote='quant([0+12|24,3,6,10]+62, C@minor), quant([0,3,10]+62|74, F@minor)').out(i, 3, 0.25)
    S('boop:rand*40').out()
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('bip:rand*20',
            orbit=2, room=0.7, size='r', dry='0.1',
            shape=0.4, midinote='quant([0+12|24,3,6,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    S('bip:rand*20+20',
            orbit=2, room=0.5, size='r', dry='0.1',
            shape=0.4, midinote='quant([0+12|24,3,6,10]+62, C@minor), quant([0,3,10]+62|74, F@minor)').out(i, 3, 0.25)
    S('boop:rand*40').out()
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('bip:rand*20',
            orbit=2, room=0.7, size='r', dry='0.1', legato=1,
            shape=0.4, midinote='quant([0+12|24,3,6,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    S('bip:rand*20, boop:rand*200',
            orbit=2, room=0.7, size='r', dry='0.1', legato=1,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+80, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 3, 1)
    S('(ff):rand*20', # ulh electrowave ff
            orbit=2, room=0.7, size='r', dry='0.1', legato=0.2, hcutoff=500,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+50, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 2, 1)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('ff', shape=0.5).out(i, 4)
    S('ll', shape=0.5).out(i, 4)
    S('gameboysnare', cutoff=800).out(i, 8)
    S('., hhh:rand*40', hcutoff=9000).out(i, 1)
    S('., hhh:rand*40', hcutoff=9000, speed='1~50').out(i, 1)
    S('bip:rand*20',
            orbit=2, room=0.7, size='r', dry='0.1', legato=1,
            shape=0.4, midinote='quant([0+12|24,3,6,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    S('bip:rand*20, boop:rand*200',
            orbit=2, room=0.7, size='r', dry='0.1', legato=1,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+80, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 3, 1)
    S('(ulh):rand*20', # ulh electrowave ff
            orbit=2, room=0.7, size='r', dry='0.1', legato=0.2, hcutoff=500,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+50, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 2, 1)
    a(baba, d=1/8, i=i+1)

# <-> des allers retours

@swim
def baba(d=0.5, i=0):
    # S('ff, gg:rand*29', shape=0.8, leslie=1, leslierate=5, lesliespeed=2).out(i, 2)
    # S('ll', shape=0.8).out(i, 4)
    S('gameboysnare', cutoff=800).out(i, 8)
    # S('., hhh:rand*40', hcutoff=9000).out(i, 1)
    S('., hhh:rand*40', hcutoff=9000, speed='1~50').out(i, 1)
    # S('bip:rand*20', lesliespeed='2*8', leslierate='rand*5', leslie=1,
    #         orbit=2, room=0.7, size='r', dry='0.1', legato=1,
    #         shape=0.4, midinote='quant([0+12|24,3,6,10]+50, C@minor), quant([0,3,10]+50, F@minor)').out(i, 1, 0.25)
    S('bip:rand*20, boop:rand*200', lesliespeed='2*8', leslierate='rand*5', leslie=1,
            orbit=2, room=0.7, size='r', dry='0.1', legato=1,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+80, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 3, 1)
    S('(ulh):rand*20', # ulh electrowave ff
            orbit=2, room=0.7, size='r', dry='0.1', legato=0.2, hcutoff=500,
            shape=0.4, midinote='quant([0+12|24,1~20,6,0~20]+50, C@minor), quant([0~20,3,10]+50, F@minor)').out(i, 2, 1)
    a(baba, d=1/8, i=i+1)

# --|--> transition du coq à l'âne

@swim
def baba(d=0.5, i=0):
    S('m, ..., m, ...', shape=0.5).out(i, 2)
    S('rev([s,a,l,u,t, z,o,r,b,a]:rand*8)',
            legato=0.1, pan='tan(r/100)', accelerate=0.2,
            room=0.1, dry=0.1, size=0.1,
    ).out(i, 2)
    S('perca:[1:20], ..',
            speed=2 if rarely() else 'rand*4',
    ).out(i, 2)
    a(baba, d=1/16, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('m, ..., m, ...', shape=0.5).out(i, 2)
    S('long:13', shape=0.5,
            begin='0.5, 0.5, 0.42, 0.5!2, 0.6', orbit=3,
            cut=1, legato=2).out(i, 8, 0.25)
    S('perca:[1:20], ..', speed=2).out(i, 2)
    a(baba, d=1/16, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('f, ..., f, ...').out(i, 2)
    S('gg, ...', shape=0.5, orbit=4, room=0.2, size=0.2, dry=0.2).out(i, 2)
    S('perca:[1: 20], ..', speed='1+rand*4', cutoff='200+rand*8000').out(i, 2)
    S('perca:[20: 1], .', speed='0.1+sin($)', cutoff='200+rand*8000').out(i, 3)
    S('long:13', shape=0.7,
            begin='0.1, 0.2, 0.3, 0.5',
            orbit=3,
            cut=1).out(i, 8, 0.25) # 0.5 0.6
    a(baba, d=1/16, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('m, ..., m, ...', shape=0.5).out(i, 2)
    S('hhh:rand*49', amp=0.3, hcutoff='sin(i.i/40)*7000').out(i, 2)
    S('long:13', shape=0.5,
            begin='0.6, 0.5, 0.42, 0.6, 0.7', orbit=3,
            cut=1, legato=2).out(i, 8, 0.25)
    S('q:[1:20], ..', speed=2).out(i, 2)
    a(baba, d=1/16, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('m, ..., m, ...', shape=0.5).out(i, 2)
    S('hhh:rand*49', amp=0.3, hcutoff='sin(i.i/40)*7000').out(i, 2)
    S('long:13', shape=0.5,
            begin='0.5, 0.5, 0.42, 0.5!2, 0.6', orbit=3,
            cut=1, legato=2).out(i, 8, 0.25)
    S('q:[1:20], ..', speed=2).out(i, 2)
    a(baba, d=1/16, i=i+1)

# une petite transition jsp

@swim
def baba(d=0.5, i=0):
    # S('m, ..., m, ...', shape=0.5).out(i, 2)
    # S('hhh:rand*49', amp=0.3, hcutoff='sin(i.i/40)*7000').out(i, 2)
    S('jupfx:rand*20', shape=0.5, hcutoff='200 + rand*8000',
            begin='0.5, 0.5, 0.42, 0.5!2, 0.6', orbit=3,
            cut=1, legato=2).out(i, 8, 0.25)
    S('q:[1:20], ..', speed=2).out(i, 2)
    a(baba, d=1/16, i=i+1)

# Débrouille toi


# ██████████████████████████████████████████████████████████████████████████████
# █                                                                            █
# █ █ █ █   ▄▄   ▀█▀ ▄▀█ █▀█ ▀█▀ █▀▀   █ █▄░█ ▀█▀ █▀█   ▀█▀ █▀▀ █▀█            █
# █ █ █ █   ░░   ░█░ █▀█ █▀▄ ░█░ ██▄   █ █░▀█ ░█░ █▄█   ░█░ ██▄ █▀▄            █
# █                                                                            █
# █ █▀ ▄▀█ █ █▄░█ ▀█▀ ▄▄ █▀▀ ▀█▀ █ █▀▀ █▄░█ █▄░█ █▀▀                           █
# █ ▄█ █▀█ █ █░▀█ ░█░ ░░ ██▄ ░█░ █ ██▄ █░▀█ █░▀█ ██▄                           █
# █                                                                            █
# ██████████████████████████████████████████████████████████████████████████████

*,,,,,,,,,,,,.,,,,..*****,,.  .. ,*,.   . ..        ........,,,.,,,,,,,,,,.,,.*
*(**/**,/*(**,**///**,,*////*,,..,,//.. ..,       . ....,,,,,,,,**********(//(((
*/***/***,/******/,. . ....,,,,..,,**.   ...     ..,.....,,.,,,,,*********(((//*
*((*//**,,/,,,,,*,.  .. ....,,,.../#%%%%#(,..    .,,,....,...,,,,.,,,,**,,****(/
*****/,,*,**,**,,,...,.,.,*/#%%%%%%%%%%%%%%%#(. .,,..,,...,...,..,..,,,,****/***
*//,**//*****/**,.....%#%&&%%&&&&%&%%%%%%%%%%%%##%#...... ,,..,..*.,,,,/**/*,,**
*//*,,*,,******,,/,,.#%&&&&&&&&&&&&&%#%&%%&%%%%###%( .. ..,,..,,,..,,..,.****,**
*//*,,,,****,,,,,,,,#%&&&&&&&%&%&%%%%&&&%&&&%%%%%###(#*  ....,,,,.,........,,**,
*//*****,***...... #%&%&&&%&%%&%%%%#%%%&&&&&%%%%%%%%%%%#**,*,....,,.,*.......,,,
*/*******,,,......#%%&%&%&&&%%%&&%%%%%#%%%%%%%%%###%%#%%(,,,,*, ....   ,..,,,,./
*/*****,,*,.....,(&&&%&&&%(***(&**,****,,*((##%/#/#%%%#%%(////**,,......,,,,,.*,
*/*,,,,,,.,*, ..#%%&&&&%#/***************,**,... .*#%%%%%#//*.,..........,*/*,.,
*/****,,,...,.,.#%&&&&%(/********,,,****,***,...   ,/%%#(*.............,.,,,..,*
*/**,,,,,,,,....,%%%&&&/**/////***,**/***,,*,*,...  ./(#,...,..,.,,..,,,,...,,,*
*/****,**,**,*,..*%&&&/**#(///(//((/*/*,**/////***,..#%(, .....,.....,,. ....,*,
*//**********,,,,//%%%**//(%#&%#////(,,#/*(*###*/*.../#(/,.,.,,.. ... .   .,,..,
*//*///**,,,,***,,//%%***/((((((//((,,,,((/(((//.*...,#/,...,,.,...,.. .,......,
*///(**//*****/***,(%#****///**/****,,,,.,**,,,,,.,,,(#*..,,...,.. ............,
*//*/*,,**,,,,**/***#%(**********/****,....,,,,,.....#(*,,......,.  .,,.,**,,,..
*//*/*,*/**,*,,,,,,.,##//**********,**,,..,,,,....  ,((******//(*.....*,,.,....*
*//*///**//,,.**,..,,,%#***********((/./(,*,,,,. ..,*((,,,....,,..,.,,,*,.,...,*
/#/((***/,***,,,.,,,**(((***,*,****((#/*/,,,...   .,(#,,.......  ..,,,***,,,.,.*
*((/(/***.*,,,..,.,*,//(#(//****//(((((/(///.*...,.//(/***,.,*..  ....,***,...,*
*((//*,,,,,.......,,..,*(##(//***//((//(*(*,*,...,*/*,... ..    .  ......,..,.,,
/((((/,,,,...........,,**/(#((/**/***/*,,,***,.,.////*******,,.,,.   .... ...,.,
*(((/*,,,,,,,./, .......*,,/###(/********,,.,*(*,.,,.....,,,..     ..  ......,..
*((//**,,.,,****,.,,,...*%(..(###%#((((((//(#(/.   ,.*,,,,..,..   ..... ......,.
*((///*,.,*,****...,*/.&&&%,.,,*(##%%%%%%%##(/.   .%#((,.,.., ...........,,,,,,.
/(/(**,,,..,,,,,**/**/&&@&&&/*,/.,*((((((((. ...,(%#%%%%%,,,,.......   ...,.,,,,
*(//**,***,**,*****(%&&&@&&@&%*,.,,*//(((/,..,/%%%%%%%%%%##*,*,...............,.
*(//***,,*/,*,,(&&&&@&@@@&&&&(,,,,,,,*(#,,,,,,*#%%%%%%%%%%%%%#**.........,/*****
*(///****,,(&&&&@@@@@@&&@&&&&##%&&&&&&&&&&&&%##%%&&&%%&%%%%%%%#(%(/*....***,*,,,
*((((((&@&&&&@@@@&&@&@&&&&&%%&&&&&@&&&&&@@@&&%%%%%%%%%%%%%#%%#%%*,,...,,,*,,***,
/%&%&@&%&@@@@@&&&&&%%&&@&&&@&&@@&&&#,,,,,##&%(%%&%%%%#%%##%/.  /#/...,,..,,,,...
/&&&%&&&&&%&&@&&&&&&&&&&&&&&%#%&&&&%(.,,,,,,/&&&&%%%%%##**,. ,,/,.,.,.*,*,(#&(..
/&&%#%%%###%&@&&&&&%%%%%%%&&&&&%&%&%*,.,,...%%%&%%%(%#*,,.,,./,,.,,,./(#(*#(%(#(
/&&@&@&%&@@@&@&&&%(#%%%#%&&&&&&&%%&&%#.... %%%%&&%#(*,,...(/,,,*,(%%###(####/*%(
/&@@@&@&&#%##&%(/*/(#%%%#%&@&&%#%%&&%&%,./&&&%%%#***....**,*/*%%%%&%(#%#####(/(*
/#%&@%%##&&&%(/((,,(%&%#%%%&&%%#%#%%&%&%&&&&%%(*.....(.*.,/#%%%&%%%%###%#%%###(.
/######/(%%&%(%%#(((/#%&@&&&&&%&&%%&&%&&&&&%/,,.. ,.,.,(#%&%&%%%&%%%#%%%(((//%%(
/%%%#(#%%%%%%%%#######(((%&&@&&%%%%%&&%%%(,,,,..,,,(%%&&%#%%%####((%#%(/(#%#(#**
/%%%%%%%&&&%&&%%%%#(((((((#*%&@&%%&&%%%/,*,,.*,//&%%&%%%%&&%####(/*#%(/(#%%#**,*
/&&%&%%%%%%%##%%###(#((#*#%((//(////***/*.**##%%%#%##%&%#(##%%%%#*/#//*/#(/#(***
# C'EST PIERRE BONNARD, IL FAUT ALLER LE VOIR.


@swim
def baba(d=0.5, i=0):
    M(velocity='90~110', note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    M(velocity='90~110', dur=1, note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    M(velocity='90~110|70', dur='15~20', note="F', ..., G'', ..., [D, E, F, A]+12").out(i, 2)
    a(baba, d=1/8, i=i+1)


@swim
def baba(d=0.5, i=0):
    M(velocity='90~110', dur=1, note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    M(velocity='90~110|90', dur='15~20', note="F., ..., F.., ...").out(i, 2)
    M(velocity='90~110|90', dur='15~20', note="F., A, .., F.., ...").out(i, 2)
    a(baba, d=1/8, i=i+1)

# <-> alterner

@swim
def baba(d=0.5, i=0):
    M(dur='2~5', note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    M(dur='2~5', note='disco(inrot(C@maj7, F@maj7))').out(i, 5)
    M(dur='2~12', note='adisco(inrot(inrot(C@maj7, F@maj7), G@fifths))').out(i, 4)
    a(baba, d=1/8, i=i+1)

@swim
def baba(d=0.5, i=0):
    M(note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    if rarely():
        M(note='disco(inrot(C@maj7, F@maj7))').out(i, 5)
    if sometimes():
        M(note='adisco(inrot(inrot(C@maj7, F@maj7), G@fifths))').out(i, 4)
    a(baba, d=1/8, i=i+1)


c._midi_nudge = 0.30

@swim
def baba(d=0.5, i=0):
    S('ff').out(i, 4)
    M(velocity='90~110', dur=1, note='inrot(C@maj7, F@maj7)-12').out(i, 2)
    M(velocity='90~110|90', dur='15~20', note="F., ..., F.., ...").out(i, 2)
    M(velocity='90~110|90', dur='15~20', note="F., A, .., F.., ...").out(i, 2)
    a(baba, d=1/8, i=i+1)



# ██████████████████████████████████████████████████████████████████████████████
# █                                                                            █
# █  █ █░█   ▄▄   █░░ █▀▀   █▀█ █ ▄▀█ █▄░█ █▀█   ▄▀█ █▄░█ ▄▀█ █░░ █▀█          █
# █  █ ▀▄▀   ░░   █▄▄ ██▄   █▀▀ █ █▀█ █░▀█ █▄█   █▀█ █░▀█ █▀█ █▄▄ █▄█          █
# █                                                                            █
# ██████████████████████████████████████████████████████████████████████████████

panic()


@swim
def baba(d=0.5, i=0):
    S('kit3:[1,2,1,2,4,5,4,6]', legato=1).out(i, 8)
    S('long:42', begin='r', cut=1).out(i, 8)
    a(baba, d=1/32, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    if sometimes():
        S('z:6' if random() > 0.5 else 'z:7', shape=0.9, hcutoff=7000).out(i, 4)
    a(baba, d=1/32, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('kit3:[1,2~10,1,2,4~10,5,4,6]', legato=1).out(i, 8)
    S('long:42', begin='r', cut=1).out(i, 8)
    a(baba, d=1/32, i=i+1)

@swim
def baba(d=0.5, i=0):
    # Ce truc est quand même giga fade :'(((((((((((((
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    if sometimes():
        S('z:6' if random() > 0.5 else 'z:7', shape=0.9, hcutoff=7000).out(i, 4)
    # Du du du du dudududududu dudu du du dud udu dudu
    a(baba, d=1/32, i=i+1)

# Réponse :

@swim
def baba(d=0.5, i=0):
    S('kit3:[0, 1,2,1,2,4,5,4,6,7,8, 1, 0]', legato=1).out(i, 8)
    S('long:42', begin='r', cut=1).out(i, 8)
    S('long:42~46', begin='r', cut=1, speed=0.5).out(i, 8)
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    if sometimes():
        S('z:6' if random() > 0.1 else 'z:7',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    if sometimes():
        S('dd:6|7|8' if random() > 0.5 else 'j:0~7',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    a(baba, d=1/32, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('kit3:[1,2,1,2,4,5,4,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('long:20~33', begin='r', cut=1).out(i, 8)
    S('long:42~46', begin='r', cut=1, speed=0.5).out(i, 8)
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    if sometimes():
        S('z:6' if random() > 0.1 else 'z:8~400',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    if sometimes():
        S('dd:6|7|8' if random() > 0.5 else 'z:7~200',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    a(baba, d=1/32, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('kit3:[1,2,1,2,4,5,4,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    # S('long:42', begin='{0,2,0.4}', cut=1).out(i, 16)
    S('long:42', begin='[0:1, 0.08]', cut=1).out(i, 16) # -> éplucher comme un oignon (solo de fichier .wav)
    # S('long:42~46', begin='r', cut=1, speed=0.5).out(i, 8)
    # S('jupbass:28|44, jupbass:28', octave=4,
    #     legato=1, cut=1, orbit=3).out(i, 24, 1)
    if sometimes():
        S('z:6' if random() > 0.1 else 'z:8~400',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    if sometimes():
        S('dd:6|7|8' if random() > 0.5 else 'z:7~200',
                pan='r',
                legato=1, shape=0.9, hcutoff=7000).out(i, 4)
    a(baba, d=1/32, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('kit3:[1,2,1,2,4,5,4,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('long:10~33', begin='r', cut=1, speed="1~8").out(i, 8)
    S('long:20~46', begin='r', cut=1, speed="1~8").out(i, 8)
    a(baba, d=1/32, i=i+1)

# Réponse :

@swim
def baba(d=0.5, i=0):
    S('kit2:[0, 1,2, 0, 1,2,4,5,4,0,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('kit3:[1,2,1,2,4,5,4,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('long:103', begin='0.1, 0.5', cut=1, speed="1~8").out(i, 16)
    S('long:20', begin='0.1, 0.5', cut=1, speed="1~8").out(i, 8)
    a(baba, d=1/32, i=i+1)


@swim
def baba(d=0.5, i=0):
    S('cc').out(i, 12)
    S('kit2:[0, 1,2, 0, 1,2,4,5,4,0,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('kit3:[1,2,1,2,4,5,4,6,1,2,3,1,2,3,2,3,4,5~8!5]', legato=1).out(i, 8)
    S('long:103', begin='0.1, 0.5', cut=1, speed="1~8").out(i, 16)
    S('long:20', begin='0.1, 0.5', cut=1, speed="1~8").out(i, 8)
    a(baba, d=1/32, i=i+1)

@swim
def baba(d=0.5, i=0):
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    S('kit4:rand*20', legato=0.4, begin=0.01).out(i, 12)
    S('kit3:[1,2,1,2,4,5,4,6]').out(i, 8)
    S('long:40', begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    S('long:40', speed=1.01, begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    if sometimes():
        S('z:6', shape=0.9, hcutoff=5000).out(i, 4)
    a(baba, d=1/32, i=i+1)

panic()

@swim
def baba(d=0.5, i=0):
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    S('kit4:rand*20', legato=0.4, begin=0.01).out(i, 12)
    S('kit3:[1,2,1,2,4,5,4,6]').out(i, 8)
    S('long:26', amp=0.5, begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    S('long:26', speed=1.01, begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    if sometimes():
        S('z:6', shape=0.9).out(i, 4)
    a(baba, d=1/32, i=i+1)

# Variation 3
@swim
def baba(d=0.5, i=0):
    S('jupbass:28|44, jupbass:28', octave=4,
        legato=1, cut=1, orbit=3).out(i, 24, 1)
    S('kit4:rand*20', legato=0.4, begin=0.01).out(i, 12)
    S('kit3:[1,2,1,2,4,5,4,6]').out(i, 8)
    S('long:40', begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    S('long:40', speed=1.01, begin='0.60!4, 0.555!2, 0.27!4, 0.25!2', orbit=2, cut=1).out(i, 32)
    if sometimes():
        S('z:6', shape=0.9).out(i, 4)
    a(baba, d=1/32, i=i+1)

panic()

Artificial Life

Experimental use of the Taichi numerical simulation and graphics language for live visuals and artificial life simulation. Made by Jack Armitage. For more info, see the iil-python-tools. Using an alpha version of Sardine. Video n°1, Video n°2.

from sardine import *
import taichi as ti
import numpy as np
import math
import tulvera as tul

ti.init(arch=ti.vulkan)
c.bpm = 250
c.link()
resx = 1920
resy = 1080
n = 8192
boids = tul.vera.Boids(resx, resy, n)
window = ti.ui.Window("Boids", (resx, resy))
canvas = window.get_canvas()

@swim
def gui_loop(d=0.5, i=0):
    boids.update()
    canvas.set_image(boids.world.to_numpy().astype(np.uint8))
    window.show()
    a(gui_loop, d=1/16, i=i+1)

@swim
def param_loop(d=16, i=0):
    # boids.vis_radius[None] = P('40.0,80.0,150.0',i)
    # boids.max_speed[None] = P('1.0,2.0,3.0',i)
    boids.max_speed[None] = P('2*sin($/2)')
    a(param_loop, d=8, i=i+1)

hush()

Claude

Claude is a tool for synchronizing visuals with audio in a live-coding context.

It comes as a Sardine extension, and allows you to control an OpenGL shader from Sardine.

@swim
def arpy(p=.25, i=0):
    build_up = '[1:100,.2]'
    D('(neu arpy:rand*20 2|5 8)', i=i,
        lpf=f'100+{build_up}*50',
        room=1, size=f'.5+{build_up}*(.4/100)'
    )
    again(arpy, p=.25, i=i+1)

@swim
def stomp(p=.5, i=0):
    D('(eu stomp:(6|4|1) 5 8)', i=i)
    Claude('cellsize', '.3 .2 .1 .', i=i, d=4)
    D('. stomp:3', i=i, d=4, speed='[1 .5]!!2')
    Claude('noiseamp', '0 0 .02 0', i=i, d=2)
    again(stomp, p=.5, i=i+1)

@swim
def slowmvt(p=2, i=0):
    Claude('mvt', ['.1 (-.1) 0', '0 rand*.2'], i=i)
    Claude('gap', '2 3|4', dt='i', i=i, d=3)
    again(slowmvt, p=2, i=i+1)
#version 330

uniform vec2 resolution;
uniform float time;

uniform float cellsize = .5;
uniform float pixsize = .01;
uniform float noiseamp = .0;
uniform int gap = 2;
uniform vec2 mvt = vec2(0.1, 0.0);

out vec3 frag_color;

float noise(float x) {
    return fract(sin(x) * 43758.5453123);
}

ivec2 pixelate(vec2 uv, float size) {
    return ivec2(floor(uv / size));
}

vec2 cell_point(ivec2 cell) {
    float rand = noise(dot(cell, ivec2(12, 45)));
    float phase = rand * 100;
    float speed = 1 + rand;
    float angle = speed * time + phase;
    return (vec2(cos(angle), sin(angle)) + 1.0) * .5;
}

void main() {
    // Retrieve, normalize and unsqueeze fragment coordinates
    vec2 uv = gl_FragCoord.xy / resolution;
    uv = 2.0 * uv - 1.0;
    uv.x *= resolution.x / resolution.y;

    uv += mvt * time;

    ivec2 pix = pixelate(uv, pixsize);
    uv = pix * pixsize;
    float disp = 2*noise(pix.y) - 1;
    uv += disp * noiseamp;

    ivec2 coords = pixelate(uv, cellsize);

    // Compute Voronoi cell
    float dist_min = 1000.;
    ivec2 cell_min = coords;
    for (int dx = -1; dx <= 1; dx++) {
        for (int dy = -1; dy <= 1; dy++) {
            ivec2 cell = coords + ivec2(dx, dy);
            vec2 pos = (cell_point(cell) + vec2(cell)) * cellsize;
            float dist = distance(uv, pos);
            if (dist < dist_min) {
                dist_min = dist;
                cell_min = cell;
            }
        }
    }

    // Output color based on cell coordinates
    if (cell_min.x % gap == 0) {
        frag_color = vec3(1.0, 0.7, 0.9);
    } else {
        frag_color = vec3(0.0, 0.4, 0.6);
    }
}

About

This section will fit articles about various topics that do not fit elsewhere. Think of it as a Meta-Sardine section.

Why Sardine?

Sardine is a side-project that initially started as an attempt to demonstrate some live coding techniques for my PhD dissertation in musicology at the University of Saint-Etienne / Paris 8 University. Sardine is trying to encompass various programming paradigms and gestures used by live coders: declarative and imperative programming, DSLs for pattern processing, clock synchronisation with other tools, etc. Initially, everything was working according to the plan. And then, the tool started mutating from all sides and it gained its own identity. It is now a new live coding library in need of support :)

Obviously, I'm also trying to develop Sardine for my friends and I. Programming new tools is fun and rewarding! I am playing music with the Cookie Collective in Paris and with the Creative Code Lyon community. I am also very much attached to the values of the TOPLAP collective once was contributing to the documentation of TidalCycles. Live coding is a fun musical practice, that I nowadays consider very much alike playing a different kind of musical instrument!

I learned how to program by live coding music! The next logical step I could see in my learning was to code my own tool so I can share it with others and make people see the incredibly fruitful intersection between code and music. Since I started live coding a few years ago, I always wished to develop my own tool just to see how things work! I am now finding the need to write documentation for my own work.. heh;

Contributions

Contributions of any sort are really welcome! I am not a professional developer and am just trying to do my best with Sardine! I might code things in a weird or unefficient way. Everything takes place on the main GitHub repo. Don't be afraid about proposing breaking changes or to take a different direction from mine! It can be a fun adventure.

About this website

This website is generated using mdbook and its deployment is automated. Every commit on the main branch of the Sardine repository will trigger a rebuild. This website is a collective effort. You can contribute by amending or adding new Markdown files and linking them in the SUMMARY.md file.

Building good documentation is hard. If you want to make it better, you are very welcome to do it and I will accept your changes most of the time :)

Sardine Strategies

The Oblique Strategies are a set of cards once written by Brian Eno and Peter Schmidt. These are cards that you should take a look at when you are feeling down when working in the studio. They are pretty famous and were used by many musicians worldwide. To my knowledge, nobody ever tried to write a live coding flavour of these cards until now. Here they are, have fun:

1) Computers are good at maths.
2) Do what musicians can't do. Elaborate.
3) This is not the function you are looking for.
4) If my grandmother had wheels, she would have been a bike.
5) Your code is totally fine. It just doesn't sound good.
6) Record, Listen, Reuse the recording!
7) What is truly yours?
8) Complex things are simple if you cut them up.
9) Simple things are infinitely difficult if you look at them closely.
10) What is a computer keyboard?
11) Text is for speaking.
12) That one thing that deserves to be live coded
13) What would you do if you had to apy each letter you type
14) Write the most complex thing you can think of.
15) Computers love files. Use lots of files.
16) Will you ever be able to sound human?
17) Cybernetic hour. Add more to the system.
18) Why did you learn how to program?
19) Beautiful things are scarce. Think of colors.
20) Ugly things are plentiful, but they can be pretty if you try.
21) Computer musicians have invented DAWs. Why?
22) Perspective and layers.
23) What is time. Does your computer think the same?.
24) Digital sound is a material. We want to hear it.
25) The triangle of your orchestra.
26) Solos are great.
27) What's the worst thing about a computer? Can you get your revenge?
28) Beep beep, this is internet.
29) There are musicians who play their instruments well.
30) You are not yourself anymore. You are free!.
31) Computers are stubborn and you are not!
32) The beginning and the end. See them.
33) Bigger font, bigger ideas.
34) Ping-pong with your friend.
35) What is computer folklore?
36) Tappity tap, you write in rhythm.
37) Computers are not helping you at all.
38) Get political!
39) Do it yourself, fail hard doing it.
40) Where does the sound comes from?
41) Music is all about expressiveness and mannerisms.
42) Will animals like it?
43) Pretend that you can play jazz music.
44) Nobody is watching, people don't get it.
45) Remember that your favorite music is probably made with a DAW.
46) You often don't play the music you want, same thing goes for code.
47) Live code is temporary, what are you going to do after?
48) Expect bugs, not joy!
49) Everything you do has been done before.
50) And again, and again, and again, and again!
51) BOOM, pshhht, plip plop, and all over again.
52) What is the ABC of what you do?
53) Remember that everything is in your own mind.
54) Doing things alone is ok. Doing it again with someone, funnier!
55) Show us what you are hiding behind your back.
56) Computer code, more like Pomputer pode.
57) "In the end you get tired of this old world".
58) Snacks, forbidden snacks. Code, forbidden code.
59) Hit it with a stick, now blow inside it.
60) Tools are making us. Change your tools, change yourself.
61) Tout le monde ne parle pas anglais, vous saviez ?
62) Linear or cyclic? Is that everything there is?
63) You will always be a cliché for someone else.
64) Yeah, TOPLAP. But now think of something else.
65) Borrow code that you don't understand at all.
66) Computer keyboard is lava!
67) I got shoes, you got shoes. All o' God's chillun got shoes.
68) Is there some sort of progress?
69) Just clicking around is probably fine!
70) Read about computers: LISP, FORTH, and the nerdy stuff.
71) Water is everywhere.
72) It's not because you code that you do computer music. Kraftwerk does it better.
73) Everybody is going to love that gimmick!
74) Stay fresh!
75) If your computer could sing, what would the lyrics look like?
76) Why do modular synthesizers still exist?
77) We have more and more computers but people are more and more alone.
78) 100 years of electric guitar. 100 years of computer music?
79) Design your music/code to be universally appreciated.
80) Clever yes, but is it interesting?
81) You have a very big brush and big boots.
82) The sound is all around. No need to spatialize if you listen in the right place.
83) Think of all the silly code that runs important things.
84) Nobody sees you live coding if you don't show it. Is it really necessary to see what you do?
85) Welcome to my TED Talk, live coding is not going to change the world.
86) We have to stop making new music. We already know how it works.
87) Yeah, well, go on.
88) Three rules. If you lose, you start again.
89) Write for one minute, delete for thirty seconds.
90) Beat around the bush.
91) Why don't you code everything yourself?
92) Oh no, midnight already! Your code has become a score.
93) Perhaps you should consider stopping taking advice.
94) AZERTY, QWERTY, DVORAK, does it matter?
95) Everything fits on one line, the rest is superfluous.
96) Think about others.
97) If you're looking for something complicated, maybe it's for the good. Some thoughts don't have words.. yet.
98) Make a list, put everything down on paper.
99) Planes don't fly without buttons and knobs. Wings, ok? But what about the other stuff?
100) It always works better when you press twice.
101) The Greeks already had computers. They were better at philosophy and poetry.
102) If you feel sad, clean your computer. Look around.
103) Why do we feel compelled to type?
104) Food and beverage pairing, live coding version.
105) The things kids like, excessive and brutal.
106) Go to your neighbor, tell him you live code. So what?
107) Live code something that shouldn't be.
108) Essay on the border between the virtual and the actual.
109) We do this mostly because it's fun.
110) At least the ugly things work.
111) If it crashes, that's your fault.
112) Does the computer really let you scream?
113) Find your motto: mine is: "Sail on sight".
114) I don't work with computers.
115) Frankly, there are programs that deserve not to be live.
116) If it works, maybe we should break it?
117) No one cares about the code. We want records, words, thoughts.
118) The device you use is not a computer.
119) To do is to understand. Well, not always.
120) Try to find a profound aphorism about live coding too. It's not easy, is it?
121) 127 cards, 127 MIDI notes. Is MIDI 2.0 killing the mood?
122) Why does language matter?
123) Be non binary.
124) Is there anything really divisive? Why is that?
125) Pianists play chords on their keyboards. You can do too, but what does it mean?
126) Does your computer seem to like anything in particular? Think about its well-being.
127) Who could have coded live but never did?