Skip to content

Instantly share code, notes, and snippets.

@brianfay
Last active February 19, 2019 03:20
Show Gist options
  • Save brianfay/f117b0d5094b5b58b8f869f8c922eaac to your computer and use it in GitHub Desktop.
Save brianfay/f117b0d5094b5b58b8f869f8c922eaac to your computer and use it in GitHub Desktop.
A simple looper for Bela. Press a button attached to P9_12 to start recording. Press again to start playback. Loops until button is pressed again. Additionally plays the input signal, regardless of whether or not the loop is being played. Can control the volume with a nexus ui slider (browser websockets->node server->cpp osc server)
#include <Bela.h>
#include <OSCServer.h>
#include <OSCClient.h>
#include <math.h>
#include <digital_gpio_mapping.h>
//44100 * 60 * 4 (4 minute loop)
#define LOOP_BUFFER_SIZE 10584000
float gLoopBuffer_l[LOOP_BUFFER_SIZE] = {0};
float gLoopBuffer_r[LOOP_BUFFER_SIZE] = {0};
int gLoopWritePtr = 0;
int gLoopReadPtr = 1;
int gLoopEndPtr = 0;
bool is_recording = true;
int debounce_frames;
OSCServer oscServer;
OSCClient oscClient;
int localPort = 7562;
int remotePort = 7563;
const char* remoteIp = "127.0.0.1";
typedef struct
{
int pin;
uint64_t last_frame_sampled;
int state;
} Button;
Button btn1, btn2, btn3;
float volume = 1.0;
typedef enum {UNCHANGED, BTN_DOWN, BTN_UP} button_event;
/**
* Returns whether btn was pressed down, released, or unchanged at the beginning of the current audio block.
* Tries to debounce - the button value will be sampled every block until a transition is detected.
* When the transition is detected, stops sampling until a certain number of frames equal to the global variable, debounce_frames, have passed.
* The assumption is that button values are always stable except when you're pushing it down or releasing it.
* This is probably not a universally true assumption. Interference could cause false button reads.
*/
button_event detectButtonEvent(BelaContext * context, Button *btn){
if ((context->audioFramesElapsed - btn->last_frame_sampled) > debounce_frames){
int old_state = btn->state;
btn->state = digitalRead(context, 0, btn->pin);
if (old_state == 0 && btn->state > 0){
btn->last_frame_sampled = context->audioFramesElapsed;
return BTN_DOWN;
} else if (old_state > 0 && btn->state == 0){
btn->last_frame_sampled = context->audioFramesElapsed;
return BTN_UP;
}
}
return UNCHANGED;
}
bool setup(BelaContext *context, void *userData)
{
oscServer.setup(localPort);
oscClient.setup(remotePort, remoteIp);
//TODO: osc handshake like in example
debounce_frames = ceilf(0.05 * context->audioSampleRate); //50 ms delay, 2205 samples at CD rate
pinMode(context, 0, P9_12, INPUT);
pinMode(context, 0, P9_14, INPUT);
pinMode(context, 0, P9_16, INPUT);
btn1.pin = P9_12;
btn1.last_frame_sampled = 0;
btn1.state = 0;
btn2.pin = P9_14;
btn2.last_frame_sampled = 0;
btn2.state = 0;
btn3.pin = P9_14;
btn3.last_frame_sampled = 0;
btn3.state = 0;
return true;
}
//want to listen for set volume request
//should this happen on an aux task?
//sounds like it can happen on an aux task if desired
void render(BelaContext *context, void *userData)
{
float new_volume;
while(oscServer.messageWaiting()){
oscpkt::Message msg = oscServer.popMessage();
if (msg.match("/set-volume").popFloat(new_volume)){
if(0.0 <= new_volume <= 1.0){
volume = new_volume;
}
}
}
float in_l = 0;
float in_r = 0;
float out_l = 0;
float out_r = 0;
button_event b_ev1 = detectButtonEvent(context, &btn1);
switch(b_ev1){
case BTN_DOWN:
if (is_recording){
gLoopEndPtr = gLoopWritePtr;
gLoopReadPtr = 0;
} else{
gLoopWritePtr = 0;
}
is_recording = !is_recording;
break;
case BTN_UP:
break;
case UNCHANGED:
break;
}
for(unsigned int n = 0; n < context->audioFrames; n++){
in_l = audioRead(context,n,0);
in_r = audioRead(context,n,1);
audioWrite(context, n, 0, out_l);
audioWrite(context, n, 1, out_r);
out_l = in_l;
out_r = in_r;
if(is_recording){
gLoopBuffer_l[gLoopWritePtr] = in_l;
gLoopBuffer_r[gLoopWritePtr] = in_r;
gLoopWritePtr++;
gLoopWritePtr %= LOOP_BUFFER_SIZE;
}else{
out_l += gLoopBuffer_l[gLoopReadPtr];
out_r += gLoopBuffer_r[gLoopReadPtr];
gLoopReadPtr++;
gLoopReadPtr %= gLoopEndPtr;
}
audioWrite(context, n, 0, out_l * volume);
audioWrite(context, n, 1, out_r * volume);
}
}
void cleanup(BelaContext *context, void *userData)
{
}
<!doctype html>
<html language="en-US">
<head>
<script src="resources/NexusUI.js"></script>
<meta charset="utf-8">
<title>Bela Looper</title>
</head>
<body>
<div id="slider"></div>
</body>
<script src="resources/app.js"></script>
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const port = 3000
const WebSocket = require('ws')
const server = require('http').createServer()
const osc = require('osc-min')
const udp = require('dgram')
const sock = udp.createSocket('udp4', function(msg, rinfo){
try {
return console.log(osc.fromBuffer(msg))
} catch (error) {
return console.log('invalid soc packet')
}
});
sock.bind(7563);
const wss = new WebSocket.Server({server: server})
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
let volume = JSON.parse(message).volume
let buf
buf = osc.toBuffer({address: '/set-volume',
args: [volume]})
sock.send(buf, 0, buf.length, 7562, "localhost")
})
})
app.use(bodyParser.json())
app.use(express.static('.'))
server.on('request', app)
server.listen(port, function() {console.log(`listening on ${port}`)})
var slider = new Nexus.Slider('#slider');
let socket = new WebSocket('ws://localhost:3000/');
//socket.onopen = function(){}
//
//socket.onmessage = function(){}
let ts = performance.now();
function sendSetVolume(volume){
var newTs = performance.now();
if((newTs - ts) >= 50){
ts = newTs;
socket.send(JSON.stringify({volume: volume}));
}
}
slider.on('change', function(v){sendSetVolume(v)});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment