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:
+1
-1
@@ -74,7 +74,7 @@ func main() {
|
||||
devicePersistTask := task.NewDevicePersistTask(rdb, deviceRepo, log)
|
||||
devicePersistTask.Start(5 * time.Minute)
|
||||
|
||||
engine := router.New(log, db, searchClient, rdb)
|
||||
engine := router.New(log, db, searchClient, rdb, cfg.Equalize)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.Addr(),
|
||||
|
||||
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
curveLockTTL = 10 * time.Second
|
||||
curveLockRetryDelay = 100 * time.Millisecond
|
||||
curveLockMaxRetries = 50
|
||||
)
|
||||
|
||||
// CurveCache 曲线数据缓存(Redis Hash)
|
||||
type CurveCache struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewCurveCache(rdb *redis.Client) *CurveCache {
|
||||
return &CurveCache{rdb: rdb}
|
||||
}
|
||||
|
||||
// Get 从 Redis Hash 中获取曲线缓存
|
||||
func (c *CurveCache) Get(ctx context.Context, brand, name, target string) (string, error) {
|
||||
key := brand + " " + name
|
||||
val, err := c.rdb.HGet(ctx, key, target).Result()
|
||||
if err == redis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hget curve cache: %w", err)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Set 将曲线数据存入 Redis Hash
|
||||
func (c *CurveCache) Set(ctx context.Context, brand, name, target, data string) error {
|
||||
key := brand + " " + name
|
||||
return c.rdb.HSet(ctx, key, target, data).Err()
|
||||
}
|
||||
|
||||
// AcquireLock 获取分布式锁(SETNX),防止并发请求同一个曲线数据
|
||||
func (c *CurveCache) AcquireLock(ctx context.Context, brand, name, target string) (bool, error) {
|
||||
lockKey := brand + " " + name + ":" + target + ":lock"
|
||||
return c.rdb.SetNX(ctx, lockKey, "locked", curveLockTTL).Result()
|
||||
}
|
||||
|
||||
// ReleaseLock 释放分布式锁
|
||||
func (c *CurveCache) ReleaseLock(ctx context.Context, brand, name, target string) error {
|
||||
lockKey := brand + " " + name + ":" + target + ":lock"
|
||||
return c.rdb.Del(ctx, lockKey).Err()
|
||||
}
|
||||
|
||||
// GetWithLock 获取缓存数据,缓存不存在时尝试加锁后重新获取
|
||||
// 返回值: (data, acquired, error)
|
||||
// - data: 缓存数据(空字符串表示无数据)
|
||||
// - acquired: 是否成功获取锁(缓存不存在时需要加锁)
|
||||
func (c *CurveCache) GetWithLock(ctx context.Context, brand, name, target string) (data string, acquired bool, err error) {
|
||||
// 先查缓存
|
||||
data, err = c.Get(ctx, brand, name, target)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if data != "" {
|
||||
return data, false, nil // 缓存命中
|
||||
}
|
||||
|
||||
// 缓存不存在,尝试加锁
|
||||
acquired, err = c.AcquireLock(ctx, brand, name, target)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("acquire curve lock: %w", err)
|
||||
}
|
||||
|
||||
if acquired {
|
||||
// 加锁成功,再次检查缓存(双重检查)
|
||||
data, err = c.Get(ctx, brand, name, target)
|
||||
if err != nil {
|
||||
return "", true, err
|
||||
}
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
// 未获取锁,等待后重试
|
||||
return "", false, nil
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type Config struct {
|
||||
Database DatabaseConfig
|
||||
Meilisearch MeilisearchConfig
|
||||
Redis RedisConfig
|
||||
Equalize EqualizeConfig
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
@@ -41,6 +42,8 @@ func Load() (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eq := loadEqualize(env)
|
||||
|
||||
return &Config{
|
||||
Env: env,
|
||||
Host: getEnv("APP_HOST", "0.0.0.0"),
|
||||
@@ -48,6 +51,7 @@ func Load() (*Config, error) {
|
||||
Database: db,
|
||||
Meilisearch: ms,
|
||||
Redis: rd,
|
||||
Equalize: eq,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func loadDatabase(env string) (DatabaseConfig, error) {
|
||||
}, nil
|
||||
default:
|
||||
return DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Host: "192.168.9.137",
|
||||
Port: 3306,
|
||||
Name: "audio",
|
||||
User: "root",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
type EqualizeConfig struct {
|
||||
APIURL string
|
||||
MeasurementBasePath string
|
||||
TargetBasePath string
|
||||
}
|
||||
|
||||
func loadEqualize(env string) EqualizeConfig {
|
||||
if os.Getenv("EQ_API_URL") != "" {
|
||||
return EqualizeConfig{
|
||||
APIURL: os.Getenv("EQ_API_URL"),
|
||||
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
|
||||
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
|
||||
}
|
||||
}
|
||||
|
||||
switch env {
|
||||
case "production":
|
||||
return EqualizeConfig{
|
||||
APIURL: "https://autoeq.app/equalize",
|
||||
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
|
||||
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
|
||||
}
|
||||
default:
|
||||
return EqualizeConfig{
|
||||
APIURL: "https://autoeq.app/equalize",
|
||||
MeasurementBasePath: getEnv("MEASUREMENT_BASE_PATH", ""),
|
||||
TargetBasePath: getEnv("TARGET_BASE_PATH", ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func loadRedis(env string) RedisConfig {
|
||||
}
|
||||
default:
|
||||
return RedisConfig{
|
||||
Host: "localhost",
|
||||
Host: "192.168.9.137",
|
||||
Port: 6379,
|
||||
Password: "",
|
||||
Database: 1,
|
||||
|
||||
@@ -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
|
||||
|
||||
+34
-10
@@ -1,27 +1,51 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BoolInt 数据库存储为 int(1/0),JSON 序列化为 bool(true/false)
|
||||
type BoolInt bool
|
||||
|
||||
func (b *BoolInt) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*b = false
|
||||
return nil
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case int64:
|
||||
*b = v == 1
|
||||
case []byte:
|
||||
*b = len(v) > 0 && v[0] == '1'
|
||||
default:
|
||||
*b = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ sql.Scanner = (*BoolInt)(nil)
|
||||
|
||||
// OTA 固件升级记录
|
||||
type OTA struct {
|
||||
ID int `json:"id"`
|
||||
ID int `json:"-"`
|
||||
VerCode int `json:"verCode"`
|
||||
VerName string `json:"verName"`
|
||||
URL string `json:"url"`
|
||||
MD5 string `json:"md5"`
|
||||
Force int `json:"force"`
|
||||
Force BoolInt `json:"force"`
|
||||
Desc *string `json:"desc,omitempty"`
|
||||
Model *string `json:"model,omitempty"`
|
||||
HW int `json:"hw"`
|
||||
Target int `json:"target"`
|
||||
Beta int `json:"beta"`
|
||||
PawVerCode int `json:"pawVerCode"`
|
||||
PawVerName string `json:"pawVerName"`
|
||||
PawURL string `json:"pawUrl"`
|
||||
PawMD5 string `json:"pawMd5"`
|
||||
Target BoolInt `json:"target"`
|
||||
Beta BoolInt `json:"beta"`
|
||||
PawVerCode int `json:"-"`
|
||||
PawVerName string `json:"-"`
|
||||
PawURL string `json:"-"`
|
||||
PawMD5 string `json:"-"`
|
||||
StartTime *time.Time `json:"startTime,omitempty"`
|
||||
EndTime *time.Time `json:"endTime,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// BlackList OTA 黑名单
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Target 目标曲线
|
||||
type Target struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
ReadCSV BoolInt `json:"readCsv"`
|
||||
File *string `json:"file,omitempty"`
|
||||
BassBoost *string `json:"bassBoost,omitempty"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxsin/app-api/internal/model"
|
||||
)
|
||||
|
||||
type CurveRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCurveRepository(db *sql.DB) *CurveRepository {
|
||||
return &CurveRepository{db: db}
|
||||
}
|
||||
|
||||
// GetModelByBrandAndName 按 brand_name + name 查询 Model(唯一索引)
|
||||
func (r *CurveRepository) GetModelByBrandAndName(ctx context.Context, brandName, name string) (*model.Model, error) {
|
||||
const query = `SELECT id, brand_name, name, form, rig, source, eq_key, create_at FROM model WHERE brand_name = ? AND name = ?`
|
||||
|
||||
var m model.Model
|
||||
var form, rig, source, eqKey sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, brandName, name).Scan(
|
||||
&m.ID, &m.BrandName, &m.Name, &form, &rig, &source, &eqKey, &m.CreateAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query model: %w", err)
|
||||
}
|
||||
|
||||
m.Form = nullStringPtr(form)
|
||||
m.Rig = nullStringPtr(rig)
|
||||
m.Source = nullStringPtr(source)
|
||||
m.EqKey = nullStringPtr(eqKey)
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// GetTargetByLabel 按 label 查询 Target
|
||||
func (r *CurveRepository) GetTargetByLabel(ctx context.Context, label string) (*model.Target, error) {
|
||||
const query = `SELECT id, label, read_csv, file, bass_boost, create_at FROM target WHERE label = ?`
|
||||
|
||||
var t model.Target
|
||||
var file, bassBoost sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, label).Scan(
|
||||
&t.ID, &t.Label, &t.ReadCSV, &file, &bassBoost, &t.CreateAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query target: %w", err)
|
||||
}
|
||||
|
||||
t.File = nullStringPtr(file)
|
||||
t.BassBoost = nullStringPtr(bassBoost)
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func NewOTARepository(db *sql.DB) *OTARepository {
|
||||
|
||||
// GetLatestOTA 按 model+hw+beta+status=1 查询最新一条 OTA 记录(verCode 降序)
|
||||
func (r *OTARepository) GetLatestOTA(ctx context.Context, modelVal string, hw, beta int) (*model.OTA, error) {
|
||||
const query = `SELECT id, verCode, verName, url, md5, force, desc, model, hw, target, beta,
|
||||
const query = `SELECT id, verCode, verName, url, md5, ` + "`force`" + `, ` + "`desc`" + `, model, hw, target, beta,
|
||||
pawVerCode, pawVerName, pawUrl, pawMd5, startTime, endTime, status
|
||||
FROM ota WHERE model = ? AND hw = ? AND beta = ? AND status = 1
|
||||
ORDER BY verCode DESC LIMIT 1`
|
||||
@@ -56,7 +56,7 @@ func (r *OTARepository) GetLatestOTA(ctx context.Context, modelVal string, hw, b
|
||||
|
||||
// GetLatestOTANotInBlackList 查询最新一条不在黑名单中的 OTA 记录
|
||||
func (r *OTARepository) GetLatestOTANotInBlackList(ctx context.Context, modelVal string, hw, beta int, mac string) (*model.OTA, error) {
|
||||
const query = `SELECT id, verCode, verName, url, md5, force, desc, model, hw, target, beta,
|
||||
const query = `SELECT id, verCode, verName, url, md5, ` + "`force`" + `, ` + "`desc`" + `, model, hw, target, beta,
|
||||
pawVerCode, pawVerName, pawUrl, pawMd5, startTime, endTime, status
|
||||
FROM ota WHERE status = 1 AND model = ? AND hw = ? AND beta = ?
|
||||
AND id NOT IN (SELECT ota_id FROM black_list WHERE mac = ?)
|
||||
@@ -95,7 +95,7 @@ func (r *OTARepository) GetLatestOTANotInBlackList(ctx context.Context, modelVal
|
||||
|
||||
// GetLatestOTANotTarget 查询最新一条非定向的 OTA 记录
|
||||
func (r *OTARepository) GetLatestOTANotTarget(ctx context.Context, modelVal string, hw, beta int) (*model.OTA, error) {
|
||||
const query = `SELECT id, verCode, verName, url, md5, force, desc, model, hw, target, beta,
|
||||
const query = `SELECT id, verCode, verName, url, md5, ` + "`force`" + `, ` + "`desc`" + `, model, hw, target, beta,
|
||||
pawVerCode, pawVerName, pawUrl, pawMd5, startTime, endTime, status
|
||||
FROM ota WHERE status = 1 AND model = ? AND hw = ? AND beta = ? AND target = 0
|
||||
ORDER BY verCode DESC LIMIT 1`
|
||||
|
||||
@@ -7,23 +7,23 @@ import (
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, Body{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
Code: 200,
|
||||
Msg: "操作成功",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.JSON(httpStatus, Body{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Code: code,
|
||||
Msg: message,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"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/handler"
|
||||
"github.com/luxsin/app-api/internal/middleware"
|
||||
"github.com/luxsin/app-api/internal/repository"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Client) *gin.Engine {
|
||||
func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Client, eqCfg config.EqualizeConfig) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.RequestID())
|
||||
@@ -23,11 +24,13 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
|
||||
// Cache
|
||||
brandCache := cache.NewBrandCache(rdb)
|
||||
modelCache := cache.NewModelCache(rdb)
|
||||
curveCache := cache.NewCurveCache(rdb)
|
||||
|
||||
// Repository
|
||||
brandRepo := repository.NewBrandRepository(db, brandCache)
|
||||
modelRepo := repository.NewModelRepository(db, modelCache)
|
||||
otaRepo := repository.NewOTARepository(db)
|
||||
curveRepo := repository.NewCurveRepository(db)
|
||||
|
||||
// Handler
|
||||
health := handler.NewHealthHandler()
|
||||
@@ -36,6 +39,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
|
||||
modelList := handler.NewModelListHandler(searchClient, log)
|
||||
device := handler.NewDeviceHandler(rdb, log)
|
||||
ota := handler.NewOTAHandler(otaRepo, log)
|
||||
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, log)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
@@ -49,6 +53,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
|
||||
audio.GET("/modelList", modelList.ModelList)
|
||||
audio.GET("/reportDevInfo", device.ReportDevInfo)
|
||||
audio.GET("/ota", ota.GetOTA)
|
||||
audio.GET("/getCurve", curve.GetCurve)
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.luxsin.app.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.luxsin.app.annotation.Base64Response;
|
||||
import com.luxsin.app.bean.OTA;
|
||||
import com.luxsin.app.service.BrandService;
|
||||
import com.luxsin.app.service.ModelService;
|
||||
import com.luxsin.app.service.OTAService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eafon.data.EResponseTool;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@CrossOrigin(origins = "*")
|
||||
public class EqualizeController {
|
||||
private final ModelService modelService;
|
||||
|
||||
private final OTAService otaService;
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final BrandService brandService;
|
||||
public EqualizeController(ModelService modelService, OTAService otaService, RedisTemplate<String, String> redisTemplate, BrandService brandService) {
|
||||
this.modelService = modelService;
|
||||
this.otaService = otaService;
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.brandService = brandService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 耳机列表
|
||||
* @param key
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("modelList")
|
||||
@Base64Response
|
||||
public String getModelList(String key, @RequestParam(value = "count", defaultValue = "100") int count){
|
||||
return modelService.modelList(key, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标曲线
|
||||
* @param brand
|
||||
* @param name
|
||||
* @param target
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("getCurve")
|
||||
@Base64Response
|
||||
public String getCurve(String brand, String name, String target, @RequestParam(value = "includeRaw", defaultValue = "false") boolean includeRaw){
|
||||
String result = modelService.getCurvePoint(brand, name, target);
|
||||
if(!includeRaw){
|
||||
JSONObject resp = JSONUtil.parseObj(result);
|
||||
if(resp.containsKey("code") && resp.getInt("code") != 0){
|
||||
resp.remove("fr");
|
||||
}
|
||||
return resp.toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("modelCurve")
|
||||
@Base64Response
|
||||
public String modelCurve(String brand, String name){
|
||||
return modelService.getModelCurve(brand, name);
|
||||
}
|
||||
|
||||
@GetMapping("test/modelCurve")
|
||||
public String testModelCurve(String brand, String name){
|
||||
return modelService.getModelCurve(brand, name);
|
||||
}
|
||||
|
||||
@GetMapping("test/getCurve")
|
||||
public String testGetCurve(String brand, String name, String target,
|
||||
@RequestParam(value = "includeRaw", defaultValue = "false") boolean includeRaw){
|
||||
String result = modelService.getCurvePoint(brand, name, target);
|
||||
if(!includeRaw){
|
||||
JSONObject resp = JSONUtil.parseObj(result);
|
||||
if(resp.containsKey("code") && resp.getInt("code") != 0){
|
||||
resp.remove("fr");
|
||||
}
|
||||
return resp.toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级
|
||||
* @param model
|
||||
* @param hw
|
||||
* @param mac
|
||||
* @param beta
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("ota")
|
||||
public EResponseTool<OTA> OTA(String model, String hw, @RequestParam(value = "mac", defaultValue = "null") String mac,
|
||||
@RequestParam(value = "beta", defaultValue = "0") int beta){
|
||||
return otaService.getOTA(model, hw, mac, beta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报用户设备信息
|
||||
* @param mac
|
||||
* @param model
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("reportDevInfo")
|
||||
public EResponseTool<String> reportDevInfo(String mac, String model,String ver,
|
||||
@RequestHeader(value = "X-Forwarded-For", required = false) String xForwardedFor){
|
||||
if(StrUtil.isEmpty(mac) || StrUtil.isEmpty(model)){
|
||||
return EResponseTool.error(EResponseTool.ResultCode.VALIDATE_FAILED);
|
||||
}
|
||||
log.info("report dev info remote ip = {}, now time = {}", xForwardedFor, DateUtil.now());
|
||||
ver = null == ver ? "" : ver;
|
||||
xForwardedFor = null == xForwardedFor ? "" : xForwardedFor;
|
||||
|
||||
redisTemplate.opsForHash().put("devices", mac, JSONUtil.toJsonStr(Map.of("mac_addr", mac, "model", model,
|
||||
"active_date", LocalDate.now().toString(),
|
||||
"ip_addr", xForwardedFor,
|
||||
"ver", ver)));
|
||||
return EResponseTool.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取品牌列表
|
||||
* @param brandName
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("getBrand")
|
||||
@Base64Response
|
||||
public String getBrand(String brandName){
|
||||
return brandService.brandList(brandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取型号列表
|
||||
* @param brandName
|
||||
* @param modelName
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("getModel")
|
||||
@Base64Response
|
||||
public String getModel(String brandName, String modelName){
|
||||
return modelService.getModels(brandName, modelName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.luxsin.app.service;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.luxsin.app.bean.Model;
|
||||
import com.luxsin.app.bean.Target;
|
||||
import com.luxsin.app.bean.table.ModelTableDef;
|
||||
import com.luxsin.app.bean.table.TargetTableDef;
|
||||
import com.luxsin.app.config.MyConfig;
|
||||
import com.luxsin.app.dao.ModelMapper;
|
||||
import com.luxsin.app.dao.TargetMapper;
|
||||
import com.meilisearch.sdk.Index;
|
||||
import com.meilisearch.sdk.SearchRequest;
|
||||
import com.meilisearch.sdk.model.Searchable;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eafon.data.EResponseTool;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ModelService extends ServiceImpl<ModelMapper, Model> {
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final TargetMapper targetMapper;
|
||||
|
||||
private final MyConfig myConfig;
|
||||
|
||||
private final Index modelIdx;
|
||||
|
||||
public ModelService(RedisTemplate<String, String> redisTemplate, TargetMapper targetMapper,
|
||||
MyConfig myConfig, Index modelIdx) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.targetMapper = targetMapper;
|
||||
this.myConfig = myConfig;
|
||||
this.modelIdx = modelIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手机列表
|
||||
* @param key
|
||||
* @param count
|
||||
* @return
|
||||
*/
|
||||
public String modelList(String key, int count){
|
||||
List<Model> list = Collections.emptyList();
|
||||
Searchable searchable = modelIdx.search(SearchRequest.builder()
|
||||
.q(key)
|
||||
.attributesToRetrieve(new String[]{"rig", "form", "name", "brand_name", "source", "eq_key"})
|
||||
.limit(count)
|
||||
.build());
|
||||
if(!searchable.getHits().isEmpty()){
|
||||
return JSONUtil.toJsonStr(searchable.getHits());
|
||||
}else {
|
||||
return JSONUtil.toJsonStr(list);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标曲线
|
||||
* @param brand
|
||||
* @param name
|
||||
* @param targetName
|
||||
* @param raw 是否只是需要机型的raw数据
|
||||
* @return
|
||||
*/
|
||||
public String getCurvePoint(String brand, String name, String targetName){
|
||||
if(StrUtil.isEmpty(brand) || StrUtil.isEmpty(name) || StrUtil.isEmpty(targetName)){
|
||||
return EResponseTool.error(EResponseTool.ResultCode.VALIDATE_FAILED).toString();
|
||||
}
|
||||
String cacheKey = brand + " " + name;
|
||||
String cache = String.valueOf(redisTemplate.opsForHash().get(cacheKey, targetName));
|
||||
if(StrUtil.isEmpty(cache) || "null".equals(cache)){
|
||||
String lockKey = cacheKey+":"+targetName+":lock";
|
||||
boolean locked = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS));
|
||||
if(locked){
|
||||
try {
|
||||
cache = String.valueOf(redisTemplate.opsForHash().get(cacheKey, targetName));
|
||||
if(StrUtil.isEmpty(cache) || "null".equals(cache)){
|
||||
//redis中没有数据的,需要请求eq接口
|
||||
JSONObject resp = getCurvePointFromPEQ(brand, name, targetName);
|
||||
if(null!=resp && resp.getInt("code") == EResponseTool.ResultCode.SUCCESS.getCode()){
|
||||
//将请求接口得到的数据,放入缓存
|
||||
redisTemplate.opsForHash().put(brand+" "+name, targetName, resp.toString());
|
||||
return resp.toString();
|
||||
}
|
||||
}
|
||||
}finally {
|
||||
redisTemplate.delete(lockKey);
|
||||
}
|
||||
}else{
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return getCurvePoint(brand, name, targetName);
|
||||
}
|
||||
}else{
|
||||
log.info("brand: {} name: {} target {} read cache.", brand, name, targetName);
|
||||
return cache;
|
||||
}
|
||||
|
||||
return EResponseTool.error(EResponseTool.ResultCode.EMPTY).toString();
|
||||
}
|
||||
|
||||
private JSONObject getCurvePointFromPEQ(String brand, String name, String targetName){
|
||||
Model model = getOne(QueryWrapper.create().where(ModelTableDef.MODEL.BRAND_NAME.eq(brand)).
|
||||
and(ModelTableDef.MODEL.NAME.eq(name)));
|
||||
if(!Objects.isNull(model)){
|
||||
Target target = targetMapper.selectOneByQuery(QueryWrapper.create().where(TargetTableDef.TARGET.LABEL.eq(targetName)));
|
||||
if(!Objects.isNull(target)){
|
||||
String eq_key = model.getEq_key();
|
||||
String headPhone = brand+" "+name;
|
||||
if(StrUtil.isNotEmpty(eq_key) && eq_key.equals("name")){
|
||||
headPhone = name;
|
||||
}
|
||||
|
||||
JSONObject measurement = null;
|
||||
if("Eafonyoung".equals(model.getSource())){
|
||||
measurement = getCSV(myConfig.getMeasurementBasePath()+"/Eafonyoung/data/"+model.getForm()+"/"+headPhone+".csv");
|
||||
if(Objects.isNull(measurement)){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject resp;
|
||||
if(target.isRead_csv()){
|
||||
//读取自定义的target
|
||||
JSONObject targetRaw = getCSV(myConfig.getTargetBasePath()+"/"+target.getFile());
|
||||
resp = reqEqualize(headPhone, measurement, targetRaw, JSONUtil.parseObj(target.getBassBoost()),
|
||||
model.getSource(), model.getRig());
|
||||
}else{
|
||||
resp = reqEqualize(headPhone, measurement, targetName, JSONUtil.parseObj(target.getBassBoost()),
|
||||
model.getSource(), model.getRig());
|
||||
}
|
||||
|
||||
if(resp.getInt("code") == EResponseTool.ResultCode.SUCCESS.getCode()){
|
||||
return resp;
|
||||
}else{
|
||||
log.info("请求reqEqualize接口失败,没有数据");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private JSONObject getCSV(String path){
|
||||
if(FileUtil.exist(path)){
|
||||
List<String> list = FileUtil.readLines(path, "UTF-8");
|
||||
JSONArray frequency = new JSONArray();
|
||||
JSONArray raw = new JSONArray();
|
||||
for(int i=1;i<list.size();i++){
|
||||
String[] values = list.get(i).split(",");
|
||||
frequency.add(NumberUtil.parseFloat(values[0]));
|
||||
raw.add(NumberUtil.parseFloat(values[1]));
|
||||
}
|
||||
return new JSONObject(Map.of("frequency",frequency,"raw",raw));
|
||||
}
|
||||
log.info("找不到耳机频响文件:{}", path);
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param headPhone 耳机名称(品牌+型号)
|
||||
* @param measurement 耳机csv文件,和耳机名称二选一,如果耳机名称不为空优先耳机名称
|
||||
* @param target 目标曲线名称,或者目标曲线的csv文件
|
||||
* @param bassBoost
|
||||
* @param source
|
||||
* @param rig
|
||||
* @return
|
||||
*/
|
||||
private JSONObject reqEqualizeInternal(String headPhone,JSONObject measurement, Object target, JSONObject bassBoost, String source, String rig) {
|
||||
Map<String, Object> reqMap = new HashMap<>();
|
||||
reqMap.put("target", target);
|
||||
reqMap.put("sound_signature", null);
|
||||
reqMap.put("sound_signature_smoothing_window_size", 1);
|
||||
reqMap.put("bass_boost_gain", bassBoost.getFloat("gain"));
|
||||
reqMap.put("bass_boost_fc", bassBoost.getInt("fc"));
|
||||
reqMap.put("bass_boost_q", bassBoost.getFloat("q"));
|
||||
reqMap.put("treble_boost_gain",0);
|
||||
reqMap.put("treble_boost_fc", 10000);
|
||||
reqMap.put("treble_boost_q", 0.7);
|
||||
reqMap.put("tilt", 0);
|
||||
reqMap.put("fs",48000);
|
||||
reqMap.put("bit_depth", 16);
|
||||
reqMap.put("phase", "minimum");
|
||||
reqMap.put("f_res", 16);
|
||||
reqMap.put("preamp",0);
|
||||
reqMap.put("max_gain" ,12);
|
||||
reqMap.put("max_slope", 18);
|
||||
reqMap.put("window_size", 0.08);
|
||||
reqMap.put("treble_window_size", 2);
|
||||
reqMap.put("treble_f_lower", 6000);
|
||||
reqMap.put("treble_f_upper", 8000);
|
||||
reqMap.put("treble_gain_k", 1);
|
||||
reqMap.put("graphic_eq", false);
|
||||
reqMap.put("parametric_eq", true);
|
||||
reqMap.put("fixed_band_eq", false);
|
||||
reqMap.put("convolution_eq", false);
|
||||
|
||||
|
||||
if(null!=measurement){
|
||||
reqMap.put("measurement", measurement);
|
||||
}
|
||||
else if(StrUtil.isNotEmpty(headPhone)){
|
||||
reqMap.put("name", headPhone);
|
||||
}
|
||||
|
||||
reqMap.put("source", source);
|
||||
reqMap.put("rig", rig);
|
||||
reqMap.put("parametric_eq_config", "MINIDSP_IL_DSP");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
JSONArray fr_fields = new JSONArray();
|
||||
fr_fields.addAll(List.of("raw"));
|
||||
response.put("fr_f_step", 1.02);
|
||||
response.put("base64fp16" ,false);
|
||||
response.put("fr_fields", fr_fields);
|
||||
|
||||
reqMap.put("response", response);
|
||||
|
||||
String resp = HttpUtil.post("https://autoeq.app/equalize", new JSONObject(reqMap).toString(), 10000);
|
||||
JSONObject result = new JSONObject();
|
||||
if(StrUtil.isNotEmpty(resp)){
|
||||
JSONObject respJson = JSONUtil.parseObj(resp);
|
||||
JSONObject parametric_eq = respJson.getJSONObject("parametric_eq");
|
||||
if(Objects.isNull(parametric_eq)){
|
||||
result.putOnce("code", 0);
|
||||
result.putOnce("msg", "req param error");
|
||||
result.putOnce("param", reqMap);
|
||||
return result;
|
||||
}else {
|
||||
result.putOnce("parametric_eq", respJson.getJSONObject("parametric_eq"));
|
||||
result.putOnce("fr", respJson.getJSONObject("fr"));
|
||||
}
|
||||
}
|
||||
result.putOnce("code", EResponseTool.ResultCode.SUCCESS.getCode());
|
||||
result.putOnce("msg", "ok");
|
||||
return result;
|
||||
}
|
||||
|
||||
public JSONObject reqEqualize(String headPhone, JSONObject measurement, String target, JSONObject bassBoost, String source, String rig) {
|
||||
return reqEqualizeInternal(headPhone, measurement, target, bassBoost, source, rig);
|
||||
}
|
||||
|
||||
public JSONObject reqEqualize(String headPhone, JSONObject measurement, JSONObject target, JSONObject bassBoost, String source, String rig) {
|
||||
return reqEqualizeInternal(headPhone, measurement, target, bassBoost, source, rig);
|
||||
}
|
||||
|
||||
public String getModels(String brandName, String modelName){
|
||||
List<Model> list;
|
||||
if(StrUtil.isNotEmpty(brandName)){
|
||||
list = list(QueryWrapper.create().where(ModelTableDef.MODEL.BRAND_NAME.eq(brandName)).orderBy(ModelTableDef.MODEL.NAME, true));
|
||||
}
|
||||
else if(StrUtil.isNotEmpty(modelName)){
|
||||
list = list(QueryWrapper.create().where(ModelTableDef.MODEL.NAME.like(modelName)).orderBy(ModelTableDef.MODEL.NAME, true));
|
||||
}
|
||||
else{
|
||||
list = Collections.emptyList();
|
||||
}
|
||||
return JSONUtil.toJsonStr(list);
|
||||
}
|
||||
|
||||
public String getModelCurve(String brand, String name){
|
||||
return getCurvePoint(brand, name, "Harman over-ear 2018");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user