Questioning Everything Propaganda

Home Tags
Login RSS
ply.sh
You are viewing an old revision from 2025-10-29 14:33.
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 <host>      Icecast host/IP (e.g., 192.168.1.10)."
    echo "  -p <port>      Icecast port (default: 8000)."
    echo "  -P <password>  Icecast source password."
    echo "  -m <mount>     Icecast mount point (default: stream.mp3)."
    echo "  -b <bitrate>   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