├── server ├── NOTES ├── start.sh ├── bashttpd.conf └── bashttpd ├── config └── rc.local ├── synth ├── fluidsynth.service └── fluidsynth_service.sh ├── LICENSE └── README.md /server/NOTES: -------------------------------------------------------------------------------- 1 | 2 | You must install socat to use this version: 3 | 4 | apt-get install socat 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | socat TCP4-LISTEN:9987,fork EXEC:/home/synth/server/bashttpd > /dev/null & 4 | 5 | -------------------------------------------------------------------------------- /config/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | /home/synth/synth/fluidsynth_service.sh start >/var/log/fluid.log 2>/var/log/fluid.err 4 | /home/synth/server/start.sh >/var/log/fluid_server.log 2>/var/log/fluid_server.err 5 | 6 | -------------------------------------------------------------------------------- /synth/fluidsynth.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fluidsynth 3 | 4 | [Service] 5 | Type=forking 6 | # The PID file is optional, but recommended in the manpage 7 | # "so that systemd can identify the main process of the daemon" 8 | PIDFile=/var/run/fluidsynth.pid 9 | ExecStart=/home/synth/bin/fluidsynth_service.sh start 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /synth/fluidsynth_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # start/stop service and create pid file for a non-daemon app 3 | # from https://blog.sleeplessbeastie.eu/2014/11/04/how-to-monitor-background-process-using-monit/ 4 | 5 | # test the audio 6 | function playnote() { 7 | NOTE=$1 8 | 9 | echo "noteon 1 $NOTE 100" > /dev/tcp/localhost/9988 10 | sleep 0.2 11 | echo "noteoff 1 $NOTE" > /dev/tcp/localhost/9988 12 | } 13 | 14 | # service command 15 | service_cmd="nice -n -19 fluidsynth -is -o "shell.port=9988" --gain 2 --audio-driver=alsa -z=2048 /usr/share/sounds/sf2/FluidR3_GM.sf2" 16 | 17 | # pid file location 18 | service_pid="/var/run/fluidsynth.pid" 19 | 20 | if [ $# -eq 1 -a "$1" = "start" ]; then 21 | if [ -f "$service_pid" ]; then 22 | kill -0 $(cat $service_pid) 2>/dev/null # check pid 23 | if [ $? -eq 0 ]; then 24 | exit 2; # process is running, exit 25 | else 26 | unlink $service_pid # process is not running 27 | # remove stale pid file 28 | fi 29 | fi 30 | 31 | # start service in background and store process pid 32 | $service_cmd >/dev/null & 33 | echo $! > $service_pid 34 | sleep 10 35 | aconnect 20:0 128:0 36 | 37 | playnote 60 38 | playnote 67 39 | playnote 72 40 | 41 | 42 | elif [ $# -eq 1 -a "$1" = "stop" -a -f "$service_pid" ]; then 43 | kill -0 $(cat $service_pid) 2>/dev/null # check pid 44 | if [ $? -eq 0 ]; then 45 | kill $(cat $service_pid) # kill process if it is running 46 | fi 47 | unlink $service_pid 48 | aconnect -x 49 | fi 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluidPi 2 | A headless Raspberry Pi synth with web-controlled patches 3 | 4 | ## Intro 5 | I have several Raspberry Pi's that are not earning their keep, so I decided to buy some SD cards 6 | and turn them into permanently-configured embedded devices. The first one is this - a synth! 7 | 8 | It uses SF2 soundfont files to generate the noises, and is controlled by a USB MIDI controller. 9 | 10 | It uses a web server in bash (really!) to control the sounds remotely, so it can be run headless. 11 | 12 | ## Installation 13 | There are several moving parts here: 14 | * OS : Raspbian : https://www.raspberrypi.org/downloads/raspbian/ 15 | * Software : Utility software: apt get install fluidsynth alsa socat 16 | * Configuration : Append the config/rc.local to etc/rc.local 17 | 18 | The guide with which I started was at andrewdotni.ch/blog/2015/02/28/midi-synth-with-raspberry-p/ 19 | 20 | I use this as a base, with all the code in /home/synth but run by the root user on boot-up. This 21 | isn't recommended (especially since it runs the web server as root) but it gets the job done, on 22 | a local network. 23 | 24 | Copy all the SF2 soundfont files you have into /home/synth/sf2, as this where the webserver will 25 | scan. 26 | 27 | Start the synth service by running rc.local, or starting it via systemd 28 | 29 | There maybe some other steps which I've forgotten. 30 | 31 | ## Features 32 | On boot-up the device: 33 | * Loads the FluidSynth software 34 | * Opens a session on port 9987 into which you can telnet, to control FluidSynth 35 | * Opens a web server on port 9988 to change SF2 files 36 | * Connects a MIDI keyboard input device (ID 128), to the FluidSynth output device (ID 20) 37 | * Plays some notes to let you know it's ready 38 | 39 | ## Credits 40 | Inspiration: http://andrewdotni.ch/blog/2015/02/28/midi-synth-with-raspberry-p/ 41 | FluidSynthh: http://www.fluidsynth.org 42 | Service scripts: https://gist.github.com/oostendo/b22a57aacc9439f80f74b0545d38148d 43 | https://gist.github.com/oostendo/7bd7efeebd2dc135e17a077326469b24 (not used) 44 | bashhttpd: https://github.com/avleen/bashttpd 45 | 46 | 47 | -------------------------------------------------------------------------------- /server/bashttpd.conf: -------------------------------------------------------------------------------- 1 | SYNTH_ROOT=/home/synth/sf2 2 | FLUID_PORT=9988 3 | 4 | 5 | function ui_css() { 6 | echo "body { margin: 0; font-size:20px; } body, html, .container { height: 100%; } .container, .content { display: flex; } .container { flex-direction: column; } .content, .content div { flex: 1; } .content div { overflow: scroll; }" 7 | } 8 | 9 | function ui_bar() { 10 | echo "
$1
" 11 | } 12 | 13 | function ui_page() { 14 | TOP=$1 15 | LHS=$2 16 | RHS=$3 17 | BOTTOM=$4 18 | 19 | echo ' ' 20 | echo '' 23 | echo '
' 24 | echo "
$TOP
" 25 | 26 | echo '
'; 27 | echo $LHS 28 | echo '
' 29 | echo $RHS 30 | echo '
' 31 | 32 | echo "
$BOTTOM
" 33 | echo '
' 34 | } 35 | 36 | function ui_page_standard() { 37 | FOLDER="$2" 38 | TOP=$(ui_bar "$1") 39 | LHS=$(ui_html_folders "$FOLDER") 40 | RHS=$(ui_html_files "$FOLDER") 41 | BOTTOM=$(ui_standard_bottom "$FOLDER") 42 | 43 | RESPONSE=$(ui_page "$TOP" "$LHS" "$RHS" "$BOTTOM") 44 | 45 | add_response_header "Content-Type" "text/html" 46 | send_response_ok_exit <<< $RESPONSE 47 | } 48 | 49 | function ui_html_folders() { 50 | DIRNAME=$1 51 | FULLPATH=$SYNTH_ROOT/$DIRNAME 52 | 53 | echo "[ parent ]
" 54 | 55 | for entry in "$FULLPATH"/* 56 | do 57 | if [ -d "$entry" ]; then 58 | SUBDIR=`basename $entry` 59 | echo "[ dir ] $SUBDIR
" 60 | fi 61 | done 62 | } 63 | 64 | function ui_html_files() { 65 | DIRNAME=$1 66 | FULLPATH=$SYNTH_ROOT/$DIRNAME 67 | 68 | for entry in "$FULLPATH"/* 69 | do 70 | if [ -f "$entry" ]; then 71 | SFNAME=`basename "$entry"` 72 | echo "$SFNAME
" 73 | fi 74 | done 75 | } 76 | 77 | function ui_standard_bottom() { 78 | DIRNAME=$1 79 | echo "[ Vol+ ]" 80 | echo "[ Vol++ ]" 81 | echo "[ Vol+++ ]" 82 | echo "[ Vol++++ ]" 83 | echo " | " 84 | echo "[ HALT ]" 85 | echo "[ REBOOT ]
" 86 | } 87 | 88 | function get_query() { 89 | ARG=$1 90 | QUERY_ARG=$2 91 | if [[ "$QUERY_ARG" =~ [\&\?]$ARG\=([^&]*) ]]; 92 | then 93 | echo "${BASH_REMATCH[1]}" 94 | fi 95 | } 96 | 97 | # More information about Fluidsynth at 98 | # https://github.com/FluidSynth/fluidsynth/wiki/UserManual 99 | 100 | function change_sf() { 101 | SF2=$1 102 | echo "load $SF2" > /dev/tcp/localhost/$FLUID_PORT 103 | } 104 | 105 | function change_gain() { 106 | GAIN=$1 107 | echo "gain $GAIN" > /dev/tcp/localhost/$FLUID_PORT 108 | } 109 | 110 | 111 | function request_sf2() { 112 | SF2=$SYNTH_ROOT/$2 113 | QUERY_ARG=$3 114 | 115 | # Fix up spaces to be prefixed with \ 116 | SF2=$(echo "$SF2" | sed 's/ /\\ /g') 117 | SF2=$(echo "$SF2" | sed 's/%20/\\ /g') 118 | change_sf "$SF2" 119 | 120 | FOLDER=$(get_query dir $QUERY_ARG) 121 | 122 | TOP="Loaded SF2, $SF2" 123 | 124 | ui_page_standard "$TOP" "$FOLDER" 125 | } 126 | 127 | function request_gain() { 128 | GAIN=$2 129 | FOLDER=$(get_query dir $3) 130 | 131 | change_gain $GAIN 132 | 133 | TOP="Gain set to $GAIN" 134 | 135 | ui_page_standard "$TOP" "$FOLDER" 136 | } 137 | 138 | function request_browse() { 139 | FOLDER=$(get_query dir $2) 140 | 141 | TOP="Browsing:"$FOLDER 142 | 143 | ui_page_standard "$TOP" "$FOLDER" 144 | } 145 | 146 | function request_sys_halt() { 147 | /sbin/halt -p 148 | serve_static_string "Halting..." 149 | } 150 | 151 | function request_sys_reboot() { 152 | /sbin/reboot 153 | serve_static_string "Rebooting..." 154 | } 155 | 156 | on_uri_match '^/sf2/([^?]*)(\?.*)?$' request_sf2 157 | on_uri_match '^/browse(\?.*)?$' request_browse 158 | on_uri_match '^/gain/([^?]*)(\?.*)?$' request_gain 159 | on_uri_match '^/sys/halt$' request_sys_halt 160 | on_uri_match '^/sys/reboot$' request_sys_reboot 161 | 162 | 163 | unconditionally add_response_header "Content-Type" "text/html"; request_browse "" "." ; 164 | 165 | 166 | -------------------------------------------------------------------------------- /server/bashttpd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A simple, configurable HTTP server written in bash. 4 | # 5 | # See LICENSE for licensing information. 6 | # 7 | # Original author: Avleen Vig, 2012 8 | # Reworked by: Josh Cartwright, 2012 9 | 10 | warn() { echo "WARNING: $@" >&2; } 11 | 12 | [ -r bashttpd.conf ] || { 13 | cat >bashttpd.conf <<'EOF' 14 | # 15 | # bashttpd.conf - configuration for bashttpd 16 | # 17 | # The behavior of bashttpd is dictated by the evaluation 18 | # of rules specified in this configuration file. Each rule 19 | # is evaluated until one is matched. If no rule is matched, 20 | # bashttpd will serve a 500 Internal Server Error. 21 | # 22 | # The format of the rules are: 23 | # on_uri_match REGEX command [args] 24 | # unconditionally command [args] 25 | # 26 | # on_uri_match: 27 | # On an incoming request, the URI is checked against the specified 28 | # (bash-supported extended) regular expression, and if encounters a match the 29 | # specified command is executed with the specified arguments. 30 | # 31 | # For additional flexibility, on_uri_match will also pass the results of the 32 | # regular expression match, ${BASH_REMATCH[@]} as additional arguments to the 33 | # command. 34 | # 35 | # unconditionally: 36 | # Always serve via the specified command. Useful for catchall rules. 37 | # 38 | # The following commands are available for use: 39 | # 40 | # serve_file FILE 41 | # Statically serves a single file. 42 | # 43 | # serve_dir_with_tree DIRECTORY 44 | # Statically serves the specified directory using 'tree'. It must be 45 | # installed and in the PATH. 46 | # 47 | # serve_dir_with_ls DIRECTORY 48 | # Statically serves the specified directory using 'ls -al'. 49 | # 50 | # serve_dir DIRECTORY 51 | # Statically serves a single directory listing. Will use 'tree' if it is 52 | # installed and in the PATH, otherwise, 'ls -al' 53 | # 54 | # serve_dir_or_file_from DIRECTORY 55 | # Serves either a directory listing (using serve_dir) or a file (using 56 | # serve_file). Constructs local path by appending the specified root 57 | # directory, and the URI portion of the client request. 58 | # 59 | # serve_static_string STRING 60 | # Serves the specified static string with Content-Type text/plain. 61 | # 62 | # Examples of rules: 63 | # 64 | # on_uri_match '^/issue$' serve_file "/etc/issue" 65 | # 66 | # When a client's requested URI matches the string '/issue', serve them the 67 | # contents of /etc/issue 68 | # 69 | # on_uri_match 'root' serve_dir / 70 | # 71 | # When a client's requested URI has the word 'root' in it, serve up 72 | # a directory listing of / 73 | # 74 | # DOCROOT=/var/www/html 75 | # on_uri_match '/(.*)' serve_dir_or_file_from "$DOCROOT" 76 | # When any URI request is made, attempt to serve a directory listing 77 | # or file content based on the request URI, by mapping URI's to local 78 | # paths relative to the specified "$DOCROOT" 79 | # 80 | 81 | unconditionally serve_static_string 'Hello, world! You can configure bashttpd by modifying bashttpd.conf.' 82 | 83 | # More about commands: 84 | # 85 | # It is possible to somewhat easily write your own commands. An example 86 | # may help. The following example will serve "Hello, $x!" whenever 87 | # a client sends a request with the URI /say_hello_to/$x: 88 | # 89 | # serve_hello() { 90 | # add_response_header "Content-Type" "text/plain" 91 | # send_response_ok_exit <<< "Hello, $2!" 92 | # } 93 | # on_uri_match '^/say_hello_to/(.*)$' serve_hello 94 | # 95 | # Like mentioned before, the contents of ${BASH_REMATCH[@]} are passed 96 | # to your command, so its possible to use regular expression groups 97 | # to pull out info. 98 | # 99 | # With this example, when the requested URI is /say_hello_to/Josh, serve_hello 100 | # is invoked with the arguments '/say_hello_to/Josh' 'Josh', 101 | # (${BASH_REMATCH[0]} is always the full match) 102 | EOF 103 | warn "Created bashttpd.conf using defaults. Please review it/configure before running bashttpd again." 104 | exit 1 105 | } 106 | 107 | recv() { echo "< $@" >&2; } 108 | send() { echo "> $@" >&2; 109 | printf '%s\r\n' "$*"; } 110 | 111 | [[ $UID = 0 ]] && warn "It is not recommended to run bashttpd as root." 112 | 113 | DATE=$(date +"%a, %d %b %Y %H:%M:%S %Z") 114 | declare -a RESPONSE_HEADERS=( 115 | "Date: $DATE" 116 | "Expires: $DATE" 117 | "Server: Slash Bin Slash Bash" 118 | ) 119 | 120 | add_response_header() { 121 | RESPONSE_HEADERS+=("$1: $2") 122 | } 123 | 124 | declare -a HTTP_RESPONSE=( 125 | [200]="OK" 126 | [400]="Bad Request" 127 | [403]="Forbidden" 128 | [404]="Not Found" 129 | [405]="Method Not Allowed" 130 | [500]="Internal Server Error" 131 | ) 132 | 133 | send_response() { 134 | local code=$1 135 | send "HTTP/1.0 $1 ${HTTP_RESPONSE[$1]}" 136 | for i in "${RESPONSE_HEADERS[@]}"; do 137 | send "$i" 138 | done 139 | send 140 | while read -r line; do 141 | send "$line" 142 | done 143 | } 144 | 145 | send_response_ok_exit() { send_response 200; exit 0; } 146 | 147 | fail_with() { 148 | send_response "$1" <<< "$1 ${HTTP_RESPONSE[$1]}" 149 | exit 1 150 | } 151 | 152 | serve_file() { 153 | local file=$1 154 | 155 | CONTENT_TYPE= 156 | case "$file" in 157 | *\.css) 158 | CONTENT_TYPE="text/css" 159 | ;; 160 | *\.js) 161 | CONTENT_TYPE="text/javascript" 162 | ;; 163 | *) 164 | read -r CONTENT_TYPE < <(file -b --mime-type "$file") 165 | ;; 166 | esac 167 | 168 | add_response_header "Content-Type" "$CONTENT_TYPE"; 169 | 170 | read -r CONTENT_LENGTH < <(stat -c'%s' "$file") && \ 171 | add_response_header "Content-Length" "$CONTENT_LENGTH" 172 | 173 | send_response_ok_exit < "$file" 174 | } 175 | 176 | serve_dir_with_tree() 177 | { 178 | local dir="$1" tree_vers tree_opts basehref x 179 | 180 | add_response_header "Content-Type" "text/html" 181 | 182 | # The --du option was added in 1.6.0. 183 | read x tree_vers x < <(tree --version) 184 | [[ $tree_vers == v1.6* ]] && tree_opts="--du" 185 | 186 | send_response_ok_exit < \ 187 | <(tree -H "$2" -L 1 "$tree_opts" -D "$dir") 188 | } 189 | 190 | serve_dir_with_ls() 191 | { 192 | local dir=$1 193 | 194 | add_response_header "Content-Type" "text/plain" 195 | 196 | send_response_ok_exit < \ 197 | <(ls -la "$dir") 198 | } 199 | 200 | serve_dir() { 201 | local dir=$1 202 | 203 | # If `tree` is installed, use that for pretty output. 204 | which tree &>/dev/null && \ 205 | serve_dir_with_tree "$@" 206 | 207 | serve_dir_with_ls "$@" 208 | 209 | fail_with 500 210 | } 211 | 212 | serve_dir_or_file_from() { 213 | local URL_PATH=$1/$3 214 | shift 215 | 216 | # sanitize URL_PATH 217 | URL_PATH=${URL_PATH//[^a-zA-Z0-9_~\-\.\/]/} 218 | [[ $URL_PATH == *..* ]] && fail_with 400 219 | 220 | # Serve index file if exists in requested directory 221 | [[ -d $URL_PATH && -f $URL_PATH/index.html && -r $URL_PATH/index.html ]] && \ 222 | URL_PATH="$URL_PATH/index.html" 223 | 224 | if [[ -f $URL_PATH ]]; then 225 | [[ -r $URL_PATH ]] && \ 226 | serve_file "$URL_PATH" "$@" || fail_with 403 227 | elif [[ -d $URL_PATH ]]; then 228 | [[ -x $URL_PATH ]] && \ 229 | serve_dir "$URL_PATH" "$@" || fail_with 403 230 | fi 231 | 232 | fail_with 404 233 | } 234 | 235 | serve_static_string() { 236 | add_response_header "Content-Type" "text/plain" 237 | send_response_ok_exit <<< "$1" 238 | } 239 | 240 | on_uri_match() { 241 | local regex=$1 242 | shift 243 | 244 | [[ $REQUEST_URI =~ $regex ]] && \ 245 | "$@" "${BASH_REMATCH[@]}" 246 | } 247 | 248 | unconditionally() { 249 | "$@" "$REQUEST_URI" 250 | } 251 | 252 | # Request-Line HTTP RFC 2616 $5.1 253 | read -r line || fail_with 400 254 | 255 | # strip trailing CR if it exists 256 | line=${line%%$'\r'} 257 | recv "$line" 258 | 259 | read -r REQUEST_METHOD REQUEST_URI REQUEST_HTTP_VERSION <<<"$line" 260 | 261 | [ -n "$REQUEST_METHOD" ] && \ 262 | [ -n "$REQUEST_URI" ] && \ 263 | [ -n "$REQUEST_HTTP_VERSION" ] \ 264 | || fail_with 400 265 | 266 | # Only GET is supported at this time 267 | [ "$REQUEST_METHOD" = "GET" ] || fail_with 405 268 | 269 | declare -a REQUEST_HEADERS 270 | 271 | while read -r line; do 272 | line=${line%%$'\r'} 273 | recv "$line" 274 | 275 | # If we've reached the end of the headers, break. 276 | [ -z "$line" ] && break 277 | 278 | REQUEST_HEADERS+=("$line") 279 | done 280 | 281 | source "${BASH_SOURCE[0]%/*}"/bashttpd.conf 282 | fail_with 500 283 | --------------------------------------------------------------------------------