#####################################################################
#####################################################################
MUSIC_DIR="./music" JINGLE_TEXT="This is Hand Is Radio, your source for unique music. This month is music by .... Listen on eighty-eight point zero F.M. and online at hand is radio dot com." PREGEN_JINGLE_FILE="jingle.mp3"
ICECAST_HOST="" ICECAST_PORT="" ICECAST_PASS="" ICECAST_MOUNT="stream" BITRATE="126k" # Default bitrate for Icecast STREAM_TO_ICECAST=false ICE_NAME="Hand Is Radio" ICE_DESC="Unique Music"
AUDIO_PIPE="radio.pipe" AUDIO_FORMAT="s16le" # 16-bit signed little-endian PCM AUDIO_RATE="44100" # 44.1kHz Sample Rate AUDIO_CHANNELS="2" # Stereo
usage() { echo "Usage: $0 [options]" echo echo "Options:" echo " -s Enable streaming to Icecast (requires -H and -P)." echo " -H Icecast host/IP (e.g., 192.168.1.10)." echo " -p Icecast port (default: 8000)." echo " -P Icecast source password." echo " -m Icecast mount point (default: stream.mp3)." echo " -b Streaming bitrate (default: 32k, e.g., 64k, 128k)." echo " -h Show this help message." }
check_deps() { local missing=0 local cmds=("find" "shuf" "mktemp" "festival" "text2wave" "lame" "ffprobe" "mpg123")
if [ "$STREAM_TO_ICECAST" = true ]; then
cmds+=("ffmpeg")
else
cmds+=("aplay")
fi
for cmd in "${cmds[@]}"; do
if ! command -v $cmd &> /dev/null; then
echo "ERROR: Required command '$cmd' not found." >&2
missing=1
fi
done
if [ $missing -eq 1 ]; then
echo "Please install dependencies and try again." >&2
exit 1
fi
}
generate_jingle() { if [ ! -f "$PREGEN_JINGLE_FILE" ]; then echo "---" >&2 echo "One-time setup: Generating jingle file '$PREGEN_JINGLE_FILE'..." >&2
local temp_wav
temp_wav=$(mktemp --suffix=.wav)
echo "$JINGLE_TEXT" | text2wave -o "$temp_wav"
ffmpeg -i "$temp_wav" -ac 2 -q:a 9 -ar "$AUDIO_RATE" "$PREGEN_JINGLE_FILE" -loglevel error
rm "$temp_wav"
if [ ! -f "$PREGEN_JINGLE_FILE" ]; then
echo "ERROR: Failed to create jingle file. Exiting." >&2
exit 1
fi
echo "Jingle generation complete." >&2
echo "---" >&2
fi
}
generate_tts_mp3() { local text="$1" local output_file="$2"
local temp_wav
temp_wav=$(mktemp --suffix=.wav)
echo "$text" | text2wave -o "$temp_wav"
ffmpeg -i "$temp_wav" -ac 2 -q:a 9 -ar "$AUDIO_RATE" "$output_file" -loglevel error
rm "$temp_wav"
}
start_streamer() {
(
while true; do
echo "STREAMER: (Re)starting ffmpeg process..." >&2
ffmpeg -f "$AUDIO_FORMAT" -ar "$AUDIO_RATE" -ac "$AUDIO_CHANNELS" -i "$AUDIO_PIPE" \
-filter_complex "[0:a]asplit=2[local][ice]" \
\
-map "[local]" \
-f alsa "plughw:0" \
\
-map "[ice]" \
-acodec libmp3lame -ab "$BITRATE" -ar "$AUDIO_RATE" -ac "$AUDIO_CHANNELS" \
-content_type "audio/mpeg" \
-ice_name "$ICE_NAME" \
-ice_description "$ICE_DESC" \
-ice_public 1 \
-f mp3 "icecast://source:${ICECAST_PASS}@${ICECAST_HOST}:${ICECAST_PORT}/${ICECAST_MOUNT}" \
-loglevel warning
echo "STREAMER: ffmpeg process died. Restarting in 5 seconds..." >&2
sleep 5
done
) &
FFMPEG_PID=$!
echo "Streamer reconnect loop started with PID $FFMPEG_PID." >&2
}
start_local_player() { echo "Starting persistent local player (aplay)..." >&2 aplay -f "$AUDIO_FORMAT" -r "$AUDIO_RATE" -c "$AUDIO_CHANNELS" < "$AUDIO_PIPE" & PLAYER_PID=$! echo "aplay process started with PID $PLAYER_PID." >&2 }
run_radio_loop() {
while true; do
# --- 1. Generate Playlist ---
echo "WRITER: Scanning '$MUSIC_DIR' and shuffling playlist..." >&2
if [ ! -d "$MUSIC_DIR" ]; then
echo "WRITER ERROR: Directory $MUSIC_DIR does not exist." >&2
sleep 10 # Wait before trying again
continue
fi
mapfile -t PLAYLIST < <(find "$MUSIC_DIR" -type f -iname "*.mp3" | shuf)
if [ ${#PLAYLIST[@]} -eq 0 ]; then
echo "WRITER ERROR: No .mp3 files found in $MUSIC_DIR." >&2
sleep 10 # Wait before trying again
continue
fi
echo "WRITER: Playlist generated with ${#PLAYLIST[@]} tracks." >&2
counter=0
# --- 2. Loop Through Playlist ---
for file in "${PLAYLIST[@]}"; do
((counter++))
# --- Play Jingle ---
if (( counter % 4 == 0 )); then
echo "[JINGLE] Playing jingle..." >&2
mpg123 -s -r "$AUDIO_RATE" -c "$AUDIO_CHANNELS" -e "s16" "$PREGEN_JINGLE_FILE" 2>/dev/null
fi
# --- Generate Announcement ---
title=$(ffprobe -v quiet -show_entries format_tags=title -of default:noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
artist=$(ffprobe -v quiet -show_entries format_tags=artist -of default:noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
if [ -z "$title" ]; then
title=$(basename "$file" .mp3 | sed 's/_/ /g')
fi
if [ -z "$artist" ]; then
artist="an unknown artist"
fi
announce_text="Next up is $title by $artist."
# --- Create & Play Announcement ---
ANNOUNCE_MP3="/tmp/announce_$(date +%s).mp3"
echo "[ANNOUNCE] Generating: $announce_text" >&2
generate_tts_mp3 "$announce_text" "$ANNOUNCE_MP3"
mpg123 -s -r "$AUDIO_RATE" -c "$AUDIO_CHANNELS" -e "s16" "$ANNOUNCE_MP3" 2>/dev/null
rm -f "$ANNOUNCE_MP3"
# --- Play Song ---
echo "[PLAYING] $(basename "$file")" >&2
mpg123 -s -r "$AUDIO_RATE" -c "$AUDIO_CHANNELS" -e "s16" "$file" 2>/dev/null
done
echo "WRITER: Playlist finished. Re-shuffling..." >&2
done
}
cleanup() { echo "\nCaught exit signal. Shutting down..."
# Kill all background processes associated with this script
# This is more robust than just killing PIDs
pkill -P $$
echo "Removing audio pipe '$AUDIO_PIPE'..."
rm -f "$AUDIO_PIPE"
echo "Cleaning up any temporary announcement files..."
rm -f /tmp/announce_*.mp3
echo "Shutdown complete."
exit 0
}
trap cleanup SIGINT SIGTERM
while getopts "sH:p:P:m:b:h" opt; do case $opt in s) STREAM_TO_ICECAST=true ;; H) ICECAST_HOST="$OPTARG" ;; p) ICECAST_PORT="$OPTARG" ;; P) ICECAST_PASS="$OPTARG" ;; m) ICECAST_MOUNT="$OPTARG" ;; b) BITRATE="$OPTARG" ;; h) usage; exit 0 ;; *) usage; exit 1 ;; esac done
if [ "$STREAM_TO_ICECAST" = true ]; then if [ -z "$ICECAST_HOST" ] || [ -z "$ICECAST_PASS" ]; then echo "ERROR: When streaming (-s), you must provide a host (-H) and password (-P)." >&2 usage exit 1 fi echo "--- Icecast Streaming + Local Playback Enabled ---" >&2 else echo "--- Local Playback Only Mode ---" >&2 fi
check_deps
generate_jingle
if [ -e "$AUDIO_PIPE" ]; then rm -f "$AUDIO_PIPE" fi mkfifo "$AUDIO_PIPE"
if [ "$STREAM_TO_ICECAST" = true ]; then start_streamer else start_local_player fi
echo "Starting persistent radio writer loop..." >&2 ( run_radio_loop ) > "$AUDIO_PIPE" & RADIO_WRITER_PID=$! echo "Writer process started with PID $RADIO_WRITER_PID." >&2
echo "--- Radio is LIVE. Press Ctrl+C to stop. ---" >&2
wait