-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New instrument with microphone-recorded sample
- Loading branch information
Showing
2 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,340 @@ | ||
local patch = {} | ||
patch.__index = patch | ||
|
||
local l = require('lume') | ||
local hexgrid = require('hexgrid') | ||
local sampler = require('sampler') | ||
local fretboard = require('fretboard') | ||
local hexpad = require('hexpad') | ||
local freeform = require('freeform') | ||
local efx = require('efx') | ||
|
||
local colorScheme = { | ||
background = {l.rgba(0xffb762ff)}, | ||
highlight = {l.rgba(0xff823bff)}, | ||
surface = {l.rgba(0xffffe4ff)}, | ||
black = {l.rgba(0x041528ff)}, | ||
outline = {l.rgba(0x604b46ff)}, | ||
} | ||
|
||
-- Possible combination testing | ||
local samplingFrequencies = {48000, 44100, 32000, 22050, 16000, 8000} | ||
local bitDepths = {16, 8} | ||
local bufferSize = 16384 | ||
-- on pixel 2 valid combinations are 22050 (8 & 16) and 48000 (8 & 16) | ||
|
||
function patch.load() | ||
local self = setmetatable({}, patch) | ||
self.hexpad = hexpad.new(false, 0, 3) | ||
self.fretboard = fretboard.new(false, {-12}) | ||
self.fretboard.neckHeight = 0.25 | ||
self.fretboard.fretWidth = 0.15 | ||
|
||
self.pitchboard = fretboard.new(false, {-48}) | ||
self.pitchboard.neckHeight = 0.2 | ||
self.pitchboard.fretWidth = 0.04 | ||
|
||
patch.triggers = { | ||
{type='whitekey', note= -12, x=-0.054, y= 0.492, r= 0.20}, | ||
{type='whitekey', note= -10, x= 0.220, y= 0.481, r= 0.20}, | ||
{type='whitekey', note= -8, x= 0.493, y= 0.507, r= 0.20}, | ||
{type='whitekey', note= -7, x= 0.769, y= 0.484, r= 0.20}, | ||
{type='whitekey', note= -5, x= 1.043, y= 0.484, r= 0.20}, | ||
{type='whitekey', note= -3, x= 1.319, y= 0.475, r= 0.20}, | ||
{type='whitekey', note= -1, x= 1.596, y= 0.488, r= 0.20}, | ||
{type='blackkey', note= -11, x= 0.059, y= 0.270, r= 0.16}, | ||
{type='blackkey', note= -9, x= 0.396, y= 0.256, r= 0.16}, | ||
{type='blackkey', note= -6, x= 0.880, y= 0.244, r= 0.16}, | ||
{type='blackkey', note= -4, x= 1.193, y= 0.263, r= 0.16}, | ||
{type='blackkey', note= -2, x= 1.509, y= 0.261, r= 0.16}, | ||
{type='whitekey', note= 0, x=-0.072, y=-0.129, r= 0.20}, | ||
{type='whitekey', note= 2, x= 0.200, y=-0.151, r= 0.20}, | ||
{type='whitekey', note= 4, x= 0.467, y=-0.138, r= 0.20}, | ||
{type='whitekey', note= 5, x= 0.744, y=-0.153, r= 0.20}, | ||
{type='whitekey', note= 7, x= 1.011, y=-0.190, r= 0.20}, | ||
{type='whitekey', note= 9, x= 1.283, y=-0.210, r= 0.20}, | ||
{type='whitekey', note= 11, x= 1.556, y=-0.219, r= 0.20}, | ||
{type='blackkey', note= 1, x= 0.037, y=-0.378, r= 0.16}, | ||
{type='blackkey', note= 3, x= 0.346, y=-0.372, r= 0.16}, | ||
{type='blackkey', note= 6, x= 0.857, y=-0.415, r= 0.16}, | ||
{type='blackkey', note= 8, x= 1.165, y=-0.443, r= 0.16}, | ||
{type='blackkey', note= 10, x= 1.444, y=-0.443, r= 0.16}, | ||
} | ||
patch.freeform = freeform.new(patch.triggers) | ||
|
||
self.sampler = sampler.new({ | ||
{path='patches/chromakey/sustain.ogg', note= 0}, | ||
looped = true, | ||
envelope = { attack = 0, decay = 0.40, sustain = 1, release = 0.2 }, | ||
}) | ||
|
||
self.efx = efx.load() | ||
|
||
self.button = { | ||
placement = {-0.25, -0.88}, | ||
pressed = nil, | ||
} | ||
local devices = love.audio.getRecordingDevices() | ||
self.recorder = devices[1] | ||
self.parameters = self:findRecordingParams() | ||
self.recording = false -- currently storing buffers being recorded | ||
self.recordingLength = 0 -- collected recording length in number of samples | ||
self.collectedSamples = {} -- table holding recorded buffers | ||
self.crossBuffer = nil -- store first buffer separately to crossfade | ||
|
||
self.hexpad.drawCell = function(self, q, r, s, touch) | ||
love.graphics.scale(touch and 0.8 or 0.75) | ||
love.graphics.setColor(touch and colorScheme.highlight or colorScheme.surface) | ||
love.graphics.polygon('fill', hexgrid.roundhex) | ||
love.graphics.setLineWidth(0.05) | ||
love.graphics.scale(not touch and 1 or 1 + 0.02 * math.sin(s.time * 50)) | ||
love.graphics.setColor(colorScheme.outline) | ||
love.graphics.polygon('line', hexgrid.roundhex) | ||
end | ||
self.fretboard.colorScheme.neck = {0, 0, 0, 0} | ||
self.fretboard.colorScheme.fret = {0, 0, 0, 0} | ||
self.fretboard.colorScheme.dot = {0, 0, 0, 0} | ||
self.fretboard.colorScheme.shade = {0, 0, 0, 0} | ||
self.fretboard.colorScheme.string = colorScheme.background | ||
self.fretboard.colorScheme.light = colorScheme.outline | ||
self.pitchboard.colorScheme.neck = {0, 0, 0, 0} | ||
self.pitchboard.colorScheme.fret = {0, 0, 0, 0} | ||
self.pitchboard.colorScheme.dot = {0, 0, 0, 0} | ||
self.pitchboard.colorScheme.shade = {0, 0, 0, 0} | ||
self.pitchboard.colorScheme.string = colorScheme.surface | ||
self.pitchboard.colorScheme.light = {0, 0, 0, 0} | ||
self.freeform.colorScheme.whitekey = colorScheme.surface | ||
self.freeform.colorScheme.blackkey = colorScheme.black | ||
love.graphics.setBackgroundColor(colorScheme.background) | ||
return self | ||
end | ||
|
||
|
||
function patch:findRecordingParams() | ||
local success = false -- success locally before you hit it big | ||
-- test all possible combos | ||
for _, samplingFrequency in ipairs(samplingFrequencies) do | ||
for _, bitDepth in ipairs(bitDepths) do | ||
success = self.recorder:start(bufferSize, samplingFrequency, bitDepth, 1) | ||
if success then | ||
return { | ||
samplingFrequency = samplingFrequency, | ||
bitDepth = bitDepth | ||
} | ||
else | ||
self.recorder:stop() | ||
end | ||
end | ||
end | ||
log('Please enable microphone permission') | ||
return {} | ||
end | ||
|
||
|
||
local function withFretboard(fun, ...) | ||
love.graphics.push() | ||
love.graphics.translate(0, 0.95) | ||
love.graphics.scale(1, 0.6) | ||
fun(...) | ||
love.graphics.pop() | ||
end | ||
|
||
|
||
local function withPitchboard(fun, ...) | ||
love.graphics.push() | ||
love.graphics.translate(0.95, -0.85) | ||
love.graphics.scale(0.3, 1) | ||
fun(...) | ||
love.graphics.pop() | ||
end | ||
|
||
|
||
local function withHexpad(fun, ...) | ||
love.graphics.push() | ||
love.graphics.translate(-1, -0.1) | ||
love.graphics.scale(0.6) | ||
fun(...) | ||
love.graphics.pop() | ||
end | ||
|
||
|
||
local function waveformInit() | ||
patch.waveVisualization = {-1, 0} | ||
end | ||
|
||
|
||
local function waveformAdd(timeFraction, amplitude) | ||
table.insert(patch.waveVisualization, -1 + 2 * timeFraction) | ||
table.insert(patch.waveVisualization, math.log(1 + amplitude / 50)) | ||
end | ||
|
||
|
||
local function waveformEnd() | ||
table.insert(patch.waveVisualization, 1) | ||
table.insert(patch.waveVisualization, 0) | ||
end | ||
|
||
|
||
local function waveformDraw() | ||
if patch.waveVisualization then | ||
love.graphics.setColor(colorScheme.highlight) | ||
love.graphics.polygon('line', patch.waveVisualization) | ||
end | ||
end | ||
|
||
|
||
function patch:process(s) | ||
-- while recording constantly collect samples | ||
if self.recording then | ||
local data = self.recorder:getData() | ||
if data then | ||
if not self.crossBuffer then | ||
self.crossBuffer = data | ||
else | ||
self.recordingLength = self.recordingLength + data:getSampleCount() | ||
table.insert(self.collectedSamples, data) | ||
end | ||
end | ||
end | ||
-- recording stopped, assemble all collected samples | ||
if self.button.touchId and not next(s.touches) then | ||
self.button.touchId = nil | ||
self.recording = false | ||
if self.recordingLength > 0 then | ||
local recording = love.sound.newSoundData(self.recordingLength, self.parameters.samplingFrequency, self.parameters.bitDepth, 1) | ||
local sampleIndex = 0 | ||
local crossfade = math.min(self.crossBuffer:getSampleCount(), self.recordingLength/2) | ||
--waveformInit() | ||
for _, v in ipairs(self.collectedSamples) do | ||
for i = 0, v:getSampleCount() - 1 do | ||
if sampleIndex < self.recordingLength - crossfade then | ||
recording:setSample(sampleIndex, v:getSample(i)) | ||
else | ||
local fadeout = l.remap(sampleIndex, self.recordingLength - crossfade, self.recordingLength, 1, 0, 'clamp') | ||
local fadein = 1 - fadeout | ||
local crossSample = self.crossBuffer:getSample(self.crossBuffer:getSampleCount() - (self.recordingLength - sampleIndex)) | ||
recording:setSample(sampleIndex, v:getSample(i) * fadeout + crossSample * fadein) | ||
end | ||
--waveformAdd(sampleIndex / self.recordingLength, recording:getSample(sampleIndex)) | ||
sampleIndex = sampleIndex + 1 | ||
end | ||
v:release() | ||
end | ||
self.sampler.samples[1].soundData = recording | ||
--waveformEnd() | ||
end | ||
end | ||
-- recording start when button is pressed | ||
for id,touch in pairs(s.touches) do | ||
local x, y = love.graphics.inverseTransformPoint(touch[1], touch[2]) | ||
if (x - self.button.placement[1])^2 + (y - self.button.placement[2])^2 < 0.03 then | ||
if not self.button.touchId then | ||
self.recorder:getData() -- clear data captured so far | ||
self.recording = true | ||
self.crossBuffer = nil | ||
self.recordingLength = 0 | ||
self.collectedSamples = {} | ||
end | ||
self.button.touchId = id | ||
s.touches[id] = nil -- sneakily remove touch from stream to prevent unintended notes | ||
end | ||
end | ||
withHexpad(self.hexpad.interpret, self.hexpad, s) | ||
withFretboard(self.fretboard.interpret, self.fretboard, s) | ||
withPitchboard(self.pitchboard.interpret, self.pitchboard, s) | ||
patch.freeform:interpret(s) | ||
self.efx:process() | ||
self.sampler:processTouches(s.dt, s.touches, self.efx) | ||
end | ||
|
||
|
||
function patch.drawMike(time) | ||
-- stand | ||
love.graphics.setColor(colorScheme.black) | ||
love.graphics.rectangle('fill', -0.15, -1.3, 0.3, 0.5) | ||
-- mike | ||
love.graphics.setStencilTest("greater", 0) | ||
local mikeStencil = function() | ||
love.graphics.setColorMask() -- enable drawing inside stencil function | ||
love.graphics.setColor(colorScheme.outline) | ||
love.graphics.rectangle('fill', -0.6, -1, 1.2, 2, 0.4, 0.6) | ||
end | ||
love.graphics.stencil(mikeStencil, "replace", 1) | ||
-- shading | ||
love.graphics.setColor(colorScheme.surface) | ||
love.graphics.circle('fill', -2.5, -0.7 + 0.2 * math.sin(time), 2.7) | ||
-- gills | ||
for i = -0.6, 0.8, 0.3 do | ||
love.graphics.setColor(colorScheme.outline) | ||
love.graphics.rectangle('fill', -0.65, i, 0.53, 0.1, 0.1) | ||
love.graphics.setColor(colorScheme.black) | ||
love.graphics.rectangle('fill', 0.25, i, 0.45, 0.1, 0.1) | ||
end | ||
-- mount | ||
love.graphics.setColor(colorScheme.outline) | ||
love.graphics.push() | ||
love.graphics.translate(0, -1) | ||
love.graphics.rotate(math.pi/4) | ||
love.graphics.rectangle('fill', -0.25, -.25, 0.5, 0.5, 0.2) | ||
love.graphics.pop() | ||
love.graphics.setStencilTest() -- disable stencil | ||
end | ||
|
||
|
||
function patch:draw(s) | ||
withHexpad(self.hexpad.draw, self.hexpad, s) | ||
withFretboard(self.fretboard.draw, self.fretboard, s) | ||
withPitchboard(self.pitchboard.draw, self.pitchboard, s) | ||
love.graphics.setColor(colorScheme.highlight) | ||
love.graphics.circle('fill', 0.35, -0.85, 0.05) | ||
patch.freeform:draw(s) | ||
--waveformDraw() | ||
love.graphics.translate(self.button.placement[1], self.button.placement[2]) | ||
love.graphics.scale(0.1) | ||
self.drawMike(s.time) | ||
if self.recording then | ||
love.graphics.push() | ||
love.graphics.scale(0.03) | ||
love.graphics.translate(30, -20) | ||
love.graphics.setFont(hexpad.font) | ||
love.graphics.setColor(colorScheme.highlight) | ||
love.graphics.print('REC') | ||
love.graphics.pop() | ||
love.graphics.translate(0, 0.4) | ||
love.graphics.scale(1.5) | ||
end | ||
end | ||
|
||
|
||
function patch.icon(time) | ||
love.graphics.setColor(colorScheme.highlight) | ||
love.graphics.rectangle('fill', -1, -1, 2, 2) | ||
love.graphics.scale(0.8 + 0.03 * math.sin(time)^10) | ||
love.graphics.setColor(colorScheme.background) | ||
love.graphics.circle('fill', -3, -1 + 0.2 * math.cos(time), 4) | ||
patch.drawMike(time) | ||
end | ||
|
||
--[[ | ||
function love.keypressed(key) | ||
if key == 'tab' then | ||
freeform.editing = true | ||
patch.freeform.selected = (patch.freeform.selected % #patch.triggers) + 1 | ||
end | ||
if key == '`' then | ||
patch.freeform.selected = ((patch.freeform.selected - 1) % #patch.triggers) | ||
end | ||
if key == 'return' then | ||
for i,v in ipairs(patch.triggers) do | ||
print(string.format('x=% 1.3f, y=% 1.3f, r=% 1.2f},',v.x, v.y, v.r)) | ||
end | ||
end | ||
if key == '=' then | ||
patch.triggers[patch.freeform.selected].r = patch.triggers[patch.freeform.selected].r * 1.02 | ||
elseif key == '-' then | ||
patch.triggers[patch.freeform.selected].r = patch.triggers[patch.freeform.selected].r / 1.02 | ||
end | ||
end | ||
--]] | ||
|
||
return patch |