Предисловие: у меня оборудована студия, в студию я решил докупить электронные midi ударные, инструмент с падами из линейки: medeli, akai, novation.
Для разработки на компьютере установлен Linux (Ubuntu), программное обеспечение выше упомянутых девайсов в Linux не поддерживается, а заморочки с wine и виртуальной машиной или переключение между операционными системами того не стоят.
Решил разработать простой инструмент для написания ритмов.
Скачать и протестировать программу можно по этой ссылке.
Проектирование
Проектирование начал с рисования интерфейса в NetBeans:
Принцип работы
Активное текстовое поле для загрузки сэмпла на линию.
16 кнопок при нажатии на которые происходит воспроизведение сэмпла установаленного на линию.
Кнопка Play воспроизводит звуки по колонкам с установаленными на них сэплами с определенной задержкой (если на линии установлен сэмпл и кнопка нажата).
Код наглядно
JDrum.java в этом классе расположены:
- Запуск фрейма.
- Основная частить логики.
- Наборы переменных.
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
/**
*
* @author dj DNkey
*/
public class JDrum {
/**
* pads values
*/
public static int[] pads = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
/*
* pads in line 1
*/
public static Integer[] line1Pads = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
/**
* pads in line 2
*/
public static Integer[] line2Pads = {17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
/**
* pads in line 3
*/
public static Integer[] line3Pads = {33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};
/**
* pads in line 4
*/
public static Integer[] line4Pads = {49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64};
/**
* pads in line 5
*/
public static Integer[] line5Pads = {65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80};
/**
* pads in line 6
*/
public static Integer[] line6Pads = {81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96};
/**
* pads in column 1
*/
public static int[] column1 = {1,17,33,49,65,81};
/**
* pads in column 2
*/
public static int[] column2 = {2,18,34,50,66,82};
/**
* pads in column 3
*/
public static int[] column3 = {3,19,35,51,67,83};
/**
* pads in column 4
*/
public static int[] column4 = {4,20,36,52,68,84};
/**
* pads in column 5
*/
public static int[] column5 = {5,21,37,53,69,85};
/**
* pads in column 6
*/
public static int[] column6 = {6,22,38,54,70,86};
/**
* pads in column 7
*/
public static int[] column7 = {7,23,39,55,71,87};
/**
* pads in column 8
*/
public static int[] column8 = {8,24,40,56,72,88};
/**
* pads in column 9
*/
public static int[] column9 = {9,25,41,57,73,89};
/**
* pads in column 10
*/
public static int[] column10 = {10,26,42,58,74,90};
/**
* pads in column 11
*/
public static int[] column11 = {11,27,43,59,75,91};
/**
* pads in column 12
*/
public static int[] column12 = {12,28,44,60,76,92};
/**
* pads in column 13
*/
public static int[] column13 = {13,29,45,61,77,93};
/**
* pads in column 14
*/
public static int[] column14 = {14,30,46,62,78,94};
/**
* pads in column 15
*/
public static int[] column15 = {15,31,47,63,79,95};
/**
* pads in column 16
*/
public static int[] column16 = {16,32,48,64,80,96};
/**
* Sound files bind on lines 1-10
*/
public static Sound line1Sound = null;
public static Sound line2Sound = null;
public static Sound line3Sound = null;
public static Sound line4Sound = null;
public static Sound line5Sound = null;
public static Sound line6Sound = null;
/**
* play speed
*/
public static int speed = 35;
public static boolean play = false;
public static Main frame;
/**
*
* @param args the command line arguments
*/
public static void main(String[] args) {
new Player().start();
frame = new Main();
frame.setVisible(true);
}
/**
* Play object Sound in new Thread
* @param sound
*/
public static synchronized void play(Sound sound){
if(sound != null){
new PlaySound(sound).start();
}
}
public static synchronized void loadSound(File file){
// sound
}
/**
* Play pressed pad
* @param padNum
*/
public static synchronized void playPad(int padNum){
//change pads value 1 to 0, 0 to 1
if(pads[padNum - 1] == 0){
JDrum.pads[padNum - 1] = 1;
} else{
JDrum.pads[padNum - 1] = 0;
}
/**
* Check line
*/
if(pads[padNum - 1] == 1){
playLine(padNum);
}
}
/**
* play sound file on line where press pad
* @param padNum
*/
public static synchronized void playLine(int padNum){
int line = getPadLine(padNum);
/**
* Play sound from line
*/
if(line == 1){
JDrum.play(line1Sound);
}
if(line == 2){
JDrum.play(line2Sound);
}
if(line == 3){
JDrum.play(line3Sound);
}
if(line == 4){
JDrum.play(line4Sound);
}
if(line == 5){
JDrum.play(line5Sound);
}
if(line == 6){
JDrum.play(line6Sound);
}
}
/**
* get line of pressed pad
* @param padNum
* @return
*/
public static synchronized int getPadLine(int padNum){
int line = 0;
List<Integer> list;
list = Arrays.asList(line1Pads);
if(list.contains(padNum)){
line = 1;
}
list = Arrays.asList(line2Pads);
if(list.contains(padNum)){
line = 2;
}
list = Arrays.asList(line3Pads);
if(list.contains(padNum)){
line = 3;
}
list = Arrays.asList(line4Pads);
if(list.contains(padNum)){
line = 4;
}
list = Arrays.asList(line5Pads);
if(list.contains(padNum)){
line = 5;
}
list = Arrays.asList(line6Pads);
if(list.contains(padNum)){
line = 6;
}
return line;
}
/**
* Save JDrum project to file .drum
* @param fileName
*/
public static void save(String fileName){
//load JDrum settings to save class
Save save = new Save();
save.pads = JDrum.pads;
if(line1Sound != null){
save.line1Sound = line1Sound.file.getAbsolutePath();
}
if(line2Sound != null){
save.line2Sound = line2Sound.file.getAbsolutePath();
}
if(line3Sound != null){
save.line3Sound = line3Sound.file.getAbsolutePath();
}
if(line3Sound != null){
save.line4Sound = line4Sound.file.getAbsolutePath();
}
if(line5Sound != null){
save.line5Sound = line5Sound.file.getAbsolutePath();
}
if(line6Sound != null){
save.line6Sound = line6Sound.file.getAbsolutePath();
}
save.save(fileName);
}
/**
* Open saved file and load to JDrum
* @param filePath
*/
public static void open(String filePath){
Save save = new Save();
save = save.load(filePath);
Sound sound;
//line1Sound = new File(save.line1Sound);
if(save.line1Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line1Sound));
line1Sound = sound;
Main.jTextField1.setText(line1Sound.file.getName());
}
if(save.line2Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line2Sound));
line2Sound = sound;
Main.jTextField2.setText(line2Sound.file.getName());
}
if(save.line3Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line3Sound));
line3Sound = sound;
Main.jTextField3.setText(line3Sound.file.getName());
}
if(save.line4Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line4Sound));
line4Sound = sound;
Main.jTextField4.setText(line4Sound.file.getName());
}
if(save.line5Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line5Sound));
line5Sound = sound;
Main.jTextField5.setText(line5Sound.file.getName());
}
if(save.line6Sound != null){
sound = new Sound();
sound.loadFile(new File(save.line6Sound));
line6Sound = sound;
Main.jTextField6.setText(line6Sound.file.getName());
}
JDrum.pads = save.pads;
frame.changeButton(JDrum.pads);
}
public static void startRecording() {
String command = "audio-recorder -c start";
String output = executeCommand(command);
}
public static void stopRecording() {
String command = "audio-recorder -c stop";
String output = executeCommand(command);
}
public static String executeCommand(String command) {
StringBuffer output = new StringBuffer();
Process p;
try {
p = Runtime.getRuntime().exec(command);
p.waitFor();
BufferedReader reader =
new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
while ((line = reader.readLine())!= null) {
output.append(line + "n");
}
} catch (Exception e) {
e.printStackTrace();
}
return output.toString();
}
}
Player.java демон:
- Запуск звуков по колонкам, если на линии расположен сэмпл и нажата кнопка.
- Player запускает классы PlaySound которые отрабатывают в отдельном потоке.
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import static jdrum.JDrum.playLine;
/**
*
* @author nn
*/
public class Player extends Thread {
Field field;
String columnName;
int[] column;
public int step = 1;
public int stopFlag = 0;
public Player() {
setDaemon(true);
}
public static int[] column1;
public static int[] column2;
public static int[] column3;
public static int[] column4;
public static int[] column5;
public static int[] column6;
public static int[] column7;
public static int[] column8;
public static int[] column9;
public static int[] column10;
public static int[] column11;
public static int[] column12;
public static int[] column13;
public static int[] column14;
public static int[] column15;
public static int[] column16;
public void run() {
while (true) {
if(JDrum.play){
try {
//get column from JDrum by step 1-10
columnName = "column" + step;
field = JDrum.class.getDeclaredField(columnName);
field.setAccessible(true);
column = (int[]) field.get(null);
//play pads from column
for(int i = 0;i <= 5;i++ ){
//System.out.println(columnName);
if(JDrum.pads[column[i] - 1] == 1){
JDrum.playLine(column[i]);
}
}
//next step
step++;
if(step == 17){
step = 1;
stopFlag++;
if(stopFlag == 2){
JDrum.play = false;
stopFlag = 0;
}
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
} catch (NoSuchFieldException ex) {
Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
}
}
//speed sleep
try {
sleep(JDrum.speed * 10);
} catch (InterruptedException e) {
// handle exception here
}
}
}
}
PlaySound.java запуск звука (класса Sound) в отдельном потоке
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
/**
*
* @author nn
*/
public class PlaySound extends Thread{
public Sound sound;
public PlaySound(Sound sound){
this.sound = sound;
}
public void run() {
if(sound != null){
sound.play();
}
}
}
Sound.java класс воспроизведения звука
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
import java.io.File;
import java.io.IOException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
*
* @author nn
*/
public class Sound {
public boolean playCompleted;
public File file;
public AudioInputStream stream;
public AudioFormat format;
public DataLine.Info info;
public Clip clip;
private final int BUFFER_SIZE = 128000;
private File soundFile;
private AudioInputStream audioStream;
private AudioFormat audioFormat;
private SourceDataLine sourceLine;
public void loadFile(File file){
this.file = file;
}
public void play(){
if(file != null){
soundFile = file;
try {
audioStream = AudioSystem.getAudioInputStream(soundFile);
} catch (Exception e){
e.printStackTrace();
System.exit(1);
}
audioFormat = audioStream.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
if (!AudioSystem.isLineSupported(info)) {
System.out.println("Line not supported"+ info);
}
try {
sourceLine = (SourceDataLine) AudioSystem.getLine(info);
//
sourceLine.open(audioFormat);
} catch (LineUnavailableException e) {
e.printStackTrace();
System.exit(1);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
sourceLine.start();
int nBytesRead = 0;
byte[] abData = new byte[BUFFER_SIZE];
while (nBytesRead != -1) {
try {
nBytesRead = audioStream.read(abData, 0, abData.length);
} catch (IOException e) {
e.printStackTrace();
}
if (nBytesRead >= 0) {
@SuppressWarnings("unused")
int nBytesWritten = sourceLine.write(abData, 0, nBytesRead);
}
}
/**
try {
Clip clip = new Clip();
int waitTime = (int)Math.ceil(clip.getMicrosecondLength()/1000.0);
Thread.sleep(waitTime);
} catch (InterruptedException ex) {
Logger.getLogger(Sound.class.getName()).log(Level.SEVERE, null, ex);
} catch (LineUnavailableException ex) {
Logger.getLogger(Sound.class.getName()).log(Level.SEVERE, null, ex);
}
**/
sourceLine.drain();
sourceLine.close();
}
}
}
Выкладывать Main.java не буду там генерация интерфейсов средствами NetBeans, только отдельные интересные моменты:
public Main() {
initComponents();
//bind load sample
jTextField1.addMouseListener(new SampleEvent(1,this));
jTextField2.addMouseListener(new SampleEvent(2,this));
jTextField3.addMouseListener(new SampleEvent(3,this));
jTextField4.addMouseListener(new SampleEvent(4,this));
jTextField5.addMouseListener(new SampleEvent(5,this));
jTextField6.addMouseListener(new SampleEvent(6,this));
//bind pad click
Field field;
JButton dynamicButton;
try {
for (int buttonNum = 1; buttonNum <= 96; buttonNum++) {
field = this.getClass().getDeclaredField("jButton" + buttonNum);
field.setAccessible(true);
dynamicButton = (JButton) field.get(this);
dynamicButton.setMargin(new Insets(0, 0, 0, 0));
dynamicButton.addMouseListener(new PadEvent(buttonNum,this));
}
} catch (NoSuchFieldException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalArgumentException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
После инициализации компонентов, нужно назначить события на кнопки:
- События вынесены в отдельные классы.
- Для назначения событий для 96 кнопок применен Reflection API, который назначает события в цикле по названию (name + i).
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.JTextField;
import javax.swing.filechooser.FileNameExtensionFilter;
/**
*
* @author nn
*/
public class SampleEvent implements MouseListener{
public int fieldNum;
Main frame;
public SampleEvent(int fieldNum, Main frame){
this.fieldNum = fieldNum;
this.frame = frame;
}
public void mouseClicked(MouseEvent evt) {
if(evt.getButton() == MouseEvent.BUTTON1) {
JFileChooser fileopen = new JFileChooser();
fileopen.setCurrentDirectory(new java.io.File(System.getProperty("user.dir")));
FileNameExtensionFilter filter = new FileNameExtensionFilter("wav", "wav");
fileopen.setFileFilter(filter);
int ret = fileopen.showDialog(null, "Открыть файл");
if (ret == JFileChooser.APPROVE_OPTION) {
try {
File file = fileopen.getSelectedFile();
//setup file name to sample field
Field field = frame.getClass().getDeclaredField("jTextField" + fieldNum);
field.setAccessible(true);
JTextField value = (JTextField) field.get(this);
value.setText(file.getName());
Sound sound = new Sound();
sound.loadFile(file);
//play
JDrum.play(sound);
//setup path
Field f = JDrum.class.getField("line"+ fieldNum +"Sound");
f.setAccessible(true);
f.set(null, sound);
//System.out.print(JDrum.line1SoundFile);
//set full path
//System.out.println(file.getAbsolutePath());
} catch (SecurityException | IllegalArgumentException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (NoSuchFieldException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
if(evt.getButton() == MouseEvent.BUTTON3) {
try {
Field field = frame.getClass().getDeclaredField("jTextField" + fieldNum);
field.setAccessible(true);
JTextField value = (JTextField) field.get(this);
value.setText(" ");
Field f = JDrum.class.getField("line"+ fieldNum +"SoundFile");
f.setAccessible(true);
f.set(null, null);
} catch (NoSuchFieldException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalArgumentException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
public void mousePressed(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseReleased(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseEntered(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseExited(MouseEvent e) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jdrum;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
/**
*
* @author nn
*/
public class PadEvent implements MouseListener{
public int pudNum;
Main frame;
public PadEvent(int pudNum,Main frame){
this.pudNum = pudNum;
this.frame = frame;
}
@Override
public void mouseClicked(MouseEvent evt) {
if(evt.getButton() == MouseEvent.BUTTON1) {
Field field;
JButton dynamicButton;
try {
// change pad color
field = frame.getClass().getDeclaredField("jButton" + pudNum);
field.setAccessible(true);
dynamicButton = (JButton) field.get(this);
//change color and play pad
if(!dynamicButton.getBackground().equals(new Color(145,145,145))){
dynamicButton.setBackground(new Color(145,145,145));
}else{
dynamicButton.setBackground(null);
}
//play pad
JDrum.playPad(pudNum);
} catch (SecurityException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalArgumentException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} catch (NoSuchFieldException ex) {
Logger.getLogger(PadEvent.class.getName()).log(Level.SEVERE, null, ex);
}
//cменить значение пада с 1 на 0 или с 0 на 1
//запустить звук назначенный на линнии
//Сменить цвет кнопки сс зеленой на серру и с серой на зеленую
//System.out.println("press" + pudNum);
//cменить значение пада с 1 на 0 или с 0 на 1
//запустить звук назначенный на линнии
//Сменить цвет кнопки сс зеленой на серру и с серой на зеленую
//System.out.println("press" + pudNum);
}
}
@Override
public void mousePressed(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseReleased(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseEntered(MouseEvent e) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void mouseExited(MouseEvent e) {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
Конечно как у любой альфа версии программы возникают ошибки:
javax.sound.sampled.LineUnavailableException: line with format PCM_SIGNED 44100.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian not supported. at com.sun.media.sound.DirectAudioDevice$DirectDL.implOpen(DirectAudioDevice.java:513) at com.sun.media.sound.AbstractDataLine.open(AbstractDataLine.java:121) at com.sun.media.sound.AbstractDataLine.open(AbstractDataLine.java:153) at jdrum.Sound.play(Sound.java:68) at jdrum.PlaySoundThread.run(PlaySoundThread.java:24) /home/nn/.cache/netbeans/8.2/executor-snippets/run.xml:53: Java returned: 1 BUILD FAILED (total time: 1 minute 57 seconds)
Ошибка возникает насколько я понял после многоклатного назначения и нажатия клавиш из за занятой линии.
Думаю дальнейшие развитие программы будет в сторону:
- Изменение воспроизведения wav файлов на midi.
- Добавления нот.
- Регулятор звука на дорожке.
Автор: Иван