├── 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 |
--------------------------------------------------------------------------------