Initial commit: call recorder with MP3 output and --show-inputs monitor
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
recordings/
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
For the precise terms and conditions for copying, distribution and
|
||||||
|
modification, follow below. Terms and Conditions for copying,
|
||||||
|
distribution and modification are not listed here, but the full text
|
||||||
|
is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
The GNU General Public License is published by the Free Software
|
||||||
|
Foundation. You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new new free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or preventing you from asking you to surrender the rights.
|
||||||
|
Therefore, you have certain responsibilities if you distribute copies
|
||||||
|
of the software, or if you modify it: responsibilities to respect the
|
||||||
|
freedom of others.
|
||||||
|
|
||||||
|
For the full license text, see: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
Copyright (C) 2026 Markus Graf
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Call Recorder
|
||||||
|
|
||||||
|
Records microphone and system audio (everything playing on your speakers/headphones) into a single MP3 file — ideal for transcription with Whisper, Gemini, or similar services.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux with PipeWire / PulseAudio
|
||||||
|
- `ffmpeg`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start recording before your call
|
||||||
|
./record.sh
|
||||||
|
|
||||||
|
# Join your call normally, then press Ctrl+C when done.
|
||||||
|
# Recording is saved to recordings/call_YYYYMMDD_HHMMSS.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check which inputs are active
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./record.sh --show-inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays a live bar chart of both inputs in the terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mikrofon : [████████████░░░░░░░░░░░░░░░░░░] -18 dB
|
||||||
|
System Audio : [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] -60 dB
|
||||||
|
```
|
||||||
|
|
||||||
|
Colour coding: green (normal) → yellow (−12 dB) → red (−3 dB). Press Ctrl+C to exit.
|
||||||
|
|
||||||
|
### List available audio sources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./list-sources.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
`record.sh` detects the default PipeWire/PulseAudio sink, then runs `ffmpeg` to simultaneously capture:
|
||||||
|
|
||||||
|
- **Microphone** — PulseAudio default source
|
||||||
|
- **System audio** — monitor of the default sink (everything you hear)
|
||||||
|
|
||||||
|
Both streams are mixed and encoded as MP3 at 128 kbps (~11 MB / 15 min).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Missing system audio or microphone?**
|
||||||
|
Run `./list-sources.sh` to inspect available devices. The script auto-detects the default sink at startup — if you switched audio output after launching (e.g. plugged in headphones), restart the script.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "=== Default devices ==="
|
||||||
|
pw-metadata 0 2>/dev/null | awk -F"'" '/default\.audio\.(sink|source)/ { print $2, $4 }'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All audio nodes (sinks, sources, monitors) ==="
|
||||||
|
pw-cli ls Node 2>/dev/null \
|
||||||
|
| awk '/id [0-9]/ { id=$2 } /node\.name/ { print id, $0 }' \
|
||||||
|
| grep -i "alsa\|bluez\|monitor" || true
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RECORDINGS_DIR="$SCRIPT_DIR/recordings"
|
||||||
|
mkdir -p "$RECORDINGS_DIR"
|
||||||
|
|
||||||
|
OUTPUT="$RECORDINGS_DIR/call_$(date +%Y%m%d_%H%M%S).mp3"
|
||||||
|
|
||||||
|
# Argument parsing
|
||||||
|
SHOW_INPUTS=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$arg" == "--show-inputs" ]] && SHOW_INPUTS=true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Detect the default audio sink via PipeWire metadata
|
||||||
|
DEFAULT_SINK=$(pw-metadata 0 2>/dev/null \
|
||||||
|
| awk -F"'" '/default\.audio\.sink/ { print $4 }' \
|
||||||
|
| grep -o '"name":"[^"]*"' \
|
||||||
|
| cut -d'"' -f4)
|
||||||
|
|
||||||
|
if [[ -z "$DEFAULT_SINK" ]]; then
|
||||||
|
echo "ERROR: Could not detect default audio sink." >&2
|
||||||
|
echo "Run ./list-sources.sh to see available devices." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MONITOR_SOURCE="${DEFAULT_SINK}.monitor"
|
||||||
|
|
||||||
|
# --- show-inputs mode ---
|
||||||
|
if $SHOW_INPUTS; then
|
||||||
|
BAR_WIDTH=30
|
||||||
|
DB_MIN=-60
|
||||||
|
DB_MAX=0
|
||||||
|
|
||||||
|
render_bar() {
|
||||||
|
local label="$1"
|
||||||
|
local db="$2"
|
||||||
|
local bar=""
|
||||||
|
local filled=0
|
||||||
|
|
||||||
|
if [[ -n "$db" ]]; then
|
||||||
|
# Clamp to range
|
||||||
|
local clamped
|
||||||
|
clamped=$(awk -v v="$db" -v lo="$DB_MIN" -v hi="$DB_MAX" \
|
||||||
|
'BEGIN { if (v < lo) v=lo; if (v > hi) v=hi; print v }')
|
||||||
|
filled=$(awk -v v="$clamped" -v lo="$DB_MIN" -v hi="$DB_MAX" -v w="$BAR_WIDTH" \
|
||||||
|
'BEGIN { printf "%d", (v - lo) / (hi - lo) * w }')
|
||||||
|
fi
|
||||||
|
|
||||||
|
local empty=$(( BAR_WIDTH - filled ))
|
||||||
|
|
||||||
|
# Color based on level
|
||||||
|
local color="\033[32m" # green
|
||||||
|
if [[ -n "$db" ]]; then
|
||||||
|
color=$(awk -v v="$db" 'BEGIN {
|
||||||
|
if (v >= -3) { print "\033[31m" }
|
||||||
|
else if (v >= -12) { print "\033[33m" }
|
||||||
|
else { print "\033[32m" }
|
||||||
|
}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
bar="${color}"
|
||||||
|
local i
|
||||||
|
for (( i=0; i<filled; i++ )); do bar+="█"; done
|
||||||
|
for (( i=0; i<empty; i++ )); do bar+="░"; done
|
||||||
|
bar+="\033[0m"
|
||||||
|
|
||||||
|
local db_str="${db:- ---}"
|
||||||
|
printf " %-13s: [%b] %6s dB\n" "$label" "$bar" "$db_str"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Input Monitor (Ctrl+C to exit)"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
TMP_MIC=$(mktemp)
|
||||||
|
TMP_SYS=$(mktemp)
|
||||||
|
|
||||||
|
ffmpeg -loglevel info -f pulse -i default \
|
||||||
|
-af ebur128 -f null - 2>"$TMP_MIC" &
|
||||||
|
PID_MIC=$!
|
||||||
|
|
||||||
|
ffmpeg -loglevel info -f pulse -i "$MONITOR_SOURCE" \
|
||||||
|
-af ebur128 -f null - 2>"$TMP_SYS" &
|
||||||
|
PID_SYS=$!
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
kill "$PID_MIC" "$PID_SYS" 2>/dev/null || true
|
||||||
|
rm -f "$TMP_MIC" "$TMP_SYS"
|
||||||
|
tput cnorm
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap cleanup INT TERM
|
||||||
|
|
||||||
|
tput civis # hide cursor
|
||||||
|
|
||||||
|
# Reserve two lines
|
||||||
|
printf "\n\n"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
MIC_DB=$(grep -oP 'M:\s*\K[-0-9.]+' "$TMP_MIC" 2>/dev/null | tail -1 || true)
|
||||||
|
SYS_DB=$(grep -oP 'M:\s*\K[-0-9.]+' "$TMP_SYS" 2>/dev/null | tail -1 || true)
|
||||||
|
|
||||||
|
printf "\033[2A" # move up 2 lines
|
||||||
|
render_bar "Mikrofon" "$MIC_DB"
|
||||||
|
render_bar "System Audio" "$SYS_DB"
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Recording mode ---
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Call Recorder"
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Microphone : default"
|
||||||
|
echo " System audio: $MONITOR_SOURCE"
|
||||||
|
echo " Output file : $OUTPUT"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
echo " Press Ctrl+C to stop recording."
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ffmpeg -loglevel warning \
|
||||||
|
-f pulse -i default \
|
||||||
|
-f pulse -i "$MONITOR_SOURCE" \
|
||||||
|
-filter_complex "amix=inputs=2:duration=longest:normalize=0" \
|
||||||
|
-c:a libmp3lame -b:a 128k \
|
||||||
|
"$OUTPUT"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Recording saved to: $OUTPUT"
|
||||||
Reference in New Issue
Block a user