Questioning Everything Propaganda

Home Tags
Login RSS
ply.sh
You are viewing an old revision from 2025-10-29 14:43.
View the current, live version.

#!/bin/bash

#####################################################################

Hand Is Radio - Optimized Player & Streamer

This script includes dynamic track announcements.

Dependencies:

- Local/Stream: festival, lame, ffmpeg (provides ffprobe)

- Local Playback: mpg123

Install dependencies on Raspbian/Debian:

sudo apt update

sudo apt install mpg123 festival lame ffmpeg

Recommends at least a Pi2B preferably a Pi3B. Bottleneck issues tested on a Pi1B.

#####################################################################

--- Configuration ---

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 Defaults (override with command-line flags) ---

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"

--- Stream/Pipe Configuration ---

AUDIO_PIPE="radio.pipe" AUDIO_FORMAT="s16le" # 16-bit signed little-endian PCM AUDIO_RATE="44100" # 44.1kHz Sample Rate AUDIO_CHANNELS="2" # Stereo

--- Function Definitions ---

Print usage instructions

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 for required command-line tools

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 the jingle MP3 file (as stereo)

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 a temporary MP3 file (as stereo)

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 the single, persistent ffmpeg process (the READER)

This new version runs in a loop to auto-reconnect if it dies.

start_streamer() {

Run this whole function in the background

(
    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 a simple local-only player (the READER)

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 }

--- THIS IS THE NEW WRITER PROCESS ---

This entire function will be run in a subshell with its

stdout redirected to the pipe.

run_radio_loop() {

This loop runs forever, writing audio to stdout

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 Function (runs on Ctrl+C) ---

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 Ctrl+C (SIGINT) and script exit (SIGTERM)

trap cleanup SIGINT SIGTERM

--- Main Script ---

Parse command-line arguments

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

Validate streaming options

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 dependencies

check_deps

Generate jingle (if needed)

generate_jingle

Create the named pipe

if [ -e "$AUDIO_PIPE" ]; then rm -f "$AUDIO_PIPE" fi mkfifo "$AUDIO_PIPE"

Start the READER process (ffmpeg or aplay)

if [ "$STREAM_TO_ICECAST" = true ]; then start_streamer else start_local_player fi

Start the WRITER process (the main radio loop)

The ( ... ) runs it in a subshell

The > "$AUDIO_PIPE" redirects its stdout to the pipe

The & runs it in the background

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

Keep the main script alive to wait for the cleanup trap

wait