Modeled Modules

A SuperCollider Extension Project

Aiden Benton | April 21, 2024

Outline

  1. History
  2. Project Overview

From Analog to Digital and Back Again

A brief history of computer music

What is an Analog Synthesizer?

  • Teleharmonium (1896)
  • Theremin (1920)
  • Ondes Martenot (1928)
  • Orgues des Ondes (1929)

Orgues and Ondes

Trautonium (1930)

  • Dr. Freidrich Tratuwein
  • Single tube oscillator passed through resonant filters
  • Oskar Sala

Volkstrautonium

Trautwein, Hindemith, and Sala

Trautwein (L), Paul Hindemith and Oskar Sala playing the Trautonium. Berlin, c 1933

RCA Synthesizer I & II (1951)

  • Harry Olson and Herbert Belar
  • Cost cutting
  • Millions of settings

RCA MkII RCA Mk II

RCA MkII Schematic

RCA MkII

Moog and Modularity

  • Modular Prototype
  • “Synthesizer” Series

Synthesizer IC

Synthesizer 2P

Synthesizer 3P

The Minimoog Model D

  • Most iconic synthesizer of all time
  • Hard-wired synthesizer routings
  • First model to be sold by instrument dealers

Dawn of Digital: Prophet-5

  • Dave Smith and John Bowen
  • Combined analog and digital technology
  • Best sounding and most popular polyphonic analog synthesizer

A Very Brief History of Digital Computers

Electromechanical Computers

  • Z2: Konrade Zuse (1939)
  • Bombe: Alan Turing (1941)

Vacuum Tubes

  • ENIAC: John Mauchly and J. Presper Eckert (1943)
  • The Manchester Baby: Frederic Williams, Tom Kilburn, and Geoff Toothill (1948)

Commercial Applications

  • J. Lyons & Company LEO-I: (1951)

Transistors

  • Manchester TC: Tom Kilburn, Richard Grimsdale, and Douglas Webb (1953)
  • IBM 701: (1953)
  • IBM 704: (1954)

IBM 704 Mainframe

Max Mathews

MUSIC-N

The First of Its Kind

  • MUSIC I (1957)
    • In the Silver Scale
  • MUSIC II (1958)

MUSIC III (1959)

  • Unit Generators (UGens)
  • Instrument and Orchestra relationship

MUSIC IV & V (1962-68)

  • First integrated computer music system
  • FORTRAN

Max Matthews and Joan Miller

The CARL System

  • UNIX and C
  • Modular
  • cmusic

> wave -waveform sine -frequency 440Hz | spect

C Suite

  • Cmix
    • MINC
  • CLM
    • Nyquist
  • Csound

      (definstrument simp (start-time duration frequency amplitude)
      (let* ((beg (floor (* start-time *srate*)))
      (end (+ beg (floor (* duration *srate*))))
      (j 0))
        (run
          (loop for i from beg below end do
            (outa i (* amplitude (sin (* j 2.0 pi (/ frequency *srate*)))))
      (incf j)))))
    

Realtime Audio

  • PureData
  • Max/MSP

SuperCollider

  • scsynth
  • sclang
  • scide

Why?

Wendy Carlos

  • Switched on Bach (1968)
  • A Clockwork Orange, The Shining, and Tron

Kraftwerk

  • Florian Schneider and Ralf Hütter
  • Foundational to the development of techno, electro, hip-hop, synth pop, and many others
  • Autobahn (1974)

Deadmau5

  • VCV Rack
  • Analogue Solutions Colossus and the Moog Voyager

Why SuperCollider?

BooleanLogic

Concept

Wavefonix Boolean Logic

Implementation

C++


      #include "SC_PlugIn.hpp"
      #include "BooleanLogic.hpp"

      static InterfaceTable\* ft;

      namespace ModeledModules {

          BooleanLogic::BooleanLogic() {
              mCalcFunc = make_calc_function<BooleanLogic, &BooleanLogic::next>();
              next(1);
              mSelectedOperation = in0(Operation);
          }

          void BooleanLogic::next(int nSamples) {
              // Inputs
              float input1 = in0(Input1);
              float input2 = in0(Input2);


              switch (mSelectedOperation) {
                  case AND:
                      if(std::abs(input1) > 0 && std::abs(input2) > 0) {
                          out0(0) = 1.0;
                      } else {
                          out0(0) = 0.0;
                      }
                      break;
                  case OR:
                      if(std::abs(input1) > 0 || std::abs(input2) > 0) {
                          out0(0) = 1.0;
                      } else {
                          out0(0) = 0.0;
                      }
                      break;
                  case XOR:
                      if((std::abs(input1) > 0 && std::abs(input2) == 0) || (std::abs(input1) == 0 && std::abs(input2) > 0)) {
                          out0(0) = 1.0;
                      } else {
                          out0(0) = 0.0;
                      }
                      break;
                  case NAND:
                      if(std::abs(input1) > 0 && std::abs(input2) > 0) {
                          out0(0) = 0.0;
                      } else {
                          out0(0) = 1.0;
                      }
                      break;
                  case NOR:
                      if(std::abs(input1) > 0 || std::abs(input2) > 0) {
                          out0(0) = 0.0;
                      } else {
                          out0(0) = 1.0;
                      }
                      break;
                  case XNOR:
                      if((std::abs(input1) > 0 && std::abs(input2) > 0) || (std::abs(input1) == 0 && std::abs(input2) == 0)) {
                          out0(0) = 1.0;
                      } else {
                          out0(0) = 0.0;
                      }
                      break;
                  default:
                      out0(0) = 0.0;
                      break;
              }

          }

      }

SuperCollider


      BooleanLogic : UGen {
        // aliases for UGen boolean operations
        const <and = 0;
        const <or = 1;
        const <xor = 2;
        const <nand = 3;
        const <nor = 4;
        const <xnor = 5;

        *kr { |input1, input2, operation=0|
              ^this.multiNew('control', input1, input2, operation);
          }

        checkInputs {
          ^this.checkValidInputs;
        }
      }

Use Case Example

DSP is Hard

Ripples

Concept

Mutable Instruments Ripples

Implementation

C++


      void Ripples::next(int nSamples) {
        const float *input = in(INPUT);
        float* outbuf0 = out(0);
        float* outbuf1 = out(1);
        float* outbuf2 = out(2);

        float width = in0(BP_RQ) * in0(BP_CF);

        if(in0(LP2_CF) != 0){
            LPF2.setup(sampleRate(), in0(LP2_CF), in0(LP2_R));
        }
        if(in0(LP4_CF) != 0){
            LPF4.setup(sampleRate(), in0(LP4_CF), in0(LP4_R));
        }
        BPF.setup(sampleRate(), in0(BP_CF), width, in0(BP_R));

        for(int i = 0; i < nSamples; ++i){
            outbuf0[i] = zapgremlins(BPF.filter(input[i]));
            outbuf1[i] = zapgremlins(LPF2.filter(input[i]));
            outbuf2[i] = zapgremlins(LPF4.filter(input[i]));
        }
      }

Header File


      // PluginRipples.hpp
      // Aiden Benton (dev@aiden-benton.com)

      #pragma once

      #include "SC_PlugIn.hpp"
      #include "../../external/iir1/Iir.h"

      namespace ModeledModules {

          class Ripples : public SCUnit {
          public:
              Ripples();
          private:
              void next(int nSamples);

              Iir::ChebyshevI::LowPass<2> LPF2;
              Iir::ChebyshevI::LowPass<4> LPF4;
              Iir::ChebyshevI::BandPass<4> BPF;

              enum inputs {INPUT, BP_CF, BP_R, BP_RQ, LP2_CF, LP2_R, LP4_CF, LP4_R};

          };

      } // namespace ModeledModules

SuperCollider


      Ripples : MultiOutUGen {
        *ar { arg in, bpCF = 440.0, bpR = 0.5, bpRQ = 1, lp2CF= 0.0, lp2R=0.0, lp4CF=0.0, lp4R=0.0, mul=1.0, add=0.0;
          ^this.multiNew('audio', in, bpCF, bpR, bpRQ, lp2CF, lp2R, lp4CF, lp4R, fmFreq, fmMul, fmIndex).madd(mul, add);
        }

        *kr { arg in, bpCF = 440.0, bpR = 0.5, bpRQ = 1, lp2CF= 0.0, lp2R=0.0, lp4CF=0.0, lp4R=0.0, mul=1.0, add=0.0;
          ^this.multiNew('control', in, bpCF, bpR, bpRQ, lp2CF, lp2R, lp4CF, lp4R, fmFreq, fmMul, fmIndex).madd(mul, add);
        }

        init { arg ... theInputs;
          inputs = theInputs;
          channels = [
            OutputProxy(rate, this, 0),
            OutputProxy(rate, this, 1),
            OutputProxy(rate, this, 2),
          ];
          ^channels
        }

        checkInputs {
          ^this.checkValidInputs;
        }
      }

Use Case Example

DIYDelay

Concept

Erica Synths DIY Delay

Implementation

C++

Constructor


      DIYDelay::DIYDelay(){
        mCalcFunc = make_calc_function<DIYDelay, &DIYDelay::next>();
        m_maxDelay = in0(MAX_DELAY);
        m_bufSize = NEXTPOWEROFTWO(sampleRate() * m_maxDelay);
        m_mask = m_bufSize - 1;
        m_buf = (float *)RTAlloc(mWorld, m_bufSize * sizeof(float));
        if (m_buf == nullptr)
        {
            ClearUnitOutputs(this, 1);
            if (mWorld->mVerbosity > -2)
            {
                Print("DIYDelay: failed to allocate memory for buffer\n");
            }
            return;
        }
        memset(m_buf, 0, m_bufSize * sizeof(float));

        next(1);

      }

C++

next() Function


      void DIYDelay::next(int nSamples){
        // Inputs from SC
        const float *input = in(INPUT);
        float *outbuf = out(0);
        float delay = in0(DELAY_TIME);
        float fb = in0(FEEDBACK);

        auto hold = static_cast<bool>(in0(HOLD));
        auto reverse = static_cast<bool>(in0(REVERSE));
        auto tape = static_cast<bool>(in0(TAPE));

        // local stateless variables
        float const *buf = m_buf;
        int mask = m_mask;
        int write = m_readIndex;
        int read = (m_reverseIndex == 0) ? m_bufSize - 1 : m_reverseIndex;

        if (delay > m_maxDelay)
        {
            delay = m_maxDelay;
        }

        // initialize delay time
        float delay_samples = sampleRate() * delay;
        auto offset = static_cast<int>(delay_samples);
        float frac = delay_samples - (float)offset;

        for (int i = 0; i < nSamples; ++i)
        {

            int phase1 = reverse ? read - offset : write - offset;
            int phase2 = phase1 - 1;
            int phase3 = phase1 - 2;
            int phase0 = phase1 + 1;
            float d0 = buf[phase0 & mask];
            float d1 = buf[phase1 & mask];
            float d2 = buf[phase2 & mask];
            float d3 = buf[phase3 & mask];

            float delayed = cubicinterp(frac, d0, d1, d2, d3);

            float outSample = zapgremlins(input[i] + (fb * delayed));

            if (tape)
            {
                outSample = saturation(outSample);
            }

            outbuf[i] = outSample;
            if (!hold)
            {
                m_buf[write] = outSample;
            }

            write = (write + 1) & mask;
            read = (read - 1) & mask;
        }
        m_readIndex = write;
        m_reverseIndex = read;

      }

Use Case Example