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,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