用Java实现酷狗音乐3D环绕音

前言

   最近辞职在找工作,才发现今年的就业形势太难了,在家里蹲的时候突然想写个小软件玩,要用到tts功能,就白嫖了微软的tts,不过tts返回的mp3比特率是48bit,java sound播放出来声音异常,不知道是哪里的问题,这是一个悲伤的故事,没办法只好用jave修改比特率,其间无意间发现了一篇关于java sound 节律纯音 的文章,觉得挺有意思,又刚好在用酷狗的3D环绕音听歌(突然间有些怀念天天动听了,j2me时代的听歌神器,可惜凉了),就想到用声音合成的方式生成3D环绕音,经过小半天的研究后实现。

代码实现

1、参考文章

Java Sound 简明教程以及节律纯音实现

blog.mazhangjing.comhttps://blog.mazhangjing.com/2019/04/27/java_sound/ps:网站突然打不开了,不知道是网络还是服务器的问题

 2、依赖

  代码中使用的歌曲是MP3格式,java sound原生不支持的,需要依赖相关库,依赖后java sound会自动调用

  • mp3spi1.9.5.jar

  •  jl1.0.1.jar
  •  tritonus_share.jar

3.实现代码

  代码会对整首歌曲进行处理,然后可以播放和储存为文件。如果要实时处理,则需要修改代码,javasound解码后读取缓冲区数据,处理后再喂给播放管道。 

package util;

import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.util.Date;

public class AudioSynth1 extends JFrame {

    private AudioFormat audioFormat;
    private AudioInputStream audioInputStream;
    private SourceDataLine sourceDataLine;
    private static float sampleRate = 16000.0F;
    private static int channels = 1;
    //一个缓冲器,用于在 16000 sampsec 下保存 2 秒单声道数据和 1 秒立体声数据,用于 16 位采样
    private byte[] audioData = new byte[16000 * 4];//处理前数据存放的缓冲区
    private byte[] audioDataCopy = new byte[16000 * 4];//处理后数据存放的缓冲区
    private final JButton generateBtn = new JButton("开始处理");
    private final JButton playOrFileBtn = new JButton("播放/文件");
    private final JLabel elapsedTimeMeter = new JLabel("0000");
    private final JRadioButton stereoPingpong = new JRadioButton("3D旋转测试");

    private final JRadioButton listen = new JRadioButton("直接播放",true);
    private final JTextField fileName = new JTextField("E:/test.wav",10);

    public static void main(String[] args){
        new AudioSynth1();
    }

    public AudioSynth1(){
        final JPanel controlButtonPanel = new JPanel();
        controlButtonPanel.setBorder(BorderFactory.createEtchedBorder());

        final JPanel synButtonPanel = new JPanel();
        final ButtonGroup synButtonGroup = new ButtonGroup();
        final JPanel centerPanel = new JPanel();

        final JPanel outputButtonPanel = new JPanel();
        outputButtonPanel.setBorder(BorderFactory.createEtchedBorder());
        final ButtonGroup outputButtonGroup = new ButtonGroup();

        playOrFileBtn.setEnabled(false);

        generateBtn.addActionListener(e -> {
            try {
                playOrFileBtn.setEnabled(false);
                //adddata("E:/我的文件/音乐/邓丽君 - 小城故事.mp3");
                adddata("C:\\Users\\tangtang1600\\dist\\tem.mp3");
                System.out.println(audioData.length);
                new SynGen().getSyntheticData(audioData, audioDataCopy);
                playOrFileBtn.setEnabled(true);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        playOrFileBtn.addActionListener(e -> playOrFileData());

        controlButtonPanel.add(generateBtn);
        controlButtonPanel.add(playOrFileBtn);
        controlButtonPanel.add(elapsedTimeMeter);

        synButtonGroup.add(stereoPingpong);

        synButtonPanel.setLayout(new GridLayout(0,1));
        synButtonPanel.add(stereoPingpong);

        centerPanel.add(synButtonPanel);

        outputButtonGroup.add(listen);
        JRadioButton file = new JRadioButton("文件");
        outputButtonGroup.add(file);

        outputButtonPanel.add(listen,BorderLayout.WEST);
        outputButtonPanel.add(file, BorderLayout.CENTER);
        outputButtonPanel.add(fileName,BorderLayout.EAST);

        getContentPane().add(controlButtonPanel,BorderLayout.NORTH);
        getContentPane().add(centerPanel, BorderLayout.CENTER);
        getContentPane().add(outputButtonPanel, BorderLayout.SOUTH);

        setTitle("Copyright 2023,tangtang1600");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(300,275);
        setVisible(true);
    }

    private  void adddata(String filePath) throws Exception{
       AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(filePath));
        // 文件编码
       AudioFormat audioFormat = audioInputStream.getFormat();
        // 转换文件编码
        if (audioFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
            sampleRate =  audioFormat.getSampleRate();
            channels = audioFormat.getChannels();
            System.out.println("sampleRate:" + sampleRate);
            System.out.println("channels:" + channels);
            audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sampleRate, 16, audioFormat.getChannels(), audioFormat.getChannels() * 2, sampleRate, true);
            // 将数据流也转换成指定编码
            audioInputStream = AudioSystem.getAudioInputStream(audioFormat, audioInputStream);
        }

       // 打开输出设备
        DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
        // 使数据行得到一个播放设备
        SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
        // 将数据行用指定的编码打开
        sourceDataLine.open(audioFormat);
        // 使数据行得到数据时就开始播放
        for (AudioFormat format : dataLineInfo.getFormats()) {
            System.out.println("信息:" + format.toString());
        }
        sourceDataLine.start();
        int bytesPerFrame = audioInputStream.getFormat().getFrameSize();
        // 将流数据逐渐写入数据行,边写边播
        int numBytes = 1024 * bytesPerFrame;
        byte[] audioBytes = new byte[numBytes];
        while (audioInputStream.read(audioBytes) != -1) {
            audioData = ArrayUtil.addAllforByte(audioData,audioBytes);
        }
        sourceDataLine.drain();
        sourceDataLine.close();
        audioInputStream.close();
        if (channels ==  2) {
            audioDataCopy = new byte[audioData.length];
        }else {
            audioDataCopy = new byte[audioData.length*2];
        }

        System.out.println("size:" + audioDataCopy.length);
    }

    private void playOrFileData() {
        try{
            InputStream byteArrayInputStream = new ByteArrayInputStream(audioDataCopy);

            boolean bigEndian = true;
            boolean signed = true;
            //Allowable 8000,11025,16000,22050,44100
            int sampleSizeInBits = 16;
            if (channels == 1){
                channels = 2;
            }
            audioFormat = new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);
            audioInputStream = new AudioInputStream(byteArrayInputStream, audioFormat, audioDataCopy.length/audioFormat.getFrameSize());
            DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat);
            sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
            System.out.println("listen isSelected:" + listen.isSelected());
            if(listen.isSelected()){
                new ListenThread().start();
            } else{
                generateBtn.setEnabled(false);
                playOrFileBtn.setEnabled(false);
                try{
                    File file = new File(fileName.getText());
                    if (!file.exists()){
                        file.createNewFile();
                    }
                    AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE,file );
                }catch (Exception e) {
                    e.printStackTrace();
                    System.exit(0);
                }
                generateBtn.setEnabled(true);
                playOrFileBtn.setEnabled(true);
            }
        }catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
        }
    }

    class ListenThread extends Thread{
        byte[] playBuffer = new byte[16384];
        public void run(){
            try{
                generateBtn.setEnabled(false);
                playOrFileBtn.setEnabled(false);

                sourceDataLine.open(audioFormat);
                sourceDataLine.start();
                int cnt;
                long startTime = new Date().getTime();
                while((cnt = audioInputStream.read(playBuffer, 0, playBuffer.length)) != -1){
                    if(cnt > 0){
                        sourceDataLine.write(playBuffer, 0, cnt);
                    }
                }
                sourceDataLine.drain();
                int elapsedTime = (int)(new Date().getTime() - startTime);
                elapsedTimeMeter.setText("" + elapsedTime);
                sourceDataLine.stop();
                sourceDataLine.close();

                generateBtn.setEnabled(true);
                playOrFileBtn.setEnabled(true);
            }catch (Exception e) {
                e.printStackTrace();
                System.exit(0);
            }
        }
    }

    class SynGen{
        ByteBuffer byteBufferCopy;
        ByteBuffer byteBuffer;
        ShortBuffer shortBufferCopy;
        ShortBuffer shortBuffer;
        int byteLength;

       public void getSyntheticData(byte[] synDataBuffer,byte[] synDataBuffercopy){
            byteBufferCopy = ByteBuffer.wrap(synDataBuffercopy);
            shortBufferCopy = byteBufferCopy.asShortBuffer();
            byteBuffer = ByteBuffer.wrap(synDataBuffer);
            shortBuffer = byteBuffer.asShortBuffer();
            byteLength = synDataBuffer.length;
            stereoSurringRound();

        }

        public void stereoSurringRound(){
            //channels = 2;//声道
            int bytesPerSamp =channels*2;//帧所包含字节,基于声道,声道数*2
            int sampLength = byteLength/bytesPerSamp;//帧数
            double leftGain =0-sampleRate;//左增益-44100
            double rightGain =0;//右增益0 44100-sampleRate
            //我这里采用的mp3的采样率是44100,增益值设置为-44100到0
            double leftGaintemp =0-sampleRate;;
            double rightGaintemp =0;
            System.out.println("sampLength:"+sampLength);
            //将整首歌依据帧数分成28个步长,4个步长为一组环绕(趋向左-左-趋向右-右)
            //既一首歌环绕28/4=7次
            int stepLength = (int)sampLength/28;
            System.out.println("step:"+stepLength);
            int totalLength = 0;
            for(int cnt = 0; cnt < sampLength; cnt++){
                //根据帧数判断
                if(totalLength>= 0 && totalLength < stepLength ){
                    //趋向左
                    leftGaintemp -= leftGain/stepLength;//0
                    rightGaintemp += leftGain/stepLength;//-> -44100
                }
                if(totalLength>= stepLength && totalLength <stepLength*2 ){
                    //左边增益
                    leftGaintemp = rightGain;//0
                    rightGaintemp = leftGain;//-44100

                }

                if(totalLength >= stepLength*2 && totalLength < stepLength*3){
                    //趋向右
                    leftGaintemp += leftGain/stepLength;//-> -44100
                    rightGaintemp -= leftGain/stepLength;//->0

                }
                if(totalLength >= stepLength*3 && totalLength < stepLength*4){
                    //右边增益
                    leftGaintemp = leftGain;// -44100
                    rightGaintemp = rightGain;//0
                }

                //为左通道生成数据
                short sinValue =0;
                if (channels == 2) {
                  sinValue=  shortBuffer.get(cnt * 2);
                }else {
                    sinValue=  shortBuffer.get(cnt);
                }
                shortBufferCopy.put(
                        (short)(sinValue/sampleRate*(leftGaintemp+sampleRate)));
                //为右通道生成数据
                if (channels == 2) {
                    sinValue = shortBuffer.get(cnt * 2 + 1);
                }
                shortBufferCopy.put(
                        (short)(sinValue/sampleRate*(rightGaintemp+sampleRate)));

                if(totalLength == stepLength*4-1){
                    totalLength = -1;
                }

                totalLength++;
            }
        }

       
}}

4、附件 

mp3依赖包和代码里用到的歌

 java播放MP3依赖库

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值