Files
app-api/olds/ModelService.java
T
eafonyang c4cd8b6f5d 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对应功能,保持客户端接口兼容与调用逻辑一致
2026-06-01 19:08:43 +08:00

281 lines
12 KiB
Java

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");
}
}