.remake audio quality analyzer
This commit is contained in:
+21
-184
@@ -2,170 +2,26 @@ package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-flac/go-flac"
|
||||
mewflac "github.com/mewkiz/flac"
|
||||
)
|
||||
|
||||
type AnalysisResult struct {
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
SampleRate uint32 `json:"sample_rate"`
|
||||
Channels uint8 `json:"channels"`
|
||||
BitsPerSample uint8 `json:"bits_per_sample"`
|
||||
TotalSamples uint64 `json:"total_samples"`
|
||||
Duration float64 `json:"duration"`
|
||||
Bitrate int `json:"bit_rate"`
|
||||
BitDepth string `json:"bit_depth"`
|
||||
DynamicRange float64 `json:"dynamic_range"`
|
||||
PeakAmplitude float64 `json:"peak_amplitude"`
|
||||
RMSLevel float64 `json:"rms_level"`
|
||||
Spectrum *SpectrumData `json:"spectrum,omitempty"`
|
||||
}
|
||||
|
||||
func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
|
||||
if !fileExists(filepath) {
|
||||
return nil, fmt.Errorf("file does not exist: %s", filepath)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
f, err := flac.ParseFile(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse FLAC file: %w", err)
|
||||
}
|
||||
|
||||
result := &AnalysisResult{
|
||||
FilePath: filepath,
|
||||
FileSize: fileInfo.Size(),
|
||||
}
|
||||
|
||||
if len(f.Meta) > 0 {
|
||||
streamInfo := f.Meta[0]
|
||||
if streamInfo.Type == flac.StreamInfo {
|
||||
|
||||
data := streamInfo.Data
|
||||
if len(data) >= 18 {
|
||||
|
||||
result.SampleRate = uint32(data[10])<<12 | uint32(data[11])<<4 | uint32(data[12])>>4
|
||||
|
||||
result.Channels = ((data[12] >> 1) & 0x07) + 1
|
||||
|
||||
result.BitsPerSample = ((data[12]&0x01)<<4 | data[13]>>4) + 1
|
||||
|
||||
result.TotalSamples = uint64(data[13]&0x0F)<<32 |
|
||||
uint64(data[14])<<24 |
|
||||
uint64(data[15])<<16 |
|
||||
uint64(data[16])<<8 |
|
||||
uint64(data[17])
|
||||
|
||||
if result.SampleRate > 0 {
|
||||
result.Duration = float64(result.TotalSamples) / float64(result.SampleRate)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spectrum, err := AnalyzeSpectrum(filepath)
|
||||
if err != nil {
|
||||
|
||||
fmt.Printf("Warning: failed to analyze spectrum: %v\n", err)
|
||||
} else {
|
||||
result.Spectrum = spectrum
|
||||
|
||||
calculateRealAudioMetrics(result, filepath)
|
||||
}
|
||||
|
||||
result.BitDepth = fmt.Sprintf("%d-bit", result.BitsPerSample)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func calculateRealAudioMetrics(result *AnalysisResult, filepath string) {
|
||||
|
||||
samples, err := decodeFLACForMetrics(filepath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var peak float64
|
||||
var sumSquares float64
|
||||
|
||||
for _, sample := range samples {
|
||||
absVal := sample
|
||||
if absVal < 0 {
|
||||
absVal = -absVal
|
||||
}
|
||||
if absVal > peak {
|
||||
peak = absVal
|
||||
}
|
||||
sumSquares += sample * sample
|
||||
}
|
||||
|
||||
peakDB := 20.0 * math.Log10(peak)
|
||||
result.PeakAmplitude = peakDB
|
||||
|
||||
rms := math.Sqrt(sumSquares / float64(len(samples)))
|
||||
rmsDB := 20.0 * math.Log10(rms)
|
||||
result.RMSLevel = rmsDB
|
||||
|
||||
result.DynamicRange = peakDB - rmsDB
|
||||
}
|
||||
|
||||
func decodeFLACForMetrics(filepath string) ([]float64, error) {
|
||||
stream, err := mewflac.ParseFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
maxSamples := 10000000
|
||||
samples := make([]float64, 0, maxSamples)
|
||||
|
||||
for {
|
||||
frame, err := stream.ParseNext()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var channelSamples []int32
|
||||
if len(frame.Subframes) > 0 {
|
||||
channelSamples = frame.Subframes[0].Samples
|
||||
}
|
||||
|
||||
maxVal := float64(int64(1) << (stream.Info.BitsPerSample - 1))
|
||||
for _, sample := range channelSamples {
|
||||
if len(samples) >= maxSamples {
|
||||
return samples, nil
|
||||
}
|
||||
normalized := float64(sample) / maxVal
|
||||
samples = append(samples, normalized)
|
||||
}
|
||||
|
||||
if len(samples) >= maxSamples {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return samples, nil
|
||||
}
|
||||
|
||||
func GetFileSize(filepath string) (int64, error) {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return info.Size(), nil
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
SampleRate uint32 `json:"sample_rate"`
|
||||
Channels uint8 `json:"channels"`
|
||||
BitsPerSample uint8 `json:"bits_per_sample"`
|
||||
TotalSamples uint64 `json:"total_samples"`
|
||||
Duration float64 `json:"duration"`
|
||||
Bitrate int `json:"bit_rate"`
|
||||
BitDepth string `json:"bit_depth"`
|
||||
DynamicRange float64 `json:"dynamic_range"`
|
||||
PeakAmplitude float64 `json:"peak_amplitude"`
|
||||
RMSLevel float64 `json:"rms_level"`
|
||||
}
|
||||
|
||||
func GetTrackMetadata(filepath string) (*AnalysisResult, error) {
|
||||
@@ -194,20 +50,23 @@ func GetMetadataWithFFprobe(filePath string) (*AnalysisResult, error) {
|
||||
"-v", "error",
|
||||
"-select_streams", "a:0",
|
||||
"-show_entries", "stream=sample_rate,channels,bits_per_raw_sample,bits_per_sample,duration,bit_rate",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
"-of", "default=noprint_wrappers=0",
|
||||
filePath,
|
||||
}
|
||||
|
||||
cmd := exec.Command(ffprobePath, args...)
|
||||
setHideWindow(cmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ffprobe failed: %w - %s", err, string(output))
|
||||
return nil, fmt.Errorf("ffprobe failed: %v - %s", err, string(output))
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) < 4 {
|
||||
return nil, fmt.Errorf("unexpected ffprobe output: %s", string(output))
|
||||
infoMap := make(map[string]string)
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
infoMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
res := &AnalysisResult{
|
||||
@@ -218,28 +77,6 @@ func GetMetadataWithFFprobe(filePath string) (*AnalysisResult, error) {
|
||||
res.FileSize = info.Size()
|
||||
}
|
||||
|
||||
infoMap := make(map[string]string)
|
||||
|
||||
args = []string{
|
||||
"-v", "error",
|
||||
"-select_streams", "a:0",
|
||||
"-show_entries", "stream=sample_rate,channels,bits_per_raw_sample,bits_per_sample,duration,bit_rate",
|
||||
"-of", "default=noprint_wrappers=0",
|
||||
filePath,
|
||||
}
|
||||
cmd = exec.Command(ffprobePath, args...)
|
||||
setHideWindow(cmd)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
lines = strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
infoMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := infoMap["sample_rate"]; ok {
|
||||
s, _ := strconv.Atoi(val)
|
||||
res.SampleRate = uint32(s)
|
||||
|
||||
Reference in New Issue
Block a user