18
\$\begingroup\$

I wrote some code using ChucK : Strongly-timed, Concurrent, and On-the-fly Music Programming Language and would like a review. I broke down the logic into a number of classes, all of which are shown below.

This is pretty new to me, and I would appreciate any and all constructive criticism. What the main script fizzbuzz_run.ck does, besides the regular FizzBuzz modulus operations, is play different chords depending on the FizzBuzz result, and throughout print the results and primary oscillator frequency.

Clarification

I want to clarify a few things that are specific to ChucK:

  • printing statements to console: <<< "print this" >>>;
  • The ChucK operator => is a bit like your usual assignment, except reverse order. For example 42 => int theMeaningOfLifeAndEverything; is how you would otherwise int theMeaningOfLifeAndEverything = 42;.
  • Time and duration are handled by the ChucK VM.

Code on Github

I uploaded a video to YouTube showing what it sounds like. (video not embedded, link opens YouTube)

youtube

OscPitch.ck

public class OscPitch {
    this.freq => float freq;
    getSemitoneRatio() => float SEMITONE;

    // Change the pitch by N musical steps
    // @param float steps : the number of steps to change the frequency, positive or negative
    // @return float : the changed frequency
    fun float change(float steps) {  
        if (Math.isnan(steps)) {
            <<< now, ": ", steps, " is not a number" >>>;
            return freq;
        } else {
            // The semitone ratio has to be applied once for each step change
            // hence the use of Math.pow which does everything
            // in one operation, rather than having to use a loop
            return freq * Math.pow(SEMITONE, steps) => freq;
        }
    }

    // Get the ratio to apply to a frequency to go up or down one semitone
    fun float getSemitoneRatio() {
        // The ratio for a musical semitone is
        // the 12th root of 2, or approximately 1.05946309436
        // More info: https://en.wikipedia.org/wiki/Twelfth_root_of_two
        return Math.pow(2.0, 1.0/12.0);
    }
    // Getters and setters
    fun float getFreq() { return freq; }
    fun void setFreq(float val) { val => freq; }
}

OscChords.ck

// Class used to create chords.
public class OscChords {
    OscPitch oscPitch;
    this.root => float root;

    0.05 => float gain;
    0.0 => float mute;
    SawOsc I, III, V, VII;
    string currentChord;

    // initialize oscillators
    0 => I.freq;
    0 => III.freq;
    0 => V.freq;
    0 => VII.freq;
    mute => I.gain;
    mute => III.gain;
    mute => V.gain;
    mute => VII.gain;
    I   => dac;
    III => dac;
    V   => dac;
    VII => dac; 

    // Sets all chord frequencies to 0, making them inaudible.
    fun void noChord() {
        0 => I.freq;
        0 => III.freq;
        0 => V.freq;
        0 => VII.freq;
        "no chord" => currentChord;
    }

    // Creates a major chord.
    // @input float root : the root note of the chord.
    fun void major(float root) {
        root => oscPitch.freq => I.freq;
        oscPitch.change(4)    => III.freq;
        oscPitch.change(3)    => V.freq;
        0                     => VII.freq;
        "major" => currentChord;
    }
    // Creates a minor chord.
    // @input float root : the root note of the chord.
    fun void minor(float root) {
        root => oscPitch.freq => I.freq;
        oscPitch.change(3)    => III.freq;
        oscPitch.change(4)    => V.freq;
        0                     => VII.freq;
        "minor" => currentChord;
    }
    // Creates a major7 chord.
    // @input float root : the root note of the chord.
    fun void major7(float root) {
        root => oscPitch.freq => I.freq;
        oscPitch.change(4)    => III.freq;
        oscPitch.change(3)    => V.freq;
        oscPitch.change(4)    => VII.freq;
        "major7" => currentChord;
    }
    // Sets the gain on the chord to make it audible.
    fun void play() {
        gain => I.gain;
        gain => III.gain;
        gain => V.gain;
        gain => VII.gain;
    }
    // Sets the gain on the chord to make it inaudible.
    fun void stop() {        
        mute => I.gain;
        mute => III.gain;
        mute => V.gain;
        mute => VII.gain;
    }
    // @return string : the name of the current chord.
    fun string getCurrentChord() { return currentChord; }
}

TempoMeter.ck

public class TempoMeter {
    this.bpm => float bpm;
    this.upper => int upper;
    this.lower => int lower;
    60 => int SECONDS_PER_MINUTE;
    // default values
    1::second => dur beat;
    4::beat => dur measure;

    // Divide a minute into the number of beats per minute (bpm)
    //   and return the duration of 1 beat. Default 60 bpm unless set otherwise.
    // @param float bpm : the beats per minute of the tempo
    // @return dur : the duration of a beat
    fun dur setTempo(float bpm) {
        if (Math.isnan(bpm)) {
            <<< now, ": ", bpm, " is not a number" >>>;
            return beat;
        } else {
            (SECONDS_PER_MINUTE / bpm)::second => beat;
            return beat;
        }
    }

    // Set the meter (or time signature) using traditional Western
    //   music notation. Default 4/4 unless set otherwise.
    // @param int upper : the number of beats per measure
    // @param int lower : the duration ratio (or unit) of a beat
    // @return dur : the duration of a measure
    fun dur setMeter(int upper, int lower) {
        if (Math.isnan(upper) || Math.isnan(lower)) {
            <<< now, ": ", upper, " or ", lower, " is not a number" >>>;
            return measure;
        } else {
            lower / 4 => float beatDurationRatio;
            beat * beatDurationRatio => beat;
            upper::beat => measure;
            return measure;
        }
    }

    // Getters and setters
    fun float getBpm() { return bpm; }
    fun void setBpm(float val) { val => bpm; }
    fun int getUpper() { return upper; }
    fun void setUpper(int val) { val => upper; }
    fun int getLower() { return lower; }
    fun void setLower(int val) { val => lower; }
}

fizzbuzz_run.ck

TempoMeter tempoMeter;
tempoMeter.setTempo(120) => dur beat;
tempoMeter.setMeter(4,4) => dur measure;

55 => float NOTE_A1;
OscPitch oscPitch;
NOTE_A1 => oscPitch.freq;
1 => int NoteAOctave;

OscChords chord;

// oscillator to digital audio controller
TriOsc fizzbuzzOsc => dac;
oscPitch.freq => chord.root => fizzbuzzOsc.freq;

// audio controls
0.1 => fizzbuzzOsc.gain;

// variables for looping
now + 12::measure => time stop;
1 => int stepCounter;

while (now < stop) {
    chord.noChord();
    <<< "-----" >>>;
    if (stepCounter % 15 == 0) {
        chord.major7(oscPitch.getFreq());
        chord.play();
        <<< "Result: ", "FizzBuzz", "| chord:", chord.getCurrentChord() >>>;
    } else if (stepCounter % 5 == 0) {
        chord.minor(oscPitch.getFreq());
        chord.play();
        <<< "Result: ", "Fizz", "| chord:", chord.getCurrentChord() >>>;
    } else if (stepCounter % 3 == 0) {
        chord.major(oscPitch.getFreq());
        chord.play();
        <<< "Result: ", "Buzz", "| chord:", chord.getCurrentChord() >>>;
    } else {
        <<< "Result: ", stepCounter >>>;
    }
    // identify reference A notes and their octave and print them
    if (Math.floor(oscPitch.getFreq() % NOTE_A1) == 0.0) {
        <<< "Reference A", NoteAOctave >>>;
        1 +=> NoteAOctave;
    }
    <<< "Osc Freq: ", oscPitch.getFreq(), "Hz" >>>;

    beat +=> now;
    chord.stop();
    oscPitch.change(1) => fizzbuzzOsc.freq;
    1 +=> stepCounter;
}
<<< "end program" >>>;

And just for reference, though this only pulls the classes together at runtime:

fizzbuzz.ck

// fizzbuzz.ck
// This file defines which files to add to the VM
Machine.add("OscUtils/OscPitch.ck") => int OscPitchId;
Machine.add("OscUtils/OscChords.ck") => int OscChordsId;
Machine.add("TimeUtils/TempoMeter.ck") => int TempoMeterId;
if(me.args() > 0) {
    Machine.add("fizzbuzz_run:" + me.arg(0) + ".ck");
} else {
    Machine.add("fizzbuzz_run.ck");
}
\$\endgroup\$
  • 2
    \$\begingroup\$ Small variable nitpicking: I, III, V, and VII all mean something very distinct in music. Typically when musicians see Roman numerals we think of Roman Numeral Analysis where the number would represent a chord in the key, not the intervals in the chord. Instead I suggest renaming them to Root, Third, Fifth, and Seventh. \$\endgroup\$ – Dom Oct 7 '15 at 15:22
  • \$\begingroup\$ @Dom that's a really good catch, I shall correct that! \$\endgroup\$ – Phrancis Oct 7 '15 at 15:24
  • 4
    \$\begingroup\$ Just when you thought you had seen all kinds of FizzBuzz variations there could ever be... \$\endgroup\$ – Simon Forsberg Oct 7 '15 at 16:01
8
\$\begingroup\$

Really interesting thing you've created! I've never used ChucK, but I'll try to comment on some things. I also have some, but limited music theory knowledge. If language constraints is a reason for doing something I've mentioned below, please excuse my ignorance :)

Semitone ratio?

I wonder why you chose to create the getSemitoneRatio function. It's only used to initialize the SEMITONE variable. Why not just remove that function and put the body as the initializer expression instead?

Additionally, I wouldn't actually call it a ratio. It's a factor.

// The factor to apply to a frequency to go up or down one semitone
// More info: https://en.wikipedia.org/wiki/Twelfth_root_of_two
Math.pow(2.0, 1.0/12.0) => float SEMITONE_FACTOR;

Dry initialization

// initialize oscillators
0 => I.freq;
0 => III.freq;
0 => V.freq;
0 => VII.freq;
mute => I.gain;
mute => III.gain;
mute => V.gain;
mute => VII.gain;
I   => dac;
III => dac;
V   => dac;
VII => dac; 

// Sets all chord frequencies to 0, making them inaudible.
fun void noChord() {
    0 => I.freq;
    0 => III.freq;
    0 => V.freq;
    0 => VII.freq;
    "no chord" => currentChord;
}

Since you already have the noChord function you can easily remove part of this code duplication. Just call noChord instead of the first four lines after the comment. The side-effect of initializing currentChord as well is probably just good.

Dry initialization 2

Another approach to this is creating a factory function for SawOsc. Compare the following examples.

SawOsc I, III, V, VII;

0 => I.freq;
0 => III.freq;
0 => V.freq;
0 => VII.freq;
mute => I.gain;
mute => III.gain;
mute => V.gain;
mute => VII.gain;
createSawOsc(0, mute) => SawOsc I;
createSawOsc(0, mute) => SawOsc III;
createSawOsc(0, mute) => SawOsc V;
createSawOsc(0, mute) => SawOsc VII;

Gain?

In OscChord there are two variables representing different gain levels.

0.05 => float gain;
0.0 => float mute;

It's confusing that one is just named gain while the other one is called mute. Perhaps call them muteGain and playGain to show their relationship.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.