Chord + Quantized Melody Machine using Crow and Plaits

Chord + Quantized Melody Machine using Crow and Plaits

Feed Crow random voltages and let it output quantized melody that is harmonized by Plaits in chord mode.

I originally developed this script for use with the Ensemble Oscillator by 4ms but decided to reconfigure to be compatible with the ubiquitous Plaits.

This script is the result from my exploration in attempting some basic harmonic analysis leveraging bitwise operators, which I documented here: Using Bitwise Operators for Music Theory / travistingey | Observable.

The algorithm works as follows:

  1. A table of intervals are defined that match the chords available in Plaits
  2. Random voltages are streamed into Input 1, which are quantized and stored in a table that acts as buffer for the last 16 notes
  3. When Crow receives a trigger from Input 2, it looks at the note history and maps which intervals are present for each root note
  4. Found chords are scored based based on frequency of the chord notes within the note history and if the new chord shares notes with the current chord. This provides some weight to a number of choices and helps pick the the chord chord that would best fit
  5. Crow then sets the melody quantization to match the scale and Plaits receives the root note and proper voltage to select the identified chord.

There are a culmination of many ideas in this one script and I’m interested in exploring or expanding in the future. Just a few thoughts I have going but would love to hear your feedback:

  • Weighted quantizers that emphasize a specific mode of the scale
  • Build random chord progressions through voice leading and stacking intervals
  • Build an chord autoharp/quantizer that can be controlled manually rather than generatively

Requirements

Crow, Plaits, and another oscillator

Documentation

Input 1: Random voltage alla Turing Machine for the melody
Input 2: Trigger for the chord change

Output 1: Quantized v/oct melody
Output 3: to Plaits V/Oct
Output 4: to Plaits Harmonics

Before playing make sure you tune the oscillator used for the melody and Plaits together so they’re in unison. Set Plaits to chord mode and make sure Harmonics is all the way to the left.

Script

  ------ Chord + Quantized Melody Machine using Crow and Plaits
    -- Input 1: Random 0-5 Volts
    -- Input 2: Trigger Analysis & Chord Change
    -- Output 1: Quantized Melody 
    -- Output 2: None
    -- Output 3: Plaits 1v/oct / Root Note
    -- Output 4: Plaits Harmonics / Chord Select CV

    -- Definition Lists
    note_def = {'C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'}
    scale_select= {0, 0.5, 0.9, 1.4, 1.9, 2.4, 2.8, 3.3, 3.8, 4.2, 4.7} -- voltage threshold for changing chords in Plaits
    Intervals = {
        {name = " oct", notes = {0}, scale = {0,1,2,3,4,5,6,7,8,9,10,11}, select = 1},        -- 1
        {name = " 5", notes = {0,7}, scale = {0,0,0,2,4,5,7,7,7,9,10,11}, select = 2},        -- 2
        {name = " sus4", notes = {0,5,7}, scale = {0,5,5,5,5,7,7,10,10,10,14,9}, select = 3 },  -- 3
        {name = " minor", notes = {0,3,7}, scale = {0,0,3,3,3,7,7,7,2,2,5,9}, select = 4},      -- 4
        {name = " m7", notes = {0,3,10}, scale = {0,0,3,3,3,7,7,10,10,2,5,9}, select = 5},    -- 5
        {name = " m9", notes = {0,2,3,10}, scale = {0,0,3,3,7,7,10,10,10,14,14,5}, select = 6},  -- 6
        {name = " m11", notes = {0,3,5,10}, scale = {0,0,3,3,7,7,10,10,10,2,17,17}, select = 7}, -- 7
        {name = " 69", notes = {0,2,9}, scale = {0,0,4,4,7,7,9,9,9,14,14,14}, select = 8},     -- 8
        {name = " maj9", notes = {0,2,4,11}, scale = {0,0,4,4,7,7,11,11,14,14,5,2}, select = 9},  -- 9
        {name= " maj7", notes = {0,4,11}, scale = {0,0,4,4,4,7,7,11,11,2,5,9}, select = 10},    -- 10
        {name= " major", notes = {0,4,7}, scale = {0,0,0,4,4,4,7,7,2,5,9,11}, select = 11},      --11                                             -- 11
    }

    ---- Variables
    history_length = 16
    note_history = {}
    note_freq = {0,0,0,0,0,0,0,0,0,0,0,0}
    quantize_scale = {0,0,0,4,4,4,7,7,2,5,9,11}
    current_chord = {
        root = 0,
        select = 1,
        notes = Intervals[1].notes,
        intervals = Intervals[1].notes
    }

    ---- Array Functions
    table.reduce = function (t, fn, start)
        local acc
        for index, value in ipairs(t) do
            if start and index == 1 then
                acc = start
            elseif index == 1 then
                acc = value
            else
                acc = fn(acc, value)
            end
        end
        return acc
    end

    table.map = function(t,fn)
        local newTable = {}
        for index, value in ipairs(t) do
            newTable[index] = fn(value,index,t)
        end
        return newTable
    end

    table.concat = function(t1,t2)
        for i=1,#t2 do
            t1[#t1+1] = t2[i]
        end
        return t1
    end



    -- Table to Bit Mask Integer
    IntervalsToBitMask = function(scale)
        return table.reduce(scale, function(acc, cur)
            return (1 << cur) | acc
        end, 1 << scale[1])
    end

    BitMaskToIntervals = function(mask)
        local interval = {}
        for i = 1, 12, 1 do
            if mask >> (i - 1) & 1 == 1 then
                table.insert(interval,1,i - 1)
            end
        end
        return interval
    end

    -- Table of note frequency - Index is equal to note value
    NoteFrequency = function(notes)
        note_freq = {0,0,0,0,0,0,0,0,0,0,0,0}
           
        for i = 1, #notes do
            note = notes[i]
            note_freq[note + 1] = note_freq[note + 1] + 1 
        end

        return note_freq
    end

    -- Takes note frequency table and sums the frequency of table of intervals
    IntervalScore = function(freq, chord)
        local intervals =  chord.intervals
        local score = 0     
        for i = 1, #intervals do
            score = score + freq[intervals[i]+1]
        end

        if #current_chord > 1 then 
            score = (current_chord.mask & chord.mask > 1) and (score + 1) or score    
        end

        return score
    end


    -- Identify positions in which provided intervals are found
    FindIntervals = function(notes, interval, freq)
        local note_mask = IntervalsToBitMask(notes)
        local found = {}
        local interval_mask = IntervalsToBitMask(interval.notes)

        for i = 1, 12, 1 do    
            if (note_mask & interval_mask) == interval_mask then
                local chord = {
                    root = i-1,
                    name = note_def[i] .. interval.name,
                    select = interval.select,
                    scale = interval.scale,
                    notes = table.map(interval.notes,function(d) return (d + i - 1)%12 end),
                    intervals = interval.notes
                }

                chord.mask = IntervalsToBitMask(chord.notes)
                chord.score = IntervalScore(freq,chord)

                table.insert(found,1,chord)
             end

        note_mask = (note_mask >> 1) | (note_mask << 11) & 4095
        end

        return found
    end

    -- Init
    function init()
        start = time()
        input[1].mode('scale', {0,1,2,3,4,5,6,7,8,9,10,11})
        input[2].mode('change')
    end


    -- Runtime
    input[2].change = function(c)
        if c and #note_history >= 12 then
            local freq = NoteFrequency(note_history)
            local found = {}
        
            for index, interval in ipairs(Intervals) do
                table.concat(found, FindIntervals(note_history, interval, freq) )
            end
           
            if(#found > 0) then
                table.sort(found, function (a,b)
                    return a.score > b.score
                end)
                print(found[1].name)
                quantize_scale = found[1].scale
                output[3].volts = found[1].root / 12
                output[4].volts = scale_select[found[1].select]
            end
          
        end 
    end

    input[1].scale = function(s)
        if #note_history == history_length then
            table.remove(note_history,16)
        end

        table.insert(note_history,1,quantize_scale[s.index] % 12)
        output[1].volts = quantize_scale[s.index] * 1/12
    end
16 Likes

This is grand. fits well with all my other randomness to give melody. I also have an Ensemble Osc. Do I need to tweek the script to run this on that?

Ensemble Oscillator really opens up possibilities with the ability to program custom intervals!

I was actually programing my own scales to use on the EO, so I never went through the steps of configuring the script for the default 12-tone scales, but I will try to do this for you to keep it more plug and play.

Primary areas to configure is the “Interval” table and the “scale_select”, which are just the voltages that correlates to each scale setting on the EO (renaming this variable to harmonics_select would make more sense in the above code, but since I started with the EO…). Also keep in mind Plaits has 11 chord settings where EO has 10.

If you’re interested, I can share a simple code I wrote to make programing the custom scales for the EO a lot easier. I predefined all the chord intervals in a legs table, then run a function through Druid to run through the notes in Learn mode.

Last thing I’ll add is how I’m doing a weighted quantized scale—input voltages are quantized so that you can have discrete indices to lookup against a scale table defined in each interval.

Keep in mind, the “extra” notes outside the chord tones have a strong effect how the progression changes and there’s A LOT of room to experiment here. For example, if you just play the three notes in a triad, it will never change chords because that same chord is the “best” choice but throw in some extensions or notes out of the scale would throw a curveball chord (non-diatonic). There’s a balance to strike here that can make the chord changes static or chaotic.

Hope this makes sense. Happy to clarify any details and share any pearls I can.

@Bman - I spent some time this morning configuring the script for the default 12TET setting for the EO. There’s some room to experiment with how the scales in the Interval table are defined—changes are a bit more rigid here but listening to it, it has some slowly evolving moody vibes… kind of dig it. You will notice it will get stuck on the Circle of Fifths until it breaks out. Once it picks up the minor chord, theres some nice changes:

------ Chord + Quantized Melody Machine using Crow and Ensemble Oscillator
-- Input 1: Random 0-5 Volts
-- Input 2: Trigger Analysis & Chord Change
-- Output 1: Quantized Melody 
-- Output 2: None
-- Output 3: Ensemble Oscillator 1v/oct / Root Note
-- Output 4: Ensemble Oscillator Scale / Chord Select CV

-- Definition Lists
note_def = {'C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'}
scale_select= {0, 0.5, 0.9, 1.5, 2, 2.6, 3.2, 3.7, 4.4, 4.9} -- voltage threshold for changing chords in Ensemble Oscillator
Intervals = {
    {name = " oct", notes = {0}, scale = {0,0,0,0,0,0,2,4,5,7,9,11}, select = 1},                       -- 1
    {name = " 5", notes = {0,7}, scale = {0,0,0,2,4,5,7,7,7,9,10,11}, select = 2},                      -- 2
    {name= " major", notes = {0,4,7}, scale = {0,0,0,4,4,4,7,7,2,5,9,11}, select = 3},                  -- 3
    {name = " minor", notes = {0,3,7}, scale = {0,0,3,3,3,7,7,7,2,2,5,9}, select = 4},                  -- 4
    {name = " minor sus4", notes = {0,5,7}, scale = {0,5,5,5,5,7,7,10,10,10,14,9}, select = 5 },        -- 5
    {name = " m7", notes = {0,3,10}, scale = {0,0,3,3,3,7,7,10,10,2,5,9}, select = 6},                  -- 6
    {name= " maj7", notes = {0,4,11}, scale = {0,0,4,4,4,7,7,11,11,2,5,9}, select = 7},                 -- 7
    {name = " stacked fifths", notes = {0,7,2,9}, scale = {0,0,0,7,7,7,14,14,14,21,21,21}, select = 8}, -- 8
    {name = " minor b9/#9", notes = {0,3,5,10}, scale = {0,0,3,3,5,5,7,7,10,10,13,15}, select = 9},     -- 9
    {name = " semitones", notes = {0,1,2,3}, scale = {0,1,2,3,4,5,6,7,8,9,10,11}, select = 10}          -- 10
}

---- Variables
history_length = 16
note_history = {}
note_freq = {0,0,0,0,0,0,0,0,0,0,0,0}
quantize_scale = {0,0,0,4,4,4,7,7,2,5,9,11}
current_chord = {
    root = 0,
    select = 1,
    notes = Intervals[1].notes,
    intervals = Intervals[1].notes
}

---- Array Functions
table.reduce = function (t, fn, start)
    local acc
    for index, value in ipairs(t) do
        if start and index == 1 then
            acc = start
        elseif index == 1 then
            acc = value
        else
            acc = fn(acc, value)
        end
    end
    return acc
end

table.map = function(t,fn)
    local newTable = {}
    for index, value in ipairs(t) do
        newTable[index] = fn(value,index,t)
    end
    return newTable
end

table.concat = function(t1,t2)
    for i=1,#t2 do
        t1[#t1+1] = t2[i]
    end
    return t1
end



-- Table to Bit Mask Integer
IntervalsToBitMask = function(scale)
    return table.reduce(scale, function(acc, cur)
        return (1 << cur) | acc
    end, 1 << scale[1])
end

BitMaskToIntervals = function(mask)
    local interval = {}
    for i = 1, 12, 1 do
        if mask >> (i - 1) & 1 == 1 then
            table.insert(interval,1,i - 1)
        end
    end
    return interval
end

-- Table of note frequency - Index is equal to note value
NoteFrequency = function(notes)
    note_freq = {0,0,0,0,0,0,0,0,0,0,0,0}
       
    for i = 1, #notes do
        note = notes[i]
        note_freq[note + 1] = note_freq[note + 1] + 1 
    end

    return note_freq
end

-- Takes note frequency table and sums the frequency of table of intervals
IntervalScore = function(freq, chord)
    local intervals =  chord.intervals
    local score = 0     
    for i = 1, #intervals do
        score = score + freq[intervals[i]+1]
    end

    if #current_chord > 1 then 
        score = (current_chord.mask & chord.mask > 1) and (score + 1) or score    
    end

    return score
end


-- Identify positions in which provided intervals are found
FindIntervals = function(notes, interval, freq)
    local note_mask = IntervalsToBitMask(notes)
    local found = {}
    local interval_mask = IntervalsToBitMask(interval.notes)

    for i = 1, 12, 1 do    
        if (note_mask & interval_mask) == interval_mask then
            local chord = {
                root = i-1,
                name = note_def[i] .. interval.name,
                select = interval.select,
                scale = interval.scale,
                notes = table.map(interval.notes,function(d) return (d + i - 1)%12 end),
                intervals = interval.notes
            }

            chord.mask = IntervalsToBitMask(chord.notes)
            chord.score = IntervalScore(freq,chord)

            table.insert(found,1,chord)
         end

    note_mask = (note_mask >> 1) | (note_mask << 11) & 4095
    end

    return found
end

-- Init
function init()
    start = time()
    input[1].mode('scale', {0,1,2,3,4,5,6,7,8,9,10,11})
    input[2].mode('change')
end


-- Runtime
input[2].change = function(c)
    if c and #note_history >= 12 then
        local freq = NoteFrequency(note_history)
        local found = {}
    
        for index, interval in ipairs(Intervals) do
            table.concat(found, FindIntervals(note_history, interval, freq) )
        end
       
        if(#found > 0) then
            table.sort(found, function (a,b)
                return a.score > b.score
            end)
            print(found[1].name)
            quantize_scale = found[1].scale
            output[3].volts = found[1].root / 12
            output[4].volts = scale_select[found[1].select]
        end
      
    end 
end

input[1].scale = function(s)
    if #note_history == history_length then
        table.remove(note_history,16)
    end

    table.insert(note_history,1,quantize_scale[s.index] % 12)
    output[1].volts = quantize_scale[s.index] * 1/12
end

The other script I mention helps programming the EO a bit easier. After you program your EO with desired chords, you can start swapping out the intervals in the detection script. Interval Scales will need to be manually set, so I may expand on this later to make it easier:

--- Programming the 4ms Ensemble Oscillator
-- Out 1: Pitch
-- Out 2: Learn Trigger
-- Direction: Patch EO, Select scale you want to program and enter Learn mode. Run > Program(#) to cycle through intervals. Hit Learn button again to exit.

function init()
    output[1].slew = 0.001
end

Intervals = {
--[[ 1 ]] {name = "major ", notes = {0,4,7}},
--[[ 2 ]] {name = "minor", notes = {0,3,7}},
--[[ 3 ]] {name = "°", notes = {0,3,6}},
--[[ 4 ]] {name = "+", notes = {0,4,8}},
   
--[[ 5 ]] {name = "sus4", notes = {0,5,7}},
--[[ 6 ]] {name = "sus2", notes = {0,2,7}},
--[[ 7 ]] {name = "9sus4", notes = {0,5,7,10,14}},
--[[ 8 ]] {name = "13sus4", notes = {0,5,7,10,21}}, 

--[[ 9 ]] {name = "maj7", notes = {0,4,7,11}},
--[[ 10 ]] {name = "maj9", notes = {0,4,7,11,14}}, 
--[[ 11 ]] {name = "maj13", notes = {0,4,7,11,21}},
--[[ 12 ]] {name = "maj9+6", notes = {0,4,7,9,11,14}},

--[[ 13 ]] {name = "7", notes = {0,4,7,10}}, 
--[[ 14 ]] {name = "9", notes = {0,4,7,10,14}},
--[[ 15 ]] {name = "13", notes = {0,4,7,10,21}},
--[[ 16 ]] {name = "7#9", notes = {0,4,7,10,15}},
--[[ 17 ]] {name = "13b9", notes = {0,4,7,10,14,21}},
--[[ 18 ]] {name = "13#9", notes = {0,4,7,10,15,21}},
--[[ 19 ]] {name = "7b9", notes = {0,4,7,10,14}},

--[[ 20 ]] {name = "m6", notes = {0,3,7,9}}, 
--[[ 21 ]] {name = "m7", notes = {0,3,7,10}},
--[[ 22 ]] {name = "m9", notes = {0,3,7,10,14}},
--[[ 23 ]] {name = "m11", notes = {0,3,7,17}},
--[[ 24 ]] {name = "m13", notes = {0,3,7,21}},

--[[ 21 ]] {name= "ø", notes = {0,3,6,10}}, 
}

    
function Program(interval)
    interval = Intervals[interval]
    step = 0
    
    print(interval.name)
    metro[1].event = function(c)
        if step == #interval.notes then 
            output[2].volts = 0
            metro[1]:stop()
            print('done') 
        end

        step = step + c%2

        if (c+1)%2 == 0 then
            print(step)
            output[1].volts = interval.notes[step] / 12
            output[2].volts = 5
        else
            output[1].volts = 0
            output[2].volts = 0
        end 

        
        
    end

    metro[1].time = 0.25
    metro[1]:start()
end

Thanks so much. I had this playing throughout the day yesterday as I did other things. Really like the concept and execution. Thanks for sharing the code. I am more of an attorney than a musician so any that helps is much appreciated. I think crow is still an underutilized module that has tremendous possibilities. Again great work and hopefully Karma rewards you :grinning:

1 Like

really cool and sounds great! fun to use with other modes than chord too : )

1 Like