Encode audio with cURL and open-source tools
Use cURL to download the audio and FFmpeg to encode it. This walkthrough produces a local M4A file from a real MP3 sample, then turns that workflow into a Bash script for AAC, M4A, MP3, or Opus output. Uploading the result requires a receiver with its own API contract and is outside this example.
Set up your environment
Use Linux with Bash, cURL, FFmpeg, and ffprobe on your PATH. The FFmpeg build needs the aac,
libmp3lame, and libopus encoders. Install maintained packages for your distribution;
FFmpeg’s download page links to package providers.
The examples were tested with Bash 5.3.15, cURL 8.22.0, and FFmpeg/ffprobe 9.0.1, with a separate
compatibility replay on FFmpeg/ffprobe 6.1.1. These are tested versions, not a recommendation to
install an old patch release.
Check the installed tools and encoders:
bash --version &&
curl --version &&
ffmpeg -version &&
ffprobe -version &&
ffmpeg -hide_banner -encoders
Start with a complete, unencrypted mono or stereo recording at 44.1 or 48 kHz. The examples cover MP3 and PCM WAV input, including integer and floating-point samples. They do not define a surround channel mapping or force a sample rate; FFmpeg may resample when the output codec needs it. Use audio you have permission to process.
Basic audio encoding with FFmpeg and cURL
Paste this into Bash from a directory where audio-example does not already exist. It downloads
viper.mp3 from the MDN Web Audio example,
then encodes its first audio stream as AAC in an M4A container. The pinned sample has about
41 seconds of stereo audio at 44.1 kHz.
(
set -euo pipefail
mkdir -- audio-example || exit 1
trap 'status=$?; if [ "$status" -ne 0 ]; then
rm -f -- audio-example/input.mp3 audio-example/output.m4a audio-example/ffmpeg-errors.log
rmdir -- audio-example 2>/dev/null || true
fi' EXIT
curl -fsSL --retry 3 --connect-timeout 10 --max-time 60 \
--proto '=https' --proto-redir '=https' \
-o audio-example/input.mp3 \
https://raw.githubusercontent.com/mdn/webaudio-examples/5af6b7a1545ad572c2dd2ce0443eeb26ab7c3e61/audio-analyser/viper.mp3 || exit 1
if ! ffmpeg -nostdin -v error -n -xerror -i audio-example/input.mp3 -map 0:a:0 \
-c:a aac -b:a 192k -f ipod audio-example/output.m4a \
2>audio-example/ffmpeg-errors.log || [ -s audio-example/ffmpeg-errors.log ]; then
cat -- audio-example/ffmpeg-errors.log >&2
exit 1
fi
rm -f -- audio-example/ffmpeg-errors.log
)
On success, audio-example contains the downloaded MP3 and output.m4a. A failed download or
encode removes this attempt’s files. If the directory already exists, the block stops before
writing anything. The parentheses keep shell options inside a subshell, so pasting it does not
change your working shell. Run this example sequentially in a directory you control.
The -map 0:a:0 option selects the first audio
stream, while -n refuses to replace an existing output and -nostdin disables interactive input.
The bitrate is a target for the encoder, not a guarantee about file size or perceptual quality.
Re-encoding an MP3 cannot restore detail already lost in its original compression.
Create an audio encoding script
Save the following as encode_audio.sh in your current directory. The format argument chooses
both an encoder and a container; changing only a filename extension does not convert audio.
The FFmpeg format documentation describes these muxers.
| Argument | Audio codec | Container | Output filename |
|---|---|---|---|
aac | AAC | ADTS | output.aac |
m4a | AAC | MPEG-4 audio | output.m4a |
mp3 | MP3 via libmp3lame | MP3 | output.mp3 |
opus | Opus via libopus | Ogg | output.opus |
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <input_url> <output_format> <output_bitrate>" >&2
exit 1
fi
INPUT_URL=$1
OUTPUT_FORMAT=$2
BITRATE=$3
case "$OUTPUT_FORMAT" in
aac) CODEC=aac; CONTAINER=adts ;;
m4a) CODEC=aac; CONTAINER=ipod ;;
mp3) CODEC=libmp3lame; CONTAINER=mp3 ;;
opus) CODEC=libopus; CONTAINER=ogg ;;
*) echo "Unsupported output format: $OUTPUT_FORMAT" >&2; exit 1 ;;
esac
if [[ ! "$BITRATE" =~ ^[1-9][0-9]*k$ ]]; then
echo "Bitrate must be a positive integer followed by k, such as 192k" >&2
exit 1
fi
WORK_DIR=$(mktemp -d ./audio-encode.XXXXXX)
INPUT_FILE="$WORK_DIR/input.audio"
OUTPUT_FILE="$WORK_DIR/output.$OUTPUT_FORMAT"
ERROR_LOG="$WORK_DIR/ffmpeg-errors.log"
# Delete the download; retain the output only after successful encoding.
trap 'status=$?; rm -f -- "$INPUT_FILE" "$ERROR_LOG";
if [ "$status" -ne 0 ]; then rm -f -- "$OUTPUT_FILE"; fi
rmdir -- "$WORK_DIR" 2>/dev/null || true' EXIT
echo "Downloading input file…"
if ! curl -fsSL --retry 3 --connect-timeout 10 --max-time 60 \
--proto '=http,https' --proto-redir '=http,https' \
-o "$INPUT_FILE" -- "$INPUT_URL"; then
echo "Error: Failed to download input file" >&2
exit 1
fi
echo "Encoding to ${OUTPUT_FORMAT} format…"
if ! ffmpeg -nostdin -v error -n -xerror -i "$INPUT_FILE" -map 0:a:0 -vn \
-c:a "$CODEC" -b:a "$BITRATE" -f "$CONTAINER" "$OUTPUT_FILE" \
2>"$ERROR_LOG" || [ -s "$ERROR_LOG" ]; then
cat -- "$ERROR_LOG" >&2
echo "Error: Failed to encode audio" >&2
exit 1
fi
echo "Successfully encoded to: ${OUTPUT_FILE}"
Run the saved script with Bash; it does not need executable permissions:
bash ./encode_audio.sh \
https://raw.githubusercontent.com/mdn/webaudio-examples/5af6b7a1545ad572c2dd2ce0443eeb26ab7c3e61/audio-analyser/viper.mp3 \
opus 128k
Success prints a path such as ./audio-encode.A1b2C3/output.opus. Each invocation creates a new
work directory, so rerunning the same URL retains earlier results. The script removes its download
and, on a download or encoding failure, removes any partial output and the empty work directory.
Argument errors occur before it creates files. This cleanup covers ordinary command failures;
a forced process kill or machine shutdown can leave temporary files.
128k means a target of 128,000 bits per second. The script checks the argument’s spelling;
the selected encoder still decides whether that bitrate is supported. Downloading finishes before
encoding, so FFmpeg can seek within the input file.
Batch processing audio files
Save this as batch_encode.sh beside encode_audio.sh. Run it from that directory. Each row in
the input list has a URL, format, and bitrate separated by spaces. Blank lines are skipped;
percent-encode spaces inside URLs. Comments and extra fields are not supported.
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <input_file_list.txt>" >&2
echo "File list format: <input_url> <output_format> <bitrate>" >&2
exit 1
fi
INPUT_LIST=$1
failed=0
while IFS=' ' read -r url format bitrate extra || [[ -n "$url" ]]; do
[[ -z "$url" ]] && continue
echo "Processing: ${url}"
if [[ -n "$extra" || -z "$format" || -z "$bitrate" ]]; then
echo "Invalid list entry: $url" >&2
failed=1
elif bash ./encode_audio.sh "$url" "$format" "$bitrate"; then
echo "Success: ${url}"
else
echo "Failed: ${url}" >&2
failed=1
fi
done < "${INPUT_LIST}"
exit "$failed"
For example, save these two rows as audio-list.txt:
https://raw.githubusercontent.com/mdn/webaudio-examples/5af6b7a1545ad572c2dd2ce0443eeb26ab7c3e61/audio-analyser/viper.mp3 m4a 192k
https://raw.githubusercontent.com/mdn/webaudio-examples/5af6b7a1545ad572c2dd2ce0443eeb26ab7c3e61/audio-analyser/viper.mp3 mp3 128k
bash ./batch_encode.sh audio-list.txt
The batch runs sequentially and keeps successful outputs even when another row fails. It continues past invalid entries, failed downloads, and failed encodes, then exits with status 1 if any job failed. Status 0 means every processed row succeeded; an empty list performs no work.
Security considerations
Use HTTPS URLs from sources you trust. cURL
verifies server certificates by default; do not add -k to
bypass that verification. The script’s
--proto and --proto-redir restrictions allow only
HTTP and HTTPS, including redirects. HTTP remains available for a local test server, but does not
provide transport encryption.
These scripts are local conversion examples. They do not sandbox FFmpeg, impose download size limits, or make arbitrary user-supplied URLs safe for a server to fetch. The cURL timeouts bound each transfer attempt, and retries can make the total download take longer. Leave enough disk space for the complete input and output.
Error handling and validation
Check the basic example’s actual output, using this block from the same directory where you ran it:
(
set -euo pipefail
VERIFY_LOG=$(mktemp ./audio-verify.XXXXXX)
trap 'rm -f -- "$VERIFY_LOG"' EXIT
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels:format=format_name,duration \
-of json audio-example/output.m4a || exit 1
if ! ffmpeg -nostdin -v error -xerror -i audio-example/output.m4a \
-map 0:a:0 -f null - 2>"$VERIFY_LOG" || [ -s "$VERIFY_LOG" ]; then
cat -- "$VERIFY_LOG" >&2
exit 1
fi
)
Expect codec_name to be aac, two channels, and a duration near 41 seconds. ffprobe reports the
M4A container as part of the mov,mp4,m4a,3gp,3g2,mj2 family. The second command decodes the whole
output without saving another file. For a script result, substitute the exact path printed by that
invocation in both commands. Raw ADTS AAC includes encoder delay and padding, and ffprobe may
estimate its duration from bitrate. Check the decoded audio against the source timeline instead of
treating that estimate as an exact length.
-xerror requests that FFmpeg stop on errors.
FFmpeg 6.1.1 can report a late decoder error yet return status 0. The blocks therefore also capture
stderr at the error log level and reject a nonempty error log, printing its diagnostic before
cleanup. This catches reported errors, but it is not an integrity proof: a recording truncated at a
decodable boundary can still encode or decode successfully, and some damaged packets can be silently
discarded. Compare the duration with a known complete source and listen through the end;
use a trusted publisher checksum when one is available. File existence, a readable header, and an
exit status of zero cannot establish that all expected audio arrived.
A cURL HTTP error such as 404 stops the download before encoding. An unavailable encoder, a file with no audio stream, or an encoder-rejected bitrate stops the conversion. Keep the diagnostic on stderr when investigating a failure. If the batch exits with status 1, use its per-row messages to identify failures; already completed outputs remain available.
Conclusion
For a hosted encoding workflow, see the 🤖 /audio/encode Robot documentation. Its upload and authentication contract is separate from this local cURL download and FFmpeg conversion.
