mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-19 22:54:43 +02:00
feat: add audio quality analysis widget and fix USLT lyrics detection
This commit is contained in:
@@ -498,7 +498,13 @@ func extractUserTextFrame(data []byte) (string, string) {
|
||||
|
||||
func isLyricsDescription(description string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(description)) {
|
||||
case "lyrics", "lyric", "unsyncedlyrics", "unsynced lyrics", "lrc":
|
||||
case
|
||||
"lyrics",
|
||||
"lyric",
|
||||
"unsyncedlyrics",
|
||||
"unsynced lyrics",
|
||||
"uslt",
|
||||
"lrc":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ffmpegCommand(args ...string) *exec.Cmd {
|
||||
if ffmpegPath, err := exec.LookPath("ffmpeg"); err == nil {
|
||||
return exec.Command(ffmpegPath, args...)
|
||||
}
|
||||
return exec.Command("ffmpeg", args...)
|
||||
}
|
||||
|
||||
func runFFmpegTestCommand(t *testing.T, args ...string) {
|
||||
t.Helper()
|
||||
cmd := ffmpegCommand(args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ffmpeg failed: %v\n%s", err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLyricsReadsMp3AfterCoverEmbed(t *testing.T) {
|
||||
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
||||
t.Skip("ffmpeg not available")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
sourceFlac := filepath.Join(tempDir, "source.flac")
|
||||
baseMp3 := filepath.Join(tempDir, "base.mp3")
|
||||
finalMp3 := filepath.Join(tempDir, "final.mp3")
|
||||
coverPath := filepath.Join(tempDir, "cover.jpg")
|
||||
lyrics := "[ti:Test Song]\n[ar:Test Artist]\n[00:00.00]Hello from embedded lyrics"
|
||||
|
||||
runFFmpegTestCommand(
|
||||
t,
|
||||
"-y",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:duration=1",
|
||||
"-c:a",
|
||||
"flac",
|
||||
sourceFlac,
|
||||
)
|
||||
|
||||
runFFmpegTestCommand(
|
||||
t,
|
||||
"-y",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=red:s=32x32:d=1",
|
||||
"-frames:v",
|
||||
"1",
|
||||
coverPath,
|
||||
)
|
||||
|
||||
runFFmpegTestCommand(
|
||||
t,
|
||||
"-y",
|
||||
"-i",
|
||||
sourceFlac,
|
||||
"-b:a",
|
||||
"320k",
|
||||
"-metadata",
|
||||
"title=Test Song",
|
||||
"-metadata",
|
||||
"artist=Test Artist",
|
||||
"-metadata",
|
||||
"lyrics="+lyrics,
|
||||
baseMp3,
|
||||
)
|
||||
|
||||
runFFmpegTestCommand(
|
||||
t,
|
||||
"-y",
|
||||
"-i",
|
||||
baseMp3,
|
||||
"-i",
|
||||
coverPath,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map_metadata",
|
||||
"-1",
|
||||
"-map",
|
||||
"1:0",
|
||||
"-c:v:0",
|
||||
"copy",
|
||||
"-id3v2_version",
|
||||
"3",
|
||||
"-metadata",
|
||||
"title=Test Song",
|
||||
"-metadata",
|
||||
"artist=Test Artist",
|
||||
"-metadata",
|
||||
"lyrics="+lyrics,
|
||||
"-metadata:s:v",
|
||||
"title=Album cover",
|
||||
"-metadata:s:v",
|
||||
"comment=Cover (front)",
|
||||
"-c:a",
|
||||
"copy",
|
||||
finalMp3,
|
||||
)
|
||||
|
||||
meta, err := ReadID3Tags(finalMp3)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadID3Tags failed: %v", err)
|
||||
}
|
||||
if meta == nil {
|
||||
t.Fatalf("ReadID3Tags returned nil metadata")
|
||||
}
|
||||
|
||||
embeddedLyrics, err := ExtractLyrics(finalMp3)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractLyrics failed: %v (metadata=%+v)", err, meta)
|
||||
}
|
||||
if !strings.Contains(embeddedLyrics, "Hello from embedded lyrics") {
|
||||
t.Fatalf("embedded lyrics missing, got %q (metadata=%+v)", embeddedLyrics, meta)
|
||||
}
|
||||
if !strings.Contains(meta.Lyrics, "Hello from embedded lyrics") {
|
||||
t.Fatalf("ReadID3Tags lyrics missing, got %+v", meta)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(finalMp3); err != nil {
|
||||
t.Fatalf("expected final mp3 to exist: %v", err)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -12,6 +12,7 @@ require (
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
golang.org/x/mobile v0.0.0-20260211191516-dcd2a3258864
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/text v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -24,6 +25,5 @@ require (
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
|
||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/dop251/goja v0.0.0-20260216154549-8b74ce4618c5 h1:QckvTXtu55YMopmVeDrPQ/r+T6xjw8KMCmE3UgUldkw=
|
||||
github.com/dop251/goja v0.0.0-20260216154549-8b74ce4618c5/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/go-flac/flacpicture/v2 v2.0.2 h1:HCaJIVZpxnpdWs6G3ECEVRelzqS5xOi1Ba1AGmtXbzE=
|
||||
@@ -30,36 +28,20 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 h1:NiJtT7g4ncNFVjVZMAYNBrPSNhIjFYPj8UKA8MEw2A4=
|
||||
golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3/go.mod h1:wReH3Q1agKmmLapipWFnd4NSs8KPz3fK6mSEZjXLkrg=
|
||||
golang.org/x/mobile v0.0.0-20260209203831-923679eb55af h1:VqXrZNyqFISxo0rNDFZQlRDRIp7RXSJDeh/LbrK+W1k=
|
||||
golang.org/x/mobile v0.0.0-20260209203831-923679eb55af/go.mod h1:tbwefIr7RlQD1OpZ0KEZ9nux/uiihAOGdafgZfJkmII=
|
||||
golang.org/x/mobile v0.0.0-20260211191516-dcd2a3258864 h1:cTVynMSsMYgbUrtia2HB1jrhdUwQNtQti91vUCyjMp4=
|
||||
golang.org/x/mobile v0.0.0-20260211191516-dcd2a3258864/go.mod h1:4OGHIUSBiIqyFAQDaX1tpY0BVnO20DvNDeATBu8aeFQ=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
|
||||
@@ -5228,6 +5228,84 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Select playlists to delete'**
|
||||
String get selectionSelectPlaylistsToDelete;
|
||||
|
||||
/// Title for audio analysis section
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Audio Quality Analysis'**
|
||||
String get audioAnalysisTitle;
|
||||
|
||||
/// Description for audio analysis tap-to-analyze prompt
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verify lossless quality with spectrum analysis'**
|
||||
String get audioAnalysisDescription;
|
||||
|
||||
/// Loading text while analyzing audio
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Analyzing audio...'**
|
||||
String get audioAnalysisAnalyzing;
|
||||
|
||||
/// Sample rate metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sample Rate'**
|
||||
String get audioAnalysisSampleRate;
|
||||
|
||||
/// Bit depth metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bit Depth'**
|
||||
String get audioAnalysisBitDepth;
|
||||
|
||||
/// Channels metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Channels'**
|
||||
String get audioAnalysisChannels;
|
||||
|
||||
/// Duration metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Duration'**
|
||||
String get audioAnalysisDuration;
|
||||
|
||||
/// Nyquist frequency metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nyquist'**
|
||||
String get audioAnalysisNyquist;
|
||||
|
||||
/// File size metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Size'**
|
||||
String get audioAnalysisFileSize;
|
||||
|
||||
/// Dynamic range metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Dynamic Range'**
|
||||
String get audioAnalysisDynamicRange;
|
||||
|
||||
/// Peak amplitude metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Peak'**
|
||||
String get audioAnalysisPeak;
|
||||
|
||||
/// RMS level metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'RMS'**
|
||||
String get audioAnalysisRms;
|
||||
|
||||
/// Total samples metric label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Samples'**
|
||||
String get audioAnalysisSamples;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -3087,4 +3087,44 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3055,4 +3055,44 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3055,6 +3055,46 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
/// The translations for Spanish Castilian, as used in Spain (`es_ES`).
|
||||
|
||||
@@ -3056,4 +3056,44 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3054,4 +3054,44 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3064,4 +3064,44 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3041,4 +3041,44 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3034,4 +3034,44 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3054,4 +3054,44 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3055,6 +3055,46 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
/// The translations for Portuguese, as used in Portugal (`pt_PT`).
|
||||
|
||||
@@ -3114,4 +3114,44 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3060,4 +3060,44 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
@@ -3055,6 +3055,46 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
|
||||
|
||||
@override
|
||||
String get audioAnalysisTitle => 'Audio Quality Analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDescription =>
|
||||
'Verify lossless quality with spectrum analysis';
|
||||
|
||||
@override
|
||||
String get audioAnalysisAnalyzing => 'Analyzing audio...';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSampleRate => 'Sample Rate';
|
||||
|
||||
@override
|
||||
String get audioAnalysisBitDepth => 'Bit Depth';
|
||||
|
||||
@override
|
||||
String get audioAnalysisChannels => 'Channels';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDuration => 'Duration';
|
||||
|
||||
@override
|
||||
String get audioAnalysisNyquist => 'Nyquist';
|
||||
|
||||
@override
|
||||
String get audioAnalysisFileSize => 'Size';
|
||||
|
||||
@override
|
||||
String get audioAnalysisDynamicRange => 'Dynamic Range';
|
||||
|
||||
@override
|
||||
String get audioAnalysisPeak => 'Peak';
|
||||
|
||||
@override
|
||||
String get audioAnalysisRms => 'RMS';
|
||||
|
||||
@override
|
||||
String get audioAnalysisSamples => 'Samples';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in China (`zh_CN`).
|
||||
|
||||
@@ -4015,5 +4015,57 @@
|
||||
"selectionSelectPlaylistsToDelete": "Select playlists to delete",
|
||||
"@selectionSelectPlaylistsToDelete": {
|
||||
"description": "Hint shown when no playlists are selected for deletion"
|
||||
},
|
||||
"audioAnalysisTitle": "Audio Quality Analysis",
|
||||
"@audioAnalysisTitle": {
|
||||
"description": "Title for audio analysis section"
|
||||
},
|
||||
"audioAnalysisDescription": "Verify lossless quality with spectrum analysis",
|
||||
"@audioAnalysisDescription": {
|
||||
"description": "Description for audio analysis tap-to-analyze prompt"
|
||||
},
|
||||
"audioAnalysisAnalyzing": "Analyzing audio...",
|
||||
"@audioAnalysisAnalyzing": {
|
||||
"description": "Loading text while analyzing audio"
|
||||
},
|
||||
"audioAnalysisSampleRate": "Sample Rate",
|
||||
"@audioAnalysisSampleRate": {
|
||||
"description": "Sample rate metric label"
|
||||
},
|
||||
"audioAnalysisBitDepth": "Bit Depth",
|
||||
"@audioAnalysisBitDepth": {
|
||||
"description": "Bit depth metric label"
|
||||
},
|
||||
"audioAnalysisChannels": "Channels",
|
||||
"@audioAnalysisChannels": {
|
||||
"description": "Channels metric label"
|
||||
},
|
||||
"audioAnalysisDuration": "Duration",
|
||||
"@audioAnalysisDuration": {
|
||||
"description": "Duration metric label"
|
||||
},
|
||||
"audioAnalysisNyquist": "Nyquist",
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
"audioAnalysisFileSize": "Size",
|
||||
"@audioAnalysisFileSize": {
|
||||
"description": "File size metric label"
|
||||
},
|
||||
"audioAnalysisDynamicRange": "Dynamic Range",
|
||||
"@audioAnalysisDynamicRange": {
|
||||
"description": "Dynamic range metric label"
|
||||
},
|
||||
"audioAnalysisPeak": "Peak",
|
||||
"@audioAnalysisPeak": {
|
||||
"description": "Peak amplitude metric label"
|
||||
},
|
||||
"audioAnalysisRms": "RMS",
|
||||
"@audioAnalysisRms": {
|
||||
"description": "RMS level metric label"
|
||||
},
|
||||
"audioAnalysisSamples": "Samples",
|
||||
"@audioAnalysisSamples": {
|
||||
"description": "Total samples metric label"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
import 'package:spotiflac_android/utils/mime_utils.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/audio_analysis_widget.dart';
|
||||
|
||||
final _log = AppLogger('TrackMetadata');
|
||||
|
||||
@@ -770,6 +771,11 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
|
||||
_buildLyricsCard(context, colorScheme),
|
||||
|
||||
if (_fileExists) ...[
|
||||
const SizedBox(height: 16),
|
||||
AudioAnalysisCard(filePath: _filePath),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildActionButtons(context, ref, colorScheme, _fileExists),
|
||||
|
||||
@@ -92,9 +92,9 @@ class PlatformBridge {
|
||||
}
|
||||
|
||||
static Stream<Map<String, dynamic>> downloadProgressStream() {
|
||||
return _downloadProgressEvents
|
||||
.receiveBroadcastStream()
|
||||
.map(_decodeMapResult);
|
||||
return _downloadProgressEvents.receiveBroadcastStream().map(
|
||||
_decodeMapResult,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> exitApp() async {
|
||||
@@ -1184,9 +1184,9 @@ class PlatformBridge {
|
||||
}
|
||||
|
||||
static Stream<Map<String, dynamic>> libraryScanProgressStream() {
|
||||
return _libraryScanProgressEvents
|
||||
.receiveBroadcastStream()
|
||||
.map(_decodeMapResult);
|
||||
return _libraryScanProgressEvents.receiveBroadcastStream().map(
|
||||
_decodeMapResult,
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancel ongoing library scan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user