feat(equalize): 添加目标曲线获取及缓存功能
- 新增CurveCache实现Redis曲线数据缓存与分布式锁机制,避免重复请求 - 增加CurveHandler支持目标曲线接口,通过EQ API获取并缓存曲线数据 - Config新增EqualizeConfig配置,支持环境变量动态配置 - Repository新增CurveRepository,按brand和name查询模型与目标曲线数据 - 路由更新,集成/getCurve接口处理目标曲线请求 - OTA模型字段优化,bool类型改为BoolInt,实现数据库int与JSON bool映射 - 统一响应格式,调整response包中OK和Fail函数消息字段命名 - 修改服务端配置默认DB IP,支持Equalize参数传递到路由层 - 新增CSV解析实现,从文件读取测量与目标曲线数据 - 修改OTA处理逻辑,修复定向升级判断bug - Java层新增EqualizeController和ModelService对应功能,保持客户端接口兼容与调用逻辑一致
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luxsin/app-api/internal/cache"
|
||||
"github.com/luxsin/app-api/internal/config"
|
||||
"github.com/luxsin/app-api/internal/repository"
|
||||
"github.com/luxsin/app-api/internal/response"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CurveHandler struct {
|
||||
repo *repository.CurveRepository
|
||||
cache *cache.CurveCache
|
||||
cfg config.EqualizeConfig
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewCurveHandler(repo *repository.CurveRepository, curveCache *cache.CurveCache, cfg config.EqualizeConfig, log *zap.Logger) *CurveHandler {
|
||||
return &CurveHandler{repo: repo, cache: curveCache, cfg: cfg, log: log}
|
||||
}
|
||||
|
||||
// GetCurve 获取目标曲线
|
||||
// GET /audio/getCurve?brand=xxx&name=xxx&target=xxx&includeRaw=false
|
||||
func (h *CurveHandler) GetCurve(c *gin.Context) {
|
||||
brand := strings.TrimSpace(c.Query("brand"))
|
||||
name := strings.TrimSpace(c.Query("name"))
|
||||
target := strings.TrimSpace(c.Query("target"))
|
||||
includeRaw := c.Query("includeRaw") == "true" || c.Query("includeRaw") == "1"
|
||||
|
||||
if brand == "" || name == "" || target == "" {
|
||||
response.BadRequest(c, "brand、name和target参数不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
result, err := h.getCurvePoint(ctx, brand, name, target)
|
||||
if err != nil {
|
||||
h.log.Error("get curve point failed", zap.Error(err))
|
||||
response.InternalError(c, "获取曲线数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
response.Fail(c, http.StatusOK, 40004, "无曲线数据")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JSON 结果
|
||||
var resp map[string]any
|
||||
if err := json.Unmarshal([]byte(result), &resp); err != nil {
|
||||
response.InternalError(c, "解析曲线数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 includeRaw 为 false 且 code != 200,移除 fr 字段
|
||||
if !includeRaw {
|
||||
codeVal, _ := resp["code"]
|
||||
if code, ok := codeVal.(float64); ok && code != 200 {
|
||||
delete(resp, "fr")
|
||||
}
|
||||
}
|
||||
|
||||
response.OK(c, resp)
|
||||
}
|
||||
|
||||
// getCurvePoint 获取曲线数据:先查缓存,缓存不存在则请求 EQ 接口并缓存结果
|
||||
func (h *CurveHandler) getCurvePoint(ctx context.Context, brand, name, target string) (string, error) {
|
||||
data, acquired, err := h.cache.GetWithLock(ctx, brand, name, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 缓存命中
|
||||
if data != "" && !acquired {
|
||||
h.log.Info("curve cache hit", zap.String("brand", brand), zap.String("name", name), zap.String("target", target))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 获取了锁,缓存仍然为空,需要请求 EQ 接口
|
||||
if acquired {
|
||||
result, eqErr := h.getCurvePointFromPEQ(ctx, brand, name, target)
|
||||
if eqErr != nil {
|
||||
// 释放锁
|
||||
if lockErr := h.cache.ReleaseLock(ctx, brand, name, target); lockErr != nil {
|
||||
h.log.Warn("release curve lock failed", zap.Error(lockErr))
|
||||
}
|
||||
return "", eqErr
|
||||
}
|
||||
// 释放锁
|
||||
if lockErr := h.cache.ReleaseLock(ctx, brand, name, target); lockErr != nil {
|
||||
h.log.Warn("release curve lock failed", zap.Error(lockErr))
|
||||
}
|
||||
|
||||
if result != "" {
|
||||
// 缓存结果
|
||||
if cacheErr := h.cache.Set(ctx, brand, name, target, result); cacheErr != nil {
|
||||
h.log.Warn("curve cache set failed", zap.Error(cacheErr))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 未获取锁(其他请求正在处理),等待后重试
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return h.getCurvePoint(ctx, brand, name, target)
|
||||
}
|
||||
|
||||
// getCurvePointFromPEQ 从 EQ 接口获取曲线数据
|
||||
func (h *CurveHandler) getCurvePointFromPEQ(ctx context.Context, brand, name, targetName string) (string, error) {
|
||||
m, err := h.repo.GetModelByBrandAndName(ctx, brand, name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("query model: %w", err)
|
||||
}
|
||||
if m == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
t, err := h.repo.GetTargetByLabel(ctx, targetName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("query target: %w", err)
|
||||
}
|
||||
if t == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 确定 headPhone 名称
|
||||
headPhone := brand + " " + name
|
||||
if m.EqKey != nil && *m.EqKey == "name" {
|
||||
headPhone = name
|
||||
}
|
||||
|
||||
// 获取 measurement 数据(仅 Eafonyoung 源需要读 CSV)
|
||||
var measurement map[string]any
|
||||
if m.Source != nil && *m.Source == "Eafonyoung" {
|
||||
measurement, err = h.readCSV(h.cfg.MeasurementBasePath + "/Eafonyoung/data/" + deref(m.Form) + "/" + headPhone + ".csv")
|
||||
if err != nil || measurement == nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
var targetParam any
|
||||
var targetRaw map[string]any
|
||||
|
||||
if bool(t.ReadCSV) {
|
||||
targetRaw, err = h.readCSV(h.cfg.TargetBasePath + "/" + deref(t.File))
|
||||
if err != nil || targetRaw == nil {
|
||||
return "", nil
|
||||
}
|
||||
targetParam = targetRaw
|
||||
} else {
|
||||
targetParam = targetName
|
||||
}
|
||||
|
||||
// 解析 bassBoost
|
||||
var bassBoost map[string]any
|
||||
if t.BassBoost != nil {
|
||||
if err := json.Unmarshal([]byte(*t.BassBoost), &bassBoost); err != nil {
|
||||
h.log.Warn("parse bassBoost failed", zap.Error(err))
|
||||
bassBoost = map[string]any{"gain": 0, "fc": 100, "q": 0.7}
|
||||
}
|
||||
} else {
|
||||
bassBoost = map[string]any{"gain": 0, "fc": 100, "q": 0.7}
|
||||
}
|
||||
|
||||
resp, err := h.reqEqualize(headPhone, measurement, targetParam, bassBoost, deref(m.Source), deref(m.Rig))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// reqEqualize 调用 autoeq API
|
||||
func (h *CurveHandler) reqEqualize(headPhone string, measurement map[string]any, target any, bassBoost map[string]any, source, rig string) (string, error) {
|
||||
gain := floatVal(bassBoost, "gain", 0)
|
||||
fc := intVal(bassBoost, "fc", 100)
|
||||
q := floatVal(bassBoost, "q", 0.7)
|
||||
|
||||
reqBody := map[string]any{
|
||||
"target": target,
|
||||
"sound_signature": nil,
|
||||
"sound_signature_smoothing_window_size": 1,
|
||||
"bass_boost_gain": gain,
|
||||
"bass_boost_fc": fc,
|
||||
"bass_boost_q": q,
|
||||
"treble_boost_gain": 0,
|
||||
"treble_boost_fc": 10000,
|
||||
"treble_boost_q": 0.7,
|
||||
"tilt": 0,
|
||||
"fs": 48000,
|
||||
"bit_depth": 16,
|
||||
"phase": "minimum",
|
||||
"f_res": 16,
|
||||
"preamp": 0,
|
||||
"max_gain": 12,
|
||||
"max_slope": 18,
|
||||
"window_size": 0.08,
|
||||
"treble_window_size": 2,
|
||||
"treble_f_lower": 6000,
|
||||
"treble_f_upper": 8000,
|
||||
"treble_gain_k": 1,
|
||||
"graphic_eq": false,
|
||||
"parametric_eq": true,
|
||||
"fixed_band_eq": false,
|
||||
"convolution_eq": false,
|
||||
"source": source,
|
||||
"rig": rig,
|
||||
"parametric_eq_config": "MINIDSP_IL_DSP",
|
||||
"response": map[string]any{
|
||||
"fr_f_step": 1.02,
|
||||
"base64fp16": false,
|
||||
"fr_fields": []string{"raw"},
|
||||
},
|
||||
}
|
||||
|
||||
// measurement 或 headPhone 二选一
|
||||
if measurement != nil {
|
||||
reqBody["measurement"] = measurement
|
||||
} else if headPhone != "" {
|
||||
reqBody["name"] = headPhone
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal eq request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, h.cfg.APIURL, bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create eq request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("call eq api: %w", err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read eq response: %w", err)
|
||||
}
|
||||
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
h.log.Warn("eq api returned non-200", zap.Int("status", httpResp.StatusCode), zap.String("body", string(body)))
|
||||
return "", fmt.Errorf("eq api status: %d", httpResp.StatusCode)
|
||||
}
|
||||
|
||||
var eqResp map[string]any
|
||||
if err := json.Unmarshal(body, &eqResp); err != nil {
|
||||
return "", fmt.Errorf("unmarshal eq response: %w", err)
|
||||
}
|
||||
|
||||
parametricEq, _ := eqResp["parametric_eq"].(map[string]any)
|
||||
if parametricEq == nil {
|
||||
result := map[string]any{
|
||||
"code": 200,
|
||||
"msg": "req param error",
|
||||
"param": reqBody,
|
||||
}
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
return string(resultJSON), nil
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"parametric_eq": eqResp["parametric_eq"],
|
||||
"fr": eqResp["fr"],
|
||||
}
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
return string(resultJSON), nil
|
||||
}
|
||||
|
||||
// readCSV 读取 CSV 文件并返回 {frequency: [...], raw: [...]}
|
||||
func (h *CurveHandler) readCSV(path string) (map[string]any, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
h.log.Info("csv file not found", zap.String("path", path))
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("open csv: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader := csv.NewReader(f)
|
||||
// 跳过表头
|
||||
if _, err := reader.Read(); err != nil {
|
||||
return nil, fmt.Errorf("read csv header: %w", err)
|
||||
}
|
||||
|
||||
frequency := make([]float64, 0)
|
||||
raw := make([]float64, 0)
|
||||
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read csv row: %w", err)
|
||||
}
|
||||
if len(record) < 2 {
|
||||
continue
|
||||
}
|
||||
freq, err1 := strconv.ParseFloat(record[0], 64)
|
||||
val, err2 := strconv.ParseFloat(record[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
continue
|
||||
}
|
||||
frequency = append(frequency, freq)
|
||||
raw = append(raw, val)
|
||||
}
|
||||
|
||||
if len(frequency) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"frequency": frequency,
|
||||
"raw": raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func deref(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func floatVal(m map[string]any, key string, defaultVal float64) float64 {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n
|
||||
case int:
|
||||
return float64(n)
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(n, 64)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return f
|
||||
default:
|
||||
return defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
func intVal(m map[string]any, key string, defaultVal int) int {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
case string:
|
||||
i, err := strconv.Atoi(n)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return i
|
||||
default:
|
||||
return defaultVal
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func (h *OTAHandler) GetOTA(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 3. 检查是否为定向升级
|
||||
if ota.Target == 0 {
|
||||
if !bool(ota.Target) {
|
||||
// 非定向,直接返回
|
||||
response.OK(c, ota)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user