这周老大安排了一个活儿,前端传一组摩尔斯电码过来,将其装成音频流通过WebSocket推给前端进行播放,由于场景的特殊还需要控制"点" "划"播报的时间长短。当时心想Java还能生成这个玩意儿?通过查阅了大量的资料后,确定了Java的确可以实现,不得不感叹这Java是真滴厉害。相关资料已放到下方链接。
音频基础知识
摩尔斯电码表
Java 生成摩尔斯电码音频流代码
初始化摩尔斯电码映射
public class MorseEncoder {
/**
* 存放摩尔斯电码
*/
private final Map<String,String> codeMap = new HashMap<>();
/**
* 点 (控制播报速率倍速)
*/
private Integer dotRatio = 1;
/**
* 划 (控制播报速率倍速)
*/
private Integer rowRatio = 3;
/**
* 大间隔 (控制播报速率倍速)
*/
private Integer blankRatio = 5;
private final String dotString = ".";
private final String rowString = "-";
/**
* 小间隔
*/
private final String minInterval = "@";
/**
* 大间隔
*/
private final String maxInterval = "/";
private final String blank = " ";
@PostConstruct
public void initMap(){
//初始化HashMap 中间有空格
codeMap.put("A",". -");
codeMap.put("B","- . . .");
codeMap.put("C","- . - .");
codeMap.put("D","- . .");
codeMap.put("E",".");
codeMap.put("F",". . - .");
codeMap.put("G","- - .");
codeMap.put("H",". . . .");
codeMap.put("I",". .");
codeMap.put("J",". - - -");
codeMap.put("K","- . -");
codeMap.put("L",". - . .");
codeMap.put("M","- -");
codeMap.put("N","- .");
codeMap.put("O","- - -");
codeMap.put("P",". - - .");
codeMap.put("Q","- - . -");
codeMap.put("R",". - .");
codeMap.put("S",". . .");
codeMap.put("T","-");
codeMap.put("U",". . -");
codeMap.put("V",". . . -");
codeMap.put("W",". - -");
codeMap.put("X","- . . -");
codeMap.put("Y","- . - -");
codeMap.put("Z","- - . .");
codeMap.put("1",". - - - -");
codeMap.put("2",". . - - -");
codeMap.put("3",". . . - -");
codeMap.put("4",". . . . -");
codeMap.put("5",". . . . .");
codeMap.put("6","- . . . .");
codeMap.put("7","- - . . .");
codeMap.put("8","- - - . .");
codeMap.put("9","- - - - .");
codeMap.put("0","- - - - -");
}
}
字符串转电码
/**
*
* @param args 字码
* @param type 类型 1 单词 ["A","B","C"]2 词组 ["ABC","DEFG"]
* @return
*/
public String string2MorseCode(List<String> args,Integer type){
StringBuilder result = new StringBuilder();
if(type.compareTo(1) == 0 ){
for (String arg : args) {
String morse = codeMap.get(arg.toUpperCase());
if(morse == null){
throw new RuntimeException(arg+"不是摩尔斯电码");
}
result.append(morse)
.append(minInterval);
}
}else {
for (String arg : args) {
for (int i = 0; i < arg.length(); i++) {
String code = String.valueOf(arg.charAt(i));
String morse = codeMap.get(code.toUpperCase());
if(morse == null){
throw new RuntimeException(arg+"不是摩尔斯电码");
}
result.append(morse)
.append(minInterval);
}
result.append(maxInterval);
}
}
return result.toString();
}
电码转音频
/**
* 将摩尔斯点码转成音频
* @param codeString 摩尔斯电码
* @param reta 速率
* @return 音频流
*/
public byte[] codeConvert2Sound(String codeString,int reta){
int dot = reta * dotRatio;
int row = reta * rowRatio;
int blank = reta * blankRatio;
//存放byte
ArrayList<Byte> rawData = new ArrayList<Byte>();
//计算每一个音波
for(int i=0; i<codeString.length(); i++){
// 时长
int soundLength = 0;
// 频率
int frequency = 1450;
String code = Character.toString(codeString.charAt(i));
if (code.equals(rowString)) {
soundLength = row;
} else if (code.equals(dotString)) {
soundLength = dot;
} else if (code.equals(minInterval)) {
soundLength = row;
//将频率设置成 0
frequency = 0;
} else if (code.equals(maxInterval)) {
soundLength = blank;
//将频率设置成 0
frequency = 0;
}else if(code.equals(this.blank)){
soundLength = dot;
//将频率设置成 0
frequency = 0;
}
// add beeps for letters
for ( int k = 0; k < soundLength * (float)44100 / 1000; k++ ) {
double angle = k / ( (float)44100 / frequency ) * 2.0 * Math.PI;
rawData.add( (byte)( Math.sin( angle ) * 100 ) );
}
// add break between chars
/*for (int j=0; j < 2000; j++){
rawData.add((byte) 0);
}*/
}
//将list转array
byte[] audio = new byte[rawData.size()];
for (int i=0; i<rawData.size(); i++){
audio[i] = rawData.get(i);
}
//save2File(audio);
return audio;
}
存入到文件中 wav格式
/**
* 保存到文件中
* @param audio
*/
private void save2File(byte[] audio) {
InputStream byteArrayInputStream = new ByteArrayInputStream(audio);
AudioFormat audioFormat = new AudioFormat( (float)44100, 8, 1, true, false );
AudioInputStream audioInputStream = new AudioInputStream( byteArrayInputStream, audioFormat, audio.length / audioFormat.getFrameSize() );
try {
String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH-mm-ss"));
AudioSystem.write( audioInputStream, AudioFileFormat.Type.WAVE, new File( "D:\\morse-code-"+format +".wav" ));
}
catch ( Exception e ) {
e.printStackTrace();
}
}
生成WAV头文件
/**
2 * @param totalAudioLen 不包括header的音频数据总长度
3 * @param longSampleRate 采样率,也就是录制时使用的频率、音频采样级别 8000 = 8KHz
4 * @param channels audioRecord的声道数1/2
5 * @param audioFormat 采样精度; 譬如 16bit
6 * @throws IOException 写文件错误
7 */
public static byte[] writeWavFileHeader(long totalAudioLen, long longSampleRate,
int channels, int audioFormat) throws IOException {
byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels, audioFormat);
return header;
}
/**
15 * @param totalAudioLen 不包括header的音频数据总长度
16 * @param longSampleRate 采样率,也就是录制时使用的频率
17 * @param channels audioRecord的频道数量
18 * @param audioFormat 采样精度; 譬如 16bit
19 */
private static byte[] generateWavFileHeader(long totalAudioLen, long longSampleRate, int channels,int audioFormat) {
long totalDataLen = totalAudioLen + 36;
long byteRate = longSampleRate * 2 * channels;
byte[] header = new byte[44];
header[0] = 'R'; // RIFF
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
//文件长度 4字节文件长度,这个长度不包括"RIFF"标志(4字节)和文件长度本身所占字节(4字节),即该长度等于整个文件长度 - 8
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
//fcc type:4字节 "WAVE" 类型块标识, 大写
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
//FMT Chunk 4字节 表示"fmt" chunk的开始,此块中包括文件内部格式信息,小写, 最后一个字符是空格
header[12] = 'f'; // 'fmt '
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//过渡字节
//数据大小 4字节,文件内部格式信息数据的大小,过滤字节(一般为00000010H)
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
//编码方式 10H为PCM编码格式 FormatTag:2字节,音频数据的编码方式,1:表示是PCM 编码
header[20] = 1; // format = 1
header[21] = 0;
//通道数 Channels:2字节,声道数,单声道为1,双声道为2
header[22] = (byte) channels;
header[23] = 0;
//采样率,每个通道的播放速度
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
//音频数据传送速率,采样率*通道数*采样深度/8
//4字节,音频数据传送速率, 单位是字节。其值为采样率×每次采样大小。播放软件利用此值可以估计缓冲区的大小
//byteRate = sampleRate * (bitsPerSample / 8) * channels
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数
header[32] = (byte) (2 * channels);
header[33] = 0;
//每个样本的数据位数
//2字节,每个声道的采样精度; 譬如 16bit 在这里的值就是16。如果有多个声道,则每个声道的采样精度大小都一样的;
header[34] = (byte) audioFormat;
header[35] = 0;
//Data chunk
//ckid:4字节,数据标志符(data),表示 "data" chunk的开始。此块中包含音频数据,小写;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
//音频数据的长度,4字节,audioDataLen = totalDataLen - 36 = fileLenIncludeHeader - 44
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
return header;
}
测试
@Test
public void morseCodeTest() throws IOException {
MorseEncoder encoder = new MorseEncoder();
List<String> args = new ArrayList<>();
args.add("ttt");
args.add("000");
String code = encoder.string2MorseCode(args, 2);
byte[] bytes = encoder.codeConvert2Sound(code, 100);
byte[] header = encoder.writeWavFileHeader(bytes.length, 44100, 1, 8);
ByteArrayBuilder builder = new ByteArrayBuilder();
builder.write(header);
builder.write(bytes);
byte[] data = builder.toByteArray();
File file = new File("D:\\test.wav");
OutputStream os = new FileOutputStream(file);
os.write(data);
}
效果 播放出来的效果也是跟预期的一样
代码地址
https://gitee.com/wsl__cn/morse-code-convert-audio.git