Patcher: a voice in a subpatcher

Patcher: a voice in a subpatcher#

Goal: build a synth voice once, as a subpatcher with one inlet and one outlet, and use it like any other object. The voice takes a note as the list pitch velocity, fades in when the velocity is above 0 and fades out when it is 0.

This tutorial assumes you have worked through Patcher: modular synthesis in code. Its code is compiled and run by the test suite (Tests/patcher/test_patcher_tutorials.cpp), which plays a note, checks its pitch, releases it, and loads a saved copy of the patch. Patcher: a synth played from MIDI uses four of these voices.

The voice#

inlet 0: "pitch velocity"
    .inlet 0
       |
   .unpack 0 0
    |        \
  .mtof      ./ 127      velocity to 0-1
    |          |
  ~saw      ~line 0 20   20 ms ramp to the new level
    |          |
    ~*  <------+          (right inlet: gain)
    |
 ~outlet 0
outlet 0: audio

A subpatcher is an object of type patcher. You create its contents with the same CreateObject as everything else, and put them inside with SetContainer. Its pins are the boundary objects inside it: .inlet 0 is its inlet 0 (for messages), and ~outlet 0 is its outlet 0 (for audio).

// One voice: the list "pitch velocity" in, audio out. Velocity 0 releases it.
YSE::pHandle* CreateVoice(YSE::patcher& patch) {
  YSE::pHandle* voice = patch.CreateObject("patcher");

  YSE::pHandle* in = patch.CreateObject(".inlet", "0");
  YSE::pHandle* split = patch.CreateObject(".unpack", "0 0"); // pitch, velocity
  YSE::pHandle* mtof = patch.CreateObject(".mtof");
  YSE::pHandle* osc = patch.CreateObject("~saw");
  YSE::pHandle* scale = patch.CreateObject("./", "127"); // velocity to 0-1
  YSE::pHandle* env = patch.CreateObject("~line", "0 20"); // 20 ms ramps
  YSE::pHandle* amp = patch.CreateObject("~*");
  YSE::pHandle* out = patch.CreateObject("~outlet", "0");

  for (YSE::pHandle* h : {in, split, mtof, osc, scale, env, amp, out}) {
    patch.SetContainer(h, voice);
  }

  patch.Connect(in, 0, split, 0);
  patch.Connect(split, 0, mtof, 0); // pitch
  patch.Connect(mtof, 0, osc, 0);
  patch.Connect(split, 1, scale, 0); // velocity
  patch.Connect(scale, 0, env, 0);
  patch.Connect(osc, 0, amp, 0);
  patch.Connect(env, 0, amp, 1);
  patch.Connect(amp, 0, out, 0);
  return voice;
}

What each part does:

  • .unpack 0 0 splits the list into two ints. Its outlets fire right to left, so the velocity leaves before the pitch.

  • .mtof → ~saw sets the pitch.

  • ./ 127 scales the velocity to 0-1, and ~line 0 20 ramps to that level over 20 ms. The ramp is the envelope: it rises on a note-on and falls back to 0 on a release (velocity 0). Without it, every note would click on and off.

  • ~* multiplies the oscillator by the envelope. Its right inlet takes an audio signal here, not a number.

Draw the cords after the objects are inside the subpatcher, as the function does. Subpatchers explains why.

Using the voice#

From the outside, the voice is one object with inlet 0 and outlet 0. Connect to it by pin number, as to any other object:

YSE::pHandle* BuildMonoSynth(YSE::patcher& patch) {
  YSE::pHandle* note = patch.CreateObject(".r", "note");
  YSE::pHandle* voice = CreateVoice(patch);
  YSE::pHandle* dac = patch.CreateObject("~dac");

  patch.Connect(note, 0, voice, 0); // lands on the voice's .inlet 0
  patch.Connect(voice, 0, dac, 0); // leaves from its ~outlet 0
  patch.Connect(voice, 0, dac, 1);
  return voice;
}

Connect(note, 0, voice, 0) draws a cord to the .inlet 0 inside the voice, and Connect(voice, 0, dac, 0) draws one from its ~outlet 0. The parent never names what is inside.

Attach the patcher to a sound (see Patcher: modular synthesis in code), then play a note and release it:

patch.PassData(std::string("57 100"), "note"); // A3 at velocity 100
patch.PassData(std::string("57 0"), "note"); // velocity 0: release

The host can also skip the .r and send straight into the voice’s inlet: voice->SetListData(0, "57 100").

Several voices#

CreateVoice is a function, so a patch can hold as many voices as it calls it. Two things to know when you do:

  • A signal inlet takes one cord. To play several voices through one ~dac, add them up with ~+ first (or give each its own ~dac: a patch’s ~dac objects are summed). Patcher: a synth played from MIDI sums four voices with a chain of ~+.

  • Names are not local to a subpatcher. A .r freq inside a voice would receive every freq sent to the patcher, in every copy of the voice. That is why this voice has an inlet instead of a receiver: a parent can address one copy through its pins. Subpatchers covers this.

Saving and loading#

DumpJSON saves the voice as a record of type patcher, and every object inside it with a container key that points to it. ParseJSON rebuilds the same nesting, and the loaded copy plays the same way. The test for this page saves the patch, loads it into a new patcher and plays a note through the copy. Patch file format describes the records.

Next#