yonge 3 年之前
父节点
当前提交
df68548c58
共有 22 个文件被更改,包括 1769 次插入3 次删除
  1. 67 0
      audio-analysis/pom.xml
  2. 34 0
      audio-analysis/src/main/java/com/yonge/audio/AudioAnalysisServerApplication.java
  3. 44 0
      audio-analysis/src/main/java/com/yonge/audio/config/ResourceServerConfig.java
  4. 36 0
      audio-analysis/src/main/java/com/yonge/audio/config/WebMvcConfig.java
  5. 73 0
      audio-analysis/src/main/java/com/yonge/audio/utils/ArrayUtil.java
  6. 138 0
      audio-analysis/src/main/java/com/yonge/nettty/dto/UserChannelContext.java
  7. 117 0
      audio-analysis/src/main/java/com/yonge/nettty/entity/MusicXmlBasicInfo.java
  8. 84 0
      audio-analysis/src/main/java/com/yonge/nettty/entity/MusicXmlNote.java
  9. 42 0
      audio-analysis/src/main/java/com/yonge/netty/common/message/Message.java
  10. 34 0
      audio-analysis/src/main/java/com/yonge/netty/common/message/MessageDispatcher.java
  11. 20 0
      audio-analysis/src/main/java/com/yonge/netty/common/message/MessageHandler.java
  12. 89 0
      audio-analysis/src/main/java/com/yonge/netty/common/message/MessageHandlerContainer.java
  13. 132 0
      audio-analysis/src/main/java/com/yonge/netty/server/NettyChannelManager.java
  14. 153 0
      audio-analysis/src/main/java/com/yonge/netty/server/NettyServer.java
  15. 79 0
      audio-analysis/src/main/java/com/yonge/netty/server/handler/NettyServerHandler.java
  16. 208 0
      audio-analysis/src/main/java/com/yonge/netty/server/messagehandler/BinaryWebSocketFrameHandler.java
  17. 111 0
      audio-analysis/src/main/java/com/yonge/netty/server/messagehandler/TextWebSocketHandler.java
  18. 94 0
      audio-analysis/src/main/java/com/yonge/netty/server/processor/WaveformWriter.java
  19. 32 0
      audio-analysis/src/main/java/com/yonge/netty/server/service/UserChannelContextService.java
  20. 124 0
      audio-analysis/src/main/resources/application.yml
  21. 55 0
      audio-analysis/src/main/resources/logback-spring.xml
  22. 3 3
      pom.xml

+ 67 - 0
audio-analysis/pom.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.ym</groupId>
+		<artifactId>mec</artifactId>
+		<version>1.0</version>
+	</parent>
+	<groupId>com.yonge.audio</groupId>
+	<artifactId>audio-analysis</artifactId>
+	<name>audio-analysis</name>
+	<url>http://maven.apache.org</url>
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>de.codecentric</groupId>
+			<artifactId>spring-boot-admin-starter-client</artifactId>
+		</dependency>
+
+		<!-- swagger-spring-boot -->
+		<dependency>
+			<groupId>com.spring4all</groupId>
+			<artifactId>swagger-spring-boot-starter</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.github.xiaoymin</groupId>
+			<artifactId>swagger-bootstrap-ui</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid-spring-boot-starter</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.ym</groupId>
+			<artifactId>mec-auth-api</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.ym</groupId>
+			<artifactId>mec-biz</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>io.netty</groupId>
+			<artifactId>netty-all</artifactId>
+			<version>4.1.68.Final</version>
+		</dependency>
+	</dependencies>
+</project>

+ 34 - 0
audio-analysis/src/main/java/com/yonge/audio/AudioAnalysisServerApplication.java

@@ -0,0 +1,34 @@
+package com.yonge.audio;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+import com.spring4all.swagger.EnableSwagger2Doc;
+
+@SpringBootApplication
+@EnableDiscoveryClient
+@EnableFeignClients("com.ym.mec")
+@MapperScan("com.ym.mec.biz.dal.dao")
+@ComponentScan(basePackages = { "com.yonge.netty", "com.ym.mec", "com.yonge.log" })
+@Configuration
+@EnableSwagger2Doc
+public class AudioAnalysisServerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(AudioAnalysisServerApplication.class, args);
+	}
+
+	@Bean
+	@LoadBalanced
+	public RestTemplate restTemplate() {
+		return new RestTemplate();
+	}
+}

+ 44 - 0
audio-analysis/src/main/java/com/yonge/audio/config/ResourceServerConfig.java

@@ -0,0 +1,44 @@
+package com.yonge.audio.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
+import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
+import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
+
+import com.ym.mec.common.security.BaseAccessDeniedHandler;
+import com.ym.mec.common.security.BaseAuthenticationEntryPoint;
+
+@Configuration
+@EnableResourceServer
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
+
+	@Autowired
+	private BaseAccessDeniedHandler baseAccessDeniedHandler;
+
+	@Autowired
+	private BaseAuthenticationEntryPoint baseAuthenticationEntryPoint;
+
+	@Override
+	public void configure(HttpSecurity http) throws Exception {
+		http.authorizeRequests()
+		.antMatchers("/task/**")
+		.hasIpAddress("0.0.0.0/0")
+				.antMatchers("/v2/api-docs")
+				.permitAll()
+				// 任何人不登录都可以获取的资源
+				// .antMatchers("/ipController/**").hasIpAddress("127.0.0.1") //特定ip可以不登录获取资源
+				// .antMatchers("/ipControll/**").access("isAuthenticated() and hasIpAddress('127.0.0.1')")// 特定ip必须登录才能获取
+				.anyRequest().authenticated().and().csrf().disable().exceptionHandling().accessDeniedHandler(baseAccessDeniedHandler)
+				.authenticationEntryPoint(baseAuthenticationEntryPoint).and();
+	}
+
+	@Override
+	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
+		resources.authenticationEntryPoint(baseAuthenticationEntryPoint).accessDeniedHandler(baseAccessDeniedHandler);
+	}
+
+}

+ 36 - 0
audio-analysis/src/main/java/com/yonge/audio/config/WebMvcConfig.java

@@ -0,0 +1,36 @@
+package com.yonge.audio.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.format.FormatterRegistry;
+import org.springframework.http.MediaType;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import com.ym.mec.common.config.EnumConverterFactory;
+import com.ym.mec.common.config.LocalFastJsonHttpMessageConverter;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+	
+	/**
+	 * 枚举类的转换器 addConverterFactory
+	 */
+	@Override
+	public void addFormatters(FormatterRegistry registry) {
+		registry.addConverterFactory(new EnumConverterFactory());
+	}
+	
+	@Bean
+    public HttpMessageConverters fastJsonHttpMessageConverters(){
+		LocalFastJsonHttpMessageConverter converter = new LocalFastJsonHttpMessageConverter();
+        List<MediaType> fastMediaTypes =  new ArrayList<MediaType>();
+        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
+        converter.setSupportedMediaTypes(fastMediaTypes);
+        return new HttpMessageConverters(converter);
+    }
+
+}

+ 73 - 0
audio-analysis/src/main/java/com/yonge/audio/utils/ArrayUtil.java

@@ -0,0 +1,73 @@
+package com.yonge.audio.utils;
+
+public class ArrayUtil {
+
+	/**
+	 * 合并2个数组
+	 * @param bt1
+	 * @param bt2
+	 * @return bt1 + bt2
+	 */
+	public static byte[] mergeByte(byte[] bt1, byte[] bt2) {
+		if (bt2.length == 0) {
+			return bt1;
+		}
+		byte[] bt3 = new byte[bt1.length + bt2.length];
+		System.arraycopy(bt1, 0, bt3, 0, bt1.length);
+		System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
+		return bt3;
+	}
+
+	/**
+	 * 根据指定的起始、结束为止提取数组中的数据,并返回
+	 * @param src
+	 * @param startIndex
+	 * @param endIndex
+	 * @return
+	 */
+	public static byte[] extractByte(byte[] src, int startIndex, int endIndex) {
+		byte[] target = new byte[endIndex - startIndex];
+		System.arraycopy(src, startIndex, target, 0, endIndex - startIndex);
+
+		return target;
+	}
+
+	/**
+	 * 合并2个数组
+	 * @param bt1
+	 * @param bt2
+	 * @return bt1 + bt2
+	 */
+	public static float[] mergeFloat(float[] bt1, float[] bt2) {
+		if (bt2.length == 0) {
+			return bt1;
+		}
+		float[] bt3 = new float[bt1.length + bt2.length];
+		System.arraycopy(bt1, 0, bt3, 0, bt1.length);
+		System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
+		return bt3;
+	}
+
+	/**
+	 * 根据指定的起始、结束为止提取数组中的数据,并返回
+	 * @param src
+	 * @param startIndex
+	 * @param endIndex
+	 * @return
+	 */
+	public static float[] extractFloat(float[] src, int startIndex, int endIndex) {
+		float[] target = new float[endIndex - startIndex];
+		System.arraycopy(src, startIndex, target, 0, endIndex - startIndex);
+
+		return target;
+	}
+
+	public static void main(String[] args) {
+		byte[] b1 = { 1, 2, 3, 4, 5 };
+		byte[] b2 = { 3, 2, 1 };
+		byte[] r = extractByte(b1, 1, 3);
+		for (int i = 0; i < r.length; i++) {
+			System.out.println(r[i]);
+		}
+	}
+}

+ 138 - 0
audio-analysis/src/main/java/com/yonge/nettty/dto/UserChannelContext.java

@@ -0,0 +1,138 @@
+package com.yonge.nettty.dto;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import com.yonge.nettty.entity.MusicXmlBasicInfo;
+import com.yonge.nettty.entity.MusicXmlNote;
+import com.yonge.netty.server.processor.WaveformWriter;
+
+/**
+ * 用户通道上下文
+ */
+public class UserChannelContext {
+
+	// 曲目与musicxml对应关系
+	private ConcurrentHashMap<Integer, MusicXmlBasicInfo> songMusicXmlMap = new ConcurrentHashMap<Integer, MusicXmlBasicInfo>();
+
+	private WaveformWriter waveFileProcessor;
+
+	// 当前音符索引
+	private AtomicInteger currentNoteIndex = new AtomicInteger(0);
+
+	// 当前乐谱小节索引
+	private AtomicInteger currentMusicSectionIndex = new AtomicInteger(0);
+	
+	// 缓存字节数据
+	private byte[] bufferBytes = new byte[0];
+
+	public ConcurrentHashMap<Integer, MusicXmlBasicInfo> getSongMusicXmlMap() {
+		return songMusicXmlMap;
+	}
+
+	public void setSongMusicXmlMap(ConcurrentHashMap<Integer, MusicXmlBasicInfo> songMusicXmlMap) {
+		this.songMusicXmlMap = songMusicXmlMap;
+	}
+
+	public WaveformWriter getWaveFileProcessor() {
+		return waveFileProcessor;
+	}
+
+	public void setWaveFileProcessor(WaveformWriter waveFileProcessor) {
+		this.waveFileProcessor = waveFileProcessor;
+	}
+
+	public int incrementMusicNoteIndex() {
+		return currentNoteIndex.incrementAndGet();
+	}
+
+	public int getCurrentMusicNoteIndex() {
+		return currentNoteIndex.intValue();
+	}
+
+	public void resetCurrentMusicNoteIndex() {
+		currentNoteIndex.set(0);
+	}
+
+	public int incrementMusicSectionIndex() {
+		return currentMusicSectionIndex.incrementAndGet();
+	}
+
+	public int getCurrentMusicSectionIndex() {
+		return currentMusicSectionIndex.intValue();
+	}
+	
+	public void resetCurrentMusicSectionIndex(){
+		currentMusicSectionIndex.set(0);
+	}
+
+	public MusicXmlNote getCurrentMusicNote(Integer songId) {
+		if (songMusicXmlMap.size() == 0) {
+			return null;
+		}
+		MusicXmlBasicInfo musicXmlBasicInfo = null;
+		if (songId == null) {
+			musicXmlBasicInfo = songMusicXmlMap.values().stream().findFirst().get();
+		} else {
+			musicXmlBasicInfo = songMusicXmlMap.get(songId);
+		}
+
+		if (musicXmlBasicInfo != null) {
+			if(getCurrentMusicNoteIndex() >= musicXmlBasicInfo.getMusicXmlInfos().size()){
+				return musicXmlBasicInfo.getMusicXmlInfos().get(musicXmlBasicInfo.getMusicXmlInfos().size() - 1);
+			}
+			return musicXmlBasicInfo.getMusicXmlInfos().get(getCurrentMusicNoteIndex());
+		}
+
+		return null;
+	}
+
+	public List<MusicXmlNote> getCurrentMusicSection(Integer songId) {
+		if (songMusicXmlMap.size() == 0) {
+			return null;
+		}
+		MusicXmlBasicInfo musicXmlBasicInfo = null;
+		if (songId == null) {
+			musicXmlBasicInfo = songMusicXmlMap.values().stream().findFirst().get();
+		} else {
+			musicXmlBasicInfo = songMusicXmlMap.get(songId);
+		}
+
+		if (musicXmlBasicInfo != null) {
+			return musicXmlBasicInfo.getMusicXmlInfos().stream().filter(t -> t.getMusicalNotesIndex() == getCurrentMusicSectionIndex())
+					.sorted(Comparator.comparing(MusicXmlNote::getMusicalNotesIndex)).collect(Collectors.toList());
+		}
+
+		return null;
+	}
+
+	public int getTotalMusicSectionIndexNum(Integer songId) {
+		if (songMusicXmlMap.size() == 0) {
+			return 0;
+		}
+		MusicXmlBasicInfo musicXmlBasicInfo = null;
+		if (songId == null) {
+			musicXmlBasicInfo = songMusicXmlMap.values().stream().findFirst().get();
+		} else {
+			musicXmlBasicInfo = songMusicXmlMap.get(songId);
+		}
+
+		if (musicXmlBasicInfo != null) {
+			return musicXmlBasicInfo.getMusicXmlInfos().stream().map(t -> t.getMeasureIndex()).distinct().max(Integer::compareTo).get();
+		}
+
+		return 0;
+	}
+
+	public byte[] getBufferBytes() {
+		return bufferBytes;
+	}
+
+	public void setBufferBytes(byte[] bufferBytes) {
+		this.bufferBytes = bufferBytes;
+	}
+
+}

+ 117 - 0
audio-analysis/src/main/java/com/yonge/nettty/entity/MusicXmlBasicInfo.java

@@ -0,0 +1,117 @@
+package com.yonge.nettty.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MusicXmlBasicInfo {
+
+	private Integer id;
+
+	private Integer subjectId;
+
+	private Integer detailId;
+
+	private Integer examSongId;
+
+	private String xmlUrl;
+
+	private String behaviorId;
+
+	private String platform;
+
+	private int speed;
+
+	private String heardLevel;
+
+	private String uuid;
+
+	private List<MusicXmlNote> musicXmlInfos = new ArrayList<MusicXmlNote>();
+
+	public Integer getId() {
+		return id;
+	}
+
+	public void setId(Integer id) {
+		this.id = id;
+	}
+
+	public Integer getSubjectId() {
+		return subjectId;
+	}
+
+	public void setSubjectId(Integer subjectId) {
+		this.subjectId = subjectId;
+	}
+
+	public Integer getDetailId() {
+		return detailId;
+	}
+
+	public void setDetailId(Integer detailId) {
+		this.detailId = detailId;
+	}
+
+	public Integer getExamSongId() {
+		return examSongId;
+	}
+
+	public void setExamSongId(Integer examSongId) {
+		this.examSongId = examSongId;
+	}
+
+	public String getXmlUrl() {
+		return xmlUrl;
+	}
+
+	public void setXmlUrl(String xmlUrl) {
+		this.xmlUrl = xmlUrl;
+	}
+
+	public String getBehaviorId() {
+		return behaviorId;
+	}
+
+	public void setBehaviorId(String behaviorId) {
+		this.behaviorId = behaviorId;
+	}
+
+	public String getPlatform() {
+		return platform;
+	}
+
+	public void setPlatform(String platform) {
+		this.platform = platform;
+	}
+
+	public int getSpeed() {
+		return speed;
+	}
+
+	public void setSpeed(int speed) {
+		this.speed = speed;
+	}
+
+	public String getHeardLevel() {
+		return heardLevel;
+	}
+
+	public void setHeardLevel(String heardLevel) {
+		this.heardLevel = heardLevel;
+	}
+
+	public String getUuid() {
+		return uuid;
+	}
+
+	public void setUuid(String uuid) {
+		this.uuid = uuid;
+	}
+
+	public List<MusicXmlNote> getMusicXmlInfos() {
+		return musicXmlInfos;
+	}
+
+	public void setMusicXmlInfos(List<MusicXmlNote> musicXmlInfos) {
+		this.musicXmlInfos = musicXmlInfos;
+	}
+}

+ 84 - 0
audio-analysis/src/main/java/com/yonge/nettty/entity/MusicXmlNote.java

@@ -0,0 +1,84 @@
+package com.yonge.nettty.entity;
+
+/**
+ * 音符信息
+ */
+public class MusicXmlNote {
+
+	// 音符起始时间戳(第一个音符是0ms)
+	private int timeStamp;
+
+	// 当前音符持续的播放时间(ms)
+	private int duration;
+
+	// 当前音符的频率
+	private float frequency;
+
+	// 下一个音的频率(不是乐谱中下一个音符的频率)
+	private float nextFrequency;
+
+	// 当前音符所在的小节下标(从0开始)
+	private int measureIndex;
+
+	// 当前音符是否需要评测
+	private boolean dontEvaluating;
+
+	// 当前音符在整个曲谱中的下标(从0开始)
+	private int musicalNotesIndex;
+
+	public int getTimeStamp() {
+		return timeStamp;
+	}
+
+	public void setTimeStamp(int timeStamp) {
+		this.timeStamp = timeStamp;
+	}
+
+	public int getDuration() {
+		return duration;
+	}
+
+	public void setDuration(int duration) {
+		this.duration = duration;
+	}
+
+	public float getFrequency() {
+		return frequency;
+	}
+
+	public void setFrequency(float frequency) {
+		this.frequency = frequency;
+	}
+
+	public float getNextFrequency() {
+		return nextFrequency;
+	}
+
+	public void setNextFrequency(float nextFrequency) {
+		this.nextFrequency = nextFrequency;
+	}
+
+	public int getMeasureIndex() {
+		return measureIndex;
+	}
+
+	public void setMeasureIndex(int measureIndex) {
+		this.measureIndex = measureIndex;
+	}
+
+	public boolean isDontEvaluating() {
+		return dontEvaluating;
+	}
+
+	public void setDontEvaluating(boolean dontEvaluating) {
+		this.dontEvaluating = dontEvaluating;
+	}
+
+	public int getMusicalNotesIndex() {
+		return musicalNotesIndex;
+	}
+
+	public void setMusicalNotesIndex(int musicalNotesIndex) {
+		this.musicalNotesIndex = musicalNotesIndex;
+	}
+}

+ 42 - 0
audio-analysis/src/main/java/com/yonge/netty/common/message/Message.java

@@ -0,0 +1,42 @@
+package com.yonge.netty.common.message;
+
+/**
+ * 通信协议的消息体
+ */
+public class Message<T> {
+
+	/**
+	 * 类型
+	 */
+	private String type;
+
+	/**
+	 * 消息
+	 */
+	private T data;
+
+	// 空构造方法
+	public Message() {
+	}
+
+	public Message(String type, T data) {
+		this.type = type;
+		this.data = data;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public T getData() {
+		return data;
+	}
+
+	public void setData(T data) {
+		this.data = data;
+	}
+}

+ 34 - 0
audio-analysis/src/main/java/com/yonge/netty/common/message/MessageDispatcher.java

@@ -0,0 +1,34 @@
+package com.yonge.netty.common.message;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class MessageDispatcher extends SimpleChannelInboundHandler<Message<?>> {
+
+	@Autowired
+	private MessageHandlerContainer messageHandlerContainer;
+
+	private final ExecutorService executor = Executors.newFixedThreadPool(200);
+
+	@Override
+	protected void channelRead0(ChannelHandlerContext ctx, Message<?> message) {
+		// 获得 type 对应的 MessageHandler 处理器
+		MessageHandler messageHandler = messageHandlerContainer.getMessageHandler(message.getType());
+		// 获得 MessageHandler 处理器 的消息类
+		// Class<? extends Message> messageClass = MessageHandlerContainer.getMessageClass(messageHandler);
+		// 执行逻辑
+		executor.submit(new Runnable() {
+
+			@Override
+			public void run() {
+				messageHandler.execute(ctx.channel(), message.getData());
+			}
+
+		});
+	}
+}

+ 20 - 0
audio-analysis/src/main/java/com/yonge/netty/common/message/MessageHandler.java

@@ -0,0 +1,20 @@
+package com.yonge.netty.common.message;
+
+import io.netty.channel.Channel;
+
+public interface MessageHandler<T> {
+
+	/**
+	 * 执行处理消息
+	 *
+	 * @param channel 通道
+	 * @param message 消息
+	 */
+	void execute(Channel channel, T message);
+
+	/**
+	 * @return 消息类型,即每个 Message 实现类上的 TYPE 静态字段
+	 */
+	String getType();
+
+}

+ 89 - 0
audio-analysis/src/main/java/com/yonge/netty/common/message/MessageHandlerContainer.java

@@ -0,0 +1,89 @@
+package com.yonge.netty.common.message;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.aop.framework.AopProxyUtils;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+//@Component
+public class MessageHandlerContainer implements InitializingBean {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(MessageHandlerContainer.class);
+
+	/**
+	 * 消息类型与 MessageHandler 的映射
+	 */
+	private final Map<String, MessageHandler<?>> handlers = new HashMap<String, MessageHandler<?>>();
+
+	@Autowired
+	private ApplicationContext applicationContext;
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		// 通过 ApplicationContext 获得所有 MessageHandler Bean
+		applicationContext.getBeansOfType(MessageHandler.class).values() // 获得所有 MessageHandler Bean
+				.forEach(messageHandler -> handlers.put(messageHandler.getType(), messageHandler)); // 添加到 handlers 中
+		LOGGER.info("[afterPropertiesSet][消息处理器数量:{}]", handlers.size());
+	}
+
+	/**
+	 * 获得类型对应的 MessageHandler
+	 *
+	 * @param type 类型
+	 * @return MessageHandler
+	 */
+	MessageHandler<?> getMessageHandler(String type) {
+		MessageHandler<?> handler = handlers.get(type);
+		if (handler == null) {
+			throw new IllegalArgumentException(String.format("类型(%s) 找不到匹配的 MessageHandler 处理器", type));
+		}
+		return handler;
+	}
+
+	/**
+	 * 获得 MessageHandler 处理的消息类
+	 *
+	 * @param handler 处理器
+	 * @return 消息类
+	 */
+	static Class<? extends Message> getMessageClass(MessageHandler<?> handler) {
+		// 获得 Bean 对应的 Class 类名。因为有可能被 AOP 代理过。
+		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(handler);
+		// 获得接口的 Type 数组
+		Type[] interfaces = targetClass.getGenericInterfaces();
+		Class<?> superclass = targetClass.getSuperclass();
+		while ((Objects.isNull(interfaces) || 0 == interfaces.length) && Objects.nonNull(superclass)) { // 此处,是以父类的接口为准
+			interfaces = superclass.getGenericInterfaces();
+			superclass = targetClass.getSuperclass();
+		}
+		if (Objects.nonNull(interfaces)) {
+			// 遍历 interfaces 数组
+			for (Type type : interfaces) {
+				// 要求 type 是泛型参数
+				if (type instanceof ParameterizedType) {
+					ParameterizedType parameterizedType = (ParameterizedType) type;
+					// 要求是 MessageHandler 接口
+					if (Objects.equals(parameterizedType.getRawType(), MessageHandler.class)) {
+						Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+						// 取首个元素
+						if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
+							return (Class<Message>) actualTypeArguments[0];
+						} else {
+							throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
+						}
+					}
+				}
+			}
+		}
+		throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
+	}
+}

+ 132 - 0
audio-analysis/src/main/java/com/yonge/netty/server/NettyChannelManager.java

@@ -0,0 +1,132 @@
+package com.yonge.netty.server;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelId;
+import io.netty.util.AttributeKey;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NettyChannelManager {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(NettyChannelManager.class);
+
+	/**
+	 * {@link Channel#attr(AttributeKey)} 属性中,表示 Channel 对应的用户
+	 */
+	private static final AttributeKey<String> CHANNEL_ATTR_KEY_USER = AttributeKey.newInstance("user");
+
+	/**
+	 * Channel 映射
+	 */
+	private ConcurrentMap<ChannelId, Channel> channels = new ConcurrentHashMap<ChannelId, Channel>();
+
+	/**
+	 * 用户与 Channel 的映射。
+	 *
+	 * 通过它,可以获取用户对应的 Channel。这样,我们可以向指定用户发送消息。
+	 */
+	private ConcurrentMap<String, Channel> userChannels = new ConcurrentHashMap<String, Channel>();
+
+	/**
+	* 添加 Channel 到 {@link #channels} 中
+	*
+	* @param channel Channel
+	*/
+	public void add(Channel channel) {
+		channels.put(channel.id(), channel);
+	}
+
+	/**
+	 * 添加指定用户到 {@link #userChannels} 中
+	 *
+	 * @param channel Channel
+	 * @param user 用户
+	 */
+	public void addUser(Channel channel, String user) {
+		Channel existChannel = channels.get(channel.id());
+		if (existChannel == null) {
+			LOGGER.error("[addUser][连接({}) 不存在]", channel.id());
+			return;
+		}
+		// 设置属性
+		channel.attr(CHANNEL_ATTR_KEY_USER).set(user);
+		// 添加到 userChannels
+		userChannels.put(user, channel);
+
+		LOGGER.info("[add][用户({})加入,总数({})]", user, channels.size());
+	}
+
+	/**
+	 * 从channel中获取user
+	 * @param channel
+	 * @return
+	 */
+	public String getUser(Channel channel) {
+		if (channel.hasAttr(CHANNEL_ATTR_KEY_USER)) {
+			return channel.attr(CHANNEL_ATTR_KEY_USER).get();
+		}
+		return null;
+	}
+
+	/**
+	 * 将 Channel 从 {@link #channels} 和 {@link #userChannels} 中移除
+	 *
+	 * @param channel Channel
+	 */
+	public void remove(Channel channel) {
+		// 移除 channels
+		channels.remove(channel.id());
+
+		String user = "";
+		// 移除 userChannels
+		if (channel.hasAttr(CHANNEL_ATTR_KEY_USER)) {
+			user = channel.attr(CHANNEL_ATTR_KEY_USER).get();
+			userChannels.remove(user);
+		}
+		LOGGER.info("[remove][用户({})移除,总数({})]", user, channels.size());
+	}
+
+	/**
+	 * 向指定用户发送消息
+	 *
+	 * @param user 用户
+	 * @param message 消息体
+	 */
+	public void send(String user, Object message) {
+		// 获得用户对应的 Channel
+		Channel channel = userChannels.get(user);
+		if (channel == null) {
+			LOGGER.error("[send][连接不存在]");
+			return;
+		}
+		if (!channel.isActive()) {
+			LOGGER.error("[send][连接({})未激活]", channel.id());
+			return;
+		}
+		// 发送消息
+		channel.writeAndFlush(message);
+	}
+
+	/**
+	 * 向所有用户发送消息
+	 *
+	 * @param message 消息体
+	 */
+	public void sendAll(Object message) {
+		for (Channel channel : channels.values()) {
+			if (!channel.isActive()) {
+				LOGGER.error("[send][连接({})未激活]", channel.id());
+				return;
+			}
+			// 发送消息
+			channel.writeAndFlush(message);
+		}
+	}
+
+}

+ 153 - 0
audio-analysis/src/main/java/com/yonge/netty/server/NettyServer.java

@@ -0,0 +1,153 @@
+package com.yonge.netty.server;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+
+import java.net.InetSocketAddress;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+
+import com.yonge.netty.server.handler.NettyServerHandler;
+import com.yonge.netty.server.messagehandler.BinaryWebSocketFrameHandler;
+import com.yonge.netty.server.messagehandler.TextWebSocketHandler;
+
+@Configuration
+public class NettyServer {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);
+
+	/**
+	 * webSocket协议名
+	 */
+	private static final String WEBSOCKET_PROTOCOL = "WebSocket";
+
+	/**
+	 * 端口号
+	 */
+	private int port = 8080;
+
+	/**
+	 * webSocket路径
+	 */
+	private String webSocketPath = "/audioEvaluate";
+
+	private EventLoopGroup bossGroup = new NioEventLoopGroup();
+
+	private EventLoopGroup workGroup = new NioEventLoopGroup();
+
+	@Autowired
+	private NettyServerHandler nettyServerHandler;
+
+	@Autowired
+	private BinaryWebSocketFrameHandler binaryWebSocketFrameHandler;
+
+	@Autowired
+	private TextWebSocketHandler textWebSocketHandler;
+
+	/**
+	 * 启动
+	 * @throws InterruptedException
+	 */
+	private void start() throws InterruptedException {
+		ServerBootstrap bootstrap = new ServerBootstrap();
+
+		// bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
+		bootstrap.group(bossGroup, workGroup);
+		// 设置NIO类型的channel
+		bootstrap.channel(NioServerSocketChannel.class);
+		// 设置监听端口
+		bootstrap.localAddress(new InetSocketAddress(port));
+		// 服务端 accept 队列的大小
+		bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
+		bootstrap.option(ChannelOption.SO_RCVBUF, 1024*4);
+		// 允许较小的数据包的发送,降低延迟
+		bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
+		// 连接到达时会创建一个通道
+		bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
+
+			@Override
+			protected void initChannel(SocketChannel ch) throws Exception {
+				// 获得 Channel 对应的 ChannelPipeline
+				ChannelPipeline channelPipeline = ch.pipeline();
+
+				// 流水线管理通道中的处理程序(Handler),用来处理业务
+				// webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
+				channelPipeline.addLast(new HttpServerCodec());
+				// channelPipeline.addLast(new ObjectEncoder());
+				// 分块向客户端写数据,防止发送大文件时导致内存溢出, channel.write(new ChunkedFile(new File("bigFile.mkv")))
+				channelPipeline.addLast(new ChunkedWriteHandler());
+				/*
+				 * 说明: 1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合 2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
+				 */
+				channelPipeline.addLast(new HttpObjectAggregator(1024 * 8));
+				// webSocket 数据压缩扩展,当添加这个的时候WebSocketServerProtocolHandler的第三个参数需要设置成true
+				channelPipeline.addLast(new WebSocketServerCompressionHandler());
+				/*
+				 * 说明: 1、对应webSocket,它的数据是以帧(frame)的形式传递 2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri 3、核心功能是将http协议升级为ws协议,保持长连接
+				 */
+				channelPipeline.addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10, false, true));
+
+				// 自定义的handler,处理业务逻辑
+				channelPipeline.addLast(nettyServerHandler);
+				channelPipeline.addLast(binaryWebSocketFrameHandler);
+				channelPipeline.addLast(textWebSocketHandler);
+
+			}
+		});
+
+		// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
+		ChannelFuture channelFuture = bootstrap.bind().sync();
+
+		if (channelFuture.isSuccess()) {
+			LOGGER.info("Server started and listen on:{}", channelFuture.channel().localAddress());
+		}
+
+		// 对关闭通道进行监听
+		channelFuture.channel().closeFuture().sync();
+	}
+
+	/**
+	 * 释放资源
+	 * @throws InterruptedException
+	 */
+	@PreDestroy
+	public void destroy() throws InterruptedException {
+		if (bossGroup != null) {
+			bossGroup.shutdownGracefully().sync();
+		}
+		if (workGroup != null) {
+			workGroup.shutdownGracefully().sync();
+		}
+	}
+
+	@PostConstruct()
+	public void init() {
+		// 需要开启一个新的线程来执行netty server 服务器
+		new Thread(() -> {
+			try {
+				start();
+			} catch (InterruptedException e) {
+				e.printStackTrace();
+			}
+		}).start();
+	}
+
+}

+ 79 - 0
audio-analysis/src/main/java/com/yonge/netty/server/handler/NettyServerHandler.java

@@ -0,0 +1,79 @@
+package com.yonge.netty.server.handler;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.stereotype.Component;
+
+import com.yonge.netty.server.NettyChannelManager;
+
+@Component
+@ChannelHandler.Sharable
+public class NettyServerHandler extends ChannelInboundHandlerAdapter {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(NettyServerHandler.class);
+
+	@Autowired
+	private NettyChannelManager channelManager;
+
+	@Override
+	public void channelActive(ChannelHandlerContext ctx) {
+		// 从管理器中添加
+		channelManager.add(ctx.channel());
+	}
+
+	@Override
+	public void channelUnregistered(ChannelHandlerContext ctx) {
+		// 从管理器中移除
+		channelManager.remove(ctx.channel());
+	}
+
+	@Override
+	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+		LOGGER.error("[exceptionCaught][连接({}) 发生异常]", ctx.channel().id(), cause);
+		// 断开连接
+		ctx.channel().close();
+	}
+
+	@Override
+	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+		if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
+			WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
+			String requestUri = handshakeComplete.requestUri();
+			
+			String userId = StringUtils.substringAfterLast(requestUri, "?");
+			
+			if(StringUtils.isBlank(userId) || !StringUtils.isNumeric(userId)){
+				userId = StringUtils.substringAfterLast(requestUri, "/");
+			}
+			
+			Channel channel = ctx.channel();
+			
+			if(!StringUtils.isNumeric(userId)){
+				// 断开连接
+				channel.close();
+			}
+			
+			channelManager.addUser(channel, userId);
+			
+			LOGGER.info("userId:[{}]", userId);
+			
+			HttpHeaders httpHeaders = handshakeComplete.requestHeaders();
+			String authHeader = httpHeaders.get("Authorization");
+			
+			String tokenValue = authHeader.toLowerCase().replace(OAuth2AccessToken.BEARER_TYPE.toLowerCase(), StringUtils.EMPTY).trim();
+			LOGGER.info("token:[{}]", tokenValue);
+		}
+		super.userEventTriggered(ctx, evt);
+	}
+
+}

+ 208 - 0
audio-analysis/src/main/java/com/yonge/netty/server/messagehandler/BinaryWebSocketFrameHandler.java

@@ -0,0 +1,208 @@
+package com.yonge.netty.server.messagehandler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+
+import java.io.File;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import be.tarsos.dsp.AudioDispatcher;
+import be.tarsos.dsp.AudioEvent;
+import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter;
+import be.tarsos.dsp.io.TarsosDSPAudioFormat;
+import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
+import be.tarsos.dsp.pitch.PitchDetectionHandler;
+import be.tarsos.dsp.pitch.PitchDetectionResult;
+import be.tarsos.dsp.pitch.PitchDetector;
+import be.tarsos.dsp.pitch.PitchProcessor;
+import be.tarsos.dsp.pitch.PitchProcessor.PitchEstimationAlgorithm;
+
+import com.yonge.audio.utils.ArrayUtil;
+import com.yonge.nettty.dto.UserChannelContext;
+import com.yonge.nettty.entity.MusicXmlNote;
+import com.yonge.netty.server.NettyChannelManager;
+import com.yonge.netty.server.processor.WaveformWriter;
+import com.yonge.netty.server.service.UserChannelContextService;
+
+@Component
+@ChannelHandler.Sharable
+public class BinaryWebSocketFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(BinaryWebSocketFrameHandler.class);
+
+	@Autowired
+	private NettyChannelManager nettyChannelManager;
+
+	@Autowired
+	private UserChannelContextService userChannelContextService;
+
+	/**
+	 * @describe 采样率
+	 */
+	private float sampleRate = 44100;
+
+	/**
+	 * 每个采样大小(Bit)
+	 */
+	private int bitsPerSample = 16;
+
+	/**
+	 * 通道数
+	 */
+	private int channels = 1;
+
+	/**
+	 * @describe 采样大小
+	 */
+	private int sampleSize = 1024 * 4;
+	/**
+	 * @describe 帧覆盖大小
+	 */
+	private int overlap = 256;
+
+	private boolean signed = true;
+
+	private boolean bigEndian = false;
+
+	private AudioFormat audioFormat = new AudioFormat(sampleRate, bitsPerSample, channels, signed, bigEndian);
+
+	private TarsosDSPAudioFloatConverter converter = TarsosDSPAudioFloatConverter.getConverter(new TarsosDSPAudioFormat(sampleRate, bitsPerSample, channels,
+			signed, bigEndian));
+
+	private PitchEstimationAlgorithm algorithm = PitchProcessor.PitchEstimationAlgorithm.FFT_YIN;
+
+	private PitchDetector pitchDetector = algorithm.getDetector(sampleRate, sampleSize);
+
+	/**
+	 * @describe 有效分贝大小
+	 */
+	private int validDb = 35;
+	/**
+	 * @describe 有效频率
+	 */
+	private int validFrequency = 20;
+	/**
+	 * @describe 音准前后音分误差范围
+	 */
+	private int intonationCentsRange = 3;
+	/**
+	 * @describe 节奏有效阈值
+	 */
+	private float cadenceValidDuty = 0.09f;
+	/**
+	 * @describe 完整性有效频率误差范围
+	 */
+	private int integrityFrequencyRange = 30;
+
+	private String tmpFileDir = "e:/soundRecords/";
+
+	@Override
+	protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) throws Exception {
+
+		Channel channel = ctx.channel();
+
+		ByteBuf buf = frame.content().retain();
+
+		byte[] datas = ByteBufUtil.getBytes(buf);
+
+		String user = nettyChannelManager.getUser(channel);
+
+		UserChannelContext channelContext = userChannelContextService.getChannelContext(channel);
+
+		if (channelContext == null) {
+			return;
+		}
+		
+		// 写录音文件
+		WaveformWriter waveFileProcessor = channelContext.getWaveFileProcessor();
+		if (waveFileProcessor == null) {
+			File file = new File(tmpFileDir + user + "_" + System.currentTimeMillis() + ".wav");
+			waveFileProcessor = new WaveformWriter(file.getAbsolutePath());
+			channelContext.setWaveFileProcessor(waveFileProcessor);
+		}
+		waveFileProcessor.process(datas);
+
+		LOGGER.info("服务器接收到二进制消息长度[{}]", datas.length);
+
+		AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(datas, audioFormat, sampleSize, overlap);
+
+		dispatcher.addAudioProcessor(new PitchProcessor(algorithm, sampleRate, sampleSize, new PitchDetectionHandler() {
+
+			@Override
+			public void handlePitch(PitchDetectionResult pitchDetectionResult, AudioEvent audioEvent) {
+
+				// 获取字节流
+				int byteOverlap = audioEvent.getOverlap() * audioFormat.getFrameSize();
+				int byteStepSize = audioEvent.getBufferSize() * audioFormat.getFrameSize() - byteOverlap;
+				byte[] acceptDatas = ArrayUtils.subarray(audioEvent.getByteBuffer(), byteOverlap, byteStepSize);
+
+				// 粘合数据
+				byte[] totalBytes = ArrayUtil.mergeByte(channelContext.getBufferBytes(), acceptDatas);
+				channelContext.setBufferBytes(totalBytes);
+
+				LOGGER.info("新增字节数:{} 最新剩余字节长度:{}", acceptDatas.length, totalBytes.length);
+
+				// 获取当前音符信息
+				MusicXmlNote musicXmlNote = channelContext.getCurrentMusicNote(null);
+
+				// 计算当前音符的数据长度 公式:数据量(字节/秒)= 采样频率(Hz)× (采样位数(bit)/ 8) × 声道数
+				int length = (int) (audioFormat.getSampleRate() * (audioFormat.getSampleSizeInBits() / 8) * channels * musicXmlNote.getDuration() / 1000);
+
+				if (totalBytes.length >= length) {
+					// 处理当前音符
+					byte[] noteByteData = new byte[length];
+					System.arraycopy(totalBytes, 0, noteByteData, 0, length);
+
+					float[] noteFloatData = new float[length / audioFormat.getFrameSize()];
+
+					converter.toFloatArray(noteByteData, noteFloatData);
+
+					// 获取频率数据
+					float pitch = getPitch(noteFloatData, sampleSize);
+
+					LOGGER.info("第{}个音符的样本频率:{} 实际频率:{}", channelContext.getCurrentMusicNoteIndex(), musicXmlNote.getFrequency(), pitch);
+
+					// 准备处理下一个音符
+					channelContext.incrementMusicNoteIndex();
+					// 剩余未处理的数据
+					channelContext.setBufferBytes(ArrayUtil.extractByte(totalBytes, 0, length - 1));
+				}
+
+			}
+		}));
+		dispatcher.run();
+	}
+
+	private float getPitch(float[] audioBuffer, int bufferSize) {
+
+		int blankNum = audioBuffer.length % bufferSize;
+		float[] zeroBytes = new float[blankNum];
+
+		audioBuffer = ArrayUtil.mergeFloat(audioBuffer, zeroBytes);
+
+		int times = audioBuffer.length / bufferSize;
+
+		float totalPitch = 0f;
+
+		float[] bufferByte = new float[bufferSize];
+		for (int i = 0; i < times; i++) {
+			bufferByte = ArrayUtil.extractFloat(audioBuffer, i * bufferSize, (i + 1) * bufferSize);
+			totalPitch += pitchDetector.getPitch(bufferByte).getPitch();
+		}
+
+		return totalPitch / times;
+	}
+
+}

+ 111 - 0
audio-analysis/src/main/java/com/yonge/netty/server/messagehandler/TextWebSocketHandler.java

@@ -0,0 +1,111 @@
+package com.yonge.netty.server.messagehandler;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.JSONPath;
+import com.ym.mec.biz.dal.entity.SysMusicCompareRecord;
+import com.ym.mec.biz.dal.enums.FeatureType;
+import com.ym.mec.biz.service.SysMusicCompareRecordService;
+import com.yonge.nettty.dto.UserChannelContext;
+import com.yonge.nettty.entity.MusicXmlBasicInfo;
+import com.yonge.netty.server.processor.WaveformWriter;
+import com.yonge.netty.server.service.UserChannelContextService;
+
+@Component
+@ChannelHandler.Sharable
+public class TextWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(TextWebSocketHandler.class);
+
+	@Autowired
+	private SysMusicCompareRecordService sysMusicCompareRecordService;
+
+	@Autowired
+	private UserChannelContextService userChannelContextService;
+
+	@Override
+	protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
+
+		Channel channel = ctx.channel();
+
+		String jsonMsg = frame.text();
+		String commond = (String) JSONPath.extract(jsonMsg, "$.header.commond");
+
+		JSONObject dataObj = (JSONObject) JSONPath.extract(jsonMsg, "$.body");
+
+		LOGGER.info("接收到客户端的指令[{}]:{}", commond, dataObj);
+
+		UserChannelContext channelContext = userChannelContextService.getChannelContext(channel);
+
+		switch (commond) {
+		case "musicXml": // 同步music xml信息
+
+			MusicXmlBasicInfo musicXmlBasicInfo = JSONObject.toJavaObject(dataObj, MusicXmlBasicInfo.class);
+
+			userChannelContextService.remove(channel);
+
+			if (channelContext == null) {
+				channelContext = new UserChannelContext();
+			}
+
+			channelContext.getSongMusicXmlMap().put(musicXmlBasicInfo.getExamSongId(), musicXmlBasicInfo);
+
+			userChannelContextService.register(channel, channelContext);
+
+			break;
+		case "recordStart": // 开始评测
+
+			SysMusicCompareRecord sysMusicCompareRecord = new SysMusicCompareRecord(FeatureType.CLOUD_STUDY_EVALUATION);
+			sysMusicCompareRecordService.insert(sysMusicCompareRecord);
+			
+			//清空缓存信息
+			channelContext.setWaveFileProcessor(null);
+			channelContext.resetCurrentMusicNoteIndex();
+			channelContext.resetCurrentMusicSectionIndex();
+
+			break;
+		case "recordEnd": // 结束评测
+		case "recordCancel": // 取消评测
+			if (channelContext == null) {
+				return;
+			}
+
+			WaveformWriter waveFileProcessor = channelContext.getWaveFileProcessor();
+			if (waveFileProcessor != null) {
+				// 写文件头
+				waveFileProcessor.processingFinished();
+			}
+
+			if (StringUtils.equals(commond, "recordEnd")) {
+				// 生成评测报告
+			}
+
+			break;
+		case "proxyMessage": // ???
+
+			break;
+		case "videoUpload": // 上传音频
+
+			break;
+		case "checkSound": // 校音
+
+			break;
+
+		default:
+			// 非法请求
+			break;
+		}
+	}
+
+}

+ 94 - 0
audio-analysis/src/main/java/com/yonge/netty/server/processor/WaveformWriter.java

@@ -0,0 +1,94 @@
+/*
+ *      _______                       _____   _____ _____  
+ *     |__   __|                     |  __ \ / ____|  __ \ 
+ *        | | __ _ _ __ ___  ___  ___| |  | | (___ | |__) |
+ *        | |/ _` | '__/ __|/ _ \/ __| |  | |\___ \|  ___/ 
+ *        | | (_| | |  \__ \ (_) \__ \ |__| |____) | |     
+ *        |_|\__,_|_|  |___/\___/|___/_____/|_____/|_|     
+ *                                                         
+ * -------------------------------------------------------------
+ *
+ * TarsosDSP is developed by Joren Six at IPEM, University Ghent
+ *  
+ * -------------------------------------------------------------
+ *
+ *  Info: http://0110.be/tag/TarsosDSP
+ *  Github: https://github.com/JorenSix/TarsosDSP
+ *  Releases: http://0110.be/releases/TarsosDSP/
+ *  
+ *  TarsosDSP includes modified source code by various authors,
+ *  for credits and info, see README.
+ * 
+ */
+
+package com.yonge.netty.server.processor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import be.tarsos.dsp.writer.WaveHeader;
+
+/**
+ * 写wav文件
+ */
+public class WaveformWriter {
+
+	private RandomAccessFile randomAccessFile;
+
+	private final String fileName;
+
+	private short channelNum = 1;
+
+	private int sampleRate = 44100;
+
+	private short bitsPerSample = 16;
+
+	private static final int HEADER_LENGTH = 44;
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(WaveformWriter.class);
+
+	public WaveformWriter(String fileName) {
+
+		this.fileName = fileName;
+		try {
+			randomAccessFile = new RandomAccessFile(fileName, "rw");
+			randomAccessFile.write(new byte[HEADER_LENGTH]);
+		} catch (IOException e) {
+			LOGGER.error("创建WAV文件出现异常[{}]:{}", fileName, e.getMessage());
+			e.printStackTrace();
+		}
+
+	}
+
+	public boolean process(byte[] datas) {
+
+		try {
+			randomAccessFile.write(datas);
+		} catch (IOException e) {
+			LOGGER.error("写WAV文件出现异常[{}]:{}", fileName, e.getMessage());
+			e.printStackTrace();
+		}
+
+		return true;
+	}
+
+	public void processingFinished() {
+		try {
+			WaveHeader waveHeader = new WaveHeader(WaveHeader.FORMAT_PCM, channelNum, sampleRate, bitsPerSample, (int) randomAccessFile.length());// 16
+																																					// is
+			ByteArrayOutputStream header = new ByteArrayOutputStream();
+			waveHeader.write(header);
+			randomAccessFile.seek(0);
+			randomAccessFile.write(header.toByteArray());
+			randomAccessFile.close();
+		} catch (IOException e) {
+			LOGGER.error("关闭WAV文件出现异常[{}]:{}", fileName, e.getMessage());
+			e.printStackTrace();
+		}
+	}
+}

+ 32 - 0
audio-analysis/src/main/java/com/yonge/netty/server/service/UserChannelContextService.java

@@ -0,0 +1,32 @@
+package com.yonge.netty.server.service;
+
+import io.netty.channel.Channel;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.stereotype.Component;
+
+import com.yonge.nettty.dto.UserChannelContext;
+
+@Component
+public class UserChannelContextService {
+
+	private ConcurrentHashMap<Channel, UserChannelContext> channelContextMap = new ConcurrentHashMap<Channel, UserChannelContext>();
+	
+	public boolean register(Channel channel,UserChannelContext userChannelContext){
+		channelContextMap.put(channel, userChannelContext);
+		return true;
+	}
+	
+	public boolean remove(Channel channel){
+		if(channel != null){
+			channelContextMap.remove(channel);
+		}
+		return true;
+	}
+	
+	public UserChannelContext getChannelContext(Channel channel){
+		return channelContextMap.get(channel);
+	}
+	
+}

+ 124 - 0
audio-analysis/src/main/resources/application.yml

@@ -0,0 +1,124 @@
+server:
+  port: 9004
+  tomcat:
+    accesslog:
+      enabled: true
+      buffered: true
+      directory: /var/logs
+      file-date-format: -yyyy-MM-dd
+      pattern: common
+      prefix: tomcat-audio
+      rename-on-rotate: false
+      request-attributes-enabled: false
+      rotate: true
+      suffix: .log
+
+eureka:
+  client:
+    serviceUrl:
+      defaultZone: http://admin:admin123@localhost:8761/eureka/eureka/
+    instance: 
+      lease-renewal-interval-in-seconds: 5
+
+spring:
+  application:
+    name: audio-analysis-server
+    
+  datasource:
+    name: test
+    url: jdbc:mysql://47.114.1.200:3306/mec_dev?useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
+    username: mec_dev
+    password: dayaDataOnline@2019
+    # 使用druid数据源
+    type: com.alibaba.druid.pool.DruidDataSource
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    filters: stat
+    maxActive: 20
+    initialSize: 1
+    maxWait: 60000
+    minIdle: 1
+    timeBetweenEvictionRunsMillis: 60000
+    minEvictableIdleTimeMillis: 300000
+    validationQuery: select 'x'
+    testWhileIdle: true
+    testOnBorrow: false
+    testOnReturn: false
+    poolPreparedStatements: true
+    maxOpenPreparedStatements: 20
+  
+  redis:
+    host: 47.114.1.200
+    port: 6379
+    password: dyym
+    database: 1
+    #连接超时时间(毫秒)
+    timeout: 10000
+    jedis:
+      pool:
+        #连接池最大连接数(使用负值表示没有限制)
+        max-active: 20
+        #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: 10000
+        #连接池中的最大空闲连接
+        max-idle: 10
+        #连接池中的最小空闲连接
+        min-idle: 5
+    
+
+mybatis:
+    mapperLocations: classpath:config/mybatis/*.xml
+    
+swagger:
+  base-package: com.yonge.audo.controller
+          
+##认证 
+security:
+  oauth2:
+    client:
+      client-id: app
+      client-secret: app
+    resource:
+      token-info-uri: http://localhost:8001/oauth/check_token
+  
+#spring boot admin 相关配置
+management:
+  endpoints:
+    web:
+      exposure:
+        include: "*"
+  endpoint:
+    health:
+      show-details: ALWAYS
+      
+
+ribbon:  
+    ReadTimeout: 60000  
+    ConnectTimeout: 60000
+
+message:
+  debugMode: true
+  
+##支付流水隐藏
+payment:
+  hiddenMode: false
+  #隐藏的支付方式
+  channel: YQPAY
+  
+eseal:
+  tsign:
+    projectid: 4438776254
+    projectSecret: a94cf63d6361084d232f345d71321691
+    apisUrl: http://smlitsm.tsign.cn:8080/tgmonitor/rest/app!getAPIInfo2
+
+push:
+  jiguang:
+    reqURL: https://api.jpush.cn/v3/push
+    appKey:
+      student: 0e7422e1d6e73637e678716a
+      teacher: 7e0282ca92c12c8c45a93bb3
+      system: 496fc1007dea59b1b4252d2b
+    masterSecret:
+      student: c2361016604eab56ab2db2ac
+      teacher: d47430e2f4755ef5dc050ac5
+      system: a5e51e9cdb25417463afbf7a
+    apns_production: false

+ 55 - 0
audio-analysis/src/main/resources/logback-spring.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="10 seconds">
+
+	<property name="LOG_HOME" value="/mdata/logs/audio-analysis-%d{yyyy-MM-dd_HH}-%i.log" />
+	<property name="CONSOLE_LOG_PATTERN"
+		value="[%X{username} %X{ip} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}] : %msg%n" />
+
+	<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<appender name="file"
+		class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>${LOG_HOME}</FileNamePattern>
+			<MaxHistory>90</MaxHistory>
+			<TimeBasedFileNamingAndTriggeringPolicy
+				class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>20MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<logger name="com.yonge.audio" level="INFO" />
+
+	<!--开发环境:打印控制台 -->
+	<springProfile name="dev">
+		<root level="INFO">
+			<appender-ref ref="stdout" />
+			<appender-ref ref="file" />
+		</root>
+	</springProfile>
+	
+	<springProfile name="test">
+		<root level="INFO">
+			<appender-ref ref="stdout" />
+			<appender-ref ref="file" />
+		</root>
+	</springProfile>
+
+	<!--生产环境:输出到文件 -->
+	<springProfile name="prod">
+		<root level="WARN">
+			<appender-ref ref="stdout" />
+			<appender-ref ref="file" />
+		</root>
+	</springProfile>
+
+</configuration>

+ 3 - 3
pom.xml

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
 
 	<groupId>com.ym</groupId>
@@ -372,5 +371,6 @@
 		<module>mec-student</module>
 		<module>mec-teacher</module>
 		<module>mec-biz</module>
-	</modules>
+	  <module>audio-analysis</module>
+  </modules>
 </project>