Skip to content

Commit e909cfd

Browse files
committed
feat: add FLAC lossless format support and refactor quality pickers (while staying backward compatible)
- Add `FLAC_FLAC` (16), `FLAC_FLAC_24BIT` (22) format codes (based on librespot-org/librespot#796, librespot-org/librespot#1424) - Regenerate Metadata_pb2.py with protoc 3.20.1 - Add enums: `SuperAudioFormat.FLAC`, `AudioQuality.LOSSLESS` - Refactor to generic DRY `FormatOnlyAudioQuality` base class while maintaining existing `VorbisOnlyAudioQuality` as wrapper - Remove `AAC_24_NORM` (replaced by `FLAC_FLAC` at code 16)
1 parent ee2c110 commit e909cfd

4 files changed

Lines changed: 143 additions & 3706 deletions

File tree

librespot/audio/decoders.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ class AudioQuality(enum.Enum):
1212
NORMAL = 0x00
1313
HIGH = 0x01
1414
VERY_HIGH = 0x02
15+
LOSSLESS = 0x03
1516

1617
@staticmethod
1718
def get_quality(audio_format: AudioFile.Format) -> AudioQuality:
1819
if audio_format in [
1920
AudioFile.MP3_96,
2021
AudioFile.OGG_VORBIS_96,
21-
AudioFile.AAC_24_NORM,
2222
]:
2323
return AudioQuality.NORMAL
2424
if audio_format in [
@@ -35,6 +35,11 @@ def get_quality(audio_format: AudioFile.Format) -> AudioQuality:
3535
AudioFile.AAC_48,
3636
]:
3737
return AudioQuality.VERY_HIGH
38+
if audio_format in [
39+
AudioFile.FLAC_FLAC,
40+
AudioFile.FLAC_FLAC_24BIT,
41+
]:
42+
return AudioQuality.LOSSLESS
3843
raise RuntimeError("Unknown format: {}".format(audio_format))
3944

4045
def get_matches(self,
@@ -47,35 +52,71 @@ def get_matches(self,
4752
return file_list
4853

4954

50-
class VorbisOnlyAudioQuality(AudioQualityPicker):
51-
logger = logging.getLogger("Librespot:Player:VorbisOnlyAudioQuality")
55+
class FormatOnlyAudioQuality(AudioQualityPicker):
56+
# Generic quality picker; filters files by container format
57+
58+
logger = logging.getLogger("Librespot:Player:FormatOnlyAudioQuality")
5259
preferred: AudioQuality
60+
format_filter: SuperAudioFormat
5361

54-
def __init__(self, preferred: AudioQuality):
62+
def __init__(self, preferred: AudioQuality, format_filter: SuperAudioFormat):
5563
self.preferred = preferred
64+
self.format_filter = format_filter
5665

5766
@staticmethod
58-
def get_vorbis_file(files: typing.List[Metadata.AudioFile]):
67+
def get_file_by_format(files: typing.List[Metadata.AudioFile],
68+
format_type: SuperAudioFormat) -> typing.Optional[Metadata.AudioFile]:
5969
for file in files:
6070
if file.HasField("format") and SuperAudioFormat.get(
61-
file.format) == SuperAudioFormat.VORBIS:
71+
file.format) == format_type:
6272
return file
6373
return None
6474

65-
def get_file(self, files: typing.List[Metadata.AudioFile]):
66-
matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(
67-
files)
68-
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
69-
matches)
70-
if vorbis is None:
71-
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
72-
files)
73-
if vorbis is not None:
75+
def get_file(self, files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
76+
quality_matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(files)
77+
78+
selected_file = self.get_file_by_format(quality_matches, self.format_filter)
79+
80+
if selected_file is None:
81+
# Try using any file matching the format, regardless of quality
82+
selected_file = self.get_file_by_format(files, self.format_filter)
83+
84+
if selected_file is not None:
85+
# Found format match (different quality than preferred)
7486
self.logger.warning(
75-
"Using {} because preferred {} couldn't be found.".format(
76-
Metadata.AudioFile.Format.Name(vorbis.format),
77-
self.preferred))
87+
"Using {} format file with {} quality because preferred {} quality couldn't be found.".format(
88+
self.format_filter.name,
89+
AudioQuality.get_quality(selected_file.format).name,
90+
self.preferred.name))
7891
else:
92+
available_formats = [SuperAudioFormat.get(f.format).name
93+
for f in files if f.HasField("format")]
7994
self.logger.fatal(
80-
"Couldn't find any Vorbis file, available: {}")
81-
return vorbis
95+
"Couldn't find any {} file. Available formats: {}".format(
96+
self.format_filter.name,
97+
", ".join(set(available_formats)) if available_formats else "none"))
98+
99+
return selected_file
100+
101+
102+
# Backward-compatible wrapper classes
103+
104+
class VorbisOnlyAudioQuality(FormatOnlyAudioQuality):
105+
logger = logging.getLogger("Librespot:Player:VorbisOnlyAudioQuality")
106+
107+
def __init__(self, preferred: AudioQuality):
108+
super().__init__(preferred, SuperAudioFormat.VORBIS)
109+
110+
@staticmethod
111+
def get_vorbis_file(files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
112+
return FormatOnlyAudioQuality.get_file_by_format(files, SuperAudioFormat.VORBIS)
113+
114+
class LosslessOnlyAudioQuality(FormatOnlyAudioQuality):
115+
logger = logging.getLogger("Librespot:Player:LosslessOnlyAudioQuality")
116+
117+
def __init__(self, preferred: AudioQuality):
118+
super().__init__(preferred, SuperAudioFormat.FLAC)
119+
120+
@staticmethod
121+
def get_flac_file(files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
122+
return FormatOnlyAudioQuality.get_file_by_format(files, SuperAudioFormat.FLAC)

librespot/audio/format.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class SuperAudioFormat(enum.Enum):
66
MP3 = 0x00
77
VORBIS = 0x01
88
AAC = 0x02
9+
FLAC = 0x03
910

1011
@staticmethod
1112
def get(audio_format: Metadata.AudioFile.Format):
@@ -26,7 +27,11 @@ def get(audio_format: Metadata.AudioFile.Format):
2627
if audio_format in [
2728
Metadata.AudioFile.Format.AAC_24,
2829
Metadata.AudioFile.Format.AAC_48,
29-
Metadata.AudioFile.Format.AAC_24_NORM,
3030
]:
3131
return SuperAudioFormat.AAC
32+
if audio_format in [
33+
Metadata.AudioFile.Format.FLAC_FLAC,
34+
Metadata.AudioFile.Format.FLAC_FLAC_24BIT,
35+
]:
36+
return SuperAudioFormat.FLAC
3237
raise RuntimeError("Unknown audio format: {}".format(audio_format))

0 commit comments

Comments
 (0)