diff options
-rw-r--r-- | COPYING | 7 | ||||
-rw-r--r-- | Makefile | 41 | ||||
-rwxr-xr-x | radish | 298 | ||||
-rw-r--r-- | radish.stations | 15 |
4 files changed, 361 insertions, 0 deletions
diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..4d13a5b --- /dev/null +++ b/COPYING | |||
@@ -0,0 +1,7 @@ | |||
1 | Copyright (C) 2022 Case Duckworth <acdw@acdw.net> | ||
2 | |||
3 | Usage of the works is permitted provided that this instrument is retained with | ||
4 | the works, so that any entity that uses the works is notified of this | ||
5 | instrument. | ||
6 | |||
7 | DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. | ||
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2b2656d --- /dev/null +++ b/Makefile | |||
@@ -0,0 +1,41 @@ | |||
1 | # Radish | ||
2 | |||
3 | DESTDIR = | ||
4 | PREFIX = /usr/local | ||
5 | |||
6 | BIN = $(DESTDIR)$(PREFIX)/bin | ||
7 | RADISH_SHARE = $(DESTDIR)$(PREFIX)/share/radish | ||
8 | |||
9 | RADISH_BIN = $(BIN)/radish | ||
10 | RADISH_STATIONS = $(RADISH_SHARE)/stations | ||
11 | |||
12 | .PHONY: help | ||
13 | help: | ||
14 | @echo "RADISH : Play online radio" | ||
15 | @echo "(C) 2022 Case Duckworth <acdw@acdw.net>" | ||
16 | @echo "Licensed under the Fair License; see COPYING for details." | ||
17 | @echo | ||
18 | @echo "TARGETS:" | ||
19 | @echo " install Install radish to $(DESTDIR)$(PREFIX)/bin/radish." | ||
20 | @echo " An example configuration is at $(DESTDIR)$(PREFIX)/share/radish/stations." | ||
21 | @echo " link Install radish using symlinks." | ||
22 | @echo " Probably only useful for development." | ||
23 | @echo " uninstall Uninstall radish-related files." | ||
24 | |||
25 | $(BIN) $(RADISH_SHARE): | ||
26 | mkdir -p $@ | ||
27 | |||
28 | .PHONY: install | ||
29 | install: radish radish.stations $(BIN) $(RADISH_SHARE) | ||
30 | install -D radish $(RADISH_BIN) | ||
31 | install -D radish.stations $(RADISH_STATIONS) | ||
32 | |||
33 | .PHONY: link | ||
34 | link: $(BIN) $(RADISH_SHARE) | ||
35 | ln -sf $(PWD)/radish $(RADISH_BIN) | ||
36 | ln -sf $(PWD)/radish.stations $(RADISH_STATIONS) | ||
37 | |||
38 | .PHONY: uninstall | ||
39 | uninstall: | ||
40 | rm $(RADISH_BIN) | ||
41 | rm -r $(RADISH_SHARE) | ||
diff --git a/radish b/radish new file mode 100755 index 0000000..d8c55b1 --- /dev/null +++ b/radish | |||
@@ -0,0 +1,298 @@ | |||
1 | #!/bin/sh | ||
2 | # RADISH | ||
3 | # a new and improved RADIO that can play local files and noise. | ||
4 | # XXX: WIP | ||
5 | |||
6 | usage() { | ||
7 | cat <<EOF | ||
8 | RADISH: radio, music, static | ||
9 | USAGE: radish [-h|-k|-r|-s|-S] | ||
10 | radish -l [NAME] | ||
11 | radish [STATION] | ||
12 | |||
13 | FLAGS: | ||
14 | -h Show this help and exit. | ||
15 | -s Show radish's status and exit. | ||
16 | -S Show radish's status indefinitely. | ||
17 | -k Kill the currently-playing radish invocation. | ||
18 | -r Replay most recently-played station. | ||
19 | |||
20 | OPTIONS: | ||
21 | -l [NAME] List available stations. | ||
22 | If NAME is given, narrow the list to those matching it. | ||
23 | |||
24 | PARAMETERS: | ||
25 | STATION Which configured station to play. | ||
26 | Stations are defined in the \$RADISH_STATION_FILE | ||
27 | (default: $RADISH_STATION_FILE). | ||
28 | If STATION is not present, or if it matches | ||
29 | more than one station, radish will present a menu. | ||
30 | EOF | ||
31 | exit ${1:-0} | ||
32 | } | ||
33 | |||
34 | config() { | ||
35 | : "${RADISH_STATION_FILE:=${XDG_CONFIG_HOME:-$HOME/.config}/radish/stations}" | ||
36 | RADISH_PID_FILE=/tmp/radish.pid | ||
37 | RADISH_STATUS_FILE=/tmp/radish.status | ||
38 | RADISH_LP_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/radish.lp" | ||
39 | } | ||
40 | |||
41 | main() { | ||
42 | config | ||
43 | while getopts :hkrsSl: opt; do | ||
44 | case "$opt" in | ||
45 | h) usage ;; | ||
46 | k) radish_kill ;; | ||
47 | r) set -- "$(cat "$RADISH_LP_FILE")" ;; | ||
48 | s) radish_status ;; | ||
49 | S) radish_status -follow ;; | ||
50 | l) radish_list "$OPTARG" ;; | ||
51 | :) | ||
52 | case "$OPTARG" in | ||
53 | l) radish_list ;; | ||
54 | *) | ||
55 | echo >&2 "Option -$OPTARG requires an argument" | ||
56 | usage 1 | ||
57 | ;; | ||
58 | esac | ||
59 | ;; | ||
60 | \?) | ||
61 | echo >&2 "Unknown option: -$OPTARG" | ||
62 | usage 1 | ||
63 | ;; | ||
64 | esac | ||
65 | done | ||
66 | shift "$((OPTIND - 1))" | ||
67 | radish_play "$@" | ||
68 | } | ||
69 | |||
70 | cleanup() { | ||
71 | rm /tmp/radish.* | ||
72 | } | ||
73 | |||
74 | ### "Private" functions | ||
75 | |||
76 | _radish_player() { | ||
77 | command -v "${1:-}" || | ||
78 | command -v "${RADISH_PLAYER:-}" || | ||
79 | command -v mpv || | ||
80 | command -v vlc || | ||
81 | { | ||
82 | echo >&2 "No suitable player found." | ||
83 | echo >&2 "Set \$RADISH_PLAYER." | ||
84 | exit 3 | ||
85 | } | ||
86 | } | ||
87 | |||
88 | _radish_stations() { | ||
89 | if [ -f "$RADISH_STATION_FILE" ]; then | ||
90 | sed -e '/^#/d' "$RADISH_STATION_FILE" | ||
91 | else | ||
92 | echo noise: | ||
93 | fi | ||
94 | } | ||
95 | |||
96 | _radish_kill_impl() { | ||
97 | [ -f "$RADISH_PID_FILE" ] || return 1 | ||
98 | x= | ||
99 | while xargs -a "$RADISH_PID_FILE" kill 2>/dev/null; do | ||
100 | case "$x" in | ||
101 | xxx*) | ||
102 | printf . | ||
103 | x= | ||
104 | ;; | ||
105 | *) x=x$x ;; | ||
106 | esac | ||
107 | done | ||
108 | echo | ||
109 | } | ||
110 | |||
111 | _radish_desc() { | ||
112 | cut -f2 "${1:--}" 2>/dev/null | ||
113 | } | ||
114 | _radish_url() { | ||
115 | cut -f1 "${1:--}" 2>/dev/null | ||
116 | } | ||
117 | _radish_tags() { | ||
118 | cut -f3 "${1:--}" 2>/dev/null | ||
119 | } | ||
120 | |||
121 | echo() { printf '%s\n' "$*"; } | ||
122 | |||
123 | ### Main functionality | ||
124 | |||
125 | radish_kill() { | ||
126 | printf >&2 '%s' "Killing radish..." | ||
127 | _radish_kill_impl || { | ||
128 | echo >&2 "I don't think radish is running." | ||
129 | exit 2 | ||
130 | } | ||
131 | cleanup | ||
132 | exit | ||
133 | } | ||
134 | |||
135 | radish_status() { | ||
136 | trap 'echo;exit 0' INT | ||
137 | if [ -n "${1:-}" ]; then | ||
138 | follow=-f | ||
139 | else | ||
140 | follow= | ||
141 | fi | ||
142 | tail $follow "$RADISH_STATUS_FILE" | ||
143 | echo | ||
144 | exit | ||
145 | } | ||
146 | |||
147 | radish_list() { | ||
148 | _radish_stations | grep -i "${1:-}" | awk -F '\t' '{ | ||
149 | desc = $'$_schema_desc' | ||
150 | url = $'$_schema_url' | ||
151 | tags = $'$_schema_tags' | ||
152 | printf "%-23s |", substr(desc,1,20) (length(desc)>20?"...":"") | ||
153 | printf "%-23s |", substr(tags,1,20) (length(tags)>20?"...":"") | ||
154 | printf "%-23s\n", substr(url,1,20) (length(url)>20?"...":"") | ||
155 | }' | ||
156 | exit | ||
157 | } | ||
158 | |||
159 | radish_choose_station() { | ||
160 | cands="$(_radish_stations | grep -i "$1")" | ||
161 | [ -z "$cands" ] && cands="$(_radish_stations)" | ||
162 | |||
163 | if [ "$(echo "$cands" | wc -l)" -gt 1 ]; then | ||
164 | echo "$cands" | _radish_desc | cat -n - | ||
165 | while true; do | ||
166 | printf "Radish> " | ||
167 | read station | ||
168 | if (echo "$station" | grep -qE '^[0-9]+$'); then | ||
169 | station="$(echo "$cands" | sed -n "${station}p;${station}q")" | ||
170 | if [ -z "$station" ]; then | ||
171 | echo >&2 "No station with that number." | ||
172 | continue | ||
173 | else | ||
174 | break | ||
175 | fi | ||
176 | fi | ||
177 | echo >&2 "Please input a station number." | ||
178 | done | ||
179 | else | ||
180 | station="$(echo "$cands")" | ||
181 | fi | ||
182 | echo >&2 "Selected: $(echo "$station" | _radish_desc)" | ||
183 | station="$(echo "$station" | _radish_url)" | ||
184 | } | ||
185 | |||
186 | ### Player functions | ||
187 | |||
188 | radish_play() { | ||
189 | case "${1:-}" in | ||
190 | https:* | http:*) | ||
191 | _radish_kill_impl || : | ||
192 | echo >&2 "Streaming $1..." | ||
193 | radish_play_web "$1" | ||
194 | ;; | ||
195 | noise:*) | ||
196 | _radish_kill_impl || : | ||
197 | echo >&2 "Playing noise: ${1#noise:}" | ||
198 | radish_play_noise "${1:-}" | ||
199 | ;; | ||
200 | shuf:*) | ||
201 | _radish_kill_impl || : | ||
202 | echo >&2 "Shuffling from ${1#shuf:}" | ||
203 | SHUF=1 radish_play_file "$1" 2>&1 | ||
204 | ;; | ||
205 | file:*) | ||
206 | _radish_kill_impl || : | ||
207 | echo >&2 "Playing from ${1#file:}" | ||
208 | SHUF=0 radish_play_file "$1" 2>&1 | ||
209 | ;; | ||
210 | *) | ||
211 | if [ -f "$RADISH_STATION_FILE" ]; then | ||
212 | radish_choose_station "${1:-}" | ||
213 | _radish_kill_impl || : | ||
214 | radish_play "$station" | ||
215 | else | ||
216 | _radish_kill_impl || : | ||
217 | radish_play_noise | ||
218 | fi | ||
219 | ;; | ||
220 | esac | ||
221 | |||
222 | echo "${1:-}" | _radish_url >"$RADISH_LP_FILE" | ||
223 | exit | ||
224 | } | ||
225 | |||
226 | radish_play_web() { | ||
227 | "$(_radish_player)" "$1" >"$RADISH_STATUS_FILE" 2>&1 & | ||
228 | echo $! >"$RADISH_PID_FILE" | ||
229 | } | ||
230 | |||
231 | radish_play_file() { | ||
232 | dir="$(echo "$1" | sed 's@^[^:]*:\(//\)\?@@')" | ||
233 | find "$dir" -depth -print0 \ | ||
234 | -iname '*flac' -o \ | ||
235 | -iname '*mp3' -o \ | ||
236 | -iname '*ogg' -o \ | ||
237 | -iname '*opus' | | ||
238 | xargs -0 realpath | | ||
239 | case "$SHUF" in | ||
240 | 1) shuf ;; | ||
241 | 0) sort ;; | ||
242 | esac >/tmp/radish.m3u | ||
243 | player="$(_radish_player)" | ||
244 | case "$player" in | ||
245 | *vlc | *mpv) args="--no-video" ;; | ||
246 | *) args= ;; | ||
247 | esac | ||
248 | "$player" $args /tmp/radish.m3u "$1" >"$RADISH_STATUS_FILE" 2>&1 & | ||
249 | echo $! >"$RADISH_PID_FILE" | ||
250 | } | ||
251 | |||
252 | radish_play_noise() { | ||
253 | # REQUIRES PLAY (from SOX) -- based on https://gist.github.com/rsvp/1209835 | ||
254 | play="$(command -v play)" || { | ||
255 | echo >&2 "Noise playback requires sox(1)." | ||
256 | exit 3 | ||
257 | } | ||
258 | # URL format: noise:type/center?time=60;wave=0.033;volume=1 | ||
259 | url_format='noise:\([^/]\+\)/\([^?]\+\)?\(.*\)' | ||
260 | noise_type="$(echo "${1:-}" | sed -n "s@${url_format}@\1@")" | ||
261 | noise_center="$(echo "${1:-}" | sed -n "s@${url_format}@\2@")" | ||
262 | noise_params="$(echo "${1:-}" | sed -n "s@${url_format}@\3@")" | ||
263 | time=60 | ||
264 | wave=0.03333333 | ||
265 | volume=1 | ||
266 | if [ -z "$noise_type" ] && [ -z "$noise_center" ]; then | ||
267 | noise_type=brown | ||
268 | noise_center=1786 | ||
269 | elif [ -z "$noise_type" ] || [ -z "$noise_center" ]; then | ||
270 | echo >&2 "URL format: 'noise:TYPE/CENTER?[PARAMS]" | ||
271 | echo >&2 "TYPE is one of 'brown','white','pink','tpdf'." | ||
272 | echo >&2 "CENTER is the center of the band-pass filter." | ||
273 | echo >&2 "PARAMS are TIME to generate noise;" | ||
274 | echo >&2 "WAVE, denoting volume variation; and VOLUME." | ||
275 | exit 4 | ||
276 | else | ||
277 | eval "$noise_params" | ||
278 | fi | ||
279 | |||
280 | export RADISH_PLAYER=play | ||
281 | echo >&2 "$(_radish_player)" \ | ||
282 | -c 2 --null synth "$time" "${noise_type}noise" \ | ||
283 | band -n "$noise_center" 499 \ | ||
284 | tremolo "$wave" 43 reverb 19 \ | ||
285 | bass -11 treble -1 \ | ||
286 | vol 14dB vol "$volume" \ | ||
287 | repeat "$(expr $time - 1)" | ||
288 | "$(_radish_player)" \ | ||
289 | -c 2 --null synth "$time" "${noise_type}noise" \ | ||
290 | band -n "$noise_center" 499 \ | ||
291 | tremolo "$wave" 43 reverb 19 \ | ||
292 | bass -11 treble -1 \ | ||
293 | vol 14dB vol "$volume" \ | ||
294 | repeat "$(expr $time - 1)" >"$RADISH_STATUS_FILE" 2>&1 & | ||
295 | echo $! >"$RADISH_PID_FILE" | ||
296 | } | ||
297 | |||
298 | main "$@" | ||
diff --git a/radish.stations b/radish.stations new file mode 100644 index 0000000..7ff60ef --- /dev/null +++ b/radish.stations | |||
@@ -0,0 +1,15 @@ | |||
1 | URL Description Tags | ||
2 | https://somafm.com/synphaera256.pls Synphaera soma | ||
3 | https://somafm.com/bagel.pls BAGel Radio soma | ||
4 | https://somafm.com/bootliquor320.pls Boot Liquor soma | ||
5 | https://somafm.com/deepspaceone.pls Deep Space One soma | ||
6 | https://somafm.com/fluid.pls Fluid soma | ||
7 | https://somafm.com/u80s256.pls Underground 80s soma | ||
8 | https://azuracast.tilderadio.org/radio/8000/radio.ogg tilderadio friends | ||
9 | https://s2.radio.co/s2b2b68744/listen BadRadio: 24/7 PHONK phonk | ||
10 | http://radio.plaza.one/opus Nightwave Plaza vaporwave | ||
11 | https://www.scenesat.com/listen/normal/mid.m3u SceneSat demoscene | ||
12 | http://nectarine.ers35.net:8000/necta192.mp3 Nectarine demoscene | ||
13 | https://kexp-mp3-128.streamguys1.com/kexp128.mp3 KEXP fm college | ||
14 | https://rainwave.cc/tune_in/5.ogg.m3u Rainwave chiptune videogame | ||
15 | noise: brown noise noise | ||