Permalink
Cannot retrieve contributors at this time
| -- Bletchley Park | |
| -- 2 voice x 3 bit shift register | |
| -- with or without grid, midi capable | |
| -- | |
| -- enc1: bpm | |
| -- enc2: pulse division | |
| -- enc3: voice 1 trigger probability | |
| -- key2: start/stop | |
| -- key3: next UI page (0/1) | |
| -- | |
| -- voices default to midi channels 1 and 2 | |
| -- (use 3 and 4 for cvpal) | |
| -- | |
| -- GRID UI | |
| -- PAGE 0: | |
| -- Each column denotes a step in the register (1-16) | |
| -- Each row is a possible value of the register (1-8) | |
| -- | |
| -- PAGE 1: | |
| -- Top 3 rows denote shift registers in binary | |
| -- Same # of registers, same # of possible values | |
| -- Row 4: Voice 1 Probability (1-8) Voice 2 Probability (9-16) | |
| -- Row 5: Loop length (1-16) | |
| -- Row 6: Voice 1 Octave Range (1-4) Register Change Chance (5-12) Voice 2 Octave Range (13-16) | |
| -- Row 7: Voice 1 Pitch Offset (1-8) Voice 2 Pitch Offset (9-16) | |
| -- Row 8: Key/Root Select (Circle of 5ths) (1-12) Scale Select (13-14) Start/Stop (16) | |
| beatclock = require 'beatclock' | |
| engine.name = 'PolyPerc' | |
| function midi_to_hz(note) | |
| return (440/32) * (2 ^ ((note - 9) / 12)) | |
| end | |
| local page = 1 | |
| -- default per-voice settings for custom hacking | |
| local voices = { | |
| -- switch to 3/4 for CVpal | |
| {channel=1, range=2, offset=35, prob=8, accum=1, accumulator=0, gate_time=0.35, off_metro=metro.alloc()}, | |
| {channel=2, range=3, offset=35, prob=1, accum=1, accumulator=0, gate_time=0.35, off_metro=metro.alloc()} | |
| } | |
| local clock_divide = 2 | |
| local g -- grid | |
| local m -- midi | |
| local clk = beatclock.new() | |
| clk.steps_per_beat = clock_divide | |
| clk:bpm_change(clk.bpm) | |
| local started = false | |
| local pulse_on = metro.alloc() | |
| pulse_on.time = 0.1 | |
| pulse_on.count = -1 | |
| -- since I'm only dealing with triggers note-off is mostly untested | |
| local pulse_off = metro.alloc() | |
| pulse_off.time = 0.35 | |
| pulse_off.count = 1 | |
| local chance = 5 | |
| local loop_length = 5 | |
| local circle_of_fifths = { | |
| {n=0, name="C"}, {n=7, name="G"}, {n=2, name="D"}, {n=9, name="A"}, | |
| {n=4, name="E"}, {n=11, name="B"}, {n=6, name="F#"}, {n=1, name="C#"}, | |
| {n=8, name="G#"}, {n=3, name="E#"}, {n=10, name="A#"}, {n=5, name="F"} | |
| } | |
| local circle_of_fifths_labels = {} | |
| for l = 1,#circle_of_fifths do | |
| circle_of_fifths_labels[l] = circle_of_fifths[l].name | |
| end | |
| -- my scaling logic is kind of weak but this works | |
| -- note that each scale starts with 0 and ends with 12 | |
| local scales = { | |
| {name="CHROMATIC", scale={0,1,2,3,4,5,6,7,8,9,10,11, 12}}, | |
| {name="DIATONIC", scale={0,2,4,5,7,9,11, 12}}, | |
| {name="PENTATONIC", scale={0,3,5,7,9,12}}, | |
| {name="ONEFOURFIVE", scale={0,5,7,12}}, | |
| {name="ONEFIVE", scale={0,7,12}}, | |
| {name="OCTAVE", scale={0,12}} | |
| } | |
| local scale_labels = {} | |
| for s = 1, #scales do | |
| scale_labels[s] = scales[s].name | |
| end | |
| -- 8 levels of trigger probability (all are * 1/16) | |
| local trigger_prob_table = { | |
| 0, | |
| 1, | |
| 3, | |
| 5, | |
| 8, | |
| 11, | |
| 15, | |
| 16 | |
| } | |
| -- nominal octave range levels | |
| -- since the algorithm is center-biased they don't sound this wide | |
| local range_options = { | |
| 2,3,4,5 | |
| } | |
| -- base midi note - should offer an additional tweakable param | |
| local offset_options = { | |
| 12,24,36,42,48,54,60,66,72 | |
| } | |
| -- Set up 16 registers, with a value from 1-8 | |
| local MAX_STEP = 16 | |
| local current_step = MAX_STEP | |
| local registers = {} | |
| for i = 1,MAX_STEP do | |
| registers[i] = 1 | |
| end | |
| -- simple quantizer, could use some love | |
| function to_scale(raw_n) | |
| local scale_offsets = scales[params:get("scale")].scale | |
| local scale_root = circle_of_fifths[params:get("root")].n | |
| local n = raw_n - scale_root | |
| local pitch_class = n % 12 | |
| local octave = math.floor(n / 12) | |
| local rounded = 0 | |
| for offset=1,6 do | |
| if pitch_class < scale_offsets[offset] then | |
| rounded = scale_offsets[offset - 1] | |
| -- print("rounded to " .. rounded) | |
| break | |
| end | |
| end | |
| local output = scale_root + rounded + (12 * octave) | |
| return output | |
| end | |
| -- utilities for viewing the register as 3 binary digits | |
| function hi_bit(n) | |
| if n == 8 then return 1 | |
| elseif n == 7 then return 1 | |
| elseif n == 6 then return 1 | |
| elseif n == 5 then return 1 | |
| else return 0 | |
| end | |
| end | |
| function mid_bit(n) | |
| if n == 8 then return 1 | |
| elseif n == 7 then return 1 | |
| elseif n == 3 then return 1 | |
| elseif n == 4 then return 1 | |
| else return 0 | |
| end | |
| end | |
| function lo_bit(n) | |
| if n == 8 then return 1 | |
| elseif n == 2 then return 1 | |
| elseif n == 4 then return 1 | |
| elseif n == 6 then return 1 | |
| else return 0 | |
| end | |
| end | |
| function get_next_step() | |
| local next_step = current_step + 1 | |
| if next_step > MAX_STEP then | |
| next_step = 1 | |
| end | |
| return next_step | |
| end | |
| -- in page 0, directly draw the register values 1-8 onto the grid | |
| function redraw_grid_page0() | |
| local next_step = get_next_step() | |
| print("redraw_grid_page0") | |
| for i=1,MAX_STEP do | |
| for j=1,8 do | |
| if (registers[i] == j) then | |
| g.led(i, 9 - j, 10) | |
| elseif next_step == i then | |
| g.led(i, 9 - j, 1) | |
| else | |
| g.led(i, 9 - j, 0) | |
| end | |
| end | |
| end | |
| g.refresh() | |
| end | |
| -- in page 1, draw the registers in binary to the top 3 rows, use the bottom 5 for UI | |
| function redraw_grid_page1() | |
| local next_step = get_next_step() | |
| -- print("redraw_grid_expert") | |
| local r = registers | |
| for i=1,MAX_STEP do | |
| -- row 1 - high bit - value of 4 | |
| if hi_bit(r[i]) == 1 then | |
| g.led(i, 1, 4) | |
| elseif next_step == i then | |
| g.led(i, 1, 2) | |
| else | |
| g.led(i, 1, 0) | |
| end | |
| -- row 2 - middle bit - value of 2 | |
| if mid_bit(r[i]) == 1 then | |
| g.led(i, 2, 4) | |
| elseif next_step == i then | |
| g.led(i, 2, 2) | |
| else | |
| g.led(i, 2, 0) | |
| end | |
| -- row 3 - low bit - value of 1 | |
| if lo_bit(r[i]) == 1 then | |
| g.led(i, 3, 4) | |
| elseif next_step == i then | |
| g.led(i, 3, 2) | |
| else | |
| g.led(i, 3, 0) | |
| end | |
| for j=4,8 do | |
| -- row 4 - trigger prob voice 1/2 | |
| if j == 4 then | |
| if (i < 9) and ( i < voices[1].prob) then | |
| g.led(i,j,2) | |
| elseif (i < 9) and ( i == voices[1].prob) then | |
| g.led(i,j,4) | |
| elseif (i >= 9) and ( (i - 8) < voices[2].prob) then | |
| g.led(i,j,2) | |
| elseif (i >= 9) and ( (i - 8) == voices[2].prob) then | |
| g.led(i,j,4) | |
| else | |
| g.led(i,j,0) | |
| end | |
| -- row 5 - loop length | |
| elseif j == 5 then | |
| if i < loop_length then | |
| g.led(i,j,2) | |
| elseif i == loop_length then | |
| g.led(i,j,4) | |
| else | |
| g.led(i,j,0) | |
| end | |
| -- row 6 -- voice 1 range / chance / voice 2 range | |
| elseif j == 6 then | |
| if i < 5 and ( i < params:get("voice_1_pitch_range")) then | |
| g.led(i,j,2) | |
| elseif i < 5 and ( i == params:get("voice_1_pitch_range")) then | |
| g.led(i,j,4) | |
| elseif i < 5 then | |
| g.led(i,j,0) | |
| elseif i >= 5 and i <= 12 and ((i - 4) < chance) then | |
| g.led(i,j,2) | |
| elseif i >= 5 and i <= 12 and (chance == (i - 4)) then | |
| g.led(i,j,4) | |
| elseif i >= 13 and ( (i - 12) < params:get("voice_2_pitch_range")) then | |
| g.led(i,j,2) | |
| elseif i >= 13 and ( (i - 12) == params:get("voice_2_pitch_range")) then | |
| g.led(i,j,4) | |
| else | |
| g.led(i,j,0) | |
| end | |
| -- row 7 -- voice 1 offset / voice 2 offset | |
| elseif j == 7 then | |
| if (i < 9) and ( i < params:get("voice_1_pitch_offset")) then | |
| g.led(i,j,2) | |
| elseif (i < 9) and ( i == params:get("voice_1_pitch_offset")) then | |
| g.led(i,j,4) | |
| elseif i >= 9 and ( (i - 8) < params:get("voice_2_pitch_offset")) then | |
| g.led(i,j,2) | |
| elseif i >= 9 and ( (i - 8) == params:get("voice_2_pitch_offset")) then | |
| g.led(i,j,4) | |
| else | |
| g.led(i,j,0) | |
| end | |
| -- row 8 -- key (circle of fifths) / scale / start/stop | |
| elseif j == 8 then | |
| if i <= 12 and i == params:get("root") then | |
| g.led(i,j,4) | |
| elseif i <= 12 then | |
| g.led(i,j,2) | |
| elseif i <= 14 then | |
| g.led(i,j,4) | |
| elseif i == 15 then | |
| g.led(i,j,0) | |
| elseif i == 16 then | |
| g.led(i,j,4) | |
| end | |
| else | |
| g.led(i,j,g_default_exp[j][i]) | |
| end | |
| end | |
| end | |
| g.refresh() | |
| end | |
| function redraw_grid() | |
| if page == 1 then | |
| redraw_grid_page1() | |
| else | |
| redraw_grid_page0() | |
| end | |
| end | |
| function g_event_page0(x,y,z) | |
| local val = 9 - y | |
| print("grid - x: " .. x .. " y: " .. y .. " z: " .. z .. " VALUE: " .. 9 - y) | |
| registers[x] = val | |
| redraw_grid() | |
| end | |
| function g_event_page1(x,y,z) | |
| print("grid - x: " .. x .. " y: " .. y .. " z: " .. z .. " VALUE: " .. 9 - y) | |
| if y == 1 and z == 1 then | |
| if hi_bit(registers[x]) == 1 then | |
| registers[x] = registers[x] - 4 | |
| else | |
| registers[x] = registers[x] + 4 | |
| end | |
| elseif y == 2 and z == 1 then | |
| if mid_bit(registers[x]) == 1 then | |
| registers[x] = registers[x] - 2 | |
| else | |
| registers[x] = registers[x] + 2 | |
| end | |
| elseif y == 1 and z == 1 then | |
| if low_bit(registers[x]) == 1 then | |
| registers[x] = registers[x] - 1 | |
| else | |
| registers[x] = registers[x] + 1 | |
| end | |
| elseif y == 4 and z == 1 and x < 9 then | |
| params:set("voice_1_trigger_prob", x) | |
| elseif y == 4 and z == 1 and x >= 9 then | |
| params:set("voice_2_trigger_prob", x - 8) | |
| elseif y == 5 and z == 1 then | |
| params:set("loop_length", x) | |
| elseif y == 6 and z == 1 and x <= 4 then | |
| params:set("voice_1_pitch_range", x) | |
| elseif y == 6 and z == 1 and x > 4 and x < 13 then | |
| params:set("chance", x - 4) | |
| elseif y == 6 and z == 1 and x >= 13 then | |
| params:set("voice_2_pitch_range", x - 12) | |
| elseif y == 7 and z == 1 and x < 9 then | |
| params:set("voice_1_pitch_offset", x) | |
| elseif y == 7 and z == 1 and x >= 9 then | |
| params:set("voice_2_pitch_offset", x - 8) | |
| elseif y == 8 and z == 1 and x < 13 then | |
| params:set("root", x) | |
| elseif y == 8 and z == 1 and x == 13 then | |
| params:delta("scale",-1) | |
| elseif y == 8 and z == 1 and x == 14 then | |
| params:delta("scale",1) | |
| elseif y == 8 and z == 1 and x == 16 then | |
| toggle_clock() | |
| end | |
| redraw() | |
| redraw_grid() | |
| end | |
| function init() | |
| g = grid.connect() | |
| g.event = function(x,y,z) | |
| if page == 0 then | |
| g_event_page0(x,y,z) | |
| else | |
| g_event_page1(x,y,z) | |
| end | |
| end | |
| m = midi.connect(1) | |
| m.event = function(data) | |
| return | |
| end | |
| print(dump(m)) | |
| --midi panic on startup | |
| for i = 1,128 do | |
| m.note_off(i,0,voices[1].channel) | |
| m.note_off(i,0,voices[2].channel) | |
| end | |
| pulse_on.callback = on_pulse | |
| params:add_number("chance","chance",1,8,5) | |
| params:set_action("chance", function(x) chance = x end) | |
| params:add_number("loop_length", "loop_length",1,16,5) | |
| params:set_action("loop_length", function(x) loop_length = x end) | |
| params:add_option("scale", "scale", scale_labels) | |
| params:set("scale",3) | |
| params:set_action("scale", function(x) print("selected scale " .. x) end) | |
| params:add_option("root", "root", circle_of_fifths_labels) | |
| params:set("root",1) | |
| params:set_action("root", function(x) print("selected root " .. x) end) | |
| params:add_trigger("clock_tog", "clock_tog", 0,1,0) | |
| params:set_action("clock_tog", function() toggle_clock() end) | |
| params:add_number("clock_divide", "clock_divide", 1, 8, 2) | |
| params:set_action("clock_divide", function(x) | |
| clock_divide = x | |
| clk.steps_per_beat = clock_divide | |
| clk:bpm_change(clk.bpm) | |
| end) | |
| clk.on_step = on_pulse | |
| clk.on_select_internal = function() print("internal clock") end | |
| clk.on_select_external = function() print("external clock") end | |
| clk:add_clock_params() | |
| local voice_params = { | |
| {name="pitch_range", min=0, max=8, default=5}, | |
| {name="pitch_offset", min=0, max=60, default=15}, | |
| {name="trigger_prob", min=1, max=16, default=1}, | |
| } | |
| for voice=1,#voices do | |
| params:add_separator() | |
| params:add_number("voice_"..voice.."_midi_channel","voice_"..voice.."_midi_channel",0,16,1) | |
| params:set_action("voice_"..voice.."_midi_channel", function(x) voices[voice].channel = x end) | |
| params:add_option("voice_"..voice.."_pitch_range","voice_"..voice.."_pitch_range", range_options) | |
| params:set("voice_"..voice.."_pitch_range",3) | |
| params:set_action("voice_"..voice.."_pitch_range", function(x) voices[voice].range = range_options[x] end) | |
| params:add_option("voice_"..voice.."_pitch_offset","voice_"..voice.."_pitch_offset", offset_options) | |
| params:set("voice_"..voice.."_pitch_offset",4) | |
| params:set_action("voice_"..voice.."_pitch_offset", function(x) voices[voice].offset = offset_options[x] end) | |
| params:add_number("voice_"..voice.."_trigger_prob","voice_"..voice.."_trigger_prob",1,8,8) | |
| params:set_action("voice_"..voice.."_trigger_prob", function(x) voices[voice].prob = x end) | |
| end | |
| redraw() | |
| redraw_grid() | |
| end | |
| function toggle_clock() | |
| if started then | |
| clk:stop() | |
| started = false | |
| else | |
| clk:start() | |
| started = true | |
| end | |
| end | |
| function key(n,z) | |
| if z == 1 and n == 2 then | |
| toggle_clock() | |
| elseif z == 1 and n == 3 then | |
| if page == 0 then | |
| page = 1 | |
| else | |
| page = 0 | |
| end | |
| redraw() | |
| redraw_grid() | |
| end | |
| end | |
| function enc(n,d) | |
| if n == 1 then | |
| params:delta("bpm",d) | |
| elseif n == 2 then | |
| params:delta("clock_divide", d) | |
| elseif n == 3 then | |
| params:delta("voice_1_trigger_prob",d) | |
| redraw() | |
| end | |
| end | |
| function make_next_note() | |
| local next_step = current_step + 1 | |
| local loop_step = ((current_step - loop_length) % MAX_STEP) + 1 -- hmmm | |
| if next_step > MAX_STEP then | |
| next_step = 1 | |
| end | |
| chance_random = math.random(8) | |
| if chance_random <= chance then | |
| registers[next_step] = math.random(8) | |
| else | |
| registers[next_step] = registers[loop_step] | |
| end | |
| redraw_grid() | |
| redraw() | |
| local output = make_raw_note(next_step) | |
| current_step = current_step + 1 | |
| if current_step > MAX_STEP then | |
| current_step = 1 | |
| end | |
| return math.floor(output) | |
| end | |
| function make_raw_note(i) | |
| local output = 0 | |
| for j = 0,7 do | |
| local weight = math.pow(2,3 - j) | |
| local this_step = (i - j) | |
| if this_step <= 0 then | |
| this_step = this_step + 16 | |
| end | |
| output = output + (weight * (registers[this_step] - 1)) | |
| end | |
| return output | |
| end | |
| last_note = nil | |
| function on_pulse() | |
| local next_note_raw = make_next_note() | |
| for voice=1,#voices do | |
| trigger_random = math.random(16) | |
| if trigger_random <= trigger_prob_table[voices[voice].prob] then | |
| local ranged = ((next_note_raw / 112) * (12 * voices[voice].range )) + voices[voice].offset | |
| local scaled = to_scale(ranged) | |
| engine.hz(midi_to_hz(scaled)) | |
| m.note_on(scaled, 100, voices[voice].channel) | |
| voices[voice].off_metro.callback = make_off_pulse(scaled,voices[voice].channel) | |
| voices[voice].off_metro:start() | |
| end | |
| end | |
| end | |
| function make_off_pulse(note,chan) | |
| function f() | |
| m.note_off(note,0,chan) | |
| end | |
| end | |
| function off_pulse() | |
| if last_note then | |
| m.note_on(last_note,0,1) | |
| last_note = nil | |
| end | |
| end | |
| function draw_ui_page1() | |
| screen.clear() | |
| screen.move(0,30) | |
| screen.text("LENGTH: "..params:get("loop_length") .. " BPM " .. params:get("bpm") .. " DIV " .. params:get("clock_divide")) | |
| screen.move(0,38) | |
| screen.text("CHANGE: "..params:get("chance") .. " PROB 1:" .. params:get("voice_1_trigger_prob") .. " 2:".. params:get("voice_2_trigger_prob")) | |
| screen.move(0,46) | |
| screen.text("V:OFFST/RNG: 1:" .. offset_options[params:get("voice_1_pitch_offset")] .. "/" .. range_options[params:get("voice_1_pitch_range")] .. " 2:" .. offset_options[params:get("voice_2_pitch_offset")] .. "/" .. range_options[params:get("voice_2_pitch_range")] ) | |
| screen.move(0,54) | |
| screen.text("SCALE: " .. circle_of_fifths[params:get("root")].name .. " " .. scales[params:get("scale")].name ) | |
| screen.move(0,62) | |
| screen.text("PUSH 2: START/STOP 3: PAGE 0") | |
| local r = registers | |
| for i=1,16 do | |
| local hi = hi_bit(r[i]) | |
| local mid = mid_bit(r[i]) | |
| local lo = lo_bit(r[i]) | |
| local rect_off = ( i - 1) * 8 | |
| if hi == 1 then | |
| screen.rect(rect_off, 0,5, 5) | |
| screen.fill() | |
| else | |
| screen.rect(1 + rect_off, 1, 4, 4) | |
| screen.stroke() | |
| end | |
| if mid == 1 then | |
| screen.rect(rect_off, 6, 5, 5) | |
| screen.fill() | |
| else | |
| screen.rect(1 + rect_off, 7, 4, 4) | |
| screen.stroke() | |
| end | |
| if lo == 1 then | |
| screen.rect(rect_off, 12, 5, 5) | |
| screen.fill() | |
| else | |
| screen.rect(1 + rect_off, 13, 4, 4) | |
| screen.stroke() | |
| end | |
| end | |
| screen.stroke() | |
| screen.update() | |
| end | |
| function draw_ui_page0() | |
| screen.clear() | |
| screen.move(0,62) | |
| screen.text("PUSH 2: START/STOP 3: PAGE 1") | |
| local r = registers | |
| for i=1,16 do | |
| local rect_off = ( (i - 1) * 8) | |
| for j=0,7 do | |
| if (8 - j) == r[i] then | |
| screen.rect(rect_off,1 + (j * 6), 5,5) | |
| screen.fill() | |
| else | |
| screen.rect(rect_off + 1,2 + (j * 6), 4,4) | |
| screen.stroke() | |
| end | |
| end | |
| local next_step = get_next_step() | |
| screen.move (1 + ((next_step - 1) * 8) ,55) | |
| screen.text("^") | |
| end | |
| screen.stroke() | |
| screen.update() | |
| end | |
| function redraw() | |
| if page == 1 then | |
| draw_ui_page1() | |
| else | |
| draw_ui_page0() | |
| end | |
| end | |
| function dump(o) | |
| if type(o) == 'table' then | |
| local s = '{ ' | |
| for k,v in pairs(o) do | |
| if type(k) ~= 'number' then k = '"'..k..'"' end | |
| s = s .. '['..k..'] = ' .. dump(v) .. ',' | |
| end | |
| return s .. '} ' | |
| else | |
| return tostring(o) | |
| end | |
| end |