2 3 4 5 6 7 last»

Tamas 3

2020-08-17 17:11:00 electronics

Here is a board I built in autumn of 2018. It is for making custom hardware controllers with sliders, buttons and LEDs. This version (v3) communicates via MIDI, Serial or over WiFi using OSC.

(The previous two versions are: Tamas and Tamas 2)

greybox version 3 pinouts

The board is just a Teensy 3.5 with a TLC5940 LED driver and an optional ESP8266-01 module. There are 16 analogue inputs, 16 digital inputs and 16 PWM outputs. It is powered from USB.

Under the hood it looks like this...

greybox version 3 circuit

When using MIDI the analogue input and PWM output resolutions are 7bits, and over Serial and OSC my firmware is using 8bit resolution. The hardware supports 10 and 12-bit resolutions but I decided that 8 is enough. It will keep the communictation protocol simple and reduce the amount of data being sent.

Attached are several examples for SuperCollider and MaxMSPJitter. They demonstrate how to talk to this box, how to control LEDs and read sensor data via MIDI, WiFi and Serial.

Also attached are the schematics and Teensy and ESP firmware files. In the file tamas3_teensy.ino one can customise debounce time, MIDI note and control numbers, update rate etc. And in the file tamas3_esp.ino there are settings for how the WiFi should behave (soft-access point with DHCP is the default).


Under the Hood Changes 3

2020-07-01 22:10:21 other

Big remake of this blog as well as my homepage. I decided to rewrite everything using the wonderful Zola engine www.getzola.org - a static site generator.

On the surface it all should look and function about the same as before. I worked hard to keep backward compatibility and to make sure URLs stay the same. I used aliases and a few .htaccess RewriteRules.

Up until now, my site was a mixture of plain HTML and Drupal (the monster). It took quite some time to change everything over, but I think it was worth the trouble.

Also now my homepage finally should look at least decent on mobile.

Serial Match

2020-04-06 17:37:56 supercollider

In MaxMSP there is a great object called [match]. It is especially useful for parsing data from a serial port. In SuperCollider there is nothing like that build-in, but one can do the same with the class extension below.

Bascially this method will match an incoming array of specific bytes where nil means match any value. For example port.match([1, 2, nil, 3]); will return a match for 1, 2, 55, 3 but not for 1, 4, 55, 10.

Save the following in a file called extSerial.sc, move the file to your SuperCollider extensions folder and recompile...

+SerialPort {
    match {|arr|
        var byte, index= 0;
        var res= arr.copy;
        while({index<arr.size}, {
            byte= this.read;
            if(arr[index].isNil or:{byte==arr[index]}, {
                res[index]= byte;
                index= index+1;
            }, {

To test it upload the following code to an Arduino...

//Arduino test code - send six 10-bit values
#define NUM_ANALOG 6
#define UPDATERATE 20  //ms to wait between each reading

void setup() {
void loop() {
  Serial.write(200);  //header
  for (byte i = 0; i < NUM_ANALOG; i++) {
    int data = analogRead(i);
    Serial.write(data & 255); //lsb
    Serial.write(data >> 8); //msb
  Serial.write(202);  //footer

and then run this code in SuperCollider (adapt serial port name where it says EDIT)...

//SuperCollider test code - read six 10bit values
var num_analog= 6;
var port= SerialPort("/dev/cu.SLAB_USBtoUART", 57600);  //EDIT
var arr= [200, 201, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 202];  //200, 201 header, 202 footer and nil the data
r= Routine.run({
        var res;
        res= port.match(arr);
        if(res.notNil, {
                var lsb= res[i*2+2];
                var msb= res[i*2+3];


Buffer xFader

2020-03-29 13:42:09 supercollider

This SuperCollider function automates the process of making seamless loops as shown in this animation...

i.e. creating continuous loops from buffers following this recipe...

  1. select a few seconds from the end.
  2. fade out the selection.
  3. cut the selection.
  4. paste the selection at time 0.
  5. select a few seconds from the beginning (usually same length as in #1).
  6. fade in the selection.
  7. mix the two tracks.
~xfader= {|inBuffer, duration= 2, curve= -2, action|
    var frames= duration*inBuffer.sampleRate;
    if(frames>inBuffer.numFrames, {
        "xfader: crossfade duration longer than half buffer - clipped.".warn;
    frames= frames.min(inBuffer.numFrames.div(2)).asInteger;
    Buffer.alloc(inBuffer.server, inBuffer.numFrames-frames, inBuffer.numChannels, {|outBuffer|
            var interleavedFrames= frames*inBuffer.numChannels;
            var startArr= arr.copyRange(0, interleavedFrames-1);
            var endArr= arr.copyRange(arr.size-interleavedFrames, arr.size-1);
            var result= arr.copyRange(0, arr.size-1-interleavedFrames);
                var fadeIn= i.lincurve(0, interleavedFrames-1, 0, 1, curve);
                var fadeOut= i.lincurve(0, interleavedFrames-1, 1, 0, 0-curve);
                result[i]= (startArr[i]*fadeIn)+(endArr[i]*fadeOut);
            outBuffer.loadCollection(result, 0, action);


//edit and load a sound file (or use an already existing buffer)
b.free;  b= Buffer.read(s, "~/Desktop/testnoise.wav".standardizePath);

//evaluate the function to create the seamless cross fade
c= ~xfader.value(b);

//try looping it - should loop smoothly and without discontinuities
d= {PlayBuf.ar(c.numChannels, c, loop:1)}.play;

//compare with the input file - this will probably have a hickup
d= {PlayBuf.ar(b.numChannels, b, loop:1)}.play;


//--save to disk example with shorter cross fade and done action function.
b.free;  b= Buffer.read(s, "~/Desktop/testnoise.wav".standardizePath);
c= ~xfader.value(b, 0.5, -3, action:{|buf| ("done with buffer"+b).postln});

The SuperCollider code works with any Buffer containing sound files, live sampled or generated sounds. The duration argument set the cross fade length in seconds and with the curve argument one can set curvature.

Note that duration can not be longer than half the duration of the input buffer and a curve of 0.0 or greater will mean that the amplitude will dip in the middle of the crossfade. So it is recommended to bend the curve a bit to get more of an equal power crossfade. -2.0 to -4.0 seem like sensible values. The function will allocate and return a new Buffer instance and not touch the buffer passed in as an argument.

An action function is optional. The function is asynchronous. Also, note that the resulting file will be shorter than the original because of the crossfade.

I remember learning this trick from Peter Lundén ~20 years ago. He showed it to me on an SGI machine running IRIX, possibly using the Snd sound editor.

ESP Mesh Network with OSC

2020-03-23 19:32:13 electronics

With the painlessMesh library, it turned out easy to set up a decentralised mesh network for a few ESP8266 modules. The example below shows how I set it up and how to send and receive OpenSoundControl (OSC) messages. The code also works on an ESP32.

  1. Install the painlessMesh and the OSC libraries for Arduino.
  2. Program a few nodes (ESP8266) so that they all run this code...
//req. libraries: OSC, painlessMesh

#include <Arduino.h>
#include <painlessMesh.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>
#include <OSCData.h>

#define MESH_NAME "networkname" //EDIT mesh name
#define MESH_PASS "networkpass" //EDIT password
#define MAX_CONN 4 //EDIT ESP32 can have more than ESP8266

#define INPORT 19998 //OSC in port
#define OUTPORT 57120 //OSC out port (SC)

IPAddress outIP;
WiFiUDP Udp;
painlessMesh mesh;

void setup() {
  mesh.init(MESH_NAME, MESH_PASS, 5555, WIFI_AP_STA, 1, 0, MAX_CONN);

void pingFunc(OSCMessage &inMsg) {
  digitalWrite(LED_BUILTIN, 1 - digitalRead(LED_BUILTIN));  //toggle led
  OSCMessage outmsg("/pong");
  outmsg.add(mesh.getNodeId()); //uint32
  IPAddress ip;
  ip= mesh.getStationIP(); // if current base station
  ip= mesh.getAPIP();
  Udp.beginPacket(outIP, OUTPORT);

void loop() {

  int packetSize= Udp.parsePacket();
  if (packetSize) {
    OSCMessage inMsg;
    while (packetSize--) {
    if (!inMsg.hasError()) {
      outIP= Udp.remoteIP();
      inMsg.dispatch("/ping", pingFunc);
  1. After that power up the nodes and a new WiFi network should show up.
  2. Connect a laptop to the new mesh network and take note of the IP number assigned.
  3. Run the test code below on the laptop. It will broadcast a \ping OSC message and listen for \pong replies.

The test code is for SuperCollider but any program that can send OSC should work.

OSCFunc({|msg| msg.postln}, \pong);
NetAddr.broadcastFlag= true;
NetAddr("", 19998).sendMsg(\ping);  //EDIT laptop ip number but leave 255 as the last number

The painlessMesh library will for sure come in handy when I need a network without a WiFi router, or for when trying to cover a larger area.

A major drawback though seems to be that the maximum number of nodes that can be used is really low. Apparently, an ESP8266 can only handle 5 (TCP/IP) connections at the same time and an ESP32 about three times that. And that is not very many.

Building SuperCollider for piCore Linux

2020-03-16 21:42:06 supercollider

These instructions show how to build, package and install SuperCollider, SC3-plugins and jackd for piCore - a variant of TinyCoreLinux for the Raspberry Pi.

piCore has many advantages over the common Raspbian system. It will boot a lot faster, is extremely light-weight and is easy to customise. And because the whole system always resides in RAM, SD card wear is minimal.

Its immutable-by-default design means one can unplug the power to the Raspberry Pi without performing and waiting for a proper shutdown nor risking corrupting the SD card. It also allows one to experiment without being afraid of messing up. A simple reboot will take the system back to a known state. For changes to be persistent, one must deliberately write them to the SD card (using the command filetool.sh -b).

Some drawbacks are that piCore is more advanced to install and configure and that much common Linux software is missing from the built-in package manager (one will have to compile it oneself - hence this guide).


  • an SD card - 2 GB is plenty
  • a Raspberry pi - here a RPi 3B v1.2
  • an ethernet cable
  • a router with an internet connection
  • a laptop - here running macOS


On the laptop, open a terminal and run the following commands:

arp -a  #figure out which IP address the RPi has (here
ssh tc@  #pass: piCore

sudo fdisk -u /dev/mmcblk0  #then press the following keys in order to delete and recreate partition2
 p  #check start of partition2 - usually 77824 (or 195693)
 77824  #enter start of partition2 from above
 <RET>  #type return to accept suggestion
sudo reboot

ssh-keygen -R  #remove the ssh keys to be able to log in again
ssh tc@  #pass: piCore

sudo resize2fs /dev/mmcblk0p2  #resize partition2


Assuming piCore is now installed and partition2 resized like above...

#download and install build dependencies
tce-load -wil cmake compiletc python squashfs-tools libudev-dev libsndfile-dev readline-dev libsamplerate-dev fftw-dev git

#download and compile jackd
cd /tmp
git clone git://github.com/jackaudio/jack2 --depth 1
cd jack2
wget https://waf.io/waf-2.0.12
chmod +x waf-2.0.12
./waf-2.0.12 configure --alsa
./waf-2.0.12 build
sudo ./waf-2.0.12 install > /tmp/jack2_tmp.list

#create the jackd tcz extension package
cd /tmp
cat jack2_tmp.list | grep "/usr/local/" | grep -v "/share/man/\|.h \|.pc " | awk '{print $3}' > jack2.list
tar -T /tmp/jack2.list -czvf /tmp/jack2.tar.gz
mkdir /tmp/pkg && cd /tmp/pkg
tar -xf /tmp/jack2.tar.gz
cd ..
mksquashfs pkg/ jack2.tcz
sudo mv jack2.tcz ~
rm -rf /tmp/pkg
tce-load -i ~/jack2.tcz
jackd  #check that it is working

On the laptop, open another terminal window and download the resulting compressed jackd package:

scp tc@ ~/Downloads  #pass: piCore


Assuming jackd is installed like above...

#download and compile SuperCollider
cd /tmp
git clone --recurse-submodules https://github.com/supercollider/supercollider.git
cd supercollider
mkdir build && cd build
sudo make install
cat install_manifest.txt | grep -v "/usr/local/include/\|/usr/local/share/pixmaps/\|/usr/local/share/mime/" > /tmp/sc.list

#create the SuperCollider tcz extension package
cd /tmp
tar -T /tmp/sc.list -czvf /tmp/sc.tar.gz
mkdir /tmp/pkg && cd /tmp/pkg
tar -xf /tmp/sc.tar.gz
cd ..
mksquashfs pkg/ supercollider.tcz
sudo mv supercollider.tcz ~
rm -rf /tmp/pkg
tce-load -i ~/supercollider.tcz
sclang -h  #just to check that it is working

On the laptop, open another terminal window and download the resulting compressed SuperCollider package:

scp tc@ ~/Downloads  #pass: piCore


Assuming SuperCollider is installed like above...

#download and compile sc3-plugins
cd /tmp
git clone --recursive https://github.com/supercollider/sc3-plugins.git
cd sc3-plugins
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE="Release" -DSUPERNOVA=OFF -DNATIVE=ON -DSC_PATH=../../supercollider/ ..
sudo make install
cat install_manifest.txt | grep -v "/HelpSource\|.html\|/Help" > /tmp/scplugs.list

#create the sc3-plugins tcz extension package
cd /tmp
tar -T /tmp/scplugs.list -czvf /tmp/scplugs.tar.gz --exclude=*/HelpSource --exclude=*.html --exclude=*/Help
mkdir /tmp/pkg && cd /tmp/pkg
tar -xf /tmp/scplugs.tar.gz
cd ..
mksquashfs pkg/ sc3-plugins.tcz
sudo mv sc3-plugins.tcz ~
rm -rf /tmp/pkg

On the laptop, open another terminal window and download the resulting compressed sc3-plugins package:

scp tc@ ~/Downloads  #pass: piCore

Now that the three tcz packages are created and downloaded to the laptop, we can erase the SD card and start afresh. (It is possible to continue working with the same piCore install, but unused build dependencies would waste some space).

restart and install

(for future installs you can skip all of the above and start here assuming you have kept the .tgz packages)

  • burn the zip file to the SD card using for example balenaEtcher
  • put the SD card in the RPi and connect ethernet and 5V power

On the laptop, open a terminal and run the following commands:

arp -a  #figure out which IP address the RPi has (here
ssh-keygen -R  #remove the ssh keys to be able to log in again
ssh tc@  #pass: piCore

sudo fdisk -u /dev/mmcblk0  #then press the following keys in order to delete and recreate partition2
 p  #check start of partition2 - usually 77824 (or 195693)
 77824  #enter start of partition2 from above
 <RET>  #type return to accept suggestion
sudo reboot

ssh-keygen -R  #remove the ssh keys to be able to log in again
ssh tc@  #pass: piCore

sudo resize2fs /dev/mmcblk0p2  #resize partition2

#install dependencies
tce-load -wi nano alsa alsa-utils libsamplerate libudev readline git fftw

On the laptop, open another terminal window and upload the three compressed packages:

cd ~/Downloads
scp jack2.tcz supercollider.tcz sc3-plugins.tcz tc@

Back on the Raspberry Pi...

cd ~
mv jack2.tcz supercollider.tcz sc3-plugins.tcz /mnt/mmcblk0p2/tce/optional/
echo jack2.tcz >> /mnt/mmcblk0p2/tce/onboot.lst
echo supercollider.tcz >> /mnt/mmcblk0p2/tce/onboot.lst
echo sc3-plugins.tcz >> /mnt/mmcblk0p2/tce/onboot.lst
echo -e '\nsudo /usr/local/sbin/alsactl -f /home/tc/mysound.state restore' >> /opt/bootlocal.sh

#autostart - optional
nano autostart.sh  #add the following lines
  jackd -P75 -p16 -dalsa -dhw:0 -r44100 -p1024 -n3 &
  sclang /home/tc/mycode.scd
chmod +x autostart.sh

nano mycode.scd  #add the following lines
      {SinOsc.ar([400, 404], 0, 0.5)}.play;

nano /opt/bootlocal.sh  #add the following lines to the end
  echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
  /home/tc/autostart.sh &

#IMPORTANT - make the changes permanent
filetool.sh -b

sudo reboot

The piCore system should now have SuperCollider installed and (optionally) start at boot.


To adjust the volume log in and run the following commands...

alsamixer  #set volume with arrow keys, ESC to exit
alsactl -f /home/tc/mysound.state store  #save in custom alsa settings file
filetool.sh -b  #make permanent


  • for RPi1 and RPi Zero you should probably get the armv6 version.
  • for RPi2 and newer get the armv7 version (even though the files seem identical).
  • running the make command with flag -j3 will usually just result in a out-of-memory freeze.
  • avahi is not activated because libavahi-client-dev is not available for piCore - maybe later.
  • waf-2.0.12 seems to be the newest version that can build jack2.
  • jackd and SuperCollider will be running as root when autostarting.
  • the start-up time from applying power to SuperCollider is making sound is ~20 seconds.
  • after the first backup (filetool.sh -b) the ssh-keygen -R will not be needed any longer.
  • CPU benchmarks are more or less the same as for running SC under Raspbian

//TODO: USB soundcard


  • 200401: simplified by using install_manifest.txt

Motion Induced Blindness

2020-01-27 21:16:40 visuals

A fascinating illusion. Stare at the green dot for a bit.

More info here en.wikipedia.org/wiki/Motion-induced_blindness.

This is just a remake of the wikipedia GIF animation, but this JavaScript code and its variables/settings opens up for experimentation.

<div style="background-color:black;">
<canvas id="canvas" width="600" height="600"></canvas>
//motion-induced blindness after en.wikipedia.org/wiki/Motion-induced_blindness
(function() {
  let rotationRate= 0.1;  //rps
  let blinkRate= 2.5;  //Hz
  let numCrosses= 7;
  let numDots= 3;
  let dotRadius= 5;
  let crossWidth= 0.1;  //percent
  let crossWeight= 3;
  let colorBackground= '#000';
  let colorCrosses= '#00F';
  let colorCenter= '#0F0';
  let colorDots= '#FF0';
  let can= document.getElementById('canvas');  //EDIT name of canvas to draw to

  function draw() {
    let bottom= can.getBoundingClientRect().bottom;
    if(bottom>=0 && bottom<=(innerHeight+can.height)) {  //only draw when visible in browser
      let w2= can.width*0.5;
      let h2= can.height*0.5;
      let uw= can.width/3;

      let ctx= can.getContext('2d');
      ctx.fillStyle= colorBackground;
      ctx.fillRect(0, 0, can.width, can.height);

      ctx.translate(w2, h2);
      ctx.lineWidth= crossWeight;
      ctx.strokeStyle= colorCrosses;
      for(let i= 0; i<numCrosses; i++) {
        let y= i*(uw*2)/(numCrosses-1)-uw;
        for(let j= 0; j<numCrosses; j++) {
          let x= j*(uw*2)/(numCrosses-1)-uw;
          ctx.moveTo(x-(crossWidth*uw), y);
          ctx.lineTo(x+(crossWidth*uw), y);
          ctx.moveTo(x, y-(crossWidth*uw));
          ctx.lineTo(x, y+(crossWidth*uw));

      if ((Date.now()/1000*blinkRate)%1>0.5) {
        ctx.fillStyle= colorCenter;
        ctx.ellipse(w2, h2, dotRadius, dotRadius, 0, 0, Math.PI*2);

      ctx.translate(w2, h2);
      ctx.fillStyle= colorDots;
      for (let i= 0; i<numDots; i++) {
        ctx.ellipse(can.width/4, 0, dotRadius, dotRadius, 0, 0, Math.PI*2);

Also attached is the same code ported to Processing and SuperCollider.


Keyboard Shortcuts

2020-01-19 14:14:49 other

MacBook Pro mid-2015 keyboard after some hard use of the cmd key. My habits shining through.

keyboard shortcuts

The left side shift and the three up/down/right arrow keys also have this nice transparency look. But strangely enough, the left arrow key must not have been used much.

2 3 4 5 6 7 last»