WebSocketHandler.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. package com.ym.mec.biz.handler;
  2. import be.tarsos.dsp.AudioDispatcher;
  3. import be.tarsos.dsp.SilenceDetector;
  4. import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
  5. import be.tarsos.dsp.pitch.PitchProcessor;
  6. import be.tarsos.dsp.util.PitchConverter;
  7. import com.alibaba.fastjson.JSON;
  8. import com.alibaba.fastjson.JSONObject;
  9. import com.ym.mec.biz.dal.dto.MusicPitchDetailDto;
  10. import com.ym.mec.biz.dal.dto.SoundCompareHelper;
  11. import com.ym.mec.biz.dal.dto.WavHeader;
  12. import com.ym.mec.biz.dal.dto.WebSocketInfo;
  13. import com.ym.mec.biz.service.SoundSocketService;
  14. import com.ym.mec.biz.service.SysMusicCompareRecordService;
  15. import com.ym.mec.common.constant.CommonConstants;
  16. import org.apache.commons.io.FileUtils;
  17. import org.slf4j.Logger;
  18. import org.slf4j.LoggerFactory;
  19. import org.springframework.beans.factory.annotation.Autowired;
  20. import org.springframework.stereotype.Service;
  21. import org.springframework.util.CollectionUtils;
  22. import org.springframework.web.socket.*;
  23. import org.springframework.web.socket.handler.AbstractWebSocketHandler;
  24. import javax.sound.sampled.AudioFormat;
  25. import javax.sound.sampled.UnsupportedAudioFileException;
  26. import java.io.File;
  27. import java.io.IOException;
  28. import java.io.RandomAccessFile;
  29. import java.math.BigDecimal;
  30. import java.time.LocalDateTime;
  31. import java.time.format.DateTimeFormatter;
  32. import java.util.*;
  33. import java.util.concurrent.ConcurrentHashMap;
  34. import java.util.stream.Collectors;
  35. /**
  36. * @Author Joburgess
  37. * @Date 2021/6/9 0009
  38. */
  39. @Service
  40. public class WebSocketHandler extends AbstractWebSocketHandler {
  41. private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketHandler.class);
  42. //存储客户端链接
  43. public static final Map<String, WebSocketSession> WS_CLIENTS = new ConcurrentHashMap<>();
  44. private final BigDecimal oneHundred = new BigDecimal(100);
  45. private final float simpleRate = 44100;
  46. private final int simpleSize = 1024;
  47. private final int overlap = 256;
  48. private final AudioFormat audioFormat = new AudioFormat(simpleRate, 16, 1, true, false);
  49. private static final PitchProcessor.PitchEstimationAlgorithm algo = PitchProcessor.PitchEstimationAlgorithm.FFT_YIN;
  50. private final SilenceDetector silenceDetecor = new SilenceDetector();
  51. private final String tmpDir = FileUtils.getTempDirectoryPath() + "/soundCompare/";
  52. //用户对应评分信息
  53. private Map<String, SoundCompareHelper> userSoundInfoMap = new ConcurrentHashMap<>();
  54. @Autowired
  55. private SysMusicCompareRecordService sysMusicCompareRecordService;
  56. public WebSocketHandler() {
  57. super();
  58. File soundDir = new File(tmpDir);
  59. if(!soundDir.exists()){
  60. soundDir.mkdir();
  61. }
  62. }
  63. @Override
  64. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  65. String phone = session.getPrincipal().getName().split(":")[1];
  66. LOGGER.info("{}上线", phone);
  67. WS_CLIENTS.put(phone, session);
  68. super.afterConnectionEstablished(session);
  69. }
  70. @Override
  71. public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
  72. super.handleMessage(session, message);
  73. }
  74. @Override
  75. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  76. String phone = session.getPrincipal().getName().split(":")[1];
  77. LOGGER.info("{}: {}", phone, message.getPayload());
  78. WebSocketInfo webSocketInfo = JSON.parseObject(message.getPayload(), WebSocketInfo.class);
  79. JSONObject bodyObject = (JSONObject) webSocketInfo.getBody();
  80. String commond = "";
  81. if(webSocketInfo.getHeader().containsKey(SoundSocketService.COMMOND)){
  82. commond = webSocketInfo.getHeader().get(SoundSocketService.COMMOND);
  83. }
  84. switch (commond){
  85. case SoundSocketService.MUSIC_XML:
  86. userSoundInfoMap.put(phone, new SoundCompareHelper());
  87. List<MusicPitchDetailDto> musicXmlInfos = JSON.parseArray(bodyObject.getString("musicXmlInfos"), MusicPitchDetailDto.class);
  88. userSoundInfoMap.get(phone).setMusicXmlInfos(musicXmlInfos);
  89. musicXmlInfos = musicXmlInfos.stream().filter(m->!m.getDontEvaluating()).collect(Collectors.toList());
  90. userSoundInfoMap.get(phone).setMusicScoreId(bodyObject.getInteger("id"));
  91. userSoundInfoMap.get(phone).setMeasureXmlInfoMap(musicXmlInfos.stream().collect(Collectors.groupingBy(MusicPitchDetailDto::getMeasureIndex)));
  92. musicXmlInfos.forEach(e->userSoundInfoMap.get(phone).getMusicalNotePitchMap().put(e.getMusicalNotesIndex(), e.getFrequency()));
  93. for (Map.Entry<Integer, List<MusicPitchDetailDto>> userMeasureXmlInfoEntry : userSoundInfoMap.get(phone).getMeasureXmlInfoMap().entrySet()) {
  94. MusicPitchDetailDto firstPitch = userMeasureXmlInfoEntry.getValue().stream().min(Comparator.comparing(MusicPitchDetailDto::getTimeStamp)).get();
  95. MusicPitchDetailDto lastPitch = userMeasureXmlInfoEntry.getValue().stream().max(Comparator.comparing(MusicPitchDetailDto::getTimeStamp)).get();
  96. MusicPitchDetailDto musicPitchDetailDto = new MusicPitchDetailDto(firstPitch.getTimeStamp(), lastPitch.getTimeStamp() + lastPitch.getDuration());
  97. musicPitchDetailDto.setDuration(musicPitchDetailDto.getEndTimeStamp()-musicPitchDetailDto.getTimeStamp());
  98. userSoundInfoMap.get(phone).getMeasureEndTime().put(userMeasureXmlInfoEntry.getKey(), musicPitchDetailDto);
  99. }
  100. break;
  101. case SoundSocketService.RECORD_START:
  102. if(!userSoundInfoMap.containsKey(phone)){
  103. break;
  104. }
  105. File file = new File(tmpDir+phone + "_"+ userSoundInfoMap.get(phone).getMusicScoreId() +"_"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) +".wav");
  106. userSoundInfoMap.get(phone).setAccessFile(new RandomAccessFile(file, "rw"));
  107. break;
  108. case SoundSocketService.RECORD_END:
  109. if(!userSoundInfoMap.containsKey(phone)){
  110. break;
  111. }
  112. if(!CollectionUtils.isEmpty(userSoundInfoMap.get(phone).getMeasureEndTime())){
  113. Integer lastMeasureIndex = userSoundInfoMap.get(phone).getMeasureEndTime().keySet().stream().min(Integer::compareTo).get();
  114. double recordTime = userSoundInfoMap.get(phone).getAccessFile().length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000;
  115. //如果结束时时长大于某小节,则此小节需要评分
  116. if(recordTime>userSoundInfoMap.get(phone).getMeasureEndTime().get(lastMeasureIndex).getEndTimeStamp()){
  117. measureCompare(phone, lastMeasureIndex);
  118. userSoundInfoMap.get(phone).getMeasureEndTime().remove(lastMeasureIndex);
  119. }
  120. }
  121. calTotalScore(phone);
  122. createHeader(phone);
  123. break;
  124. case SoundSocketService.RECORD_CANCEL:
  125. createHeader(phone);
  126. break;
  127. case SoundSocketService.PROXY_MESSAGE:
  128. if(bodyObject.containsKey(SoundSocketService.OFFSET_TIME)){
  129. int offsetTime = bodyObject.getIntValue(SoundSocketService.OFFSET_TIME);
  130. calOffsetTime(phone, offsetTime);
  131. }
  132. break;
  133. default:
  134. break;
  135. }
  136. }
  137. @Override
  138. protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
  139. String phone = session.getPrincipal().getName().split(":")[1];
  140. if(!userSoundInfoMap.containsKey(phone)){
  141. return;
  142. }
  143. if(Objects.nonNull(userSoundInfoMap.get(phone).getAccessFile())){
  144. userSoundInfoMap.get(phone).getAccessFile().write(message.getPayload().array());
  145. }
  146. List<MusicPitchDetailDto> recordInfo = new ArrayList<>();
  147. AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(message.getPayload().array(), audioFormat, simpleSize, overlap);
  148. dispatcher.addAudioProcessor(silenceDetecor);
  149. dispatcher.addAudioProcessor(new PitchProcessor(algo, simpleRate, simpleSize, (pitchDetectionResult, audioEvent) -> {
  150. int timeStamp = (int) (userSoundInfoMap.get(phone).getMeasureStartTime() + audioEvent.getTimeStamp()*1000);
  151. float pitch = pitchDetectionResult.getPitch();
  152. double cents = 0;
  153. if (pitch > 0) {
  154. cents = PitchConverter.hertzToAbsoluteCent(pitch);
  155. }
  156. // LOGGER.info("时间:{}, 频率:{}, 分贝:{}, 音分:{}", timeStamp, pitch, silenceDetecor.currentSPL(), cents);
  157. // if (silenceDetecor.currentSPL() < -66){
  158. // pitch = -1;
  159. // }
  160. recordInfo.add(new MusicPitchDetailDto(timeStamp, pitch, silenceDetecor.currentSPL()));
  161. }));
  162. dispatcher.run();
  163. if(Objects.isNull(userSoundInfoMap.get(phone).getAccessFile())){
  164. return;
  165. }
  166. double recordTime = userSoundInfoMap.get(phone).getAccessFile().length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000;
  167. userSoundInfoMap.get(phone).setMeasureStartTime(recordTime);
  168. userSoundInfoMap.get(phone).getRecordMeasurePithInfo().addAll(recordInfo);
  169. for (Map.Entry<Integer, MusicPitchDetailDto> userMeasureEndTimeMapEntry : userSoundInfoMap.get(phone).getMeasureEndTime().entrySet()) {
  170. int ot = (int) (userMeasureEndTimeMapEntry.getValue().getDuration()*0.1);
  171. if(recordTime>(userMeasureEndTimeMapEntry.getValue().getEndTimeStamp()+ot)){
  172. // LOGGER.info("频分开始{}:{}, {}, {}", recordTime, userSoundInfoMap.get(phone).getAccessFile().length(), ot, JSON.toJSONString(userMeasureEndTimeMapEntry.getValue()));
  173. measureCompare(phone, userMeasureEndTimeMapEntry.getKey());
  174. // measureCompare2(phone, userMeasureEndTimeMapEntry.getValue());
  175. userSoundInfoMap.get(phone).getMeasureEndTime().remove(userMeasureEndTimeMapEntry.getKey());
  176. break;
  177. }
  178. }
  179. }
  180. @Override
  181. protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
  182. super.handlePongMessage(session, message);
  183. LOGGER.info("心跳信息:{}", new String(message.getPayload().array(), "utf-8"));
  184. }
  185. @Override
  186. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  187. String phone = session.getPrincipal().getName().split(":")[1];
  188. session.close();
  189. if(!WS_CLIENTS.containsKey(phone)){
  190. return;
  191. }
  192. exception.printStackTrace();
  193. LOGGER.info("发生了错误,移除客户端: {}", phone);
  194. WS_CLIENTS.remove(phone);
  195. userSoundInfoMap.remove(phone);
  196. createHeader(phone);
  197. }
  198. @Override
  199. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  200. super.afterConnectionClosed(session, status);
  201. String phone = session.getPrincipal().getName().split(":")[1];
  202. LOGGER.info("{}离线", phone);
  203. WS_CLIENTS.remove(phone);
  204. userSoundInfoMap.remove(phone);
  205. createHeader(phone);
  206. }
  207. @Override
  208. public boolean supportsPartialMessages() {
  209. return super.supportsPartialMessages();
  210. }
  211. /**
  212. * @describe 处理时间偏移
  213. * @author Joburgess
  214. * @date 2021/7/5 0005
  215. * @param phone:
  216. * @param offsetTime:
  217. * @return void
  218. */
  219. private void calOffsetTime(String phone, int offsetTime){
  220. userSoundInfoMap.get(phone).setOffsetTime(offsetTime);
  221. for (Map.Entry<Integer, MusicPitchDetailDto> musicPitchDetailDtoEntry : userSoundInfoMap.get(phone).getMeasureEndTime().entrySet()) {
  222. musicPitchDetailDtoEntry.getValue().setTimeStamp(musicPitchDetailDtoEntry.getValue().getTimeStamp() + offsetTime);
  223. musicPitchDetailDtoEntry.getValue().setEndTimeStamp(musicPitchDetailDtoEntry.getValue().getEndTimeStamp() + offsetTime);
  224. }
  225. }
  226. /**
  227. * @describe 保存录音数据,并生成wav头信息
  228. * @author Joburgess
  229. * @date 2021/6/25 0025
  230. * @param phone:
  231. * @return void
  232. */
  233. private void createHeader(String phone) throws IOException {
  234. if(!userSoundInfoMap.containsKey(phone)){
  235. return;
  236. }
  237. if(Objects.nonNull(userSoundInfoMap.get(phone).getAccessFile())){
  238. RandomAccessFile randomAccessFile = userSoundInfoMap.get(phone).getAccessFile();
  239. LOGGER.info("音频时长:{}", randomAccessFile.length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000);
  240. randomAccessFile.seek(0);
  241. randomAccessFile.write(WavHeader.getWaveHeader(randomAccessFile.length(), (long) audioFormat.getFrameRate(), audioFormat.getSampleSizeInBits()));
  242. randomAccessFile.close();
  243. userSoundInfoMap.get(phone).setAccessFile(null);
  244. }
  245. // userSoundInfoMap.get(phone).setRecordMeasurePithInfo(null);
  246. userSoundInfoMap.remove(phone);
  247. }
  248. /**
  249. * @describe 数据比对,生成分数
  250. * @author Joburgess
  251. * @date 2021/6/25 0025
  252. * @param phone:
  253. * @param measureIndex:
  254. * @return void
  255. */
  256. private void measureCompare(String phone, int measureIndex) throws IOException {
  257. //相似度
  258. BigDecimal intonation = BigDecimal.ZERO;
  259. //节奏
  260. BigDecimal cadence = BigDecimal.ZERO;
  261. //完整度
  262. BigDecimal integrity = BigDecimal.ZERO;
  263. try {
  264. //最低有效频率
  265. float minValidFrequency = 20;
  266. //音准分数
  267. float intonationScore = 0;
  268. //节奏匹配数量
  269. float cadenceNum = 0;
  270. //节奏有效阈值
  271. float cadenceValidDuty = 0.01f;
  272. //完整性误差范围
  273. float integrityRange = 30;
  274. //完整性分数
  275. float integrityScore = 0;
  276. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).size();
  277. for (int i = 0; i < userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).size(); i++) {
  278. MusicPitchDetailDto musicXmlInfo = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).get(i);
  279. int ot5 = (int) (musicXmlInfo.getDuration()*0.1);
  280. int startTimeStamp = musicXmlInfo.getTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime() + ot5;
  281. int endTimeStamp = musicXmlInfo.getTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime() + musicXmlInfo.getDuration() - ot5;
  282. int preMeasureEndTimeStamp = startTimeStamp;
  283. List<MusicPitchDetailDto> ms = userSoundInfoMap.get(phone).getMusicXmlInfos().stream().filter(m -> m.getMusicalNotesIndex() == musicXmlInfo.getMusicalNotesIndex() - 1).collect(Collectors.toList());
  284. if(!CollectionUtils.isEmpty(ms)){
  285. preMeasureEndTimeStamp = ms.get(0).getEndTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime();
  286. }
  287. //时间范围内有效节奏数量
  288. float cadenceValidNum = 0;
  289. //时间范围内有效音频数量
  290. float integrityValidNum = 0;
  291. //时间范围内匹配次数
  292. float compareNum = 0;
  293. boolean newMeasure = false;
  294. float preMusicalNotesPitch = 0;
  295. if(userSoundInfoMap.get(phone).getMusicalNotePitchMap().containsKey(musicXmlInfo.getMusicalNotesIndex()-1)){
  296. preMusicalNotesPitch = userSoundInfoMap.get(phone).getMusicalNotePitchMap().get(musicXmlInfo.getMusicalNotesIndex()-1);
  297. }
  298. if(userSoundInfoMap.get(phone).getMusicalNotePitchMap().get(musicXmlInfo.getMusicalNotesIndex())==-1){
  299. newMeasure = true;
  300. }
  301. int newNum = 0;
  302. for (MusicPitchDetailDto recordInfo : userSoundInfoMap.get(phone).getRecordMeasurePithInfo()) {
  303. if(musicXmlInfo.getMusicalNotesIndex()==0){
  304. newMeasure = true;
  305. }
  306. if(newMeasure){
  307. break;
  308. }
  309. if(recordInfo.getTimeStamp()<preMeasureEndTimeStamp||recordInfo.getTimeStamp()>startTimeStamp){
  310. continue;
  311. }
  312. if(Math.abs(recordInfo.getFrequency()-preMusicalNotesPitch)>10){
  313. newNum++;
  314. }else{
  315. newNum = 0;
  316. }
  317. if(newNum>=2){
  318. newMeasure = true;
  319. }
  320. }
  321. // List<Float> musicalNotesPitchs = new ArrayList<>();
  322. // List<Float> decibels = new ArrayList<>();
  323. List<MusicPitchDetailDto> measureSoundPitchInfos = new ArrayList<>();
  324. for (int j = 0; j < userSoundInfoMap.get(phone).getRecordMeasurePithInfo().size(); j++) {
  325. MusicPitchDetailDto recordInfo = userSoundInfoMap.get(phone).getRecordMeasurePithInfo().get(j);
  326. //如果在时间范围之外直接跳过
  327. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  328. continue;
  329. }
  330. // musicalNotesPitchs.add(recordInfo.getFrequency());
  331. // decibels.add(recordInfo.getDecibel());
  332. measureSoundPitchInfos.add(recordInfo);
  333. compareNum++;
  334. if(!newMeasure){
  335. continue;
  336. }
  337. // LOGGER.info("{}频率({}-{}):{}, {}", recordInfo.getTimeStamp(), startTimeStamp, endTimeStamp, musicXmlInfo.getFrequency(), recordInfo.getFrequency());
  338. //如果在最低有效频率以下则跳过
  339. if(recordInfo.getFrequency()<minValidFrequency&&musicXmlInfo.getFrequency()!=-1){
  340. continue;
  341. }
  342. cadenceValidNum++;
  343. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  344. continue;
  345. }
  346. //如果频率差值在节奏误差范围内
  347. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=integrityRange){
  348. integrityValidNum++;
  349. }
  350. }
  351. //非正常频率次数
  352. int errPitchNum = 0;
  353. //分贝变化次数
  354. int decibelChangeNum = 0;
  355. if(CollectionUtils.isEmpty(measureSoundPitchInfos)){
  356. userSoundInfoMap.get(phone).getMusicalNotePitchMap().put(musicXmlInfo.getMusicalNotesIndex(), (float) 0);
  357. }else{
  358. Map<Integer, Long> collect = measureSoundPitchInfos.stream().map(pitch -> (int)pitch.getFrequency()).collect(Collectors.groupingBy(Integer::intValue, Collectors.counting()));
  359. //出现次数最多的频率
  360. Integer pitch = collect.entrySet().stream().max(Comparator.comparing(e -> e.getValue())).get().getKey();
  361. //当前频率
  362. double cf = -1;
  363. //频率持续数量
  364. int fnum = 0;
  365. //是否演奏中
  366. boolean ing = false;
  367. //当前分贝
  368. double cd = 0;
  369. //分贝持续数量
  370. int dnum = 0;
  371. for (MusicPitchDetailDto musicalNotesPitch : measureSoundPitchInfos) {
  372. //计算频率断层次数
  373. if (Math.abs(musicalNotesPitch.getFrequency() - cf) > 20){
  374. fnum ++;
  375. }
  376. if (fnum>=5){
  377. cf = musicalNotesPitch.getFrequency();
  378. fnum = 0;
  379. if (cf != -1){
  380. errPitchNum ++;
  381. ing = true;
  382. cd = musicalNotesPitch.getDecibel();
  383. }
  384. }
  385. //计算声音大小断层册数
  386. if(ing && Math.abs(musicalNotesPitch.getDecibel() - cd) > 5){
  387. dnum ++;
  388. }
  389. if (dnum > 2){
  390. cd = musicalNotesPitch.getDecibel();
  391. dnum = 0;
  392. decibelChangeNum++;
  393. }
  394. }
  395. userSoundInfoMap.get(phone).getMusicalNotePitchMap().put(musicXmlInfo.getMusicalNotesIndex(), (float) pitch);
  396. }
  397. //有效节奏占比
  398. float cadenceDuty = cadenceValidNum/compareNum;
  399. //如果频率出现断层或这个音量出现断层,则当前音符节奏无效
  400. if(errPitchNum>=2 || decibelChangeNum>1){
  401. cadenceDuty = 0;
  402. }
  403. //节奏
  404. if(cadenceDuty>=cadenceValidDuty){
  405. cadenceNum++;
  406. }
  407. //音准
  408. if (!CollectionUtils.isEmpty(measureSoundPitchInfos)){
  409. Double avgPitch = measureSoundPitchInfos.stream().filter(pitch -> Math.abs((pitch.getFrequency()-musicXmlInfo.getFrequency()))<5).collect(Collectors.averagingDouble(pitch -> pitch.getFrequency()));
  410. //音分
  411. double recordCents = 0;
  412. if (avgPitch > 0){
  413. recordCents = PitchConverter.hertzToAbsoluteCent(avgPitch);
  414. }
  415. double cents = PitchConverter.hertzToAbsoluteCent(musicXmlInfo.getFrequency());
  416. double score = 100 - Math.round(Math.abs(cents - recordCents)) + 3;
  417. if (score < 0){
  418. score = 0;
  419. }else if(score > 100){
  420. score = 100;
  421. }
  422. intonationScore += score;
  423. musicXmlInfo.setAvgFrequency(avgPitch.floatValue());
  424. }
  425. //完成度
  426. if(integrityValidNum>0){
  427. integrityValidNum = integrityValidNum + (float) (compareNum * 0.05);
  428. }
  429. if(integrityValidNum > compareNum){
  430. integrityValidNum = compareNum;
  431. }
  432. float integrityDuty = integrityValidNum/compareNum;
  433. integrityScore += integrityDuty;
  434. }
  435. BigDecimal measureNum = new BigDecimal(totalCompareNum);
  436. intonation = new BigDecimal(intonationScore).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).setScale(0, BigDecimal.ROUND_UP);
  437. cadence = new BigDecimal(cadenceNum).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_UP);
  438. integrity = new BigDecimal(integrityScore).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_UP);
  439. } catch (ArithmeticException e){
  440. LOGGER.info("无musicXml信息");
  441. }
  442. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("intonation")){
  443. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation.add(userSoundInfoMap.get(phone).getUserScoreMap().get("intonation")));
  444. }else{
  445. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation);
  446. }
  447. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("cadence")){
  448. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence.add(userSoundInfoMap.get(phone).getUserScoreMap().get("cadence")));
  449. }else{
  450. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence);
  451. }
  452. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("integrity")){
  453. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity.add(userSoundInfoMap.get(phone).getUserScoreMap().get("integrity")));
  454. }else{
  455. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity);
  456. }
  457. Map<String, BigDecimal> scoreData = new HashMap<>();
  458. scoreData.put("intonation", intonation);
  459. scoreData.put("cadence", cadence);
  460. scoreData.put("integrity", integrity);
  461. userSoundInfoMap.get(phone).getUserMeasureScoreMap().put(measureIndex, scoreData);
  462. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("measureScore", measureIndex, intonation, cadence, integrity))));
  463. }
  464. private void measureCompare2(String phone, MusicPitchDetailDto measureTimeInfo) throws IOException, UnsupportedAudioFileException {
  465. //小节总时长
  466. double measureTime = measureTimeInfo.getEndTimeStamp()-measureTimeInfo.getTimeStamp();
  467. double ot = measureTime * 0.1;
  468. measureTime += ot;
  469. //小节时长占用字节数
  470. int measureByteNum = (int) (measureTime/1000*(audioFormat.getFrameSize()*audioFormat.getFrameRate()));
  471. List<MusicPitchDetailDto> recordInfo = new ArrayList<>();
  472. byte[] bytes = new byte[measureByteNum];
  473. long startOffset = (userSoundInfoMap.get(phone).getAccessFile().length()-measureByteNum);
  474. userSoundInfoMap.get(phone).getAccessFile().seek(startOffset);
  475. // userSoundInfoMap.get(phone).getAccessFile().seek(0);
  476. userSoundInfoMap.get(phone).getAccessFile().readFully(bytes);
  477. userSoundInfoMap.get(phone).getAccessFile().seek(userSoundInfoMap.get(phone).getAccessFile().length());
  478. AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(bytes, audioFormat, simpleSize, 128);
  479. dispatcher.addAudioProcessor(new PitchProcessor(algo, simpleRate, simpleSize, (pitchDetectionResult, audioEvent) -> {
  480. int timeStamp = (int) (measureTimeInfo.getTimeStamp() - (ot>measureTimeInfo.getTimeStamp()?0:ot) + audioEvent.getTimeStamp()*1000);
  481. float pitch = pitchDetectionResult.getPitch();
  482. recordInfo.add(new MusicPitchDetailDto(timeStamp, pitch));
  483. }));
  484. dispatcher.run();
  485. userSoundInfoMap.get(phone).getRecordMeasurePithInfo().addAll(recordInfo);
  486. LOGGER.info("小节评分频率{}:{}", measureTimeInfo.getMeasureIndex(), JSON.toJSONString(recordInfo));
  487. scoreCal(phone, measureTimeInfo, recordInfo);
  488. }
  489. private void scoreCal(String phone, MusicPitchDetailDto measureTimeInfo, List<MusicPitchDetailDto> recordPitchDetails) throws IOException {
  490. //相似度
  491. BigDecimal intonation = BigDecimal.ZERO;
  492. //节奏
  493. BigDecimal cadence = BigDecimal.ZERO;
  494. //完整度
  495. BigDecimal integrity = BigDecimal.ZERO;
  496. try {
  497. //最低有效频率
  498. float minValidFrequency = 20;
  499. //音准匹配数量
  500. float intonationNum = 0;
  501. //音准匹配误差范围
  502. float intonationErrRange = 15;
  503. //音准有效阈值
  504. float intonationValidDuty = 0.1f;
  505. //节奏匹配数量
  506. float cadenceNum = 0;
  507. //节奏有效阈值
  508. float cadenceValidDuty = 0.1f;
  509. //完整性数量
  510. float integrityNum = 0;
  511. //完整性误差范围
  512. float integrityRange = 30;
  513. //完整性有效阈值
  514. float integrityValidDuty = 0.5f;
  515. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureTimeInfo.getMeasureIndex()).size();
  516. for (MusicPitchDetailDto musicXmlInfo : userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureTimeInfo.getMeasureIndex())) {
  517. int ot5 = (int) (musicXmlInfo.getDuration()*0.1);
  518. int startTimeStamp = musicXmlInfo.getTimeStamp() - ot5;
  519. int endTimeStamp = musicXmlInfo.getTimeStamp() + ot5;
  520. //时间范围内有效音准数量
  521. float recordValidIntonationNum = 0;
  522. //时间范围内有效节奏数量
  523. float cadenceValidNum = 0;
  524. //时间范围内有效音频数量
  525. float integrityValidNum = 0;
  526. //时间范围内匹配次数
  527. float compareNum = 0;
  528. int faultNum = 0;
  529. for (int i = 0; i < recordPitchDetails.size(); i++) {
  530. MusicPitchDetailDto recordInfo = recordPitchDetails.get(i);
  531. if(recordInfo.getTimeStamp()>(startTimeStamp-ot5)&&Math.abs((recordInfo.getFrequency()-musicXmlInfo.getFrequency()))>20){
  532. faultNum++;
  533. }else{
  534. if(faultNum<6)
  535. faultNum = 0;
  536. }
  537. if(faultNum<6){
  538. continue;
  539. }
  540. //如果在时间范围之外直接跳过
  541. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  542. continue;
  543. }
  544. // LOGGER.info("{}频率({}-{}):{}, {}", recordInfo.getTimeStamp(), startTimeStamp, endTimeStamp, musicXmlInfo.getFrequency(), recordInfo.getFrequency());
  545. compareNum++;
  546. //如果在最低有效频率以下则跳过
  547. if(recordInfo.getFrequency()<minValidFrequency&&musicXmlInfo.getFrequency()!=-1){
  548. continue;
  549. }
  550. cadenceValidNum++;
  551. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  552. continue;
  553. }
  554. //如果频率差值在节奏误差范围内
  555. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=integrityRange){
  556. integrityValidNum++;
  557. }
  558. //如果频率差值在音准误差范围内
  559. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=intonationErrRange){
  560. recordValidIntonationNum++;
  561. }
  562. }
  563. //有效音频占比
  564. float integrityDuty = integrityValidNum/compareNum;
  565. //有效音高占比
  566. float intonationDuty = recordValidIntonationNum/compareNum;
  567. //有效节奏占比
  568. float cadenceDuty = cadenceValidNum/compareNum;
  569. //节奏
  570. if(cadenceDuty>=cadenceValidDuty){
  571. cadenceNum++;
  572. if(intonationDuty>=intonationValidDuty){
  573. intonationNum++;
  574. }
  575. if(integrityDuty>=integrityValidDuty){
  576. integrityNum++;
  577. }
  578. }
  579. }
  580. intonation = new BigDecimal(intonationNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  581. cadence = new BigDecimal(cadenceNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  582. integrity = new BigDecimal(integrityNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  583. } catch (ArithmeticException e){
  584. LOGGER.info("无musicXml信息");
  585. }
  586. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("intonation")){
  587. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation.add(userSoundInfoMap.get(phone).getUserScoreMap().get("intonation")));
  588. }else{
  589. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation);
  590. }
  591. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("cadence")){
  592. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence.add(userSoundInfoMap.get(phone).getUserScoreMap().get("cadence")));
  593. }else{
  594. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence);
  595. }
  596. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("integrity")){
  597. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity.add(userSoundInfoMap.get(phone).getUserScoreMap().get("integrity")));
  598. }else{
  599. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity);
  600. }
  601. Map<String, BigDecimal> scoreData = new HashMap<>();
  602. scoreData.put("intonation", intonation);
  603. scoreData.put("cadence", cadence);
  604. scoreData.put("integrity", integrity);
  605. userSoundInfoMap.get(phone).getUserMeasureScoreMap().put(measureTimeInfo.getMeasureIndex(), scoreData);
  606. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("measureScore", measureTimeInfo.getMeasureIndex(), intonation, cadence, integrity))));
  607. }
  608. /**
  609. * @describe 计算最终评分
  610. * @author Joburgess
  611. * @date 2021/6/25 0025
  612. * @param phone:
  613. * @return void
  614. */
  615. private void calTotalScore(String phone) throws IOException {
  616. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().keySet().size();
  617. int currentCompareNum = totalCompareNum-userSoundInfoMap.get(phone).getMeasureEndTime().keySet().size();
  618. BigDecimal intonation = BigDecimal.ZERO;
  619. BigDecimal cadence = BigDecimal.ZERO;
  620. BigDecimal integrity = BigDecimal.ZERO;
  621. if(currentCompareNum>0){
  622. intonation = userSoundInfoMap.get(phone).getUserScoreMap().get("intonation").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  623. cadence = userSoundInfoMap.get(phone).getUserScoreMap().get("cadence").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  624. integrity = userSoundInfoMap.get(phone).getUserScoreMap().get("integrity").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  625. }
  626. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("overall", -1, intonation, cadence, integrity))));
  627. //存储评分数据
  628. sysMusicCompareRecordService.saveMusicCompareData(phone, userSoundInfoMap.get(phone).getMusicScoreId(), userSoundInfoMap.get(phone).getUserMeasureScoreMap());
  629. LOGGER.info("评分数据:{}", JSON.toJSONString(userSoundInfoMap.get(phone)));
  630. }
  631. /**
  632. * @describe 生成评分结果
  633. * @author Joburgess
  634. * @date 2021/6/25 0025
  635. * @param command:
  636. * @param measureIndex:
  637. * @param intonation:
  638. * @param cadence:
  639. * @param integrity:
  640. * @return com.ym.mec.biz.dal.dto.WebSocketInfo
  641. */
  642. private WebSocketInfo createPushInfo(String command, Integer measureIndex,
  643. BigDecimal intonation, BigDecimal cadence, BigDecimal integrity){
  644. WebSocketInfo webSocketInfo = new WebSocketInfo();
  645. HashMap<String, String> header = new HashMap<>();
  646. header.put("commond", command);
  647. webSocketInfo.setHeader(header);
  648. Map<String, Object> result = new HashMap<>();
  649. // BigDecimal score = intonation.multiply(new BigDecimal(0.5)).add(cadence.multiply(new BigDecimal(0.5))).setScale(0, BigDecimal.ROUND_HALF_UP);
  650. BigDecimal score = intonation.add(cadence).add(integrity).divide(new BigDecimal(3), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).setScale(0, BigDecimal.ROUND_UP);
  651. // BigDecimal score = integrity.setScale(0, BigDecimal.ROUND_HALF_UP);
  652. result.put("score", score);
  653. result.put("intonation", intonation);
  654. result.put("cadence", cadence);
  655. result.put("integrity", integrity);
  656. result.put("measureIndex", measureIndex);
  657. webSocketInfo.setBody(result);
  658. LOGGER.info("小节频分:{}", JSON.toJSONString(webSocketInfo));
  659. return webSocketInfo;
  660. }
  661. }