diff --git a/__init__.py b/__init__.py
index 5b87ed1..16ed91a 100644
--- a/__init__.py
+++ b/__init__.py
@@ -10,6 +10,7 @@ import threading
import concurrent.futures
import tempfile
import math
+import yaml
from functools import partial
from typing import List, Tuple, Dict, Optional
from picard import config, log
@@ -53,20 +54,23 @@ class AcousticBrainzNG:
binary_path += '.exe'
return binary_path
- def _get_binary_paths(self) -> Tuple[str, str]:
+ def _get_binary_paths(self) -> Tuple[str, str, str]:
binaries_path = config.setting["acousticbrainz_ng_binaries_path"]
if not binaries_path:
raise ValueError("Binaries path not configured")
musicnn_binary_path = self._get_binary_path("streaming_musicnn_predict", binaries_path)
- gaia_binary_path = self._get_binary_path("streaming_extractor_music", binaries_path)
-
+ rhythm_binary_path = self._get_binary_path("streaming_rhythmextractor_multifeature", binaries_path)
+ key_binary_path = self._get_binary_path("streaming_key", binaries_path)
+
if not os.path.exists(musicnn_binary_path):
raise FileNotFoundError(f"Binary {musicnn_binary_path} not found")
- if not os.path.exists(gaia_binary_path):
- raise FileNotFoundError(f"Binary {gaia_binary_path} not found")
-
- return musicnn_binary_path, gaia_binary_path
+ if not os.path.exists(rhythm_binary_path):
+ raise FileNotFoundError(f"Binary {rhythm_binary_path} not found")
+ if not os.path.exists(key_binary_path):
+ raise FileNotFoundError(f"Binary {key_binary_path} not found")
+
+ return musicnn_binary_path, rhythm_binary_path, key_binary_path
def _run_musicnn_models(self, models: List[Tuple[str, str]], musicnn_binary_path: str, file: str, output_path: str) -> bool:
models_path = config.setting["acousticbrainz_ng_models_path"]
@@ -130,7 +134,7 @@ class AcousticBrainzNG:
return False
try:
- musicnn_binary_path, gaia_binary_path = self._get_binary_paths()
+ musicnn_binary_path, rhythm_binary_path, key_binary_path = self._get_binary_paths()
except (ValueError, FileNotFoundError) as e:
log.error(str(e))
return False
@@ -144,36 +148,86 @@ class AcousticBrainzNG:
log.error(f"Error generating cache folder: {e}")
return False
- gaia_success = True
- def run_gaia():
- nonlocal gaia_success
- if os.path.exists(os.path.join(output_path, "gaia.json")):
+ rhythm_success = True
+ def run_rhythm():
+ nonlocal rhythm_success
+ if os.path.exists(os.path.join(output_path, "rhythm.yaml")):
return
- gaia_proc = subprocess.run(
- [gaia_binary_path, file, os.path.join(output_path, "gaia.json")],
+ rhythm_proc = subprocess.run(
+ [rhythm_binary_path, file],
capture_output=True,
text=True,
env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
- if gaia_proc.returncode != 0:
- gaia_success = False
- log.error(f"Gaia binary {gaia_binary_path} failed on file {file} with exit code {gaia_proc.returncode}")
- if gaia_proc.stdout:
- log.error(f"Gaia stdout: {gaia_proc.stdout}")
- if gaia_proc.stderr:
- log.error(f"Gaia stderr: {gaia_proc.stderr}")
+ if rhythm_proc.returncode != 0:
+ rhythm_success = False
+ log.error(f"Rhythm binary {rhythm_binary_path} failed on file {file} with exit code {rhythm_proc.returncode}")
+ if rhythm_proc.stdout:
+ log.error(f"Rhythm stdout: {rhythm_proc.stdout}")
+ if rhythm_proc.stderr:
+ log.error(f"Rhythm stderr: {rhythm_proc.stderr}")
+ return
+
+ try:
+ stdout = rhythm_proc.stdout or ""
+ lines = stdout.splitlines(keepends=True)
+ if not lines:
+ raise ValueError("Rhythm binary produced no stdout")
+
+ yaml_lines = lines[-5:] if len(lines) >= 5 else lines
+ yaml_str = "".join(yaml_lines)
+ if not yaml_str.strip():
+ raise ValueError("Empty YAML section extracted from rhythm binary output")
+
+ out_file = os.path.join(output_path, "rhythm.yaml")
+ with open(out_file, "w", encoding="utf-8") as f:
+ f.write(yaml_str)
+ except Exception as e:
+ rhythm_success = False
+ log.error(f"Failed to extract/save rhythm.yaml from rhythm binary stdout: {e}")
+ if rhythm_proc.stdout:
+ log.error(f"Rhythm stdout: {rhythm_proc.stdout}")
+ if rhythm_proc.stderr:
+ log.error(f"Rhythm stderr: {rhythm_proc.stderr}")
return
- gaia_thread = threading.Thread(target=run_gaia)
- gaia_thread.start()
+ key_success = True
+ def run_key():
+ nonlocal key_success
+ if os.path.exists(os.path.join(output_path, "key.yaml")):
+ return
+
+ key_proc = subprocess.run(
+ [key_binary_path, file, os.path.join(output_path, "key.yaml")],
+ capture_output=True,
+ text=True,
+ env=ENV,
+ creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ )
+
+ if key_proc.returncode != 0:
+ key_success = False
+ log.error(f"Key binary {key_binary_path} failed on file {file} with exit code {key_proc.returncode}")
+ if key_proc.stdout:
+ log.error(f"Key stdout: {key_proc.stdout}")
+ if key_proc.stderr:
+ log.error(f"Key stderr: {key_proc.stderr}")
+ return
+
+ rhythm_thread = threading.Thread(target=run_rhythm)
+ rhythm_thread.start()
+
+ key_thread = threading.Thread(target=run_key)
+ key_thread.start()
musicnn_success = self._run_musicnn_models(REQUIRED_MODELS, musicnn_binary_path, file, output_path)
- gaia_thread.join()
-
- return gaia_success and musicnn_success
+ rhythm_thread.join()
+ key_thread.join()
+
+ return rhythm_success and key_success and musicnn_success
def analyze_optional(self, metadata: Dict, file: str) -> bool:
if not self._check_binaries():
@@ -185,7 +239,7 @@ class AcousticBrainzNG:
return False
try:
- musicnn_binary_path, _ = self._get_binary_paths()
+ musicnn_binary_path = self._get_binary_paths()[0]
except (ValueError, FileNotFoundError) as e:
log.error(str(e))
return False
@@ -282,50 +336,48 @@ class AcousticBrainzNG:
metadata['mood'] = moods
metadata['tags'] = tags
- gaia_data = {}
- gaia_json_path = os.path.join(output_path, "gaia.json")
+ rhythm_data = {}
+ rhythm_yaml_path = os.path.join(output_path, "rhythm.yaml")
- if os.path.exists(gaia_json_path):
- try:
- with open(gaia_json_path, 'r', encoding='utf-8') as f:
- gaia_data = json.load(f)
- except (FileNotFoundError, json.JSONDecodeError) as e:
- log.error(f"Error reading Gaia JSON file: {e}")
+ key_data = {}
+ key_yaml_path = os.path.join(output_path, "key.yaml")
+
+ if os.path.exists(rhythm_yaml_path):
+ with open(rhythm_yaml_path, 'r', encoding='utf-8') as f:
+ loaded = yaml.safe_load(f)
+
+ if not isinstance(loaded, dict):
+ log.error("Invalid rhythm YAML format: expected a mapping at the top level")
return False
+
+ rhythm_data = loaded
else:
- log.error(f"Gaia JSON file not found: {gaia_json_path}")
+ log.error(f"Rhythm YAML file not found: {rhythm_yaml_path}")
+ return False
+
+ if os.path.exists(key_yaml_path):
+ with open(key_yaml_path, 'r', encoding='utf-8') as f:
+ loaded = yaml.safe_load(f)
+ if not isinstance(loaded, dict):
+ log.error("Invalid key YAML format: expected a mapping at the top level")
+ return False
+
+ key_data = loaded
+ else:
+ log.error(f"Key YAML file not found: {key_yaml_path}")
return False
try:
- metadata["bpm"] = int(round(gaia_data["rhythm"]["bpm"]))
-
+ metadata["bpm"] = int(round(rhythm_data["bpm"]))
+ metadata["key"] = "o" if key_data["tonal"]["key_scale"] == "off" else f"{key_data['tonal']['key']}{'m' if key_data['tonal']['key_scale'] == 'minor' else ''}"
+
if config.setting["acousticbrainz_ng_save_raw"]:
- metadata["ab:lo:tonal:chords_changes_rate"] = gaia_data["tonal"]["chords_changes_rate"]
- metadata["ab:lo:tonal:chords_key"] = gaia_data["tonal"]["chords_key"]
- metadata["ab:lo:tonal:chords_scale"] = gaia_data["tonal"]["chords_scale"]
-
- highestStrength = -1
- selectedAlgorithm = None
-
- for algorithm in GAIA_KEY_ALGORITHMS:
- key_data = gaia_data["tonal"][f"key_{algorithm}"]
-
- if key_data["strength"] > highestStrength:
- highestStrength = key_data["strength"]
- selectedAlgorithm = algorithm
-
- if selectedAlgorithm:
- selected_key_data = gaia_data["tonal"][f"key_{selectedAlgorithm}"]
-
- metadata["key"] = "o" if selected_key_data["scale"] == "off" else f"{selected_key_data['key']}{'m' if selected_key_data['scale'] == 'minor' else ''}"
-
- if config.setting["acousticbrainz_ng_save_raw"]:
- metadata["ab:lo:tonal:key_scale"] = selected_key_data["scale"]
- metadata["ab:lo:tonal:key_key"] = selected_key_data["key"]
+ metadata["ab:lo:tonal:key_scale"] = key_data["tonal"]["key_scale"]
+ metadata["ab:lo:tonal:key_key"] = key_data["tonal"]["key"]
return True
except Exception as e:
- log.error(f"Error processing gaia data: {e}")
+ log.error(f"Error processing feature data: {e}")
return False
def parse_optional(self, metadata: Dict, file: str) -> bool:
@@ -1395,8 +1447,8 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
def update_concurrent_processes():
concurrent_analyses = self.concurrent_analyses_input.value()
musicnn_workers = self.musicnn_workers_input.value()
- max_processes = concurrent_analyses + (concurrent_analyses * musicnn_workers)
- breakdown = f"[{concurrent_analyses} gaia processes + ({concurrent_analyses} x {musicnn_workers}) MusicNN processes]"
+ max_processes = (2 * concurrent_analyses) + (concurrent_analyses * musicnn_workers)
+ breakdown = f"[(2 x {concurrent_analyses}) feature processes + ({concurrent_analyses} x {musicnn_workers}) MusicNN processes]"
self.concurrent_processes_display.setText(f"{breakdown} = {max_processes}")
self.concurrent_analyses_input.valueChanged.connect(update_concurrent_processes)
diff --git a/bin/Qt5Core.dll b/bin/Qt5Core.dll
deleted file mode 100644
index 8009aa5..0000000
Binary files a/bin/Qt5Core.dll and /dev/null differ
diff --git a/bin/streaming_extractor_music.exe b/bin/streaming_extractor_music.exe
deleted file mode 100755
index 0236fda..0000000
Binary files a/bin/streaming_extractor_music.exe and /dev/null differ
diff --git a/bin/streaming_musicnn_predict.exe b/bin/streaming_key
similarity index 56%
rename from bin/streaming_musicnn_predict.exe
rename to bin/streaming_key
index 19e7cdb..a258c65 100755
Binary files a/bin/streaming_musicnn_predict.exe and b/bin/streaming_key differ
diff --git a/bin/streaming_md5.exe b/bin/streaming_md5.exe
deleted file mode 100755
index 1e472c6..0000000
Binary files a/bin/streaming_md5.exe and /dev/null differ
diff --git a/bin/streaming_extractor_music b/bin/streaming_rhythmextractor_multifeature
similarity index 60%
rename from bin/streaming_extractor_music
rename to bin/streaming_rhythmextractor_multifeature
index ace6b6a..662a5e2 100755
Binary files a/bin/streaming_extractor_music and b/bin/streaming_rhythmextractor_multifeature differ
diff --git a/bin/tensorflow.dll b/bin/tensorflow.dll
deleted file mode 100644
index 937a1af..0000000
Binary files a/bin/tensorflow.dll and /dev/null differ
diff --git a/constants.py b/constants.py
index 28aa411..5f67999 100644
--- a/constants.py
+++ b/constants.py
@@ -18,7 +18,7 @@ External dependencies:
This plugin is CPU heavy!
"""
-PLUGIN_VERSION = "1.1.1"
+PLUGIN_VERSION = "1.1.2"
PLUGIN_API_VERSIONS = ["2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
@@ -46,8 +46,9 @@ OPTIONAL_MODELS: List[Tuple[str, str]] = [
]
REQUIRED_BINARIES: List[str] = [
- "streaming_extractor_music",
+ "streaming_rhythmextractor_multifeature",
"streaming_musicnn_predict",
+ "streaming_key",
"streaming_md5",
]
@@ -66,6 +67,4 @@ CONFIG_OPTIONS = [
BoolOption("setting", "acousticbrainz_ng_save_raw", False),
BoolOption("setting", "acousticbrainz_ng_calculate_replaygain", True),
BoolOption("setting", "acousticbrainz_ng_save_fingerprint", True)
-]
-
-GAIA_KEY_ALGORITHMS = ["edma", "krumhansl", "temperley"]
\ No newline at end of file
+]
\ No newline at end of file