···
1
1
+
{
2
2
+
lib,
3
3
+
fetchFromGitHub,
4
4
+
swift,
5
5
+
swiftpm,
6
6
+
swiftPackages,
7
7
+
}:
8
8
+
9
9
+
swiftPackages.stdenv.mkDerivation {
10
10
+
pname = "system-audio-dump";
11
11
+
version = "0-unstable-2025-09-02";
12
12
+
13
13
+
src = fetchFromGitHub {
14
14
+
owner = "sohzm";
15
15
+
repo = "systemAudioDump";
16
16
+
rev = "19caa4f6c0661c03a10d1f08c79a11f0b00f251a";
17
17
+
hash = "sha256-H2TeB7/Meq7EvfW7zLZn5GGFrKwCyZuJvafv61ZWdrA=";
18
18
+
};
19
19
+
20
20
+
nativeBuildInputs = [
21
21
+
swift
22
22
+
swiftpm
23
23
+
];
24
24
+
25
25
+
postPatch = ''
26
26
+
# Downgrade swift-tools-version from 6.0 to 5.10 and macOS from v15 to v14
27
27
+
sed -i 's/swift-tools-version:6.0/swift-tools-version:5.10/' Package.swift
28
28
+
sed -i 's/.macOS(.v15)/.macOS(.v14)/' Package.swift
29
29
+
30
30
+
# Patch Swift 6 / macOS 15 features in source
31
31
+
# Remove @retroactive attribute (Swift 6 feature for silencing conformance warnings)
32
32
+
sed -i 's/@retroactive //' Sources/SystemAudioDump/main.swift
33
33
+
# Remove captureMicrophone (macOS 15+ only, defaults to false anyway)
34
34
+
sed -i '/captureMicrophone/d' Sources/SystemAudioDump/main.swift
35
35
+
'';
36
36
+
37
37
+
buildPhase = ''
38
38
+
runHook preBuild
39
39
+
swift build -c release
40
40
+
runHook postBuild
41
41
+
'';
42
42
+
43
43
+
installPhase = ''
44
44
+
runHook preInstall
45
45
+
mkdir -p $out/bin
46
46
+
cp .build/release/SystemAudioDump $out/bin/system-audio-dump
47
47
+
runHook postInstall
48
48
+
'';
49
49
+
50
50
+
meta = {
51
51
+
description = "macOS CLI that captures system audio and outputs raw PCM to stdout";
52
52
+
homepage = "https://github.com/sohzm/systemAudioDump";
53
53
+
license = lib.licenses.mit;
54
54
+
platforms = lib.platforms.darwin;
55
55
+
mainProgram = "system-audio-dump";
56
56
+
};
57
57
+
}
···
1
1
+
{ ... }:
2
2
+
{
3
3
+
perSystem =
4
4
+
{ pkgs, self', ... }:
5
5
+
{
6
6
+
packages.record = pkgs.writeShellApplication {
7
7
+
name = "record";
8
8
+
runtimeInputs = [
9
9
+
pkgs.ffmpeg
10
10
+
pkgs.gum
11
11
+
]
12
12
+
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.pulseaudio ]
13
13
+
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [ self'.packages.system-audio-dump ];
14
14
+
text = builtins.readFile ./rec.sh;
15
15
+
};
16
16
+
};
17
17
+
}
···
1
1
+
#!/usr/bin/env bash
2
2
+
set -euo pipefail
3
3
+
4
4
+
list_sources() {
5
5
+
if [[ "$OSTYPE" == "darwin"* ]]; then
6
6
+
# macOS: list audio devices via ffmpeg avfoundation
7
7
+
echo "system: System Audio (via ScreenCaptureKit)"
8
8
+
ffmpeg -f avfoundation -list_devices true -i "" 2>&1 | \
9
9
+
sed -n '/AVFoundation audio devices:/,/^[^[]/p' | \
10
10
+
grep -E '^\[AVFoundation.*\[[0-9]+\]' | \
11
11
+
sed 's/.*\[\([0-9]*\)\] \(.*\)/\1: \2/'
12
12
+
else
13
13
+
# Linux: list PulseAudio/PipeWire sources including monitors (for system audio)
14
14
+
# Sources ending in .monitor capture the output of that sink (system audio)
15
15
+
pactl list sources short | awk '{print $2}' | while read -r source; do
16
16
+
if [[ "$source" == *.monitor ]]; then
17
17
+
echo "$source (system audio)"
18
18
+
else
19
19
+
echo "$source"
20
20
+
fi
21
21
+
done
22
22
+
fi
23
23
+
}
24
24
+
25
25
+
get_source_name() {
26
26
+
# Strip the description suffix for actual recording
27
27
+
local name="$1"
28
28
+
echo "${name% (system audio)}"
29
29
+
}
30
30
+
31
31
+
record_source() {
32
32
+
local source="$1"
33
33
+
34
34
+
if [[ "$OSTYPE" == "darwin"* ]]; then
35
35
+
if [[ "$source" == "system" ]]; then
36
36
+
# Use system-audio-dump for system audio (ScreenCaptureKit)
37
37
+
# Output is raw PCM 24kHz 16-bit stereo, convert to WAV for compatibility
38
38
+
system-audio-dump | ffmpeg -f s16le -ar 24000 -ac 2 -i - -f wav -acodec pcm_s16le - 2>/dev/null
39
39
+
else
40
40
+
# Use avfoundation for mic input, source is the device index
41
41
+
ffmpeg -f avfoundation -i ":${source}" -f wav -acodec pcm_s16le - 2>/dev/null
42
42
+
fi
43
43
+
else
44
44
+
# Linux: use pulse
45
45
+
ffmpeg -f pulse -i "$source" -f wav -acodec pcm_s16le - 2>/dev/null
46
46
+
fi
47
47
+
}
48
48
+
49
49
+
# Show help
50
50
+
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
51
51
+
echo "Usage: record [SOURCE]"
52
52
+
echo ""
53
53
+
echo "Record audio from a source and output WAV to stdout."
54
54
+
echo "If no source is provided, prompts interactively with gum."
55
55
+
echo ""
56
56
+
echo "Options:"
57
57
+
echo " -l, --list List available audio sources"
58
58
+
echo " -h, --help Show this help"
59
59
+
echo ""
60
60
+
echo "System audio:"
61
61
+
echo " Linux: Select a .monitor source (captures sink output)"
62
62
+
echo " macOS: Use 'system' source (requires Screen Recording permission)"
63
63
+
echo ""
64
64
+
echo "Examples:"
65
65
+
echo " record | mpv -"
66
66
+
echo " record system | mpv - # macOS system audio"
67
67
+
echo " record 0 | mpv - # macOS device index"
68
68
+
echo " record alsa_output.pci-xxx.monitor | mpv - # Linux system audio"
69
69
+
exit 0
70
70
+
fi
71
71
+
72
72
+
# List sources
73
73
+
if [[ "${1:-}" == "-l" ]] || [[ "${1:-}" == "--list" ]]; then
74
74
+
list_sources
75
75
+
exit 0
76
76
+
fi
77
77
+
78
78
+
# Get source from argument or prompt
79
79
+
if [[ -n "${1:-}" ]]; then
80
80
+
SOURCE="$1"
81
81
+
else
82
82
+
# Use gum to select a source
83
83
+
if [[ "$OSTYPE" == "darwin"* ]]; then
84
84
+
SOURCE=$(list_sources | gum choose --header "Select audio source:" | cut -d: -f1)
85
85
+
else
86
86
+
SELECTION=$(list_sources | gum choose --header "Select audio source:")
87
87
+
SOURCE=$(get_source_name "$SELECTION")
88
88
+
fi
89
89
+
fi
90
90
+
91
91
+
if [[ -z "$SOURCE" ]]; then
92
92
+
echo "Error: No source selected" >&2
93
93
+
exit 1
94
94
+
fi
95
95
+
96
96
+
echo "Recording from: $SOURCE (Ctrl+C to stop)" >&2
97
97
+
record_source "$SOURCE"