Skip to content

Commit a5127af

Browse files
committed
add downmix-audio-to-stereo-rfc7845.py
1 parent 55d1d8d commit a5127af

File tree

1 file changed

+138
-0
lines changed

1 file changed

+138
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
3+
# https://superuser.com/a/1616102/951886
4+
# Properly downmix 5.1 to stereo using ffmpeg
5+
6+
# https://www.rfc-editor.org/rfc/rfc7845#section-5.1.1.5
7+
# RFC 7845 Section 5.1.1.5 Downmixing
8+
9+
# https://trac.ffmpeg.org/wiki/AudioChannelManipulation
10+
11+
# https://mediaarea.net/AudioChannelLayout # speaker positions
12+
13+
# test:
14+
# for L in 3.0 4.0 5.0 5.1 6.1 7.1; do echo; echo $L; ./downmix-audio-to-stereo-rfc7845.py $L; done
15+
16+
# TODO 5.1(side) versus 5.1
17+
18+
# note: ffmpeg says "back" instead of "rear"
19+
20+
21+
22+
from math import sqrt
23+
24+
25+
26+
def get_coefficients_for_downmix_to_stereo(
27+
input_channel_layout,
28+
scale_center = 1.0,
29+
scale_lfe = 1.0,
30+
scale_front = 1.0,
31+
scale_rear = 1.0,
32+
scale_side1 = 1.0,
33+
scale_side2 = 1.0,
34+
):
35+
36+
if input_channel_layout in ("mono", "monophonic", "1ch", "1.0"):
37+
return None # noop
38+
39+
if input_channel_layout in ("stereo", "2ch", "2.0"):
40+
return None # noop
41+
42+
if input_channel_layout in ("linear surround", "3ch", "3.0"):
43+
# Linear Surround Channel Mapping: L C R
44+
# left, center, right
45+
sides = 1
46+
center = (1/sqrt(2)) * scale_center
47+
n = 1/(sides + center)
48+
return dict(
49+
FL = dict(FL=sides*n, FC=center*n, FR=0),
50+
FR = dict(FL=0, FC=center*n, FR=sides*n),
51+
)
52+
53+
if input_channel_layout in ("quadraphonic", "4ch", "4.0"):
54+
# Quadraphonic Channel Mapping: FL FR RL RR
55+
# front left, front right, rear left, rear right
56+
front = 1 * scale_front
57+
side1 = (sqrt(3)/2) * scale_side1
58+
side2 = (1/2) * scale_side2
59+
n = 1/(front + side1 + side2)
60+
return dict(
61+
FL = dict(FL=front*n, FR=0, BL=side1*n, BR=side2*n),
62+
FR = dict(FL=0, FR=front*n, BL=side2*n, BR=side1*n),
63+
)
64+
65+
if input_channel_layout in ("5.0", "5ch"):
66+
# 5.0 Surround Mapping: FL FC FR RL RR
67+
# front left, front center, front right, rear left, rear right
68+
# 5.1 without subwoofer (LFE)
69+
front = 1 * scale_front
70+
center = (1/sqrt(2)) * scale_center
71+
side1 = (sqrt(3)/2) * scale_side1
72+
side2 = (1/2) * scale_side2
73+
n = 2/(front + center + side1 + side2) # note: 2/
74+
return dict(
75+
FL = dict(FL=front*n, FC=center*n, FR=0, BL=side1*n, BR=side2*n),
76+
FR = dict(FL=0, FC=center*n, FR=front*n, BL=side2*n, BR=side1*n),
77+
)
78+
79+
if input_channel_layout in ("5.1", "6ch"):
80+
# 5.1 Surround Mapping: FL FC FR RL RR LFE
81+
# front left, front center, front right, rear left, rear right, LFE
82+
front = 1 * scale_front
83+
center = (1/sqrt(2)) * scale_center
84+
side1 = (sqrt(3)/2) * scale_side1
85+
side2 = (1/2) * scale_side2
86+
lfe = (1/sqrt(2)) * scale_lfe
87+
n = 2/(front + center + side1 + side2 + lfe) # note: 2/
88+
return dict(
89+
FL = dict(FL=front*n, FC=center*n, FR=0, BL=side1*n, BR=side2*n, LFE=lfe*n),
90+
FR = dict(FL=0, FC=center*n, FR=front*n, BL=side2*n, BR=side1*n, LFE=lfe*n),
91+
)
92+
93+
if input_channel_layout in ("6.1", "7ch"):
94+
# 6.1 Surround Mapping: FL FC FR SL SR RC LFE
95+
# front left, front center, front right, side left, side right, rear center, LFE
96+
# 5.1 + rear center, "rear" -> "side"
97+
front = 1 * scale_front
98+
center = (1/sqrt(2)) * scale_center
99+
side1 = (sqrt(3)/2) * scale_side1
100+
side2 = (1/2) * scale_side2
101+
rearC = (sqrt(3)/2)/sqrt(2)
102+
lfe = (1/sqrt(2)) * scale_lfe
103+
n = 2/(front + center + side1 + side2 + rearC + lfe) # note: 2/
104+
return dict(
105+
FL = dict(FL=front*n, FC=center*n, FR=0, SL=side1*n, SR=side2*n, BC=rearC*n, LFE=lfe*n),
106+
FR = dict(FL=0, FC=center*n, FR=front*n, SL=side2*n, SR=side1*n, BC=rearC*n, LFE=lfe*n),
107+
)
108+
109+
if input_channel_layout in ("7.1", "8ch"):
110+
# 7.1 Surround Mapping: FL FC FR SL SR RL RR LFE
111+
# front left, front center, front right, side left, side right, rear left, rear right, LFE
112+
# 6.1 + RC -> RL RR
113+
front = 1 * scale_front
114+
center = (1/sqrt(2)) * scale_center
115+
side1 = (sqrt(3)/2) * scale_side1
116+
side2 = (1/2) * scale_side2
117+
lfe = (1/sqrt(2)) * scale_lfe
118+
n = 2/(front + center + 2*side1 + 2*side2 + lfe) # note: 2/
119+
return dict(
120+
FL = dict(FL=front*n, FC=center*n, FR=0, SL=side1*n, SR=side2*n, BL=side1*n, BR=side2*n, LFE=lfe*n),
121+
FR = dict(FL=0, FC=center*n, FR=front*n, SL=side2*n, SR=side1*n, BL=side2*n, BR=side1*n, LFE=lfe*n),
122+
)
123+
124+
raise ValueError(f"unknown input_channel_layout {input_channel_layout}")
125+
126+
127+
128+
def get_ffmpeg_audio_filter_for_downmix_to_stereo(input_channel_layout, **kwargs):
129+
coefficients = get_coefficients_for_downmix_to_stereo(input_channel_layout, **kwargs)
130+
return "pan=stereo|" + "|".join(map(lambda kv: kv[0] + "=" + "+".join(map(lambda kv2: f"{kv2[1]}*{kv2[0]}", kv[1].items())), coefficients.items()))
131+
132+
133+
134+
if __name__ == "__main__":
135+
import sys
136+
input_channel_layout = sys.argv[1]
137+
#print(get_coefficients_for_downmix_to_stereo(input_channel_layout))
138+
print(get_ffmpeg_audio_filter_for_downmix_to_stereo(input_channel_layout))

0 commit comments

Comments
 (0)