ASL3 callsigns instead of node numbers

I would like to know if anyone has successfully been able to run write_node_callsigns.sh with ASL3. I am legally blind and have been using this feature on one of my HamVoip nodes. Many thanks, Steve WB4IZC

#!/bin/bash
# N5LSN
# Make app_rpt telemetry use ASL callsigns instead of node numbers
# Intended for use on ASL3
# Directories and files
SRCDIR="/var/log/asterisk"
DESTDIR="/usr/share/asterisk/sounds/en/rpt/nodenames"
RPTSOUNDS="/usr/share/asterisk/sounds/en/rpt"
LETTERS="/usr/share/asterisk/sounds/en/letters"
NUMBERS="/usr/share/asterisk/sounds/en/digits"
PREV_DB="/tmp/previous_astdb.txt"
# Start time tracking
start_time=$(date +%s%3N)
# Usage instructions
usage() {
    cat << EOF
Usage: write_node_callsigns.sh options
OPTIONS:
   -h        Show this message
   -a        Process all nodes
   -i        Include node number with call
   -n node   Process a single node
   -d path   Specify destination directory (default: /usr/share/asterisk/sounds/en/rpt/nodenames)
   -v        Verbose output
   -f        Force run without user confirmation
Examples:
    ./write_node_callsigns.sh -a               # Process all nodes
    ./write_node_callsigns.sh -n 40000         # Process single node 40000
    ./write_node_callsigns.sh -f               # Force execution without confirmation
EOF
}
STRING=""
VERBOSE=""
INCNODE=""
FORCE_RUN=0
MAX_PREVIEW=10
# Create directories if missing
ensure_directory_exists() {
    if [ ! -d "$1" ]; then
        echo "Creating directory: $1"
        mkdir -p "$1" || { echo "Failed to create directory $1"; exit 1; }
    fi
}
# Find the correct audio file (.gsm or .ulaw)
find_audio_file() {
    basepath=$1
    if [ -f "${basepath}.gsm" ]; then
        echo "${basepath}.gsm"
    elif [ -f "${basepath}.ulaw" ]; then
        echo "${basepath}.ulaw"
    else
        echo ""
    fi
}
# Process each character in the callsign and form the audio filenames
make_call() {
    local foo=${1,,}
    STRING=""
    for (( i=0; i<${#foo}; i++ )); do
        local char=${foo:$i:1}
        case $char in
            [0-9]) FILENAME=$(find_audio_file "$NUMBERS/$char") ;;
            "/")   FILENAME=$(find_audio_file "$LETTERS/slash") ;;
            "-")   FILENAME=$(find_audio_file "$LETTERS/dash") ;;
            [a-z]) FILENAME=$(find_audio_file "$LETTERS/$char") ;;
        esac
        if [ -n "$FILENAME" ]; then
            STRING="$STRING $FILENAME"
        else
            echo "Error: Audio file for '$char' not found."
        fi
    done
}
# Handle .ulaw files with sox
process_file() {
    local file=$1
    if [[ "$file" == *.ulaw ]]; then
        echo "-t raw -e u-law -r 8000 -c 1 $file"
    else
        echo "$file"
    fi
}
# Concatenate the audio files into the final output and measure time
write_call() {
    local output_file="$DESTDIR/$f1.gsm"
    local start=$(date +%s%3N)  # Get start time in milliseconds
    # Generate processed file paths for sox
    local processed_files=""
    for file in $STRING; do
        processed_files="$processed_files $(process_file $file)"
    done
    # Execute sox to concatenate files (always overwrite)
    sox $processed_files $output_file
    local end=$(date +%s%3N)  # Get end time in milliseconds
    local duration=$((end - start))  # Calculate processing time
    # Output in the required format
    echo "[$(printf "%04d" $duration)ms] - $f1 - $f2"
}
# Load previous database into associative array for fast lookup
declare -A previous_callsigns
load_previous_database() {
    if [ -f "$PREV_DB" ]; then
        while IFS='|' read -r node callsign _; do
            # Ensure the node ID is valid (non-empty)
            if [ -n "$node" ]; then
                previous_callsigns["$node"]="$callsign"
            fi
        done < "$PREV_DB"
    fi
}
# Compare current astdb.txt with previous_db.txt to find new or modified nodes
compare_databases() {
    echo "Comparing databases..."
    new_nodes=()  # Array to store nodes that are new or changed
    changes=()    # Array to store changes for preview
    declare -A latest_node_callsigns  # To handle duplicate node numbers
    while IFS='|' read -r f1 f2 _; do
        # Skip lines starting with a semicolon or empty lines
        [[ "$f1" =~ ^\; ]] || [ -z "$f1" ] && continue
        # Handle duplicates by keeping the last occurrence of each node
        latest_node_callsigns["$f1"]="$f2"
    done < "$SRCDIR/astdb.txt"
    # Now compare the latest callsigns with the previous database
    for node in "${!latest_node_callsigns[@]}"; do
        current_callsign="${latest_node_callsigns[$node]}"
        old_callsign="${previous_callsigns[$node]}"
        if [ -z "$old_callsign" ]; then
            # New node
            new_nodes+=("$node|$current_callsign")
            changes+=("$node: NEW -> $current_callsign")
        elif [ "$old_callsign" != "$current_callsign" ]; then
            # Callsign has changed
            new_nodes+=("$node|$current_callsign")
            changes+=("$node: $old_callsign -> $current_callsign")
        fi
    done
}
# Prompt the user to confirm before proceeding, show the first 10 nodes with changes
confirm_processing() {
    local node_count=$1
    if [ $FORCE_RUN -eq 1 ]; then
        return  # Skip confirmation if forced to run
    fi
    echo "$node_count nodes need to be processed."
    echo "Preview of changes:"
    local i=0
    for change in "${changes[@]}"; do
        echo "  $change"
        ((i++))
        if [ $i -ge $MAX_PREVIEW ]; then
            echo "  ...and more."
            break
        fi
    done
    read -p "Continue? [y/n]: " response
    case "$response" in
        [yY][eE][sS]|[yY])
            echo "Starting processing..."
            ;;
        *)
            echo "Aborting."
            exit 0
            ;;
    esac
}
# Update the previous_db.txt file with the current astdb.txt data
update_previous_db() {
    cp "$SRCDIR/astdb.txt" "$PREV_DB"
}
# Format the total execution time into the appropriate unit (seconds, minutes, hours)
format_total_time() {
    local total_time_ms=$1
    if (( total_time_ms < 1000 )); then
        # Less than 1 second: display in milliseconds
        echo "${total_time_ms}ms"
    elif (( total_time_ms < 60000 )); then
        # Less than 1 minute: display in seconds
        local total_time_sec=$(echo "scale=1; $total_time_ms / 1000" | bc)
        echo "${total_time_sec}s"
    elif (( total_time_ms < 3600000 )); then
        # Less than 1 hour: display in minutes
        local total_time_min=$(echo "scale=1; $total_time_ms / 60000" | bc)
        echo "${total_time_min}min"
    else
        # More than 1 hour: display in hours
        local total_time_hr=$(echo "scale=1; $total_time_ms / 3600000" | bc)
        echo "${total_time_hr}h"
    fi
}
# Main processing logic for new or modified nodes
process_nodes() {
    # Load the previous database into memory for fast lookups
    load_previous_database
    # Compare current and previous databases
    compare_databases
    local node_count=${#new_nodes[@]}
    if [ $node_count -eq 0 ]; then
        echo "No new or changed nodes to process."
        return
    fi
    # Confirm processing with user
    confirm_processing "$node_count"
    # Process new or modified nodes
    for node_data in "${new_nodes[@]}"; do
        IFS='|' read -r f1 f2 <<< "$node_data"
        make_call "$f2"
        if [ "$INCNODE" ]; then
            STRING="$STRING $(find_audio_file "$RPTSOUNDS/node")"
            make_call "$f1"
        fi
        write_call
    done
    # Update the previous database with the current data
    update_previous_db
}
# Parse command-line options
while getopts "hail:vn:d:fv" OPTION; do
    case $OPTION in
        h) usage; exit 0 ;;
        a) ;;
        i) INCNODE=1 ;;
        n) node=$OPTARG ;;
        d) DESTDIR=$OPTARG ;;
        f) FORCE_RUN=1 ;;  # Force execution without confirmation
        v) VERBOSE=1 ;;
        ?) usage; exit 1 ;;
    esac
done
# Ensure necessary directories exist
ensure_directory_exists "$DESTDIR"
# Verify that the source file exists
if [ ! -f "$SRCDIR/astdb.txt" ]; then
    echo "$SRCDIR/astdb.txt not found. Please verify the location."
    exit 1
fi
# Start processing nodes
process_nodes
# Calculate total script execution time
end_time=$(date +%s%3N)
total_duration=$((end_time - start_time))
formatted_duration=$(format_total_time $total_duration)
echo "Total script execution time: $formatted_duration"

Thank You Mason, I will give this a try. Many thanks and 73, Steve WB4IZC

I run it every midnight via cron. The first time you run it, it will take forever. After that, it only updates the callsigns for nodes with new/updated info. It uses astdb.txt, so you’ll need to read this: Other Software Products - AllStarLink Manual

Wanted to come and say Thanks...have been using this and meant to extend my gratitude.

Hey Mason-- Does that script need to live in a particular directory when I execute it..?
The only thing I remember from using it on HamVOIP was my default root partition was insufficient so I had to gpart it bigger; doesn't look like it'll be a problem on ASL3...

LATER: Nevermind, all OK, though OMG, you're correct; that takes a long time to process all the callsigns (+/‐ 1hr on a RPi-4 2MB). Working great!

I found this script on a Web search. It seems the -n option is ignored. For example, I am passing -n 519321 to the script but it proceeds to process the entire file! Is this a bug?

Great script. I just got it working. The link for enabling the needed services was very helpful. Thanks and 73, N9KIW.

I was telling a friend about this script. He’s running a ClearNode with Hamvoip. Will this script work with Hamvoip or can it be modified to do so? Thanks.

HamVOIP (I think) comes with it’s own version, /usr/local/sbin/write_node_callsigns sh. If not bundled, than certainly there’s a reference to add it on the HamVOIP website. Sorry, it’s been years since I did it last. If memory serves, I had to expand the / (root) filesystem because it wasn’t big enough as created. YMMV.

HamVoIP has a built-in script to handle this.

/usr/local/sbin/write_node_callsigns.sh

The switches are similar to, but not identical to this script.

For example, there is no -f to force the script running, I.E. for automation purposes, because it doesn’t prompt for anything, so there is no need for it.

The bad thing about HamVoIP’s version, though, is it doesn’t do any comparing. So if a node’s call sign field has changed, it will skip that node if a file has already been generated for it, unless you specifically update just that node number, or all of them. So it doesn’t take long before some things become stale, even if you keep it updating regularly.

It is, however, much faster than the referenced script for processing all nodes, because it just sticks files together using cat, not SoX, and doesn’t do any kind of encoding or processing to the raw input or output files.

Speaking of that:

Since I make it a point to change the sound files on my ASL and HamVoIP nodes (I really hate Allison, the stock Asterisk voice), I also modified my local copy of this script to write out ulaw files instead of GSM, because the artifacts of GSM compression really bugs me. Like, seriously, I have an actual, visceral reaction to GSM artifacts. I wonder if I’m the only one.

The files I use are much faster than stock, so I’m not too much concerned about space, even with 43000 plus nodes.

I went to update things today; be advised the directory where astdb.txt lives has changed, so you need to change:

SRCDIR="/var/log/asterisk

to

SRCDIR="/var/lib/asterisk

I couldn't locate a astdb.txt in /var/lib/asterisk. There was only a file called astdb.sqlite3. I am running the latest ASL3 install with AllMon3 v1.91 on Trixie. RPi5. There is probably a switch somewhere to enable the .txt file to be created. I grabbed a copy from a HAMVoip install and the script does run.

The HamVoIP script won't work without modification anyway, because it is looking for GSM files that don't exist. Plus, it doesn't take changes to node numbers/call signs into account.

There is a script linked here that does, but it needs some optimizing.

You mis-understood. I didn't copy the HAMVoip script but the astdb.txt file. (The database text file). It doesn't exist on my new install of ASL3 but the script above requires it.

See ASL Manual : Other Software Products for information about the /var/lib/asterisk/astdb.txt file.

Thanks for the info. I had just started to research how I could get the file created on ASL3. I see that the file is updated on boot then 4 times a day using this mechanism which is great.

Ah well, so I did. In either case, when you do have astdb.txt (another post has info on making that happen with ASL3), the initial run of that script takes a while. Not as long as it did before the great node registration purge, but still much longer than it could.

Thanks for the heads up re the time to build the callsign data file the first time around. I did see a new timestamp on the file shortly after I installed the service. This only took a few minutes but it may have been doing a delta update on the file I copied over from HAMVoip. I will delete the file tonight and let it rebuild from scratch. There was a dependency that was missing when I ran the callsign/nodename script from above. This was 'bc' and easily installed with 'apt-get install bc'. Regarding the the HAMVoip version of the nodenames voice file builder, I have modified the original so that it uses .ul (ulaw) files. I also use the asl3-tts functionality, with one of the British accents, to create specific sound files for some of the well known hubs I connect to. These are stored in a protected directory and then copied back into the /var/lib/asterisk/sounds/rpt/nodenames location at the end of the update routine. These will be more wordy. For example "NWAG, the North West Allstar Group. Node id 40338, connected to, MyGateway node ID XXXX". Then mix this with the events stanza for a timestamp announcement following a connection change just prior. Great fun

#!/bin/bash
# write_node_callsigns.sh
#
# Original script by N5LSN
# Updated by Jory A. Pratt
#
# Make app_rpt telemetry use ASL callsigns instead of node numbers.
# Intended for use on ASL3.
#
# Updates vs original:
#   - Parallel sox jobs (GNU parallel / xargs -P) for much faster first runs
#   - Cached letter/digit audio paths (no per-character filesystem lookups)
#   - Resume-safe: skip existing .gsm unless callsign changed or -r
#   - Durable previous-db beside astdb.txt (not /tmp)
#   - Auto-locate astdb in /var/lib/asterisk or /var/log/asterisk
#   - Fixed -a / -n / -i / -v; added -j, -r, -s, -p

set -euo pipefail

SRCDIR=""
DESTDIR="/usr/share/asterisk/sounds/en/rpt/nodenames"
RPTSOUNDS="/usr/share/asterisk/sounds/en/rpt"
LETTERS="/usr/share/asterisk/sounds/en/letters"
NUMBERS="/usr/share/asterisk/sounds/en/digits"
PREV_DB=""
JOBS="$(nproc 2>/dev/null || echo 4)"
MAX_PREVIEW=10

VERBOSE=0
INCNODE=0
FORCE_RUN=0
FORCE_REBUILD=0
PROCESS_ALL=0
SINGLE_NODE=""

declare -A PREV_CALLSIGNS

usage() {
    cat << EOF
Usage: write_node_callsigns.sh [options]

OPTIONS:
   -h          Show this message
   -a          Process all nodes (fill gaps; skips existing unless -r)
   -i          Append "node" + node number after the callsign
   -n NODE     Process a single node
   -d PATH     Destination directory (default: $DESTDIR)
   -j N        Parallel sox jobs (default: nproc)
   -v          Verbose output
   -f          Skip confirmation prompt
   -r          Force regenerate even when output already exists
   -s PATH     Directory containing astdb.txt
   -p PATH     Previous-db path used for change detection

Examples:
    ./write_node_callsigns.sh -a -f       # First full build, no prompt
    ./write_node_callsigns.sh -f          # Only new/changed nodes
    ./write_node_callsigns.sh -n 40000 -r # Rebuild one node
EOF
}

log()  { printf '%s\n' "$*"; }
vlog() { (( VERBOSE )) && printf '%s\n' "$*" || true; }
die()  { printf 'Error: %s\n' "$*" >&2; exit 1; }

resolve_astdb_dir() {
    local dir candidates=()
    [[ -n "$SRCDIR" ]] && candidates+=("$SRCDIR")
    candidates+=("/var/lib/asterisk" "/var/log/asterisk")
    for dir in "${candidates[@]}"; do
        if [[ -f "$dir/astdb.txt" ]]; then
            SRCDIR="$dir"
            return 0
        fi
    done
    return 1
}

ensure_directory_exists() {
    if [[ ! -d "$1" ]]; then
        log "Creating directory: $1"
        mkdir -p "$1" || die "Failed to create directory $1"
    fi
}

resolve_audio() {
    local basepath=$1
    if [[ -f "${basepath}.gsm" ]]; then
        printf '%s\n' "${basepath}.gsm"
    elif [[ -f "${basepath}.ulaw" ]]; then
        printf '%s\n' "${basepath}.ulaw"
    else
        printf '\n'
    fi
}

# Cache letter/digit paths into exported env vars for parallel workers.
# Keys: AUDIO_MAP_0..9, AUDIO_MAP_a..z, AUDIO_MAP_SLASH, AUDIO_MAP_DASH
build_audio_cache() {
    local c file
    for c in {0..9}; do
        file="$(resolve_audio "$NUMBERS/$c")"
        [[ -n "$file" ]] || die "Missing digit audio for '$c' under $NUMBERS"
        export "AUDIO_MAP_$c=$file"
    done
    for c in {a..z}; do
        file="$(resolve_audio "$LETTERS/$c")"
        [[ -n "$file" ]] || die "Missing letter audio for '$c' under $LETTERS"
        export "AUDIO_MAP_$c=$file"
    done
    file="$(resolve_audio "$LETTERS/slash")"
    [[ -n "$file" ]] || die "Missing slash audio under $LETTERS"
    export AUDIO_MAP_SLASH="$file"

    file="$(resolve_audio "$LETTERS/dash")"
    [[ -n "$file" ]] || die "Missing dash audio under $LETTERS"
    export AUDIO_MAP_DASH="$file"

    if (( INCNODE )); then
        NODE_SOUND="$(resolve_audio "$RPTSOUNDS/node")"
        [[ -n "$NODE_SOUND" ]] || die "Missing node audio under $RPTSOUNDS"
        export NODE_SOUND
    fi
    export INCNODE DESTDIR VERBOSE
}

audio_lookup() {
    case "$1" in
        /) printf '%s' "${AUDIO_MAP_SLASH-}" ;;
        -) printf '%s' "${AUDIO_MAP_DASH-}" ;;
        *) local var="AUDIO_MAP_$1"; printf '%s' "${!var-}" ;;
    esac
}

process_one_node() {
    local node=$1
    local callsign=${2,,}
    local out="$DESTDIR/$node.gsm"
    local -a inputs=()
    local i char file

    for (( i = 0; i < ${#callsign}; i++ )); do
        char="${callsign:i:1}"
        file="$(audio_lookup "$char")"
        if [[ -z "$file" ]]; then
            printf 'Error: No audio mapping for %q (node %s)\n' "$char" "$node" >&2
            return 1
        fi
        if [[ "$file" == *.ulaw ]]; then
            inputs+=(-t raw -e u-law -r 8000 -c 1 "$file")
        else
            inputs+=("$file")
        fi
    done

    if (( INCNODE )); then
        if [[ "${NODE_SOUND}" == *.ulaw ]]; then
            inputs+=(-t raw -e u-law -r 8000 -c 1 "$NODE_SOUND")
        else
            inputs+=("$NODE_SOUND")
        fi
        for (( i = 0; i < ${#node}; i++ )); do
            char="${node:i:1}"
            file="$(audio_lookup "$char")"
            if [[ -z "$file" ]]; then
                printf 'Error: No audio mapping for %q (node %s)\n' "$char" "$node" >&2
                return 1
            fi
            if [[ "$file" == *.ulaw ]]; then
                inputs+=(-t raw -e u-law -r 8000 -c 1 "$file")
            else
                inputs+=("$file")
            fi
        done
    fi

    sox "${inputs[@]}" "$out" || {
        printf 'sox failed for node %s (%s)\n' "$node" "$2" >&2
        return 1
    }
    if (( VERBOSE )); then
        printf 'OK %s %s\n' "$node" "$2"
    else
        printf '.'
    fi
}

export -f process_one_node audio_lookup

load_previous_database() {
    PREV_CALLSIGNS=()
    [[ -f "$PREV_DB" ]] || return 0
    local node callsign
    while IFS='|' read -r node callsign _; do
        [[ -n "$node" ]] || continue
        [[ "$node" == \;* ]] && continue
        PREV_CALLSIGNS["$node"]="$callsign"
    done < "$PREV_DB"
}

# Unique node|callsign lines (last occurrence wins).
latest_astdb_entries() {
    awk -F'|' '
        /^;/ || NF < 2 || $1 == "" { next }
        { node[$1] = $2 }
        END { for (n in node) print n "|" node[n] }
    ' "$SRCDIR/astdb.txt"
}

lookup_node_callsign() {
    local want=$1
    awk -F'|' -v want="$want" '
        /^;/ || NF < 2 { next }
        $1 == want { call = $2 }
        END { if (call != "") print call }
    ' "$SRCDIR/astdb.txt"
}

# Return 0 if sox should run for this node.
needs_rebuild() {
    local node=$1
    local callsign=$2
    local out="$DESTDIR/$node.gsm"
    local old="${PREV_CALLSIGNS[$node]-}"

    (( FORCE_REBUILD )) && return 0
    [[ -f "$out" ]] || return 0

    # Existing file: rebuild only when we know the callsign changed.
    if [[ -n "$old" && "$old" != "$callsign" ]]; then
        return 0
    fi
    return 1
}

queue_node() {
    local node=$1
    local callsign=$2
    local old="${PREV_CALLSIGNS[$node]-}"

    printf '%s|%s\n' "$node" "$callsign" >> "$WORK_FILE"
    if [[ -z "$old" ]]; then
        printf '%s: NEW -> %s\n' "$node" "$callsign" >> "$CHANGES_FILE"
    else
        printf '%s: %s -> %s\n' "$node" "$old" "$callsign" >> "$CHANGES_FILE"
    fi
}

confirm_processing() {
    local node_count=$1
    (( FORCE_RUN )) && return 0

    log "$node_count nodes need to be processed (jobs=$JOBS)."
    log "Preview of changes:"
    local i=0 line
    while IFS= read -r line; do
        log "  $line"
        (( ++i >= MAX_PREVIEW )) && { log "  ...and more."; break; }
    done < "$CHANGES_FILE"

    local response
    read -r -p "Continue? [y/n]: " response
    case "$response" in
        [yY]|[yY][eE][sS]) log "Starting processing..." ;;
        *) log "Aborting."; exit 0 ;;
    esac
}

run_parallel() {
    local total
    total="$(wc -l < "$WORK_FILE" | tr -d ' ')"

    if command -v parallel >/dev/null 2>&1; then
        parallel -j "$JOBS" --colsep '[|]' process_one_node {1} {2} < "$WORK_FILE"
    else
        while IFS='|' read -r node callsign; do
            printf '%s\0%s\0' "$node" "$callsign"
        done < "$WORK_FILE" \
            | xargs -0 -n 2 -P "$JOBS" bash -c 'process_one_node "$1" "$2"' _
    fi

    (( VERBOSE )) || printf '\n'
    log "Processed $total nodes."
}

update_previous_db() {
    ensure_directory_exists "$(dirname "$PREV_DB")"
    cp "$SRCDIR/astdb.txt" "$PREV_DB"
    vlog "Updated previous DB: $PREV_DB"
}

format_total_time() {
    local total_time_ms=$1
    if (( total_time_ms < 1000 )); then
        echo "${total_time_ms}ms"
    elif (( total_time_ms < 60000 )); then
        awk -v ms="$total_time_ms" 'BEGIN { printf "%.1fs\n", ms / 1000 }'
    elif (( total_time_ms < 3600000 )); then
        awk -v ms="$total_time_ms" 'BEGIN { printf "%.1fmin\n", ms / 60000 }'
    else
        awk -v ms="$total_time_ms" 'BEGIN { printf "%.1fh\n", ms / 3600000 }'
    fi
}

# --- options ---
while getopts "haij:n:d:s:p:fvr" OPTION; do
    case "$OPTION" in
        h) usage; exit 0 ;;
        a) PROCESS_ALL=1 ;;
        i) INCNODE=1 ;;
        j) JOBS="$OPTARG" ;;
        n) SINGLE_NODE="$OPTARG" ;;
        d) DESTDIR="$OPTARG" ;;
        s) SRCDIR="$OPTARG" ;;
        p) PREV_DB="$OPTARG" ;;
        f) FORCE_RUN=1 ;;
        v) VERBOSE=1 ;;
        r) FORCE_REBUILD=1 ;;
        *) usage; exit 1 ;;
    esac
done

[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || die "-j must be a positive integer"

start_time="$(date +%s%3N)"

resolve_astdb_dir || die "astdb.txt not found (tried /var/lib/asterisk, /var/log/asterisk). Use -s PATH."
[[ -z "$PREV_DB" ]] && PREV_DB="$SRCDIR/previous_astdb.txt"

ensure_directory_exists "$DESTDIR"
command -v sox >/dev/null 2>&1 || die "sox is required but not installed"

build_audio_cache
load_previous_database

WORK_FILE="$(mktemp)"
CHANGES_FILE="$(mktemp)"
cleanup() { rm -f "$WORK_FILE" "$CHANGES_FILE"; }
trap cleanup EXIT

node_count=0

if [[ -n "$SINGLE_NODE" ]]; then
    callsign="$(lookup_node_callsign "$SINGLE_NODE")"
    [[ -n "$callsign" ]] || die "Node $SINGLE_NODE not found in $SRCDIR/astdb.txt"
    if needs_rebuild "$SINGLE_NODE" "$callsign"; then
        queue_node "$SINGLE_NODE" "$callsign"
        node_count=1
    fi
else
    while IFS='|' read -r node callsign; do
        [[ -n "$node" && -n "$callsign" ]] || continue
        old="${PREV_CALLSIGNS[$node]-}"

        if (( PROCESS_ALL )); then
            # All nodes: write missing (or changed / -r) files.
            if needs_rebuild "$node" "$callsign"; then
                queue_node "$node" "$callsign"
                (( ++node_count )) || true
            else
                vlog "Skipping $node ($callsign) — output exists"
            fi
            continue
        fi

        # Incremental: only new or changed callsigns.
        if [[ -n "$old" && "$old" == "$callsign" ]]; then
            continue
        fi
        if needs_rebuild "$node" "$callsign"; then
            queue_node "$node" "$callsign"
            (( ++node_count )) || true
        else
            vlog "Skipping $node ($callsign) — output exists (resume)"
        fi
    done < <(latest_astdb_entries)
fi

if (( node_count == 0 )); then
    log "No nodes need processing."
    if [[ ! -f "$PREV_DB" ]]; then
        update_previous_db
    fi
    end_time="$(date +%s%3N)"
    log "Total script execution time: $(format_total_time "$((end_time - start_time))")"
    exit 0
fi

confirm_processing "$node_count"
run_parallel
update_previous_db

end_time="$(date +%s%3N)"
log "Total script execution time: $(format_total_time "$((end_time - start_time))")"

I have had this fixed up for a few months, this will descrease the initial generation time and also fixes the argv problems. Feel free to test it out and let me know if there are any other changes that would be beneficial.