Java音乐小程序

创建MIDI音乐播放器

构思

要完成这个程序,我们需要用到:

  • JavaSound API
  • 创建Swing GUI
  • 通过网络链接到其他计算机
  • 输入\输出数据

JavaSound API

MIDI

MIDI(Musical Instrument Digital Interface)乐器数字接口,也是不同电子发声装置沟通的标准协议。
MIDI数据表示执行的动作,但没有实际的声音,实际声音靠装置发出。

JavaSound的工作原理:

  1. 取得Sequencer并将它打开。
  2. 创建新的Sequence。
  3. 从Sequence中创建新的Track。
  4. 填入MidiEvent。
  5. 让Sequencer播放。
import javax.sound.midi.*;

public class MiniMiniMusicApp {

public static void main(String[] args) {
    MiniMiniMusicApp mini = new MiniMiniMusicApp();
        mini.play();
    }

    public void play() {

        try {
            //取得Sequencer并将它打开
            Sequencer player = MidiSystem.getSequencer();
            player.open();

            //创建新的Sequence
            Sequence seq = new Sequence(Sequence.PPQ, 4);

            //从Sequence中创建新的Track
            Track track = seq.createTrack();

            //填入MidiEvent
            ShortMessage a = new ShortMessage();//创建Message
            a.setMessage(144, 1, 44, 100);//置入指令(类型,频道,音符,音量)
            MidiEvent noteOn = new MidiEvent(a, 1);//在第一拍启动a
            track.add(noteOn);

            ShortMessage b = new ShortMessage();
            b.setMessage(128, 1, 44, 100);
            MidiEvent noteOff = new MidiEvent(b, 16);//在16拍停止
            track.add(noteOff);

            //将Sequence送入Sequencer
            player.setSequence(seq);

            //让Sequencer播放
            player.start();

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

使用命令列表参数

import javax.sound.midi.*;

public class MiniMusicCmdLine {

    public static void main(String[] args) {
        MiniMusicCmdLine mini = new MiniMusicCmdLine();
        if (args.length < 2) {
            System.out.println("Don't forget the instrument and note args.");
        } else {
            int instrument = Integer.parseInt(args[0]);
            int note = Integer.parseInt(args[1]);
            mini.play(instrument, note);
        }
    }

    public void play(int instrument, int note) {
        try {
            //取得Sequencer并将它打开
            Sequencer player = MidiSystem.getSequencer();
            player.open();

            //创建新的Sequence
            Sequence seq = new Sequence(Sequence.PPQ, 4);

            //从Sequence中创建新的Track
            Track track = seq.createTrack();

            MidiEvent event =null;

            ShortMessage first = new ShortMessage();
            first.setMessage(192, 1, instrument, 0);
            MidiEvent changrInstrument = new MidiEvent(first, 1);
            track.add(changrInstrument);

            //填入MidiEvent
            ShortMessage a = new ShortMessage();
            a.setMessage(144, 1, note, 100);
            MidiEvent noteOn = new MidiEvent(a, 1);
            track.add(noteOn);

            ShortMessage b = new ShortMessage();
            b.setMessage(128, 1, note, 100);
            MidiEvent noteOff = new MidiEvent(b, 16);
            track.add(noteOff);

            //将Sequence送入Sequencer
            player.setSequence(seq);

            //让Sequencer播放
            player.start();

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

在播放音乐的同时输出图形

需要实现的功能:

  • 制作一系列MIDI信息(事件)来播放任意钢琴音
  • 对事件注册一个监听者
  • 开始sequencer的播放操作
  • 每当监听者的事件处理程序被调用时,在面板上画出一个随机的方块并调用repaint

制作程序的三步:

  1. 简单的制作MIDI事件
  2. 注册并监听事件,但没有图形。从命令栏对应每一拍输出一个信息
  3. 在第二版上加上输出图形

制作一系列MIDI信息

import javax.sound.midi.*;

public class MiniMusicPlayer1 {

    public static void main(String[] args) {

        try {
            //创建并打开Sequencer
            Sequencer sequencer = MidiSystem.getSequencer();
            sequencer.open();

            //创建Sequence并track
            Sequence seq = new Sequence(Sequence.PPQ, 4);
            Track track = seq.createTrack();

            for (int i = 5; i < 61; i++) {
                track.add(makeEvent(144,1,i,100,i));
                track.add(makeEvent(128,1,i,100,i+2));
            }

            //开始播放
            sequencer.setSequence(seq);
            sequencer.setTempoInBPM(220);
            sequencer.start();
            } catch (Exception ex) { ex.printStackTrace(); }
    }

    public static MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);
        } catch (Exception e) { e.printStackTrace(); }
        return event;
        }
}

第二版:加入监听事件

import javax.sound.midi.*;

public class MiniMusicPlayer2 implements ControllerEventListener {

    public static void main(String[] args) {
        MiniMusicPlayer2 mini = new MiniMusicPlayer2();
        mini.go();
    }

    public void go() {
        try {
            //创建并打开Sequencer
            Sequencer sequencer = MidiSystem.getSequencer();
            sequencer.open();

            int[] eventsIWant = {127};
            sequencer.addControllerEventListener(this, eventsIWant);//注册事件,监听者和监听的事件数

            //创建Sequence并track
            Sequence seq = new Sequence(Sequence.PPQ, 4);
            Track track = seq.createTrack();

            for (int i = 5; i < 61; i++) {
                track.add(makeEvent(144,1,i,100,i));

                track.add(makeEvent(176,1,127,0,i));//176是ControllerEvent的编号,用来监听

                track.add(makeEvent(128,1,i,100,i+2));
            }
            sequencer.setSequence(seq);
            sequencer.setTempoInBPM(220);
            sequencer.start();
            } catch (Exception ex) { ex.printStackTrace(); }
    }

    public void controlChange(ShortMessage event) {
        System.out.println("la");
    }

    public static MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);
        } catch (Exception e) { e.printStackTrace(); }
        return event;
        }
}

第三版:输出图形

import javax.sound.midi.*;
import java.io.*;
import javax.swing.*;
import java.awt.*;

public class MiniMusicPlayer3 {
    static JFrame f = new JFrame("My frist music video");
    static MyDrawPanel ml;

    public static void main(String[] args) {
        MiniMusicPlayer3 mini = new MiniMusicPlayer3();
        mini.go();
    }

    public void setUpGui() {
        ml = new MyDrawPanel();
        f.setContentPane(ml);
        f.setBounds(30, 30, 300, 300);
        f.setVisible(true);
    }

    public void go() {
        setUpGui();

        try {
            //创建并打开Sequencer
            Sequencer sequencer = MidiSystem.getSequencer();
            sequencer.open();

            int[] eventsIWant = {127};
            sequencer.addControllerEventListener(ml, eventsIWant);//注册事件,监听者和监    听的事件数

            //创建Sequence并track
            Sequence seq = new Sequence(Sequence.PPQ, 4);
            Track track = seq.createTrack();

            for (int i = 5; i < 61; i++) {

                int r = (int)((Math.random() * 50) + 1);
                track.add(makeEvent(144,1,r,100,i));

                track.add(makeEvent(176,1,127,0,i));//176是ControllerEvent的编号,用来监听

                track.add(makeEvent(128,1,r,100,i+2));
            }
            sequencer.setSequence(seq);
            sequencer.setTempoInBPM(220);
            sequencer.start();
            } catch (Exception ex) { ex.printStackTrace();}
    }

    public static MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);
        } catch (Exception e) { e.printStackTrace(); }
        return event;
    }

    class MyDrawPanel extends JPanel implements ControllerEventListener {
        boolean msg = false;

        public void controlChange(ShortMessage event) {
            msg = true;
            repaint();
        }


        public void paintComponent(Graphics g) {//只能由系统调用
            if (msg) {
                Graphics2D g2d = (Graphics2D) g;

                int red = (int) (Math.random() * 255);
                int green = (int) (Math.random() * 255);
                int blue = (int) (Math.random() * 255);

                Color color = new Color(red, green ,blue);

                g2d.setColor(color);
                int ht = (int) ((Math.random() * 120) + 10);
                int width = (int) ((Math.random() * 120) + 10);

                int x = (int) ((Math.random() * 40) + 10);
                int y = (int) ((Math.random() * 40) + 10);

                g2d.fillRect(x, y, ht, width);

                msg = false;

            }
        }
    }
}

创建BeatBox

  • 创建出带有256个复选框的GUI。初始的时候这些复选框都是未勾选的,乐器的名字用到16个JLabel,还有4个按钮。
  • 对4个按钮进行注册ActionListener。不需要个别监听复选框,我们不需要动态的改变样式,只需等用户点击start时再来检查复选框并制作出MIDI的track。
  • 设定MIDI系统。之前已经做过,现在还要在此基础上进行循环播放,并调整播放速度。
  • 在用户按下start时,真正开始程序,程序一次一行的取得复选框的状态并制作MIDI信息。持续播放直到用户取消

程序代码

import java.awt.*;
import javax.swing.*;
import javax.sound.midi.*;
import java.util.*;  //使用ArrayList
import java.awt.event.*;

public class BeatBox {

    JPanel mainPanel;
    ArrayList<JCheckBox> checkboxList; //把CheckBox存在ArrayList中
    Sequencer sequencer;
    Sequence sequence;
    Track track;
    JFrame theFrame;

    String [] instrumentNames = { "Bass Drum", "Closed Hi-Hat", "Open Hi-Hat",
            "Acoustic Snare", "Crash Cymbal", "Hand Clap", "High Tom", "Hi Bongo",
            "Maracas", "Whistle", "Low Conga", "Cowbell", "Vibraslap", "Low-mid Tom",
            "High Agogo", "Open Hi Conga" }; //乐器名称
    int [] instruments = { 35,42,46,38,49,39,50,60,70,72,64,56,58,47,67,63 };

    public static void main(String[] args) {
        new BeatBox().buildGUI();
    }

    public void buildGUI() {
        theFrame = new JFrame("Cyber BeatBox");//框架
        theFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        BorderLayout layout = new BorderLayout();
        JPanel background = new JPanel(layout); //背景面板,设定成BoederLayout
        background.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); //设定面板上的空白边缘

        checkboxList = new ArrayList<JCheckBox>();
        Box buttonBox = new Box(BoxLayout.Y_AXIS);

        JButton start = new JButton("start");
        start.addActionListener(new MyStartListener());
        buttonBox.add(start);

        JButton stop = new JButton("stop");
        stop.addActionListener(new MyStopListener());
        buttonBox.add(stop);

        JButton upTempo = new JButton("Tempo Up");
        upTempo.addActionListener(new MyUpTempoListener());
        buttonBox.add(upTempo);

        JButton downTempo = new JButton("Tempo Down");
        downTempo.addActionListener(new MyDownTempoListener());
        buttonBox.add(downTempo);

        Box nameBox = new Box(BoxLayout.Y_AXIS);
        for (int i = 0; i < 16; i++) {
            nameBox.add(new Label(instrumentNames[i]));
        }

        background.add(BorderLayout.EAST, buttonBox);
        background.add(BorderLayout.WEST, nameBox);


        theFrame.getContentPane().add(background);

        GridLayout grid = new GridLayout(16, 16); //表格形式的布局(参数是行、列)
        grid.setVgap(10);//表格上下间距
        grid.setHgap(2);//表格左右间距
        mainPanel = new JPanel(grid);
        background.add(mainPanel);

        for (int i = 0; i < 256; i++) { //创建checkbox组,设定为未勾选并加入到ArrayList和    mainPanel
            JCheckBox c = new JCheckBox();
            c.setSelected(false);
            checkboxList.add(c);
            mainPanel.add(c);
        }

        setUpMidi();

        theFrame.setBounds(50,50,400,400);
        theFrame.pack();//Frame.pack()是JAVA语言的一个函数,这个函数的作用就是根据窗口里面的    布局及组件的preferedSize来确定frame的最佳大小。
        theFrame.setVisible(true);
    }

    public void setUpMidi() {
        try {
            sequencer = MidiSystem.getSequencer();
            sequencer.open();
            sequence = new Sequence(Sequence.PPQ,4);
            track = sequence.createTrack();
            sequencer.setTempoInBPM(120);
        } catch (Exception e) { e.printStackTrace(); }
    }

    public void buildTrackAndStart() {
        int[] trackList = null;
        //去掉旧的做一个新的
        sequence.deleteTrack(track);
        track = sequence.createTrack();

        for (int i = 0; i < 16; i++) {
            trackList = new int[16];

            int key = instruments[i];

            for (int j = 0; j < 16; j++) {
                JCheckBox jc = (JCheckBox) checkboxList.get(j + (16 * i));
                if (jc.isSelected()) {
                    trackList[j] = key;
                } else {
                    trackList[j] = 0;
                }
            }//关闭内循环

            makeTracks(trackList);
            track.add(makeEvent(176,1,127,0,16));
        }//关闭外部循环

        track.add(makeEvent(192,9,1,0,15));//确保第16拍有事件,否则不会重复播放
        try {
            sequencer.setSequence(sequence);
            sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);//重复无限此次
            sequencer.start();
            sequencer.setTempoInBPM(120);
        } catch (Exception e) { e.printStackTrace(); }
    }

    public void makeTracks(int[] list) {

        for (int i = 0; i < 16; i++) {
            int key = list[i];

            if (key != 0) {
                track.add(makeEvent(144,9,key,100,i));
                track.add(makeEvent(128,9,key,100,i+1));
            }
        }
    }

    public MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);
        } catch (Exception e) { e.printStackTrace(); }
        return event;
    }

    public class MyStartListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            buildTrackAndStart();
        }
    }

    public class MyStopListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            sequencer.stop();
        }
    }

    public class MyUpTempoListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            float tempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor((float) (tempoFactor * 1.03));
        }
    }

    public class MyDownTempoListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            float tempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor((float) (tempoFactor * .97));
        }
    }

}

加入存取功能的BeatBox

import java.awt.*;
import javax.swing.*;
import javax.sound.midi.*;
import java.util.*;  //使用ArrayList
import java.awt.event.*;
import java.io.*;

public class BeatBox {

    JPanel mainPanel;
    ArrayList<JCheckBox> checkboxList; //把CheckBox存在ArrayList中
    Sequencer sequencer;
    Sequence sequence;
    Track track;
    JFrame theFrame;

    String [] instrumentNames = { "Bass Drum", "Closed Hi-Hat", "Open Hi-Hat",
            "Acoustic Snare", "Crash Cymbal", "Hand Clap", "High Tom", "Hi Bongo",
            "Maracas", "Whistle", "Low Conga", "Cowbell", "Vibraslap", "Low-mid Tom",
            "High Agogo", "Open Hi Conga" }; //乐器名称
    int [] instruments = { 35,42,46,38,49,39,50,60,70,72,64,56,58,47,67,63 };

    public static void main(String[] args) {
        new BeatBox().buildGUI();
    }

    public void buildGUI() {
        theFrame = new JFrame("Cyber BeatBox");//框架
        theFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        BorderLayout layout = new BorderLayout();
        JPanel background = new JPanel(layout); //背景面板,设定成BoederLayout
        background.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); //设定面板上的空白边缘

        checkboxList = new ArrayList<JCheckBox>();
        Box buttonBox = Box.createVerticalBox();//等号后面的等价替换 new Box(BoxLayout.Y_AXIS);

        JButton start = new JButton("start");
        start.addActionListener(new MyStartListener());
        buttonBox.add(start);

        JButton stop = new JButton("stop");
        stop.addActionListener(new MyStopListener());
        buttonBox.add(stop);

        JButton upTempo = new JButton("Tempo Up");
        upTempo.addActionListener(new MyUpTempoListener());
        buttonBox.add(upTempo);

        JButton downTempo = new JButton("Tempo Down");
        downTempo.addActionListener(new MyDownTempoListener());
        buttonBox.add(downTempo);

        JButton saveTrack = new JButton("Save Track");
        saveTrack.addActionListener(new MySaveTrackListener());
        buttonBox.add(saveTrack);

        JButton restore= new JButton("Restore");
        restore.addActionListener(new MyRestoreListener());
        buttonBox.add(restore);

        Box nameBox = new Box(BoxLayout.Y_AXIS);
        for (int i = 0; i < 16; i++) {
            nameBox.add(new Label(instrumentNames[i]));
        }

        background.add(BorderLayout.EAST, buttonBox);
        background.add(BorderLayout.WEST, nameBox);


        theFrame.getContentPane().add(background);

        GridLayout grid = new GridLayout(16, 16);//表格类布局
        grid.setVgap(10);//表格上下间距
        grid.setHgap(2);//表格左右间距
        mainPanel = new JPanel(grid);
        background.add(mainPanel);

        for (int i = 0; i < 256; i++) { //创建checkbox组,设定为未勾选并加入到ArrayList和mainPanel
            JCheckBox c = new JCheckBox();
            c.setSelected(false);
            checkboxList.add(c);
            mainPanel.add(c);
        }

        setUpMidi();

        theFrame.setBounds(50,50,400,400);
        theFrame.pack();//Frame.pack()是JAVA语言的一个函数,这个函数的作用就是根据窗口里面的布局及组件的preferedSize来确定frame的最佳大小。
        theFrame.setVisible(true);
    }

    public void setUpMidi() {
        try {
            sequencer = MidiSystem.getSequencer();
            sequencer.open();
            sequence = new Sequence(Sequence.PPQ,4);
            track = sequence.createTrack();
            sequencer.setTempoInBPM(120);
        } catch (Exception e) { e.printStackTrace(); }
    }

    public void buildTrackAndStart() {
        int[] trackList = null;
        //去掉旧的做一个新的
        sequence.deleteTrack(track);
        track = sequence.createTrack();

        for (int i = 0; i < 16; i++) {
            trackList = new int[16];

            int key = instruments[i];

            for (int j = 0; j < 16; j++) {
                JCheckBox jc = (JCheckBox) checkboxList.get(j + (16 * i));
                if (jc.isSelected()) {
                    trackList[j] = key;
                } else {
                    trackList[j] = 0;
                }
            }//关闭内循环

            makeTracks(trackList);
            track.add(makeEvent(176,1,127,0,16));
        }//关闭外部循环

        track.add(makeEvent(192,9,1,0,15));//确保第16拍有事件,否则不会重复播放
        try {
            sequencer.setSequence(sequence);
            sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);//重复无限此次
            sequencer.start();
            sequencer.setTempoInBPM(120);
        } catch (Exception e) { e.printStackTrace(); }
    }

    public void makeTracks(int[] list) {

        for (int i = 0; i < 16; i++) {
            int key = list[i];

            if (key != 0) {
                track.add(makeEvent(144,9,key,100,i));
                track.add(makeEvent(128,9,key,100,i+1));
            }
        }
    }

    public MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);
        } catch (Exception e) { e.printStackTrace(); }
        return event;
    }

    public class MyStartListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            buildTrackAndStart();
        }
    }

    public class MyStopListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            sequencer.stop();
        }
    }

    public class MyUpTempoListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            float tempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor((float) (tempoFactor * 1.03));
        }
    }

    public class MyDownTempoListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            float tempoFactor = sequencer.getTempoFactor();
            sequencer.setTempoFactor((float) (tempoFactor * .97));
        }
    }

    public class MySaveTrackListener implements ActionListener {
        public void actionPerformed(ActionEvent a) {
            JFileChooser fileSave = new JFileChooser();
            fileSave.showSaveDialog(theFrame);
            saveFile(fileSave.getSelectedFile());
        }
    }

    private void saveFile(File file) {
        boolean[] checkboxState = new boolean[256];

        for (int i = 0; i < 256; i++) {
            JCheckBox check = (JCheckBox) checkboxList.get(i);
            if (check.isSelected()) {
                checkboxState[i] = true;
            }
        }

        try {
            FileOutputStream fileStream = new FileOutputStream(file);
            ObjectOutputStream os = new ObjectOutputStream(fileStream);
            os.writeObject(checkboxState);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public class MyRestoreListener implements ActionListener {
        public void actionPerformed (ActionEvent a) {
            JFileChooser fileOpen = new JFileChooser();
            fileOpen.showOpenDialog(fileOpen);
            loadFile(fileOpen.getSelectedFile());
        }
    }

    private void loadFile(File file) {
        boolean[] checkboxState = null;
        try {
            FileInputStream fileIn = new FileInputStream(file);
            ObjectInputStream is = new ObjectInputStream(fileIn);
            checkboxState = (boolean[]) is.readObject();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        for (int i = 0; i < 256; i++) {
            JCheckBox check = (JCheckBox) checkboxList.get(i);
            if (checkboxState[i]) {
                check.setSelected(true);
            } else {
                check.setSelected(false);
            }
        }

        sequencer.stop();
        buildTrackAndStart();
    }


}