Creating a looper in shell part 3: multiple layers

To understand this article better it is advised to have read the previous one.

In the previous article, we saw how to add a layer to the loop, here was the script behavior:

timeline

This aims at adding multiple layers by pressing any key, and being able to remove last layer by pressing a (pedal #2).

timeline

programming pedals

As mentionned earlier, I’m using cheap pedals.

I’m using footswitch to program pedals.

For example, here is how to get one of them to simulate a a keypress:

$ sudo ./footswitch -k a

I’ll be using two pedals, one for adding a layer, one for removing last layer.

Let’s add layers

Here are diffs between last version and new one (in red are the old lines and in green the new ones).

Let’s modify the input function so that it returns dd return code and so that it sets a variable (key with the key that was pressed):

 input() {
   stty raw
-  dd bs=1 count=1 2> /dev/null
+  key=$(dd bs=1 count=1 2> /dev/null)
+  rc=$?
+  [ $rc -ne 0 ] && return $rc
   stty -raw
+  return 0
 }

Instead of playing one layer (layer.wav), we play a bunch of them (layer1.wav, layer2.wav, …) based on $layer_number variable. Instead of returning one pid, we return a list op PIDs.

 input() {
-play_layer() {
-  play_layer_pid=""
-  if [ -e "layer.wav" ]
-  then
-    aplay layer.wav &
-    play_layer_pid=$!
-  fi
+play_layers() {
+  play_layer_pids=""
+  for i in $(seq $layer_number)
+  do
+    aplay layer$i.wav &
+    play_layer_pids="$! $play_layer_pids"
+  done
 }

We change current layer to record name to layer${layer_number}.wav instead of layer.wav (after incrementing it):

 record_layer() {
   record_layer_pid=""
   if $(cat should_record_layer)
   then
-    arecord -f S16_LE -r 48000 -D hw:1,0 layer.wav &
+    layer_number=$[ $layer_number + 1 ]
+    arecord -f S16_LE -r 48000 -D hw:1,0 layer${layer_number}.wav &
     record_layer_pid=$!
     echo false > should_record_layer
   fi
 }

We add a function to remove the last layer registered if should_remove_layer file contains true, by decrementing $layer_number:

+remove_layer() {
+  record_layer_pid=""
+  if $(cat should_remove_layer)
+  then
+    layer_number=$[ $layer_number - 1 ]
+    echo false > should_remove_layer
+  fi
+}
+

We initialize should_remove_layer to false, layer_number to 0, and rename play_layer to play_layers and play_layer_pid to play_layer_pids:

 main_loop() {
   echo false > should_record_layer
+  echo false > should_remove_layer
+  layer_number=0
   while true
   do
-    play_layer
-    record_layer  
+    remove_layer
+    play_layers
+    record_layer
     aplay background.wav
     [ $? -ne 0 ] && break
-    [ -n $record_layer_pid ] && kill $record_layer_pid
-    [ -n $play_layer_pid ] && kill $play_layer_pid
+    [ -n "$record_layer_pid" ] && kill $record_layer_pid
+    [ -n "$play_layer_pids" ] && kill $play_layer_pids
   done
 }

Instead of waiting for only one input (since there was only one layer), we wait for multiple input in a loop. If “a” is pressed, we notify that we should_remove_layer. Overwise, we notify that we should_record_layer:

+input_loop() {
+  while true
+  do
+    input
+    [ "$key" = $'\003' ] && break
+    if [ "$key" = "a" ]
+    then
+      echo true > should_remove_layer
+    else
+      echo true > should_record_layer
+    fi
+    sleep 1
+  done
+}
+
 record_background
 main_loop &
 main_loop_pid=$!
-input
-echo true > should_record_layer
+input_loop
 wait $main_loop_pid

The whole script

#!/usr/bin/env sh

input() {
  stty raw
  key=$(dd bs=1 count=1 2> /dev/null)
  rc=$?
  [ $rc -ne 0 ] && return $rc
  stty -raw
  return 0
}

record_background() {
  input
  arecord -f S16_LE -r 48000 -D hw:1,0 background.wav &
  pid=$!
  sleep 1
  input
  kill $pid
}

play_layers() {
  play_layer_pids=""
  for i in $(seq $layer_number)
  do
    aplay layer$i.wav &
    play_layer_pids="$! $play_layer_pids"
  done
}

record_layer() {
  record_layer_pid=""
  if $(cat should_record_layer)
  then
    layer_number=$[ $layer_number + 1 ]
    arecord -f S16_LE -r 48000 -D hw:1,0 layer${layer_number}.wav &
    record_layer_pid=$!
    echo false > should_record_layer
  fi
}

remove_layer() {
  record_layer_pid=""
  if $(cat should_remove_layer)
  then
    layer_number=$[ $layer_number - 1 ]
    echo false > should_remove_layer
  fi
}

main_loop() {
  echo false > should_record_layer
  echo false > should_remove_layer
  layer_number=0
  while true
  do
    remove_layer
    play_layers
    record_layer
    aplay background.wav
    [ $? -ne 0 ] && break
    [ -n "$record_layer_pid" ] && kill $record_layer_pid
    [ -n "$play_layer_pids" ] && kill $play_layer_pids
  done
}

input_loop() {
  while true
  do
    input
    [ "$key" = $'\003' ] && break
    if [ "$key" = "a" ]
    then
      echo true > should_remove_layer
    else
      echo true > should_record_layer
    fi
    sleep 1
  done
}

record_background
main_loop &
main_loop_pid=$!
input_loop
wait $main_loop_pid

Conclusion

The whole script is now available in its own repo. Of course, patches are welcome !

I use this script on a daily basis, and there are tons of things that could be polished. Maybe I’ll rewrite it in some other language :)