From 7ce35192b567e585f31c8abe5a4368e0e1fa251f Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 3 Jun 2024 19:34:56 -0700 Subject: [PATCH] Add support for waveform + album cover thumbnails --- requirements.txt | 3 + tagstudio/src/core/constants.py | 1 + tagstudio/src/qt/helpers/gradient.py | 3 + tagstudio/src/qt/widgets/thumb_renderer.py | 101 ++++++++++++++++++++- 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a353c70c..e27da13b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,6 @@ numpy==1.26.4 rawpy==0.21.0 pillow-heif==0.16.0 chardet==5.2.0 +pydub==0.25.1 +mutagen==1.47.0 +numpy==1.26.4 diff --git a/tagstudio/src/core/constants.py b/tagstudio/src/core/constants.py index 7f25f374..a828db06 100644 --- a/tagstudio/src/core/constants.py +++ b/tagstudio/src/core/constants.py @@ -70,6 +70,7 @@ AUDIO_TYPES: list[str] = [ ".wma", ".ogg", ".aiff", + ".aif", ] DOC_TYPES: list[str] = [ ".txt", diff --git a/tagstudio/src/qt/helpers/gradient.py b/tagstudio/src/qt/helpers/gradient.py index dabe7639..b76844a0 100644 --- a/tagstudio/src/qt/helpers/gradient.py +++ b/tagstudio/src/qt/helpers/gradient.py @@ -46,6 +46,9 @@ def four_corner_gradient_background( image.putalpha(mask) final = image + if final.mode != "RGBA": + final = final.convert("RGBA") + hl_soft = hl.copy() hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5)) final.paste(ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3)) diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 47421b4f..1ab3a495 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -5,10 +5,9 @@ import logging import math -from pathlib import Path - import cv2 import rawpy +import numpy from pillow_heif import register_heif_opener, register_avif_opener from PIL import ( Image, @@ -19,12 +18,17 @@ from PIL import ( ImageOps, ImageFile, ) +from io import BytesIO +from pathlib import Path from PIL.Image import DecompressionBombError +from pydub import AudioSegment, exceptions +from mutagen import id3, flac, mp4 from PySide6.QtCore import QObject, Signal, QSize from PySide6.QtGui import QPixmap from src.qt.helpers.gradient import four_corner_gradient_background from src.qt.helpers.text_wrapper import wrap_full_text from src.core.constants import ( + AUDIO_TYPES, PLAINTEXT_TYPES, FONT_TYPES, VIDEO_TYPES, @@ -224,6 +228,99 @@ class ThumbRenderer(QObject): ) * draw.textbbox((0, 0), "A", font=font)[-1] image = bg + # Audio + elif _filepath.suffix.lower() in AUDIO_TYPES: + try: + artwork = None + if _filepath.suffix.lower() in [".mp3"]: + audio_tags = id3.ID3(_filepath) + covers = audio_tags.getall("APIC") + if covers: + artwork = Image.open(BytesIO(covers[0].data)) + elif _filepath.suffix.lower() in [".flac"]: + audio_tags = flac.FLAC(_filepath) + covers = audio_tags.pictures + if covers: + artwork = Image.open(BytesIO(covers[0].data)) + elif _filepath.suffix.lower() in [".mp4", ".m4a", ".aac"]: + audio_tags = mp4.MP4(_filepath) + covers = audio_tags.get("covr") + if covers: + artwork = Image.open(BytesIO(covers[0])) + if artwork: + image = artwork + except (mp4.MP4MetadataError, mp4.MP4StreamInfoError) as e: + logging.error( + f"[ThumbRenderer]{ERROR}: Couldn't read album artwork for {_filepath.name} ({type(e).__name__})" + ) + if image is None: + try: + audio: AudioSegment = AudioSegment.from_file( + _filepath, _filepath.suffix.lower()[1:] + ) + data = numpy.fromstring(audio._data, numpy.int16) + data_indices = numpy.linspace(1, len(data), num=adj_size) + + BARS = adj_size // 5 + BAR_MARGIN = 4 + BAR_HEIGHT = adj_size - (adj_size // BAR_MARGIN) + LINE_WIDTH = 6 + + length = len(data_indices) + RATIO = length / BARS + + count = 0 + maximum_item = 0 + max_array = [] + highest_line = 0 + + for i in range(1, len(data_indices)): + d = data[math.ceil(data_indices[i]) - 1] + if count < RATIO: + count = count + 1 + if abs(d) > maximum_item: + maximum_item = abs(d) + else: + max_array.append(maximum_item) + + if maximum_item > highest_line: + highest_line = maximum_item + + maximum_item = 0 + count = 1 + + line_ratio = max(highest_line / BAR_HEIGHT, 1) + + image = Image.new( + "RGB", (adj_size, adj_size), color="#1e1e1e" + ) + draw = ImageDraw.Draw(image) + + current_x = 1 + for item in max_array: + item_height = item / line_ratio + + current_y = ( + BAR_HEIGHT - item_height + (adj_size // BAR_MARGIN) + ) / 2 + draw.line( + ( + current_x, + current_y, + current_x, + current_y + item_height, + ), + fill=(169, 171, 172), + width=4, + joint="curve", + ) + + current_x = current_x + LINE_WIDTH + except exceptions.CouldntDecodeError as e: + logging.error( + f"[ThumbRenderer]{ERROR}: Couldn't render waveform for {_filepath.name} ({type(e).__name__})" + ) + # 3D =========================================================== # elif extension == 'stl': # # Create a new plot