Yudishthira's Quartet

Comments on the software by Nick Didkovsky



The circle

Yudishthira's Quartet was the first HMSL piece I programmed. That was in 1988. So it holds a special spot for me in terms of personal growth. It feels good to revisit the piece today, and to realize it with two new emerging music technologies: Phil Burk's JSyn, and Phil and my JMSL. The act of creating the work again has had some impact on JMSL's design; revealing/squashing some bugs and pointing the compass toward some helpful new tools (like the Tuning class which was motivated by the need to realize Gamelan Son of Lion's tuning, and the Chebyshev and Polynomial classes which were motivated by instrument design). I suspect that every new piece of some complexity will have an impact on the language design of JMSL as it matures.

A randomized mystery novel

Like much of Barbara Benary's music, Yudishthira's Quartet is deceptively simple when described on paper. Only through the act of hearing does the depth and complexity of the piece make itself known. Yudishthira is exactly such a piece. The complexity resulting from its few simple random choices and their interactions is addictive. You hear, say, 32 seconds of sound, followed by a 16 second pause. During that pause, you are dying to hear the next section and see what the random processes created. The piece "reads" like a mystery novel - a page turner - engrossing in the moment and compelling you to follow it forward. It will always sound like Yudishthira's Quartet, but the range of sonic experience is incredible, even with its relatively simple instrument designs (see tech comments below).

A few technical comments

Yudishthira's Quartet was realized as a JMSL Parallel Collection with a repeat count of ten, containing four Players. It has a "repeat function" plugged into it which alters its own properties every time it repeats. Let's revisit Barbara's specification one point at a time, and I will append some technical comments after each.

  1. A random selection of 4 notes from the combined pelog and slendro tunings.

    JMSL's Tuning class provides for an array of double precision floating point numbers which represent frequencies in Hertz. Here is the constructor method that realized Son of Lion's tuning.
         public LionTuning() {
              double[] t = {274.8, 298.8, 316.6, 330.0, 358.4, 368.2, 421.1, 452.4, 482.6, 533.9 };
              setPitches(t);		
         }
    
    Barbara also mentions a 5 cent stretch per octave. The Tuning class provides a method called getFreqOctave() which transposes a given pitch into a given octave. The LionTuning class overrides this method to provide the 5 cent stretch. It is listed below. Note the reference to method freqPlusCents, which is a general tool belonging to the Tuning class. It adds the specified number of cents to the specified pitch and returns the result.
        public double getFreqOctave(double pitch, int octaveNumber) {
            int cents = octaveNumber * 5;
            return Tuning.freqPlusCents(pitch * Math.pow(2.0, octaveNumber), cents);
        }
    
  2. A randomly selected timbre chosen from 10 presets. All voices use the same timbre for the given section. The ten timbres should sound distinctly different from each other and should sound "electronic." Because of the multiple voices, avoid buzzy timbres.

    While JMSL is responsible for the piece's scheduling and algorithmic implementation, JSyn delivers the sound. Each timbre is realized as a JSyn "SynthNote", which is a virtual synthesis circuit with a noteOn() method, so that it can be "played" within a pitch/loudness/duration paradigm.

    To build an instrument, I worked from the bottom up: first designing the raw JSyn Circuit (say, a simple FM pair), then wrapping that up in a JSyn SynthNote subclass so that it can be played with a noteOn() method, finally wrapping the SynthNote up inside a JMSL Instrument subclass, which can be scheduled in a JMSL hierarchy, and fired by classes of a higher organizational level.

    This implementation of Yudishthira's Quartet collects ten such instruments in a class called InstrumentFactory, which instantiates one of these ten Instruments by token (ie calling InstrumentFactory.getInstrument(int index) with an index ranging from 0..9 will return a newly allocated one of the 10 instruments it knows how to "manufacture").

    A few words about each instrument...
    1. SINE_WAVE_INS
      A sine wave with an exponentially decaying amplitude envelope applied to it. Stereo.
    2. SQUARE_WAVE_INS
      A square wave with an exponentially decaying amplitude envelope applied to it. Stereo.
    3. FILTERED_SAW_BL_INS
      A subclass of a SynthNote that is already implemented in JSyn's circuits package (FilteredSawtoothBL), adding an envelope to its amplitude to gracefully quiet down.
      Band limited sawtooth wave table played through a state variable resonant filter.
      - Phil Burk
    4. RING_MOD_BELL
      A SynthNote that is already implemented in JSyn's circuits package (RingModBell).
      Bell generated by ring modulating two triangle waves.
      Ring modulation simply involves multiplying two signals together.
      Output = Osc2.output * ( (Osc1.output * ExpLag.output) + ExpLag.output);
      Osc1.Frequency = Osc2.frequency * modIndex;

      - Phil Burk
    5. CHOW_BELL
      An FM Pair with an exponentially decaying envelope applied to both carrier amp and modulating amp.
      The ratio of Carrier Frequency to Modulating Frequency (Fc:Fm) is maintained at 5:7, with a maximum index of modulation of 10.
      After Chowning as described in Dodge/Jerse's "Computer Music, Synthesis, Composition, and Performance" (Schirmer Books), page 113.
    6. CHOW_BRASS
      An FM Pair with an ADSR envelope applied to both carrier amp and modulating amp.
      Fc:Fm is maintained at 1:1, with a maximum index of modulation of 5.
      After Chowning as described in Dodge/Jerse's "Computer Music, Synthesis, Composition, and Performance" (Schirmer Books), page 113.
    7. CHOW_RANDOM
      An FM Pair with the independently randomized ADSR envelopes applied to carrier amp and to modulating amp.
      Fc:Fm is randomized when the instrument is instantiated, and maintained throughout. Both Fc and Fm are integers that range from 1..10.
      Index of modulation is also randomized when the instrument is instantiated, and maintained throughout. Index is a double that ranges from 1.0 .. 25.0
      Note that a quartet of this instrument will have varying timbres, since each player will likely have a different sounding random instrument. This is a liberty I took when programming Yudishthira, despite the request for timbres that are invariant from one instrument to another in a given section. But it sounds so wonderful I couldn't help myself.
    8. CHEBYSHEV_RANDOM
      A WaveShaper whose transfer function (a Chebyshev polynomial) is randomized to realize between 4 and 12 harmonics, whose relative amplitudes are also chosen at random. Randomly generated ADSR envelopes are applied to the sine driving the transfer function, and to the amplitude of the waveshaper's output. This instrument's sound can vary radically. As with CHOW_RANDOM, I am jumping outside the strict specification of the piece.
    9. CHEBYSHEV_ODD_RANDOM
      Like CHEBYSHEV_RANDOM, except that only odd harmonics are used.
    10. CHEB_RING_RANDOM
      Like CHEBYSHEV_RANDOM, whose output is ring modulated with a sine oscillator (after Dodge/Jerse, p 142). A Fc:Fm ratio is randomized when the instrument is instatiated and maintained. Here, Fc refers to the frequency of the sine oscillator which is multiplied by the shaper, while Fm refers to the frequency of the waveshaper's sine which drives the transfer function. Again, the sound varies dramatically, with good effect!

  3. A randomly selected number of players from 1 to 4. These players represent the four octave range of the gamelan balungen instruments. Also select which of the four voices shall play, ie if there is only one voice, it need not be player #1.
    • Player #1 plays continuous 1 second whole notes, octave 1 (slentem)
    • Player #2 plays continuous 0.5 second half notes, octave 2 (demung)
    • Player #3 plays continuous 0.25 second quarter notes, octave 3 (saron)
    • Player #4 plays continuous 0.125 second eighth notes, octave 4 (peking)


    Yudishthira's PlayerSelector class is responsible for flagging which of four Players will perform in a particular section. JMSL's Player class contains an Instrument and musical data (JMSL's MusicShape), for it to perform (ie pitches, loudnesses, durations, etc). A YudishthiraPlayer is a subclass of Player which adds an Active flag, which is simply true or false, depending on whether that player shall perform or remain silent. The PlayerSelector select() algorithm accepts a collection of four YudishthiraPlayer's, writes them into an ordered list, scrambles the list, then loops from 1 to a random number between 1 and 4, setting each player's flag to true as it loops. Players outside the range of the loop remain inactive.

  4. Random melody: every note a player hits is randomly chosen from among the four notes that characterize the section.

    YudishthiraPlayer also has a unique octave number and a unique speed. These are set when the Yudishthira Collection is first built. Both speed and octave number are used by the ShapeBuilder class, which is responsible for building each section's MusicShapes that contain the actual pitches, loudnesses, and durations to be performed. ShapeBuilder is handed a four-pitch subset of the LionTuning, the total duration of the section, and the loudness of the section. It uses these to generate the appropriate data, handing a long rest to players that are inactive.
        /** Build and deliver a new 3 dimensional (dur, pitch, amp) MusicShape using uniform random processes. An extra dimension
        for optional stereo panning is added. */
        public MusicShape getShape(double eventDur, int octave, boolean active) {
            MusicShape s = new MusicShape(4); 
            double pitch = 0;
            double pan = 0.0;
            double[] pitches = subset.getPitches();
            if (!active) {
                s.add(totalDur, 0.0, 0.0, 0.0);
            } else {
                for (int i=0; i<(int)(totalDur/eventDur); i++) {
                    pitch = tuning.getFreqOctave(pitches[JMSLRandom.choose(pitches.length)], octave);
                    pan = (double)octave/2.0;
                    s.add(eventDur, pitch, amp, pan);
                }
            }
            return s;
        }
    
  5. A randomly selected loudness level out of four possible.

    The AmpSelector class generates a loudness level at random for a particular section. The amplitude is divided by the total number of players to keep from clipping.
    /** 0..1 divided into discrete dynamic levels.  For choices p, mp, mf, f, use numDynamics=4
    @return one of these non-zero levels chosen at random */
        public static double getRandomDynamic(int numDynamics, int numPlayers) {
            return (JMSLRandom.choose(numDynamics) + 1) / (double)(numPlayers*numDynamics);
        }
    
  6. A randomly selected duration of the sound section, of 16, 32, 64, or 128 seconds.

    The SectionBuilder class is responsible for putting all the above elements together, by calling on the appropriate classes to provide the random data in which they specialize. SectionBuilder implements the PlayFunction interface, which means it can be plugged into the repeatFunction "slot" of the Yudishthira Parallel Collection. So every time Yudishthira repeats, it automatically fires a SectionBuilder's play() method which prepares the next section.

    SectionBuilder computes a few values directly, without consulting specially designed classes. One of these values is the duration of the sound section, which is calculated with the following algorithm:
        // choose duration for sounded section: 16, 32, 64, or 128 seconds
        double soundDuration = 16 *  (double) (1 << JMSLRandom.choose(4)); ; 
    

    The values of soundDuration is passed to ShapeBuilder.

  7. A silence of randomly selected duration of 4, 8, 12, or 16 seconds after each section.

    Just as the sound duration is directly calculated by SectionBuilder, so is the duration of its silences.
        // choose duration of pause: 4, 8, 12, or 16 seconds
        double pauseDuration = 4 * (JMSLRandom.choose(4)+1);
    


Future enhancements

I am flirting with the idea of giving the listener some control over the bias of the random processes. For example, being able to select the relative likelihood that various instruments are selected. So if you are a sine wave fan, you could set its likelhood to 1 and all the others to 0. Other possibilities include biasing other processes as well, such as which pitches get selected, or typing in your own tuning tables. Or mixing the audio through a JSyn reverberator or other sound processing. Any enhancements, however, will stay within the spirit if not the strict spec of Barbara's piece.

Thanks

Thanks to Open Space, especially Mary Roberts, for encouraging a submission to the magazine. Thanks to Phil Burk for JSyn and for some clever optimizations. Thanks to Barbara Benary of course! And thanks to New York State Foundation for the Arts who awarded me a grant in 1999 to encourage such work in the computer arts.

Nick Didkovsky, May 26, 1999, NYC
Yudishthira's Quartet Home.