|
UlfZibis
(Themenstarter)
Anmeldungsdatum: 13. Juli 2011
Beiträge: 3351
|
Marc_BlackJack_Rintsch schrieb: Kannst ja ein Ticket bei Python aufmachen ob die eventuell so etwas einbauen, mit find als ”Präzedenzfall”. Insbesondere das da auch nach dem Muster gefiltert wird, verhindert das man da einfach „Nimm doch chain()“ sagen kann.
Ware eine Idee. Mal gucken ob ich dafür Zeit finde, denn ich müsste mich dann da ja auch noch registrieren und in deren Workflow einarbeiten. Bei find wird das ja über -mindepth und -maxdepth gesteuert (und ls kennt die Option -a). Weil damit mehr Möglichkeiten gegeben wären, wäre das vielleicht besser vorzuschlagen statt include_self.
Was soll denn [start_dir] + Path(start_dir).rglob("*") bedeuten? Sollte da a) das erste Argument in einen Iterator umgewandelt werden der mit dem zweiten Argument verkettet wird? Oder b) eine Liste mit den verketteten Inhalten der beiden iterierbaren Objekte erstellt werden?
Ja genau das kann der Interpreter nicht wissen. Deshalb hatte ich mit verschiedenen Klammerungen und Typumwandlungen rumprobiert, doch kam, zu keiner Lösung.
# b) Liste mit allem.
paths = [start_dir, *Path(start_dir).rglob("*")]
}}}
Ach der Stern ist der Trick um aus einem "generator" wieder eine Liste zu machen. Da bin ich _hier_ natürlich nicht drauf gekommen. Tut aber leider nicht, denn am Ende brauche ich ja eine Liste von Path-Objekten und nicht von Strings: $ ./volume-detect.py "test"
File "/mnt/Daten/Users/ich/Momentum/./volume-detect.py", line 39
path for path in [start_path + *start_path.rglob("*")] if path.is_dir()
^
SyntaxError: invalid syntaxEdit: Ich sehe nicht was an dem + so toll sein soll das man auf chain() verzichtet. Mich stört da der Speicherverbrauch, ...
Gutes Argument. Allerdings könnte der Python-Optimierer evtl. merken, dass hier nicht wirklich eine neue Liste gebaut muss, sondern nur sequentiell iteriert.
|
|
UlfZibis
(Themenstarter)
Anmeldungsdatum: 13. Juli 2011
Beiträge: 3351
|
Marc_BlackJack_Rintsch schrieb: @UlfZibis: Ich verstehe die Kritik am self nicht wirklich. Das ist ein Argument das eben erwartet wird, das ist nicht wirklich redundant.
Ja unter Betrachtung all der wirklich reichhaltigen Möglichkeiten in Python (danke für die fleißige Auflistung und Erklärung) ist das vielleicht so. Aber zumindest für den einfachsten Fall einer Klassendefinition fände ich es schön, wenn die Angabe von self sowohl in der Argumentliste als auch für Membervariablen optional wäre.
Da finde ich JavaScript deutlich unschöner. def __iter__(self): yield 42 vs. *[Symbol.iterator]() { yield 42; }.
Ja JavaSkript ist an vielen Stellen auch hässlich, aber ich bezog mich immer auf Java.
Und iter(a) + iter(b) finde ich jetzt auch nicht wirklich besser als chain(a, b).
Erspart halt den Import und das Lernen einer zusätzlichen Bibliothek.
VarArgs für sorted()? Da stehe ich gerade auf dem Schlauch? Meinst Du sorted(a, b, c) statt sorted(chain(a, b, c))?
Ja genau.
So etwas wie ein Modul kennt Java nicht.
Doch, kennt Java mittlerweile auch, ist aber ziemlich kompliziert.
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
@UlfZibis: Ich würde sagen itertools gehört zu den Modulen, ohne die man nicht wirklich auskommt. Das muss man sowieso lernen. Ebenso functools. contextlib enthält sehr nützliche Sachen für die with-Anweisung, also wenn man Ressourcen ordentlich hinter sich aufräumen will, ohne das alles über try/finally zu regeln, beziehungsweise wenn man wiederholten Code herausziehen möchte der sich nicht wiederholenden Code ”umschliesst”. Beim operator-Modul reicht es dessen Existenz zu kennen — es gibt dort für jeden Operator eine Funktion die das gleiche macht. Das sind alles Module die sehr allgemeine Werkzeuge enthalten, die man sich sonst nachprogrammieren würde, oder ohne sie umständlicheren Code schreiben müsste. Externe Bibliotheken die ich fast immer importiere sind attrs und more_itertools. attrs erleichtert das schreiben von Klassen mit weniger „boilerplate code“. Das self bei Methoden wird man nicht los, aber in der Regel die komplette __init__()-Methode und noch einiges mehr.
| from attrs import field, frozen
@frozen
class Volume_:
mean = field()
max = field()
|
Da müsste man händisch ungefähr das hier schreiben:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | class Volume:
__slots__ = __match_args__ = ("mean", "max")
def __init__(self, mean, max):
object.__setattr__(self, "mean", mean)
object.__setattr__(self, "max", max)
def __setattr__(self, name, value):
raise AttributeError(
f"can't set attributes on {self.__class__.__name__} instances"
)
def __repr__(self):
return (
f"{self.__class__.__name__}(mean={self.mean!r}, max={self.max!r})"
)
def __eq__(self, other):
return self.mean == other.mean and self.max == other.max
def __hash__(self):
return hash((self.mean, self.max))
|
Dann hat man fast das namedtuple() abzüglich dem Verhalten vom Tupel (Länge, Indexzugriff, iterierbar, …) und der totalen Ordnung. Für letzteres müsste man noch eine Methode implementieren, beispielsweise __lt__(self, other) für <= und die Klasse mit functools.total_ordering() dekorieren, damit die anderen Vergleichsmethoden nachgerüstet werden. Bei attrs würde es ausreichen statt @frozen mit @frozen(order=True) zu dekorieren.
|
|
UlfZibis
(Themenstarter)
Anmeldungsdatum: 13. Juli 2011
Beiträge: 3351
|
Nach freundlicher Unterstützung hier mal mein aktueller Stand des Skripts zur Audioauswertung. Eine Reihe von statistischen Werten werden erhoben. Vielleicht ist es ja für andere nützlich : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230 | #!/usr/bin/env python3
import re
import subprocess
import sys
from pathlib import Path
def file(file_path):
result = subprocess.check_output(
["file", "--brief", str(file_path)], text=True)
return re.split(r",\s", (result[0:-1] if result[-1:] == '\n' else result))
def mediainfo(file_path):
result = subprocess.check_output(
["mediainfo", str(file_path)], text=True)
props = re.split(r"\n", result)
general = dict()
video = dict()
audio = dict()
image = dict()
map = general
for prop in props :
key_value = re.split(r"\s*:\s", prop)
if len(key_value) > 0 and key_value[0] == "" : continue
if len(key_value) == 1 :
match key_value[0] :
case "General": map = general
case "Video": map = video
case "Audio": map = audio
case "Image": map = image
case "Image #1": map = image # there may be more images
case _: map = None; print("mediainfo: unknown section:", key_value[0])
continue
if map != None : map[key_value[0]] = key_value[1]
return general, video, audio, image
# print("[General]")
# for key in general :
# print(f"{key}={general[key]}")
# print("[Video]")
# for key in video :
# print(f"{key}={video[key]}")
# print("[Audio]")
# for key in audio :
# print(f"{key}={audio[key]}")
# print("[Image]")
# for key in image :
# print(f"{key}={image[key]}")
# ToDo: Use pymediainfo module instead
def detect_volumes(file_path):
result = subprocess.run(
["ffmpeg", "-hide_banner", "-nostdin",
*("-i", str(file_path)),
*("-af", "volumedetect"), *("-f", "null"), "/dev/null"],
capture_output=True,
check=True
)
return (
float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
float(re.search(rb"max_volume: (\S*)", result.stderr)[1])
)
# volumes = { # use dictionary (= HashMap)
# match[1]: float(match[2])
# for match in re.finditer(rb"m(ean|ax)_volume: (\S*)", result.stderr)
# }
# return (volumes[b"ean"], volumes[b"ax"])
def main():
if len(sys.argv) <= 3:
print("Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]")
sys.exit(1)
start_path = Path(sys.argv[1])
extension = sys.argv[2]
threshold = float(sys.argv[3])
verbose = "--verbose" in sys.argv[4:]
num_albums = 0
album_mean_sum = 0.0
album_mean_min = 0.0
album_mean_max = -128.0
album_peak_sum = 0.0
album_peak_min = 0.0
album_peak_max = -128.
albums_with_low_volume = []
albums_with_mixed_mode = []
albums_with_mixed_constant_rate = []
bitrate_unknown = []
files_with_errors = []
csv = open("mediainfo_albums"+extension+".csv", "w")
csv_verbose = open("mediainfo_files"+extension+".csv", "w")
directory_paths = [start_path] + sorted(
path for path in start_path.rglob("*") if path.is_dir()
)
for directory_path in directory_paths :
num_files = 0
mean_sum = 0.0
mean_min = 0.0
mean_max = -128.0
peak_sum = 0.0
peak_min = 0.0
peak_max = -128.0
rate_mode = "None"
rate_sum = 0.0
rate = 0.0
rate_old = 0.0
rate_mixed = False
# file_paths = sorted(
# directory_path.glob("*"+extension, case_sensitive=False)
# # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
# )
file_paths = sorted(path for path in
directory_path.glob("*"+extension, case_sensitive=False)
# directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
if (extension == ".flac" or not path.with_suffix(".flac").exists())
)
for file_path in file_paths :
if verbose :
print(file_path)
try:
general, video, audio, image = mediainfo(file_path)
if verbose :
print(f" Format={audio["Format"]}, Format settings={audio.get("Format settings")},",
f"Channel(s)={audio.get("Channel(s)")},",
f"Bit rate mode={audio["Bit rate mode"]}, Bit rate={audio.get("Bit rate")},",
f"Sampling rate={audio["Sampling rate"]}, Bit depth={audio.get("Bit depth")}")
FLAC = "FLAC" in audio["Format"]
MP3 = ("MPEG Audio" in audio["Format"] and "Layer 3" in audio["Format profile"]) or extension == ".mp3"
if FLAC or MP3 :
mean_volume, max_volume = detect_volumes(file_path)
mean_sum += mean_volume
mean_min = min(mean_min, mean_volume)
mean_max = max(mean_max, mean_volume)
peak_sum += max_volume
peak_min = min(peak_min, max_volume)
peak_max = max(peak_max, max_volume)
rate_mode = str(audio.get("Bit rate mode")) if num_files == 0 else\
rate_mode if str(audio.get("Bit rate mode")) == rate_mode else "Mixed"
if entry := audio.get("Bit rate") :
rate = float(re.sub(r"[^-+\d.]", "", entry))
rate_mixed = rate_mixed or (num_files > 0 and rate != rate_old)
rate_sum += (rate_old := rate)
else :
bitrate_unknown.append(file_path)
num_files += 1
if verbose :
tail = " ("+str(audio.get("Bit rate"))+", "+str(audio.get("Bit rate mode"))+")"
print(" mean volume: %5.1f dB, peak volume: %5.1f dB%s"
%(mean_volume, max_volume, tail))
# print(f" mean volume: {mean_volume: 5.1f} dB,"
# f" max volume: {max_volume: 5.1f} dB")
# ToDo: Write values to csv files
else :
print (file(file_path))
except subprocess.CalledProcessError as error :
files_with_errors.append((file_path, error.returncode))
if file_paths :
num_albums += 1
print(len(file_paths), "files,", len(file_paths) - num_files,
"errors in:", directory_path)
if not extension in [".flac", ".mp3"] :
if verbose : print(); continue
if num_files :
mean_average = mean_sum / num_files
peak_average = peak_sum / num_files
rate_average = rate_sum / num_files
print("Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB (%1.0f, %s)"
%(mean_average, mean_min, mean_max, peak_average, peak_min, peak_max, rate_average, rate_mode))
album_mean_sum += mean_average
album_mean_min = min(album_mean_min, mean_min)
album_mean_max = max(album_mean_max, mean_max)
album_peak_sum += peak_average
album_peak_min = min(album_peak_min, peak_min)
album_peak_max = max(album_peak_max, peak_max)
if peak_max < threshold :
albums_with_low_volume.append(
(directory_path, mean_sum / num_files, peak_min, peak_max))
if rate_mode == "Mixed" : albums_with_mixed_mode.append((directory_path, rate_average))
if rate_mixed and rate_mode == "Constant" :
albums_with_mixed_constant_rate.append((directory_path, rate_average))
if verbose : print()
# ToDo: Write values to csv files
print()
print("Directories total: ", len(directory_paths))
print("Albums with "+extension+" files:", num_albums)
if num_albums :
album_mean_average = album_mean_sum / num_albums
album_peak_average = album_peak_sum / num_albums
print("All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
%(album_mean_average, album_mean_min, album_mean_max, album_peak_average, album_peak_min, album_peak_max))
if albums_with_low_volume :
print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
for album, average_mean, peak_min, peak_max in albums_with_low_volume:
print("%s\n"
" volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
%(album, average_mean, peak_min, peak_max))
if albums_with_mixed_mode :
print(f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):")
for album, rate_average in albums_with_mixed_mode:
print(album, "==>", rate_average, "kb/s")
if albums_with_mixed_constant_rate :
print(f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):")
for album, rate_average in albums_with_mixed_constant_rate:
print(album, "==>", rate_average, "kb/s")
if bitrate_unknown :
print(f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):")
for file_path in bitrate_unknown:
print(file_path)
if files_with_errors :
print(f"\n{extension} files with errors: ({len(files_with_errors)}):")
for file_path, returncode in files_with_errors:
print(file_path, "==> return code:", returncode)
if csv and not csv.closed : csv.close()
if csv_verbose and not csv_verbose.closed : csv_verbose.close()
if __name__ == "__main__" :
main()
|
Noch 2 weitere Verbesserungen stehen aus:
Nutzung der pymediainfo-Bibliothek, statt selbst zu parsen. Abspeichern der Daten in CSV-Dateien, um per Libre-Office Calc weitere Berechnungen anstellen zu können.
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
@UlfZibis: Bei regulären Ausdrücken schaue ich immer ob es nicht auch ohne geht. file() zum Beispiel, da könnte die letzte Zeile auch einfach das hier sein: return [part.strip() for part in result.split(",")]. Und für Muster die einfach nur aus literalen Zeichen bestehen, braucht man das auch nicht: re.split(r"\n", text) ist doch einfach text.split("\n"). Beziehungsweise text.splitlines() wenn man sich nicht selber um den Fall kümmern will das ganz am Ende eine leere Zeichenkette übrigbleibt wenn der Text mit einem Zeilenendezeichen endet. map ist der Name einer eingebauten, recht nützlichen Funktion, den sollte man nicht an andere Werte binden..
Das Ergebnis von re.split() und auch str.split() hat immer mindestens ein Element als Ergebnis, das muss man also nicht prüfen. Auch hier würde ich keinen regulären Ausdruck verwenden, und auch nicht split() sondern partition(). Denn bei split() müsste man das auf maximal eine Trennstelle begrenzen und dann immer noch mit den ”Sonderfällen” klar kommen. partition() hat immer drei Ergebnisse. match/case wird hier ”missbraucht”. Das ist ein Mittel um Strukturen zu matchen und Namen Werte zuzuweisen. Wenn man da nicht wenigstens in einem case einen Namen sinnvoll zuweist, ist das so ähnlich wie mit regulären Ausdrücken ein Muster zu verwenden das nur aus literalen Zeichen besteht. Kanonen und Spatzen. Das wäre einfach if/elif/else oder man operiert hier gleich mit einem Wörterbuch das die Vergleichswerte auf die Datenstruktur abbildet die man da auswählt. Ich habe das mal auf eine tatsächliche Verwendung von match/case umgeschrieben.
Dateien immer mit with öffnen. Textdateien mit der Kodierung die man erwartet/haben will, und bei CSV-Dateien noch newlines="" als Argument, weil sich das csv-Modul da drum kümmert, dass egal welche Plattform, immer die gleichen verwendet werden. Da steht noch mal hart kodiert ein Test mit ".flac" im Code obwohl man die Dateiendung ja als Kommandozeilenargument übergibt‽ Vom mediainfo-Ergebnis wird nur eins von vier Elementen tatsächlich verwendet‽ Die Hauptfunktion ist IMHO deutlich zu lang und macht zu viel. Und auch Sachen mit kopiertem und leicht angepasstem Code, wo man Funktionen und/oder Schleifen verwenden kann. Wie immer ungetestet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321 | #!/usr/bin/env python3
import csv
import re
import subprocess
import sys
from pathlib import Path
def file(file_path):
result = subprocess.check_output(
["file", "--brief", str(file_path)], text=True
)
return [part.strip() for part in result.split(",")]
def mediainfo(file_path):
result = subprocess.check_output(["mediainfo", str(file_path)], text=True)
section_names = ["General", "Video", "Audio", "Image"]
results = {section_name: {} for section_name in section_names}
results["Image #1"] = results["Image"]
mapping = results["General"]
for line in result.splitlines():
match [part.strip() for part in line.partition(":")]:
case "", "", "":
pass # Ignore empty lines.
case section_name, "", "":
mapping = results.get(section_name)
if mapping is None:
print("mediainfo: unknown section:", section_name)
case key, ":", value:
if mapping is not None:
mapping[key] = value
case _:
assert False, f"should never happen: {line!r}"
return [results[section_name] for section_name in section_names]
# ToDo: Use pymediainfo module instead
def detect_volumes(file_path):
result = subprocess.run(
[
"ffmpeg",
"-hide_banner",
"-nostdin",
*("-i", str(file_path)),
*("-af", "volumedetect"),
*("-f", "null"),
"/dev/null",
],
capture_output=True,
check=True,
)
return (
float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
float(re.search(rb"max_volume: (\S*)", result.stderr)[1]),
)
def main():
if len(sys.argv) <= 3:
print(
"Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]"
)
sys.exit(1)
start_path = Path(sys.argv[1])
extension = sys.argv[2]
threshold = float(sys.argv[3])
verbose = "--verbose" in sys.argv[4:]
album_count = 0
album_mean_sum = 0.0
album_mean_min = 0.0
album_mean_max = -128.0
album_peak_sum = 0.0
album_peak_min = 0.0
album_peak_max = -128.0
albums_with_low_volume = []
albums_with_mixed_mode = []
albums_with_mixed_constant_rate = []
bitrate_unknown = []
files_with_errors = []
with (
open(
f"mediainfo_albums{extension}.csv",
"w",
encoding="utf-8",
newline="",
) as csv_file,
open(
f"mediainfo_files{extension}.csv",
"w",
encoding="utf-8",
newline="",
) as verbose_csv_file,
):
csv_writer = csv.writer(csv_file)
verbose_csv_writer = csv.writer(verbose_csv_file)
directory_paths = [start_path] + sorted(
path for path in start_path.rglob("*") if path.is_dir()
)
for directory_path in directory_paths:
file_count = 0
mean_sum = 0.0
mean_min = 0.0
mean_max = -128.0
peak_sum = 0.0
peak_min = 0.0
peak_max = -128.0
rate_mode = "None"
rate_sum = 0.0
rate = 0.0
old_rate = 0.0
rate_mixed = False
file_paths = sorted(
path
for path in directory_path.glob(
"*" + extension, case_sensitive=False
)
#
# TODO Wozu ist das hier gut?
#
if (
extension == ".flac"
or not path.with_suffix(".flac").exists()
)
)
for file_path in file_paths:
if verbose:
print(file_path)
try:
_general, _video, audio, _image = mediainfo(file_path)
if verbose:
print(
" ",
", ".join(
f"{key}={audio.get(key)}"
for key in [
"Format",
"Format settings",
"Channel(s)",
"Bit rate mode",
"Bit rate",
"Sampling rate",
"Bit depth",
]
),
)
is_flac = "FLAC" in audio["Format"]
is_mp3 = (
"MPEG Audio" in audio["Format"]
and "Layer 3" in audio["Format profile"]
) or extension == ".mp3"
if is_flac or is_mp3:
mean_volume, max_volume = detect_volumes(file_path)
mean_sum += mean_volume
mean_min = min(mean_min, mean_volume)
mean_max = max(mean_max, mean_volume)
peak_sum += max_volume
peak_min = min(peak_min, max_volume)
peak_max = max(peak_max, max_volume)
rate_mode = (
audio.get("Bit rate mode")
if file_count == 0
else (
rate_mode
if audio.get("Bit rate mode") == rate_mode
else "Mixed"
)
)
if entry := audio.get("Bit rate"):
rate = float(re.sub(r"[^-+\d.]", "", entry))
rate_mixed = rate_mixed or (
file_count > 0 and rate != old_rate
)
rate_sum += (old_rate := rate)
else:
bitrate_unknown.append(file_path)
file_count += 1
if verbose:
tail = (
f" ({audio.get('Bit rate')}),"
f" {audio.get('Bit rate mode')})"
)
print(
" mean volume: %5.1f dB, peak volume: %5.1f dB%s"
% (mean_volume, max_volume, tail)
)
# ToDo: Write values to csv files
else:
print(file(file_path))
except subprocess.CalledProcessError as error:
files_with_errors.append((file_path, error.returncode))
if file_paths:
album_count += 1
print(
len(file_paths),
"files,",
len(file_paths) - file_count,
"errors in:",
directory_path,
)
if extension not in [".flac", ".mp3"] and verbose:
print()
else:
if file_count:
mean_average = mean_sum / file_count
peak_average = peak_sum / file_count
rate_average = rate_sum / file_count
print(
"Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB (%1.0f, %s)"
% (
mean_average,
mean_min,
mean_max,
peak_average,
peak_min,
peak_max,
rate_average,
rate_mode,
)
)
album_mean_sum += mean_average
album_mean_min = min(album_mean_min, mean_min)
album_mean_max = max(album_mean_max, mean_max)
album_peak_sum += peak_average
album_peak_min = min(album_peak_min, peak_min)
album_peak_max = max(album_peak_max, peak_max)
if peak_max < threshold:
albums_with_low_volume.append(
(
directory_path,
mean_sum / file_count,
peak_min,
peak_max,
)
)
if rate_mode == "Mixed":
albums_with_mixed_mode.append(
(directory_path, rate_average)
)
if rate_mixed and rate_mode == "Constant":
albums_with_mixed_constant_rate.append(
(directory_path, rate_average)
)
if verbose:
print()
# ToDo: Write values to csv files
print()
print("Directories total: ", len(directory_paths))
print("Albums with", extension, "files:", album_count)
if album_count:
album_mean_average = album_mean_sum / album_count
album_peak_average = album_peak_sum / album_count
print(
"All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
% (
album_mean_average,
album_mean_min,
album_mean_max,
album_peak_average,
album_peak_min,
album_peak_max,
)
)
if albums_with_low_volume:
print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
for (
album,
average_mean,
peak_min,
peak_max,
) in albums_with_low_volume:
print(
"%s\n"
" volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
% (album, average_mean, peak_min, peak_max)
)
if albums_with_mixed_mode:
print(
f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):"
)
for album, rate_average in albums_with_mixed_mode:
print(album, "==>", rate_average, "kb/s")
if albums_with_mixed_constant_rate:
print(
f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):"
)
for album, rate_average in albums_with_mixed_constant_rate:
print(album, "==>", rate_average, "kb/s")
if bitrate_unknown:
print(
f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):"
)
for file_path in bitrate_unknown:
print(file_path)
if files_with_errors:
print(
f"\n{extension} files with errors: ({len(files_with_errors)}):"
)
for file_path, returncode in files_with_errors:
print(file_path, "==> return code:", returncode)
if __name__ == "__main__":
main()
|
|
|
UlfZibis
(Themenstarter)
Anmeldungsdatum: 13. Juli 2011
Beiträge: 3351
|
Ich bin beeindruckt wie gut Du die Sprachmittel kennst und damit jonglierst. Ich kenne mich halt noch nicht so aus und kenne auch all die zur Verfügung stehenden Bibliotheken nicht. Marc_BlackJack_Rintsch schrieb: @UlfZibis: Bei regulären Ausdrücken schaue ich immer ob es nicht auch ohne geht. file() zum Beispiel, da könnte die letzte Zeile auch einfach das hier sein: return [part.strip() for part in result.split(",")].
Gute Idee, auch wenn das dann zu 3-fachem (Teil-)Scannen jeder Zeile führt.
map ist der Name einer eingebauten, recht nützlichen Funktion, den sollte man nicht an andere Werte binden..
Hab' ich mir deshalb auch nur innerhalb der Funktion erlaubt.
Dateien immer mit with öffnen.
Das dachte ich mir, dass das kommt. Da ich es aber unschön fand, dafür eine weitere Einrückung für fast den gesammten Code spendieren zu müssen, dachte ich, für mein Einmal-Programm reicht's auch ohne with.
Textdateien mit der Kodierung die man erwartet/haben will, und bei CSV-Dateien noch newlines="" als Argument, weil sich das csv-Modul da drum kümmert, dass egal welche Plattform, immer die gleichen verwendet werden.
Oh das ist sehr interessant, dass Du mir da eine Vorlage lieferst, denn bisher hab' ich die CSV selbst gebaut. Spannend ist aber auch, dass man das Datei-Objekt auch außerhalb des with-Blocks verwenden kann. In Java wird eine Datei geschlossen, sobald so ein Block verlassen wird, was ich auch logisch finde, weil damit auch bestimmt ist, wann die Datei geschlossen wird. Hier muss die Datei ja bis zum Ende der Laufzeit des Programm offen bleiben, weil sie später noch benutzt werden könnte. Da Dateien aber wohl sicher auch in Python bei Programm-Ende alle automatisch geschlossen werden, verstehe ich dann den Sinn von with nun nicht ganz.
Da steht noch mal hart kodiert ein Test mit ".flac" im Code obwohl man die Dateiendung ja als Kommandozeilenargument übergibt‽
Ja in wenigen FLAC-Alben hab' ich vom Rumprobieren her schon komprimierte MP3-Duplikate drin. Die will ich beim Untersuchen bzgl. MP3 aussparen. Kann in der Endfassung dann weg.
Vom mediainfo-Ergebnis wird nur eins von vier Elementen tatsächlich verwendet‽
Bisher ja. Meinen selbstgebauten Scanner habe ich aber schon wieder rausgeschmissen, da ich ja das pymediainfo-Modul entdeckt habe.
Die Hauptfunktion ist IMHO deutlich zu lang und macht zu viel. Und auch Sachen mit kopiertem und leicht angepasstem Code, wo man Funktionen und/oder Schleifen verwenden kann.
Ja da ließe sich so manches verschönern.
Wie immer ungetestet:
Die Garantie für Fehlerfreiheit 😉 , danke noch mal sehr. Dann hier mal mein jetziger Stand (Argumente in die Senkrechte zu verteilen mag ich nicht so. Auf 19 cm Bildschirmhöhe muss ich dann zu viel scrollen. Aber in der Breite ist auf dem 16:9-Sehschlitz (wer so'n Mist durchgesetzt?) genug Platz) (noch nicht alles von Dir eingebaut): 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201 | #!/usr/bin/env python3
import re
import subprocess
import sys
from pathlib import Path
from pprint import pprint
from pymediainfo import MediaInfo
def file(file_path):
result = subprocess.check_output(
["file", "--brief", str(file_path)], text=True)
return [part.strip() for part in result.split(",")]
def detect_volumes(file_path):
result = subprocess.run(
["ffmpeg", "-hide_banner", "-nostdin",
*("-i", str(file_path)),
*("-af", "volumedetect"), *("-f", "null"), "/dev/null"],
capture_output=True,
check=True
)
return (
float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
float(re.search(rb"max_volume: (\S*)", result.stderr)[1])
)
# volumes = { # use dictionary (= HashMap)
# match[1]: float(match[2])
# for match in re.finditer(rb"m(ean|ax)_volume: (\S*)", result.stderr)
# }
# return (volumes[b"ean"], volumes[b"ax"])
def main():
if len(sys.argv) <= 3:
print("Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]")
sys.exit(1)
start_path = Path(sys.argv[1])
extension = sys.argv[2]
threshold = float(sys.argv[3])
verbose = "--verbose" in sys.argv[4:]
num_albums = 0
album_mean_sum = 0.0
album_mean_min = 0.0
album_mean_max = -128.0
album_peak_sum = 0.0
album_peak_min = 0.0
album_peak_max = -128.
albums_with_low_volume = []
albums_with_mixed_mode = []
albums_with_mixed_constant_rate = []
bitrate_unknown = []
files_with_errors = []
csv = open("mediainfo_albums"+extension+".csv", "w")
csv_verbose = open("mediainfo_files"+extension+".csv", "w")
csv.write("No;Path;Size;Mode;Bit rate;\n")
csv_verbose.write("Album;No;Path;Size;Mode;Bit rate;\n")
directory_paths = [start_path] + sorted(
path for path in start_path.rglob("*") if path.is_dir()
)
for directory_path in directory_paths :
num_files = 0
mean_sum = 0.0
mean_min = 0.0
mean_max = -128.0
peak_sum = 0.0
peak_min = 0.0
peak_max = -128.0
rate_mode = "None"
rate_sum = 0.0
rate = 0.0
rate_old = 0.0
rate_mixed = False
# file_paths = sorted(
# directory_path.glob("*"+extension, case_sensitive=False)
# # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
# )
if not (file_paths := sorted(path for path in
directory_path.glob("*"+extension, case_sensitive=False)
# directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
if (extension == ".flac" or not path.with_suffix(".flac").exists())
)) : continue
num_albums += 1
for file_path in file_paths :
if verbose :
print(file_path)
try:
media_info = MediaInfo.parse(file_path)
# general = media_info.general_tracks[0]
audio = media_info.audio_tracks[0]
# print(f" audio size: {audio.stream_size} bytes, file size: {general.file_size} bytes.")
# pprint(audio.to_data())
if verbose :
print(f" Format={audio.format}, Format settings={audio.format_settings},",
f"Format profile={audio.format_profile}, Channel(s)={audio.channel_s},",
f"Bit rate mode={audio.bit_rate_mode}, Bit rate={audio.bit_rate},",
f"Sampling rate={audio.sampling_rate}, Bit depth={audio.bit_depth}")
FLAC = "FLAC" in audio.format
MP3 = ("MPEG Audio" in audio.format and "Layer 3" in audio.format_profile) or extension == ".mp3"
if FLAC or MP3 :
mean_volume, max_volume = detect_volumes(file_path)
mean_sum += mean_volume
mean_min = min(mean_min, mean_volume)
mean_max = max(mean_max, mean_volume)
peak_sum += max_volume
peak_min = min(peak_min, max_volume)
peak_max = max(peak_max, max_volume)
rate_mode = audio.bit_rate_mode if num_files == 0 else\
rate_mode if audio.bit_rate_mode == rate_mode else "Mixed"
if entry := audio.bit_rate :
rate = float(entry)
rate_mixed = rate_mixed or (num_files > 0 and rate != rate_old)
rate_sum += (rate_old := rate)
else :
bitrate_unknown.append(file_path)
num_files += 1
if verbose :
tail = " ("+str(audio.bit_rate)+", "+str(audio.bit_rate_mode)+")"
print(" mean volume: %5.1f dB, peak volume: %5.1f dB%s"
%(mean_volume, max_volume, tail))
# print(f" mean volume: {mean_volume: 5.1f} dB,"
# f" max volume: {max_volume: 5.1f} dB")
# ToDo: Write values to csv files
csv_verbose.write(f'{num_albums};{num_files};"{file_path.name}";{audio.stream_size};{audio.bit_rate_mode};{audio.bit_rate};;\n')
else :
print (file(file_path))
except subprocess.CalledProcessError as error :
files_with_errors.append((file_path, error.returncode))
print(len(file_paths), "files,", len(file_paths) - num_files,
"errors in:", directory_path)
if not extension in [".flac", ".mp3"] :
if verbose : print(); continue
if num_files :
mean_average = mean_sum / num_files
peak_average = peak_sum / num_files
rate_average = rate_sum / num_files
print("Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB (%1.0f, %s)"
%(mean_average, mean_min, mean_max, peak_average, peak_min, peak_max, rate_average, rate_mode))
album_mean_sum += mean_average
album_mean_min = min(album_mean_min, mean_min)
album_mean_max = max(album_mean_max, mean_max)
album_peak_sum += peak_average
album_peak_min = min(album_peak_min, peak_min)
album_peak_max = max(album_peak_max, peak_max)
if peak_max < threshold :
albums_with_low_volume.append(
(directory_path, mean_sum / num_files, peak_min, peak_max))
if rate_mode == "Mixed" : albums_with_mixed_mode.append((directory_path, rate_average))
if rate_mixed and rate_mode == "CBR" :
albums_with_mixed_constant_rate.append((directory_path, rate_average))
if verbose : print()
csv.write(f'{num_albums};"{directory_path}";{num_files};{rate_mode};{int(rate_average)};;\n')
# ToDo: Write values to csv files
print()
print("Directories total: ", len(directory_paths))
print("Albums with "+extension+" files:", num_albums)
if num_albums :
album_mean_average = album_mean_sum / num_albums
album_peak_average = album_peak_sum / num_albums
print("All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
%(album_mean_average, album_mean_min, album_mean_max, album_peak_average, album_peak_min, album_peak_max))
if albums_with_low_volume :
print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
for album, average_mean, peak_min, peak_max in albums_with_low_volume:
print("%s\n"
" volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
%(album, average_mean, peak_min, peak_max))
if albums_with_mixed_mode :
print(f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):")
for album, rate_average in albums_with_mixed_mode:
print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
if albums_with_mixed_constant_rate :
print(f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):")
for album, rate_average in albums_with_mixed_constant_rate:
print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
if bitrate_unknown :
print(f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):")
for file_path in bitrate_unknown:
print(file_path)
if files_with_errors :
print(f"\n{extension} files with errors: ({len(files_with_errors)}):")
for file_path, returncode in files_with_errors:
print(file_path, "==> return code:", returncode)
if csv and not csv.closed : csv.close()
if csv_verbose and not csv_verbose.closed : csv_verbose.close()
if __name__ == "__main__" :
main()
|
Gibt's in Python eigentlich auch for-Schleifen, worin der Index direkt definiert und behandelt wird? Also sowas ähnliches wie in C und Java: | for (int i=0; i<MAX; i++) {
// i kann hier benutzt werden
}
|
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
Die zusätzliche Einrückung ist IMHO hauptsächlich ein ”Problem” weil die Funktion sowieso schon zu viel Code und zu viele Ebenen enthält. Man kann das Dateiobjekt nicht wirklich ausserhalb des with-Blocks verwenden. Das ist ja der Sinn, dass die Datei geschlossen wird, egal wann und warum der Block verlassen wird. Dass das Betriebssystem am Ende eines Prozesses auch dessen Dateien schliesst, sollte einen nicht dazu verleiten selber ordentlich mit Ressourcen umzugehen. Da machen auch ”Einmalskripte” keinen Unterschied. Wenn man das immer sauber macht, kann man es nicht vergessen wenn es wichtig(er) ist. Und ein Einmalskript ist das ja schon nicht mehr, *die* stellt man nicht anderen zur Verfügung und löscht die nach Benutzung. Meine Erfahrung: Immer wenn man das nicht löscht, benutzt man das auch irgendwann wieder. Meistens nach einer längeren Zeit, weshalb das so geschrieben werden sollte, wie ein normales Programm, das man nach der Zeit dann auch selber wieder verstehen möchte, und das auch mit anderen Versionen der verwendeten Bibliotheken und externen Programme noch arbeitet und merkt wenn sich da was geändert hat. Weswegen ich persönlich halt gerne parsen von Ausgaben so schreibe, dass das auffällt wenn das Format plötzlich ein bisschen anders ist und die Zahl dann vielleicht nicht mehr kb/s sondern mb/s ist, oder andere Tausendertrenner verwendet werden, usw. Da habe ich mir jetzt umsonst Gedanken um die mediainfo-Ausgabe gemacht. 😎 Hatte mir das mal angeschaut und da hätte ich statt der Ausgabe für menschliche Leser mit den Tausendertrennern, die ja vielleicht auch noch von den LOCALE-Einstellungen abhängen können, eine XML-Ausgabe angefordert. Beispiel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | <?xml version="1.0" encoding="UTF-8"?>
<MediaInfo xmlns="https://mediaarea.net/mediainfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://mediaarea.net/mediainfo https://mediaarea.net/mediainfo/mediainfo_2_0.xsd" version="2.0">
<creatingLibrary version="17.12" url="https://mediaarea.net/MediaInfo">MediaInfoLib</creatingLibrary>
<media ref="test.flac">
<track type="General">
<AudioCount>1</AudioCount>
<FileExtension>flac</FileExtension>
<Format>FLAC</Format>
<FileSize>18012074</FileSize>
<Duration>279.870</Duration>
<OverallBitRate_Mode>VBR</OverallBitRate_Mode>
<OverallBitRate>514870</OverallBitRate>
<StreamSize>0</StreamSize>
<File_Modified_Date>UTC 2024-05-20 22:52:47</File_Modified_Date>
<File_Modified_Date_Local>2024-05-21 00:52:47</File_Modified_Date_Local>
</track>
<track type="Audio">
<Format>FLAC</Format>
<Duration>279.870</Duration>
<BitRate_Mode>VBR</BitRate_Mode>
<BitRate>514618</BitRate>
<Channels>1</Channels>
<ChannelPositions>Front: C</ChannelPositions>
<SamplingRate>48000</SamplingRate>
<SamplingCount>13433760</SamplingCount>
<BitDepth>16</BitDepth>
<StreamSize>18003284</StreamSize>
<StreamSize_Proportion>0.99951</StreamSize_Proportion>
<Encoded_Library>reference libFLAC 1.3.2 20170101</Encoded_Library>
<Encoded_Library_Name>libFLAC</Encoded_Library_Name>
<Encoded_Library_Version>1.3.2</Encoded_Library_Version>
<Encoded_Library_Date>UTC 2017-01-01</Encoded_Library_Date>
</track>
</media>
</MediaInfo>
|
XML-Parser bringt Python ja mit. So als Ansatz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | import subprocess
from pathlib import Path
from pprint import pprint
from xml.etree import ElementTree as ET
def mediainfo(file_path):
namespace = "{https://mediaarea.net/mediainfo}"
return [
(
track.get("type"),
{
element.tag.removeprefix(namespace): element.text
for element in track.findall(f"{namespace}*")
},
)
for track in ET.fromstring(
subprocess.check_output(
["mediainfo", "--Output=XML", str(file_path)], text=True
)
).findall(f"{namespace}media/{namespace}track")
]
|
Macht aus dem Beispiel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | [('General',
{'AudioCount': '1',
'Duration': '279.870',
'FileExtension': 'flac',
'FileSize': '18012074',
'File_Modified_Date': 'UTC 2024-05-20 22:52:47',
'File_Modified_Date_Local': '2024-05-21 00:52:47',
'Format': 'FLAC',
'OverallBitRate': '514870',
'OverallBitRate_Mode': 'VBR',
'StreamSize': '0'}),
('Audio',
{'BitDepth': '16',
'BitRate': '514618',
'BitRate_Mode': 'VBR',
'ChannelPositions': 'Front: C',
'Channels': '1',
'Duration': '279.870',
'Encoded_Library': 'reference libFLAC 1.3.2 20170101',
'Encoded_Library_Date': 'UTC 2017-01-01',
'Encoded_Library_Name': 'libFLAC',
'Encoded_Library_Version': '1.3.2',
'Format': 'FLAC',
'SamplingCount': '13433760',
'SamplingRate': '48000',
'StreamSize': '18003284',
'StreamSize_Proportion': '0.99951'})]
|
So eine for-Schleife mit Initialisierung, Test, und Aktualisierung gibt es in Python nicht. Das Beispiel wäre in Python:
| for i in range(MAX):
# i kann hier benutzt werden.
|
Man kann i nicht verändern, also der Iterator den range() liefert bekommt davon nichts mit. Das ist aber sowieso ein bisschen undurchsichtig, und eine while-Schleife würde das klarer machen, dass man im Schleifenkörper mit Änderungen an i rechnen muss, die den Schleifenablauf beeinflussen. Solche Laufindexschleifen sind aber auch eher selten, denn meisten greift man mit dem Index ja auf irgendwelche Sequenzen zu und diese Indirektion braucht man in Python nicht. Falls man parallel über mehr als eine Sequenz iterieren muss gibt es zip() und falls man zusätzlich eine laufende Zahl braucht, gibt es enumerate(). Grund für das csv-Modul statt das selbst zu basteln wäre übrigens das CSV nicht so einfach ist wie man auf den ersten Blick denkt. Das Format kommt auch mit dem Trennzeichen und sogar Zeilenenden in Zellen klar. Der meiste dafür selbst gebastelte Code allerdings nicht.
|