Skip to content
Permalink
646c75966f
Go to file
 
 
Cannot retrieve contributors at this time
895 lines (830 sloc) 20.9 KB
-- oooooo v0.6.1
-- 6 x digital tape loops
--
-- llllllll.co/t/oooooo
--
--
--
-- ▼ instructions below ▼
--
-- K1 shifts
-- K2 stops
-- K2 again resets loop
-- K3 plays
-- shift+K2 clears loop
-- shift+K3 primes recording
-- shift+K3 again forces recording
-- E1 changes loops
-- E2 selects parameters
-- E3 adjusts parameters
--
local json=include "lib/json"
-- user parameters
uP={
-- initialized in init
}
-- user state
uS={
recording=0,-- 0 = not recording, 1 = armed, 2 = recording
recordingTime=0,
updateUI=false,
updateParams=0,
updateTape=false,
shift=false,
loopNum=1,-- 7 = all loops
selectedPar=0,
flagClearing=false,
flagSpecial=0,
message="",
currentBeat=0,
}
-- user constants
uC={
bufferMinMax={
{1,1,80},
{1,82,161},
{1,163,243},
{2,1,80},
{2,82,161},
{2,163,243},
},
loopMinMax={0.2,78},
radiiMinMax={3,160},
widthMinMax={8,124},
heightMinMax={0,64},
centerOffsets={
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
{0,0},
},
updateTimerInterval=0.05,
recArmThreshold=0.03,
backupNumber=1,
lfoTime=1,
discreteRates={-4,-2,-1,-0.5,-0.25,0.25,0.5,1,2,4},
}
PATH=_path.audio..'oooooo/'
function init()
init_loops(7)
-- make data directory
if not util.file_exists(PATH) then util.make_dir(PATH) end
-- initialize timer for updating screen
timer=metro.init()
timer.time=uC.updateTimerInterval
timer.count=-1
timer.event=update_timer
timer:start()
-- position poll
softcut.event_phase(update_positions)
softcut.poll_start_phase()
-- listen to audio
-- and initiate recording on incoming audio
p_amp_in=poll.set("amp_in_l")
-- set period low when primed, default 1 second
p_amp_in.time=1
p_amp_in.callback=function(val)
if uS.recording==1 then
-- print("incoming signal = "..val)
if val>params:get("rec thresh")/1000 then
tape_rec(uS.loopNum)
end
end
end
p_amp_in:start()
-- add variables into main menu
params:add_control("rec thresh","rec thresh",controlspec.new(1,100,'exp',1,10,'amp/1k'))
params:set_action("rec thresh",update_parameters)
params:add_control("backup","backup",controlspec.new(1,8,'lin',1,1))
params:set_action("backup",update_parameters)
params:add_control("vol pinch","vol pinch",controlspec.new(0,1000,'lin',1,500,'ms'))
params:set_action("vol pinch",update_parameters)
params:add_option("rec thru loops","rec thru loops",{"no","yes"},1)
params:set_action("rec thru loops",update_parameters)
params:add_option("continous rate","continous rate",{"no","yes"},2)
params:set_action("continous rate",update_parameters)
params:read(_path.data..'oooooo/'.."oooooo.pset")
redraw()
end
function init_loops(j)
audio.level_adc(1) -- input volume 1
audio.level_adc_cut(1) -- ADC to Softcut input
audio.level_cut(1) -- Softcut master level (same as in LEVELS screen)
i1=j
i2=j
if j==7 then
i1=1
i2=7
end
for i=i1,i2 do
print("initializing "..i)
uP[i]={}
uP[i].loopStart=0
uP[i].position=uP[i].loopStart
uP[i].loopLength=(60/clock.get_tempo())*i*4
uP[i].recordedLength=0
uP[i].isStopped=true
uP[i].isEmpty=true
uP[i].vol=0.5
uP[i].rate=1
uP[i].rateNum=8
uP[i].pan=0
uP[i].lfoWarble={}
uP[i].resetEveryNthBeat=0
for j=1,3 do
uP[i].lfoWarble[j]=math.random(1,60)
end
if i<7 then
-- update softcut
softcut.level(i,0.5)
softcut.level_input_cut(1,i,1)
softcut.level_input_cut(2,i,1)
softcut.pan(i,0)
softcut.play(i,0)
softcut.rate(i,1)
softcut.loop_start(i,uC.bufferMinMax[i][2])
softcut.loop_end(i,uC.bufferMinMax[i][2]+uP[i].loopLength)
softcut.loop(i,1)
softcut.rec(i,0)
softcut.fade_time(i,0.2)
softcut.level_slew_time(i,0.8)
softcut.rate_slew_time(i,0.8)
softcut.rec_level(i,1)
softcut.pre_level(i,1)
softcut.buffer(i,uC.bufferMinMax[i][1])
softcut.position(i,uC.bufferMinMax[i][2])
softcut.enable(i,1)
softcut.phase_quant(i,0.025)
end
end
end
function randomize_parameters()
for i=1,6 do
uP[i].rate=uC.discreteRates[math.random(#uC.discreteRates)]
softcut.rate(i,uP[i].rate)
uP[i].vol=math.random()*(1/math.abs(uP[i].rate))
uP[i].vol=util.clamp(uP[i].vol,0.1,0.8)
softcut.level(i,uP[i].vol)
uP[i].pan=math.random()*2-1
softcut.pan(i,uP[i].pan)
end
end
function randomize_loops()
for i=1,6 do
uP[i].loopLength=util.clamp(uP[i].loopLength+math.random()*2-1,uC.loopMinMax[1],uC.loopMinMax[2])
tape_change_loop(i)
end
end
--
-- updaters
--
function update_parameters(x)
params:write(_path.data..'oooooo/'.."oooooo.pset")
end
function update_positions(i,x)
-- adjust position so it is relative to loop start
uP[i].position=x-uC.bufferMinMax[i][2]
uS.updateUI=true
end
function update_timer()
-- -- update the count for the lfos
-- uC.lfoTime=uC.lfoTime+uC.updateTimerInterval
-- if uC.lfoTime>376.99 then -- 60 * 2 * pi
-- uC.lfoTime=0
-- end
-- tape_warble()
if uS.updateUI then
redraw()
end
if uS.recording==2 then
uS.recordingTime=uS.recordingTime+uC.updateTimerInterval
if uS.recordingTime>=uP[uS.loopNum].loopLength then
-- stop recording when reached a full loop
tape_stop_rec(uS.loopNum,false)
end
end
if math.floor(clock.get_beats())~=uS.currentBeat then
uS.currentBeat=math.floor(clock.get_beats())
for i=1,6 do
if uP[i].resetEveryNthBeat>0 then
if uS.currentBeat%uP[i].resetEveryNthBeat==0 then
if not (uS.recording>0 and uS.loopNum==i) then
tape_reset(i)
end
end
end
end
end
end
--
-- saving and loading
--
function backup_save()
print("backup_save")
clock.run(function()
uS.message="saved"
redraw()
clock.sleep(0.5)
uS.message=""
redraw()
end)
-- write file of user data
file=io.open(PATH.."oooooo"..params:get("backup")..".json","w")
io.output(file)
io.write(json.stringify(uP))
io.close(file)
-- save tape
softcut.buffer_write_stereo(PATH.."oooooo"..params:get("backup")..".wav",0,-1)
end
function backup_load()
print("backup_load")
clock.run(function()
uS.message="loaded"
redraw()
clock.sleep(0.5)
uS.message=""
redraw()
end)
-- -- load parameters from file
if util.file_exists(PATH.."oooooo"..params:get("backup")..".json") then
filecontents=readAll(PATH.."oooooo"..params:get("backup")..".json")
print(filecontents)
u=json.parse(filecontents)
for i=1,7 do
if u[i].loopStart~=nil then
uP[i].loopStart=u[i].loopStart
end
if u[i].position~=nil then
uP[i].position=u[i].position
end
if u[i].loopLength~=nil then
uP[i].loopLength=u[i].loopLength
end
if u[i].recordedLength~=nil then
uP[i].recordedLength=u[i].recordedLength
end
if u[i].isStopped~=nil then
uP[i].isStopped=u[i].isStopped
end
if u[i].isEmpty~=nil then
uP[i].isEmpty=u[i].isEmpty
end
if u[i].vol~=nil then
uP[i].vol=u[i].vol
end
if u[i].rate~=nil then
uP[i].rate=u[i].rate
end
if u[i].rateNum~=nil then
uP[i].rateNum=u[i].rateNum
end
if u[i].rate~=nil then
uP[i].rate=u[i].rate
end
if u[i].pan~=nil then
uP[i].pan=u[i].pan
end
if u[i].resetEveryNthBeat~=nil then
uP[i].resetEveryNthBeat=u[i].resetEveryNthBeat
end
if u[i].lfoWarble~=nil then
uP[i].lfoWarble=u[i].lfoWarble
end
end
end
-- load buffer from file
if util.file_exists(PATH.."oooooo"..params:get("backup")..".wav") then
softcut.buffer_clear()
softcut.buffer_read_stereo(PATH.."oooooo"..params:get("backup")..".wav",0,0,-1)
end
end
--
-- tape effects
--
function tape_warble()
for i=1,6 do
if uP[i].isStopped or uS.recording>0 then
-- do nothing
else
warblePercent=0
for j=1,3 do
warblePercent=warblePercent+math.sin(2*math.pi*uC.lfoTime/uP[i].lfoWarble[j])
end
softcut.rate(i,uP[i].rate*(1+warblePercent/200))
end
end
end
--
-- tape functions
--
function tape_stop_reset(j)
-- if uS.loopNum == 7 then stop all
i1=j
i2=j
if j==7 then
i1=1
i2=6
end
for i=i1,i2 do
if uP[i].isStopped and uS.recording==0 then
tape_reset(i)
else
tape_stop(i)
end
end
end
function tape_reset(i)
if uP[i].position==0 then
do return end
end
print("tape_reset "..i)
uP[i].position=0
softcut.position(i,uC.bufferMinMax[i][2]+uP[i].loopStart)
end
function tape_stop(i)
if uP[i].isStopped==true and uS.recording==0 then
do return end
end
print("tape_stop "..i)
if uS.recording>0 then
tape_stop_rec(i,true)
end
-- ?????
-- if this runs as softcut.rate(i,0) though, then overdubbing stops working
softcut.play(i,0)
uP[i].isStopped=true
end
function tape_stop_rec(i,change_loop)
if uS.recording==0 then
do return end
end
print("tape_stop_rec "..i)
p_amp_in.time=1
still_armed=(uS.recording==1)
uS.recording=0
uP[i].recordedLength=uS.recordingTime
uS.recordingTime=0
-- slowly stop
clock.run(function()
if params:get("vol pinch")>0 then
for j=1,10 do
softcut.rec(i,(10-j)*0.1)
clock.sleep(params:get("vol pinch")/10/1000)
end
end
softcut.rec(i,0)
end)
-- change the loop size if specified
print('params:get("rec thru loops") '..params:get("rec thru loops"))
if not still_armed then
if change_loop then
uP[i].loopLength=uP[i].recordedLength
tape_change_loop(i)
elseif params:get("rec thru loops")==2 then
-- keep recording onto the next loop
nextLoop=0
for j=1,6 do
if uP[j].isEmpty then
nextLoop=j
break
end
end
-- goto the next loop and record
if nextLoop>0 then
uS.loopNum=nextLoop
tape_rec(uS.loopNum)
end
end
end
end
function tape_clear(i)
print("tape_clear "..i)
-- prevent double clear
if uS.flagClearing then
do return end
end
-- signal clearing to prevent double clear
clock.run(function()
uS.flagClearing=true
uS.message="clearing"
redraw()
clock.sleep(0.5)
uS.flagClearing=false
uS.message=""
redraw()
end)
redraw()
if i==7 then
-- clear everything
softcut.buffer_clear()
for j=1,6 do
if uP[j].isEmpty then
init_loops(j)
uS.message="resetting"
redraw()
end
uP[j].isEmpty=true
uP[j].recordedLength=0
tape_reset(j)
end
else
-- clear a specific section of buffer
if uP[i].isEmpty then
init_loops(i)
uS.message="resetting"
redraw()
end
uP[i].isEmpty=true
uP[i].recordedLength=0
softcut.buffer_clear_region_channel(
uC.bufferMinMax[i][1],
uC.bufferMinMax[i][2],
uC.bufferMinMax[i][3]-uC.bufferMinMax[i][2])
tape_reset(i)
end
-- reinitialize?
-- init_loops(i)
end
function tape_play(j)
print("tape_play "..j)
if uS.recording>0 then
tape_stop_rec(j,true)
end
if j<7 and uP[j].isStopped==false then
do return end
end
if j<7 and uP[j].isEmpty then
do return end
end
i1=j
i2=j
if j==7 then
i1=1
i2=6
end
for i=i1,i2 do
softcut.play(i,1)
softcut.level(i,uP[i].vol)
softcut.rate(i,uP[i].rate)
uP[i].isStopped=false
end
end
function tape_arm_rec(i)
if uS.recording==1 then
do return end
end
print("tape_arm_rec "..i)
-- arm recording
uS.recording=1
-- monitor input
p_amp_in.time=0.025
end
function tape_rec(i)
if uS.recording==2 then
do return end
end
print("tape_rec "..i)
if uP[i].isStopped then
softcut.play(i,1)
-- print("setting rate to "..uP[i].rate)
softcut.rate(i,uP[i].rate)
softcut.level(i,uP[i].vol)
uP[i].isStopped=false
end
p_amp_in.time=1
uS.recordingTime=0
uS.recording=2 -- recording is live
softcut.rec_level(i,1)
softcut.pre_level(i,1)
uP[i].isEmpty=false
redraw()
-- slowly start recording
-- ease in recording signal to avoid clicks near loop points
clock.run(function()
if params:get("vol pinch")>0 then
for j=1,10 do
softcut.rec(i,j*0.1)
clock.sleep(params:get("vol pinch")/10/1000)
end
end
softcut.rec(i,1)
end)
end
function tape_change_loop(i)
print("tape_change_loop on "..i)
if uP[i].loopLength+uP[i].loopStart>uC.loopMinMax[2] then
-- loop length is too long, shorten it
uP[i].loopLength=uC.loopMinMax[2]-uP[i].loopStart
end
-- move to start of loop if position is outside of loop
if uP[i].position<uP[i].loopStart or uP[i].position>uP[i].loopStart+uP[i].loopLength then
uP[i].position=uP[i].loopStart
softcut.position(i,uP[i].position+uC.bufferMinMax[i][2])
end
softcut.loop_start(i,uP[i].loopStart+uC.bufferMinMax[i][2])
softcut.loop_end(i,uP[i].loopStart+uC.bufferMinMax[i][2]+uP[i].loopLength)
end
--
-- encoders
--
function enc(n,d)
if n==1 and uS.recording==0 then
-- do not allow changing loops if recording
d=sign(d)
uS.loopNum=util.clamp(uS.loopNum+d,1,7)
elseif n==2 then
d=sign(d)
if uS.loopNum~=7 then
uS.selectedPar=util.clamp(uS.selectedPar+d,0,7)
else
-- toggle between saving / loading
uS.flagSpecial=util.clamp(uS.flagSpecial+d,0,4)
end
elseif n==3 then
if uS.loopNum~=7 then
if uS.selectedPar==1 then
uP[uS.loopNum].loopStart=util.clamp(uP[uS.loopNum].loopStart+d/10,0,uC.loopMinMax[2])
tape_change_loop(uS.loopNum)
elseif uS.selectedPar==2 then
uP[uS.loopNum].loopLength=util.clamp(uP[uS.loopNum].loopLength+d/10,uC.loopMinMax[1],uC.loopMinMax[2])
tape_change_loop(uS.loopNum)
elseif uS.selectedPar==3 then
uP[uS.loopNum].vol=util.clamp(uP[uS.loopNum].vol+d/100,0,1)
softcut.level(uS.loopNum,uP[uS.loopNum].vol)
elseif uS.selectedPar==4 then
if params:get("continous rate")==2 then
uP[uS.loopNum].rate=util.clamp(uP[uS.loopNum].rate+d/100,-4,4)
else
d=sign(d)
uP[uS.loopNum].rateNum=util.clamp(uP[uS.loopNum].rateNum+d,1,#uC.discreteRates)
uP[uS.loopNum].rate=uC.discreteRates[uP[uS.loopNum].rateNum]
end
softcut.rate(uS.loopNum,uP[uS.loopNum].rate)
elseif uS.selectedPar==5 then
uP[uS.loopNum].pan=util.clamp(uP[uS.loopNum].pan+d/100,-1,1)
softcut.pan(uS.loopNum,uP[uS.loopNum].pan)
elseif uS.selectedPar==6 then
d=sign(d)
uP[uS.loopNum].resetEveryNthBeat=util.clamp(uP[uS.loopNum].resetEveryNthBeat+d,0,64)
elseif uS.selectedPar==7 then
-- add temporary warble
clock.run(function()
local newChange=(1+d/100)
uP[uS.loopNum].rate=uP[uS.loopNum].rate*newChange
softcut.rate(uS.loopNum,uP[uS.loopNum].rate)
clock.sync(1)
uP[uS.loopNum].rate=uP[uS.loopNum].rate/newChange
softcut.rate(uS.loopNum,uP[uS.loopNum].rate)
end)
end
else
if uS.flagSpecial==1 or uS.flagSpecial==2 then
-- update tape number
d=sign(d)
params:set("backup",util.clamp(params:get("backup")+d,1,8))
end
end
end
uS.updateUI=true
end
function key(n,z)
if n==1 then
uS.shift=not uS.shift
elseif n==2 and z==1 then
-- this key works on one or all
if uS.shift then
-- clear
tape_clear(uS.loopNum)
else
-- stop tape
-- if stopped, then reset to 0
tape_stop_reset(uS.loopNum)
end
elseif n==3 and z==1 then
if uS.shift then
if uS.loopNum~=7 then
if uS.recording==0 then
tape_arm_rec(uS.loopNum)
elseif uS.recording==1 then
tape_rec(uS.loopNum)
end
else
-- save/load functionality
if uS.flagSpecial==1 then
-- save
backup_save()
elseif uS.flagSpecial==2 then
-- load
backup_load()
elseif uS.flagSpecial==3 then
-- randomize!
randomize_parameters()
elseif uS.flagSpecial==4 then
-- randomize loops!
randomize_loops()
end
end
else
tape_play(uS.loopNum)
end
end
uS.updateUI=true
end
--
-- screen
--
function redraw()
uS.updateUI=false
screen.clear()
-- check shift
shift_amount=0
if uS.shift then
shift_amount=4
end
-- show header
screen.level(15)
-- show state symbol
if uS.recording==2 then
screen.rect(108,1,20,10)
screen.move(111,8)
screen.text("REC")
elseif uS.recording==1 then
screen.rect(108,1,20,10)
screen.level(1)
screen.move(111,8)
screen.text("REC")
screen.level(15)
elseif uP[uS.loopNum].isStopped then
screen.rect(118,1,10,10)
screen.move(121,8)
screen.text("||")
else
screen.rect(118,1,10,10)
screen.move(121,8)
screen.text(">")
screen.move(122,4)
screen.line(122,8)
end
-- show loop info
x=7+shift_amount
y=9+shift_amount
screen.move(x,y)
if uS.loopNum==7 then
screen.text("A")
else
screen.text(uS.loopNum)
end
screen.move(x,y)
screen.rect(x-3,y-7,10,10)
screen.stroke()
x=-7
y=60
if uS.loopNum==7 then
screen.move(x+10,y)
if uS.flagSpecial==1 then
screen.text("save "..params:get("backup"))
elseif uS.flagSpecial==2 then
screen.text("load "..params:get("backup"))
elseif uS.flagSpecial==3 then
screen.text("rand")
elseif uS.flagSpecial==4 then
screen.text("rand loop")
end
elseif uS.selectedPar==1 or uS.selectedPar==2 then
screen.move(x+10,y)
if uS.selectedPar==1 then
screen.level(15)
else
screen.level(1)
end
screen.text(string.format("%1.1f",uP[uS.loopNum].loopStart))
screen.move(x+24,y)
screen.level(1)
screen.text("-")
screen.move(x+30,y)
if uS.selectedPar==2 then
screen.level(15)
else
screen.level(1)
end
screen.text(string.format("%1.1fs",uP[uS.loopNum].loopStart+uP[uS.loopNum].loopLength))
elseif uS.selectedPar==3 then
screen.move(x+10,y)
screen.text("level")
elseif uS.selectedPar==4 then
screen.move(x+10,y)
screen.text(string.format("rate %1.1f%%",uP[uS.loopNum].rate*100))
elseif uS.selectedPar==5 then
screen.move(x+10,y)
screen.text("pan")
elseif uS.selectedPar==6 then
screen.move(x+10,y)
screen.text("reset every "..uP[uS.loopNum].resetEveryNthBeat.." beat")
elseif uS.selectedPar==7 then
screen.move(x+10,y)
screen.text("warble")
end
-- screen.move(x+55,y)
-- if uS.selectedPar==3 then
-- screen.level(15)
-- else
-- screen.level(1)
-- end
-- screen.text(string.format("%1.2f",uP[uS.loopNum].vol))
-- screen.move(x+80,y)
-- if uS.selectedPar==4 then
-- screen.level(15)
-- else
-- screen.level(1)
-- end
-- screen.text(string.format("%1.2f",uP[uS.loopNum].rate))
-- screen.move(x+105,y)
-- if uS.selectedPar==5 then
-- screen.level(15)
-- else
-- screen.level(1)
-- end
-- screen.text(string.format("%1.2f",uP[uS.loopNum].pan))
-- draw representation of current loop states
for i=1,6 do
-- draw circles
r=(uC.radiiMinMax[2]-uC.radiiMinMax[1])*uP[i].loopLength/(uC.bufferMinMax[i][3]-uC.bufferMinMax[i][2])+uC.radiiMinMax[1]
if r>45 then
r=45
end
x=uC.centerOffsets[i][1]+(uC.widthMinMax[2]-uC.widthMinMax[1])*(uP[i].pan+1)/2+uC.widthMinMax[1]
y=uC.centerOffsets[i][2]+(uC.heightMinMax[2]-uC.heightMinMax[1])*(1-uP[i].vol)+uC.heightMinMax[1]
if uS.loopNum==i then
screen.line_width(1)
screen.level(15)
else
screen.line_width(1)
screen.level(1)
end
screen.move(x+r,y)
screen.circle(x,y,r)
screen.stroke()
angle=360*(uP[i].loopLength-uP[i].position)/(uP[i].loopLength)+90
-- if not uP[i].isStopped then
-- -- draw arc at position
-- screen.move(x,y)
-- screen.arc(x,y,r,math.rad(angle-5),math.rad(angle+5))
-- screen.stroke()
-- end
-- draw pixels at position if it has data or
-- its being recorded/primed
if uP[i].isEmpty==false or (i==uS.loopNum and uS.recording>0) then
for j=-1,1 do
screen.pixel(x+(r-j)*math.sin(math.rad(angle)),y+(r-j)*math.cos(math.rad(angle)))
screen.stroke()
end
end
end
if uS.message~="" then
show_message(uS.message)
end
screen.update()
end
--
-- utils
--
function show_message(message)
screen.level(0)
x=34
y=28
w=string.len(message)*8
screen.rect(x,y,w,10)
screen.fill()
screen.level(15)
screen.rect(x,y,w,10)
screen.stroke()
screen.move(x+w/2,y+7)
screen.text_center(message)
end
function readAll(file)
local f=assert(io.open(file,"rb"))
local content=f:read("*all")
f:close()
return content
end
function calculate_lfo(current_time,period,offset)
if period==0 then
return 1
else
return math.sin(2*math.pi*current_time/period+offset)
end
end
function round(x)
return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5)
end
function sign(x)
if x>0 then
return 1
elseif x<0 then
return-1
else
return 0
end
end