WebSocketHandler.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  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. AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(message.getPayload().array(), audioFormat, simpleSize, overlap);
  147. dispatcher.addAudioProcessor(silenceDetecor);
  148. dispatcher.addAudioProcessor(new PitchProcessor(algo, simpleRate, simpleSize, (pitchDetectionResult, audioEvent) -> {
  149. int timeStamp = (int) (userSoundInfoMap.get(phone).getMeasureStartTime() + audioEvent.getTimeStamp()*1000);
  150. float pitch = pitchDetectionResult.getPitch();
  151. if(pitch>0 && userSoundInfoMap.get(phone).getOffsetTime() == -1){
  152. calOffsetTime(phone, (int) (CollectionUtils.isEmpty(userSoundInfoMap.get(phone).getRecordMeasurePithInfo())?0:userSoundInfoMap.get(phone).getRecordMeasurePithInfo().get(userSoundInfoMap.get(phone).getRecordMeasurePithInfo().size()-1).getTimeStamp()));
  153. }
  154. // LOGGER.info("时间:{}, 频率:{}, 分贝:{}, 音分:{}", timeStamp, pitch, silenceDetecor.currentSPL(), cents);
  155. userSoundInfoMap.get(phone).getRecordMeasurePithInfo().add(new MusicPitchDetailDto(timeStamp, pitch, silenceDetecor.currentSPL()));
  156. }));
  157. dispatcher.run();
  158. if(Objects.isNull(userSoundInfoMap.get(phone).getAccessFile())){
  159. return;
  160. }
  161. double recordTime = userSoundInfoMap.get(phone).getAccessFile().length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000;
  162. userSoundInfoMap.get(phone).setMeasureStartTime(recordTime);
  163. for (Map.Entry<Integer, MusicPitchDetailDto> userMeasureEndTimeMapEntry : userSoundInfoMap.get(phone).getMeasureEndTime().entrySet()) {
  164. int ot = (int) (userMeasureEndTimeMapEntry.getValue().getDuration()*0.1);
  165. if(recordTime>(userMeasureEndTimeMapEntry.getValue().getEndTimeStamp()+ot)){
  166. measureCompare(phone, userMeasureEndTimeMapEntry.getKey());
  167. userSoundInfoMap.get(phone).getMeasureEndTime().remove(userMeasureEndTimeMapEntry.getKey());
  168. break;
  169. }
  170. }
  171. }
  172. @Override
  173. protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
  174. super.handlePongMessage(session, message);
  175. LOGGER.info("心跳信息:{}", new String(message.getPayload().array(), "utf-8"));
  176. }
  177. @Override
  178. public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
  179. String phone = session.getPrincipal().getName().split(":")[1];
  180. session.close();
  181. if(!WS_CLIENTS.containsKey(phone)){
  182. return;
  183. }
  184. exception.printStackTrace();
  185. LOGGER.info("发生了错误,移除客户端: {}", phone);
  186. WS_CLIENTS.remove(phone);
  187. userSoundInfoMap.remove(phone);
  188. createHeader(phone);
  189. }
  190. @Override
  191. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  192. super.afterConnectionClosed(session, status);
  193. String phone = session.getPrincipal().getName().split(":")[1];
  194. LOGGER.info("{}离线", phone);
  195. WS_CLIENTS.remove(phone);
  196. userSoundInfoMap.remove(phone);
  197. createHeader(phone);
  198. }
  199. @Override
  200. public boolean supportsPartialMessages() {
  201. return super.supportsPartialMessages();
  202. }
  203. /**
  204. * @describe 处理时间偏移
  205. * @author Joburgess
  206. * @date 2021/7/5 0005
  207. * @param phone:
  208. * @param offsetTime:
  209. * @return void
  210. */
  211. private void calOffsetTime(String phone, int offsetTime){
  212. userSoundInfoMap.get(phone).setOffsetTime(offsetTime);
  213. for (Map.Entry<Integer, MusicPitchDetailDto> musicPitchDetailDtoEntry : userSoundInfoMap.get(phone).getMeasureEndTime().entrySet()) {
  214. musicPitchDetailDtoEntry.getValue().setTimeStamp(musicPitchDetailDtoEntry.getValue().getTimeStamp() + offsetTime);
  215. musicPitchDetailDtoEntry.getValue().setEndTimeStamp(musicPitchDetailDtoEntry.getValue().getEndTimeStamp() + offsetTime);
  216. }
  217. }
  218. /**
  219. * @describe 保存录音数据,并生成wav头信息
  220. * @author Joburgess
  221. * @date 2021/6/25 0025
  222. * @param phone:
  223. * @return void
  224. */
  225. private void createHeader(String phone) throws IOException {
  226. if(!userSoundInfoMap.containsKey(phone)){
  227. return;
  228. }
  229. if(Objects.nonNull(userSoundInfoMap.get(phone).getAccessFile())){
  230. RandomAccessFile randomAccessFile = userSoundInfoMap.get(phone).getAccessFile();
  231. LOGGER.info("音频时长:{}", randomAccessFile.length()/(audioFormat.getFrameSize()*audioFormat.getFrameRate())*1000);
  232. randomAccessFile.seek(0);
  233. randomAccessFile.write(WavHeader.getWaveHeader(randomAccessFile.length(), (long) audioFormat.getFrameRate(), audioFormat.getSampleSizeInBits()));
  234. randomAccessFile.close();
  235. userSoundInfoMap.get(phone).setAccessFile(null);
  236. }
  237. // userSoundInfoMap.get(phone).setRecordMeasurePithInfo(null);
  238. userSoundInfoMap.remove(phone);
  239. }
  240. /**
  241. * @describe 数据比对,生成分数
  242. * @author Joburgess
  243. * @date 2021/6/25 0025
  244. * @param phone:
  245. * @param measureIndex:
  246. * @return void
  247. */
  248. private void measureCompare(String phone, int measureIndex) throws IOException {
  249. if (userSoundInfoMap.get(phone).getOffsetTime() == -1){
  250. userSoundInfoMap.get(phone).setOffsetTime(0);
  251. }
  252. //相似度
  253. BigDecimal intonation = BigDecimal.ZERO;
  254. //节奏
  255. BigDecimal cadence = BigDecimal.ZERO;
  256. //完整度
  257. BigDecimal integrity = BigDecimal.ZERO;
  258. try {
  259. //最低有效频率
  260. float minValidFrequency = 20;
  261. //音准分数
  262. float intonationScore = 0;
  263. //节奏匹配数量
  264. float cadenceNum = 0;
  265. //节奏有效阈值
  266. float cadenceValidDuty = 0.01f;
  267. //完整性误差范围
  268. float integrityRange = 30;
  269. //完整性分数
  270. float integrityScore = 0;
  271. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).size();
  272. for (int i = 0; i < userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).size(); i++) {
  273. MusicPitchDetailDto musicXmlInfo = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureIndex).get(i);
  274. int ot5 = (int) (musicXmlInfo.getDuration()*0.1);
  275. int startTimeStamp = musicXmlInfo.getTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime() + ot5;
  276. int endTimeStamp = musicXmlInfo.getTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime() + musicXmlInfo.getDuration() - ot5;
  277. int preMeasureEndTimeStamp = startTimeStamp;
  278. List<MusicPitchDetailDto> ms = userSoundInfoMap.get(phone).getMusicXmlInfos().stream().filter(m -> m.getMusicalNotesIndex() == musicXmlInfo.getMusicalNotesIndex() - 1).collect(Collectors.toList());
  279. if(!CollectionUtils.isEmpty(ms)){
  280. preMeasureEndTimeStamp = ms.get(0).getEndTimeStamp() + userSoundInfoMap.get(phone).getOffsetTime();
  281. }
  282. //时间范围内有效节奏数量
  283. float cadenceValidNum = 0;
  284. //时间范围内有效音频数量
  285. float integrityValidNum = 0;
  286. //时间范围内匹配次数
  287. float compareNum = 0;
  288. boolean newMeasure = false;
  289. float preMusicalNotesPitch = 0;
  290. if(userSoundInfoMap.get(phone).getMusicalNotePitchMap().containsKey(musicXmlInfo.getMusicalNotesIndex()-1)){
  291. preMusicalNotesPitch = userSoundInfoMap.get(phone).getMusicalNotePitchMap().get(musicXmlInfo.getMusicalNotesIndex()-1);
  292. }
  293. if(userSoundInfoMap.get(phone).getMusicalNotePitchMap().get(musicXmlInfo.getMusicalNotesIndex())==-1){
  294. newMeasure = true;
  295. }
  296. int newNum = 0;
  297. for (MusicPitchDetailDto recordInfo : userSoundInfoMap.get(phone).getRecordMeasurePithInfo()) {
  298. if(musicXmlInfo.getMusicalNotesIndex()==0){
  299. newMeasure = true;
  300. }
  301. if(newMeasure){
  302. break;
  303. }
  304. if(recordInfo.getTimeStamp()<preMeasureEndTimeStamp||recordInfo.getTimeStamp()>startTimeStamp){
  305. continue;
  306. }
  307. if(Math.abs(recordInfo.getFrequency()-preMusicalNotesPitch)>10){
  308. newNum++;
  309. }else{
  310. newNum = 0;
  311. }
  312. if(newNum>=2){
  313. newMeasure = true;
  314. }
  315. }
  316. // List<Float> musicalNotesPitchs = new ArrayList<>();
  317. // List<Float> decibels = new ArrayList<>();
  318. List<MusicPitchDetailDto> measureSoundPitchInfos = new ArrayList<>();
  319. for (int j = 0; j < userSoundInfoMap.get(phone).getRecordMeasurePithInfo().size(); j++) {
  320. MusicPitchDetailDto recordInfo = userSoundInfoMap.get(phone).getRecordMeasurePithInfo().get(j);
  321. //如果在时间范围之外直接跳过
  322. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  323. continue;
  324. }
  325. // musicalNotesPitchs.add(recordInfo.getFrequency());
  326. // decibels.add(recordInfo.getDecibel());
  327. measureSoundPitchInfos.add(recordInfo);
  328. compareNum++;
  329. if(!newMeasure){
  330. continue;
  331. }
  332. // LOGGER.info("{}频率({}-{}):{}, {}", recordInfo.getTimeStamp(), startTimeStamp, endTimeStamp, musicXmlInfo.getFrequency(), recordInfo.getFrequency());
  333. //如果在最低有效频率以下则跳过
  334. if(recordInfo.getFrequency()<minValidFrequency&&musicXmlInfo.getFrequency()!=-1){
  335. continue;
  336. }
  337. cadenceValidNum++;
  338. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  339. continue;
  340. }
  341. //如果频率差值在节奏误差范围内
  342. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=integrityRange){
  343. integrityValidNum++;
  344. }
  345. }
  346. //非正常频率次数
  347. int errPitchNum = 0;
  348. //分贝变化次数
  349. int decibelChangeNum = 0;
  350. if(CollectionUtils.isEmpty(measureSoundPitchInfos)){
  351. userSoundInfoMap.get(phone).getMusicalNotePitchMap().put(musicXmlInfo.getMusicalNotesIndex(), (float) 0);
  352. }else{
  353. Map<Integer, Long> collect = measureSoundPitchInfos.stream().map(pitch -> (int)pitch.getFrequency()).collect(Collectors.groupingBy(Integer::intValue, Collectors.counting()));
  354. //出现次数最多的频率
  355. Integer pitch = collect.entrySet().stream().max(Comparator.comparing(e -> e.getValue())).get().getKey();
  356. //当前频率
  357. double cf = -1;
  358. //频率持续数量
  359. int fnum = 0;
  360. //是否演奏中
  361. boolean ing = false;
  362. //当前分贝
  363. double cd = 0;
  364. //分贝持续数量
  365. int dnum = 0;
  366. for (MusicPitchDetailDto musicalNotesPitch : measureSoundPitchInfos) {
  367. //计算频率断层次数
  368. if (Math.abs(musicalNotesPitch.getFrequency() - cf) > 20){
  369. fnum ++;
  370. }
  371. if (fnum>=5){
  372. cf = musicalNotesPitch.getFrequency();
  373. fnum = 0;
  374. if (cf != -1){
  375. errPitchNum ++;
  376. ing = true;
  377. cd = musicalNotesPitch.getDecibel();
  378. }
  379. }
  380. //计算声音大小断层册数
  381. if(ing && Math.abs(musicalNotesPitch.getDecibel() - cd) > 5){
  382. dnum ++;
  383. }
  384. if (dnum > 2){
  385. cd = musicalNotesPitch.getDecibel();
  386. dnum = 0;
  387. decibelChangeNum++;
  388. }
  389. }
  390. userSoundInfoMap.get(phone).getMusicalNotePitchMap().put(musicXmlInfo.getMusicalNotesIndex(), (float) pitch);
  391. }
  392. //有效节奏占比
  393. float cadenceDuty = cadenceValidNum/compareNum;
  394. //如果频率出现断层或这个音量出现断层,则当前音符节奏无效
  395. if(errPitchNum>=2 || decibelChangeNum>1){
  396. cadenceDuty = 0;
  397. }
  398. //节奏
  399. if(cadenceDuty>=cadenceValidDuty){
  400. cadenceNum++;
  401. }
  402. //音准
  403. if (!CollectionUtils.isEmpty(measureSoundPitchInfos)){
  404. Double avgPitch = measureSoundPitchInfos.stream().filter(pitch -> Math.abs((pitch.getFrequency()-musicXmlInfo.getFrequency()))<5).collect(Collectors.averagingDouble(pitch -> pitch.getFrequency()));
  405. //音分
  406. double recordCents = 0;
  407. if (avgPitch > 0){
  408. recordCents = PitchConverter.hertzToAbsoluteCent(avgPitch);
  409. }
  410. double cents = PitchConverter.hertzToAbsoluteCent(musicXmlInfo.getFrequency());
  411. double score = 100 - Math.round(Math.abs(cents - recordCents)) + 3;
  412. if (score < 0){
  413. score = 0;
  414. }else if(score > 100){
  415. score = 100;
  416. }
  417. intonationScore += score;
  418. musicXmlInfo.setAvgFrequency(avgPitch.floatValue());
  419. }
  420. //完成度
  421. if(integrityValidNum>0){
  422. integrityValidNum = integrityValidNum + (float) (compareNum * 0.05);
  423. }
  424. if(integrityValidNum > compareNum){
  425. integrityValidNum = compareNum;
  426. }
  427. float integrityDuty = integrityValidNum/compareNum;
  428. integrityScore += integrityDuty;
  429. }
  430. BigDecimal measureNum = new BigDecimal(totalCompareNum);
  431. intonation = new BigDecimal(intonationScore).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).setScale(0, BigDecimal.ROUND_UP);
  432. cadence = new BigDecimal(cadenceNum).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_UP);
  433. integrity = new BigDecimal(integrityScore).divide(measureNum, CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_UP);
  434. } catch (ArithmeticException e){
  435. LOGGER.info("无musicXml信息");
  436. }
  437. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("intonation")){
  438. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation.add(userSoundInfoMap.get(phone).getUserScoreMap().get("intonation")));
  439. }else{
  440. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation);
  441. }
  442. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("cadence")){
  443. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence.add(userSoundInfoMap.get(phone).getUserScoreMap().get("cadence")));
  444. }else{
  445. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence);
  446. }
  447. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("integrity")){
  448. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity.add(userSoundInfoMap.get(phone).getUserScoreMap().get("integrity")));
  449. }else{
  450. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity);
  451. }
  452. Map<String, BigDecimal> scoreData = new HashMap<>();
  453. scoreData.put("intonation", intonation);
  454. scoreData.put("cadence", cadence);
  455. scoreData.put("integrity", integrity);
  456. userSoundInfoMap.get(phone).getUserMeasureScoreMap().put(measureIndex, scoreData);
  457. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("measureScore", measureIndex, intonation, cadence, integrity))));
  458. }
  459. private void measureCompare2(String phone, MusicPitchDetailDto measureTimeInfo) throws IOException, UnsupportedAudioFileException {
  460. //小节总时长
  461. double measureTime = measureTimeInfo.getEndTimeStamp()-measureTimeInfo.getTimeStamp();
  462. double ot = measureTime * 0.1;
  463. measureTime += ot;
  464. //小节时长占用字节数
  465. int measureByteNum = (int) (measureTime/1000*(audioFormat.getFrameSize()*audioFormat.getFrameRate()));
  466. List<MusicPitchDetailDto> recordInfo = new ArrayList<>();
  467. byte[] bytes = new byte[measureByteNum];
  468. long startOffset = (userSoundInfoMap.get(phone).getAccessFile().length()-measureByteNum);
  469. userSoundInfoMap.get(phone).getAccessFile().seek(startOffset);
  470. // userSoundInfoMap.get(phone).getAccessFile().seek(0);
  471. userSoundInfoMap.get(phone).getAccessFile().readFully(bytes);
  472. userSoundInfoMap.get(phone).getAccessFile().seek(userSoundInfoMap.get(phone).getAccessFile().length());
  473. AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(bytes, audioFormat, simpleSize, 128);
  474. dispatcher.addAudioProcessor(new PitchProcessor(algo, simpleRate, simpleSize, (pitchDetectionResult, audioEvent) -> {
  475. int timeStamp = (int) (measureTimeInfo.getTimeStamp() - (ot>measureTimeInfo.getTimeStamp()?0:ot) + audioEvent.getTimeStamp()*1000);
  476. float pitch = pitchDetectionResult.getPitch();
  477. recordInfo.add(new MusicPitchDetailDto(timeStamp, pitch));
  478. }));
  479. dispatcher.run();
  480. userSoundInfoMap.get(phone).getRecordMeasurePithInfo().addAll(recordInfo);
  481. LOGGER.info("小节评分频率{}:{}", measureTimeInfo.getMeasureIndex(), JSON.toJSONString(recordInfo));
  482. scoreCal(phone, measureTimeInfo, recordInfo);
  483. }
  484. private void scoreCal(String phone, MusicPitchDetailDto measureTimeInfo, List<MusicPitchDetailDto> recordPitchDetails) throws IOException {
  485. //相似度
  486. BigDecimal intonation = BigDecimal.ZERO;
  487. //节奏
  488. BigDecimal cadence = BigDecimal.ZERO;
  489. //完整度
  490. BigDecimal integrity = BigDecimal.ZERO;
  491. try {
  492. //最低有效频率
  493. float minValidFrequency = 20;
  494. //音准匹配数量
  495. float intonationNum = 0;
  496. //音准匹配误差范围
  497. float intonationErrRange = 15;
  498. //音准有效阈值
  499. float intonationValidDuty = 0.1f;
  500. //节奏匹配数量
  501. float cadenceNum = 0;
  502. //节奏有效阈值
  503. float cadenceValidDuty = 0.1f;
  504. //完整性数量
  505. float integrityNum = 0;
  506. //完整性误差范围
  507. float integrityRange = 30;
  508. //完整性有效阈值
  509. float integrityValidDuty = 0.5f;
  510. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureTimeInfo.getMeasureIndex()).size();
  511. for (MusicPitchDetailDto musicXmlInfo : userSoundInfoMap.get(phone).getMeasureXmlInfoMap().get(measureTimeInfo.getMeasureIndex())) {
  512. int ot5 = (int) (musicXmlInfo.getDuration()*0.1);
  513. int startTimeStamp = musicXmlInfo.getTimeStamp() - ot5;
  514. int endTimeStamp = musicXmlInfo.getTimeStamp() + ot5;
  515. //时间范围内有效音准数量
  516. float recordValidIntonationNum = 0;
  517. //时间范围内有效节奏数量
  518. float cadenceValidNum = 0;
  519. //时间范围内有效音频数量
  520. float integrityValidNum = 0;
  521. //时间范围内匹配次数
  522. float compareNum = 0;
  523. int faultNum = 0;
  524. for (int i = 0; i < recordPitchDetails.size(); i++) {
  525. MusicPitchDetailDto recordInfo = recordPitchDetails.get(i);
  526. if(recordInfo.getTimeStamp()>(startTimeStamp-ot5)&&Math.abs((recordInfo.getFrequency()-musicXmlInfo.getFrequency()))>20){
  527. faultNum++;
  528. }else{
  529. if(faultNum<6)
  530. faultNum = 0;
  531. }
  532. if(faultNum<6){
  533. continue;
  534. }
  535. //如果在时间范围之外直接跳过
  536. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  537. continue;
  538. }
  539. // LOGGER.info("{}频率({}-{}):{}, {}", recordInfo.getTimeStamp(), startTimeStamp, endTimeStamp, musicXmlInfo.getFrequency(), recordInfo.getFrequency());
  540. compareNum++;
  541. //如果在最低有效频率以下则跳过
  542. if(recordInfo.getFrequency()<minValidFrequency&&musicXmlInfo.getFrequency()!=-1){
  543. continue;
  544. }
  545. cadenceValidNum++;
  546. if(recordInfo.getTimeStamp()<startTimeStamp||recordInfo.getTimeStamp()>endTimeStamp){
  547. continue;
  548. }
  549. //如果频率差值在节奏误差范围内
  550. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=integrityRange){
  551. integrityValidNum++;
  552. }
  553. //如果频率差值在音准误差范围内
  554. if(Math.abs(recordInfo.getFrequency()-musicXmlInfo.getFrequency())<=intonationErrRange){
  555. recordValidIntonationNum++;
  556. }
  557. }
  558. //有效音频占比
  559. float integrityDuty = integrityValidNum/compareNum;
  560. //有效音高占比
  561. float intonationDuty = recordValidIntonationNum/compareNum;
  562. //有效节奏占比
  563. float cadenceDuty = cadenceValidNum/compareNum;
  564. //节奏
  565. if(cadenceDuty>=cadenceValidDuty){
  566. cadenceNum++;
  567. if(intonationDuty>=intonationValidDuty){
  568. intonationNum++;
  569. }
  570. if(integrityDuty>=integrityValidDuty){
  571. integrityNum++;
  572. }
  573. }
  574. }
  575. intonation = new BigDecimal(intonationNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  576. cadence = new BigDecimal(cadenceNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  577. integrity = new BigDecimal(integrityNum).divide(new BigDecimal(totalCompareNum), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).multiply(oneHundred).setScale(0, BigDecimal.ROUND_HALF_UP);
  578. } catch (ArithmeticException e){
  579. LOGGER.info("无musicXml信息");
  580. }
  581. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("intonation")){
  582. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation.add(userSoundInfoMap.get(phone).getUserScoreMap().get("intonation")));
  583. }else{
  584. userSoundInfoMap.get(phone).getUserScoreMap().put("intonation", intonation);
  585. }
  586. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("cadence")){
  587. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence.add(userSoundInfoMap.get(phone).getUserScoreMap().get("cadence")));
  588. }else{
  589. userSoundInfoMap.get(phone).getUserScoreMap().put("cadence", cadence);
  590. }
  591. if(userSoundInfoMap.get(phone).getUserScoreMap().containsKey("integrity")){
  592. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity.add(userSoundInfoMap.get(phone).getUserScoreMap().get("integrity")));
  593. }else{
  594. userSoundInfoMap.get(phone).getUserScoreMap().put("integrity", integrity);
  595. }
  596. Map<String, BigDecimal> scoreData = new HashMap<>();
  597. scoreData.put("intonation", intonation);
  598. scoreData.put("cadence", cadence);
  599. scoreData.put("integrity", integrity);
  600. userSoundInfoMap.get(phone).getUserMeasureScoreMap().put(measureTimeInfo.getMeasureIndex(), scoreData);
  601. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("measureScore", measureTimeInfo.getMeasureIndex(), intonation, cadence, integrity))));
  602. }
  603. /**
  604. * @describe 计算最终评分
  605. * @author Joburgess
  606. * @date 2021/6/25 0025
  607. * @param phone:
  608. * @return void
  609. */
  610. private void calTotalScore(String phone) throws IOException {
  611. int totalCompareNum = userSoundInfoMap.get(phone).getMeasureXmlInfoMap().keySet().size();
  612. int currentCompareNum = totalCompareNum-userSoundInfoMap.get(phone).getMeasureEndTime().keySet().size();
  613. BigDecimal intonation = BigDecimal.ZERO;
  614. BigDecimal cadence = BigDecimal.ZERO;
  615. BigDecimal integrity = BigDecimal.ZERO;
  616. if(currentCompareNum>0){
  617. intonation = userSoundInfoMap.get(phone).getUserScoreMap().get("intonation").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  618. cadence = userSoundInfoMap.get(phone).getUserScoreMap().get("cadence").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  619. integrity = userSoundInfoMap.get(phone).getUserScoreMap().get("integrity").divide(new BigDecimal(currentCompareNum), 0, BigDecimal.ROUND_DOWN);
  620. }
  621. WS_CLIENTS.get(phone).sendMessage(new TextMessage(JSON.toJSONString(createPushInfo("overall", -1, intonation, cadence, integrity))));
  622. //存储评分数据
  623. sysMusicCompareRecordService.saveMusicCompareData(phone, userSoundInfoMap.get(phone).getMusicScoreId(), userSoundInfoMap.get(phone).getUserMeasureScoreMap());
  624. LOGGER.info("评分数据:{}", JSON.toJSONString(userSoundInfoMap.get(phone)));
  625. }
  626. /**
  627. * @describe 生成评分结果
  628. * @author Joburgess
  629. * @date 2021/6/25 0025
  630. * @param command:
  631. * @param measureIndex:
  632. * @param intonation:
  633. * @param cadence:
  634. * @param integrity:
  635. * @return com.ym.mec.biz.dal.dto.WebSocketInfo
  636. */
  637. private WebSocketInfo createPushInfo(String command, Integer measureIndex,
  638. BigDecimal intonation, BigDecimal cadence, BigDecimal integrity){
  639. WebSocketInfo webSocketInfo = new WebSocketInfo();
  640. HashMap<String, String> header = new HashMap<>();
  641. header.put("commond", command);
  642. webSocketInfo.setHeader(header);
  643. Map<String, Object> result = new HashMap<>();
  644. // BigDecimal score = intonation.multiply(new BigDecimal(0.5)).add(cadence.multiply(new BigDecimal(0.5))).setScale(0, BigDecimal.ROUND_HALF_UP);
  645. BigDecimal score = intonation.add(cadence).add(integrity).divide(new BigDecimal(3), CommonConstants.DECIMAL_PLACE, BigDecimal.ROUND_DOWN).setScale(0, BigDecimal.ROUND_UP);
  646. // BigDecimal score = integrity.setScale(0, BigDecimal.ROUND_HALF_UP);
  647. result.put("score", score);
  648. result.put("intonation", intonation);
  649. result.put("cadence", cadence);
  650. result.put("integrity", integrity);
  651. result.put("measureIndex", measureIndex);
  652. webSocketInfo.setBody(result);
  653. LOGGER.info("小节频分:{}", JSON.toJSONString(webSocketInfo));
  654. return webSocketInfo;
  655. }
  656. }