jDrum rhythm studio emulator
- Tutorial
- Recovery Mode
Preface: I have a studio equipped, in the studio I decided to purchase electronic midi drums, an instrument with pads from the line: medeli, akai, novation.
Linux (Ubuntu) is installed on the computer for development, the software of the above mentioned devices is not supported in Linux, and there are no problems with wine and a virtual machine or switching between operating systems.
I decided to develop a simple tool for writing rhythms.
Download and test the program at this link .
The design started by drawing an interface in NetBeans:
The active text field for loading a sample into a line.
16 buttons when you click on which plays a sample set to the line.
The Play button plays sounds by speakers with samples installed on them with a certain delay (if the sample is set on the line and the button is pressed).
JDrum.java in this class are located:
Player.java daemon:
PlaySound.java sound launch (class Sound) in a separate stream
Sound.java class audio playback
Spreading Main.java will not generate interfaces with NetBeans tools there, just some interesting points:
After initializing the components, you need to assign events to the buttons:
Of course, like any alpha version of the program, errors occur:
I think the further development of the program will be towards:
UPDATE:
1. Replaced algogitm playback.
2. Cleaned up unnecessary pieces of code.
3. Improved synchronization.
4. Added speed control.
5. Added saving playback speed in the project.
6. Stop translates playback to the beginning.
7. You can see where the loop is playing.
Linux (Ubuntu) is installed on the computer for development, the software of the above mentioned devices is not supported in Linux, and there are no problems with wine and a virtual machine or switching between operating systems.
I decided to develop a simple tool for writing rhythms.
Download and test the program at this link .
Design
The design started by drawing an interface in NetBeans:
Principle of operation
The active text field for loading a sample into a line.
16 buttons when you click on which plays a sample set to the line.
The Play button plays sounds by speakers with samples installed on them with a certain delay (if the sample is set on the line and the button is pressed).
Code clearly
JDrum.java in this class are located:
- Run the frame.
- The main part of logic.
- Sets of variables.
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
*/publicclassJDrum{
/**
* pads values
*/publicstaticint[] 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
*/publicstatic Integer[] line1Pads = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
/**
* pads in line 2
*/publicstatic Integer[] line2Pads = {17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32};
/**
* pads in line 3
*/publicstatic Integer[] line3Pads = {33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48};
/**
* pads in line 4
*/publicstatic Integer[] line4Pads = {49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64};
/**
* pads in line 5
*/publicstatic Integer[] line5Pads = {65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80};
/**
* pads in line 6
*/publicstatic Integer[] line6Pads = {81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96};
/**
* pads in column 1
*/publicstaticint[] column1 = {1,17,33,49,65,81};
/**
* pads in column 2
*/publicstaticint[] column2 = {2,18,34,50,66,82};
/**
* pads in column 3
*/publicstaticint[] column3 = {3,19,35,51,67,83};
/**
* pads in column 4
*/publicstaticint[] column4 = {4,20,36,52,68,84};
/**
* pads in column 5
*/publicstaticint[] column5 = {5,21,37,53,69,85};
/**
* pads in column 6
*/publicstaticint[] column6 = {6,22,38,54,70,86};
/**
* pads in column 7
*/publicstaticint[] column7 = {7,23,39,55,71,87};
/**
* pads in column 8
*/publicstaticint[] column8 = {8,24,40,56,72,88};
/**
* pads in column 9
*/publicstaticint[] column9 = {9,25,41,57,73,89};
/**
* pads in column 10
*/publicstaticint[] column10 = {10,26,42,58,74,90};
/**
* pads in column 11
*/publicstaticint[] column11 = {11,27,43,59,75,91};
/**
* pads in column 12
*/publicstaticint[] column12 = {12,28,44,60,76,92};
/**
* pads in column 13
*/publicstaticint[] column13 = {13,29,45,61,77,93};
/**
* pads in column 14
*/publicstaticint[] column14 = {14,30,46,62,78,94};
/**
* pads in column 15
*/publicstaticint[] column15 = {15,31,47,63,79,95};
/**
* pads in column 16
*/publicstaticint[] column16 = {16,32,48,64,80,96};
/**
* Sound files bind on lines 1-10
*/publicstatic Sound line1Sound = null;
publicstatic Sound line2Sound = null;
publicstatic Sound line3Sound = null;
publicstatic Sound line4Sound = null;
publicstatic Sound line5Sound = null;
publicstatic Sound line6Sound = null;
/**
* play speed
*/publicstaticint speed = 35;
publicstaticboolean play = false;
publicstatic Main frame;
/**
*
* @param args the command line arguments
*/publicstaticvoidmain(String[] args){
new Player().start();
frame = new Main();
frame.setVisible(true);
}
/**
* Play object Sound in new Thread
* @param sound
*/publicstaticsynchronizedvoidplay(Sound sound){
if(sound != null){
new PlaySound(sound).start();
}
}
publicstaticsynchronizedvoidloadSound(File file){
// sound
}
/**
* Play pressed pad
* @param padNum
*/publicstaticsynchronizedvoidplayPad(int padNum){
//change pads value 1 to 0, 0 to 1if(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
*/publicstaticsynchronizedvoidplayLine(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
*/publicstaticsynchronizedintgetPadLine(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
*/publicstaticvoidsave(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
*/publicstaticvoidopen(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);
}
publicstaticvoidstartRecording(){
String command = "audio-recorder -c start";
String output = executeCommand(command);
}
publicstaticvoidstopRecording(){
String command = "audio-recorder -c stop";
String output = executeCommand(command);
}
publicstatic 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 daemon:
- Trigger sounds by speakers, if a sample is located on the line and a button is pressed.
- Player runs PlaySound classes that work in a separate stream.
Player.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.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
importstatic jdrum.JDrum.playLine;
/**
*
* @author nn
*/publicclassPlayerextendsThread{
Field field;
String columnName;
int[] column;
publicint step = 1;
publicint stopFlag = 0;
publicPlayer(){
setDaemon(true);
}
publicvoidrun(){
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 columnfor(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 sleeptry {
sleep(JDrum.speed * 10);
} catch (InterruptedException e) {
// handle exception here
}
}
}
}
PlaySound.java sound launch (class Sound) in a separate stream
PlaySound.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;
/**
*
* @author nn
*/publicclassPlaySoundextendsThread{
public Sound sound;
publicPlaySound(Sound sound){
this.sound = sound;
}
publicvoidrun(){
if(sound != null){
sound.play();
}
}
}
Sound.java class audio playback
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
*/publicclassSound{
publicboolean playCompleted;
public File file;
public AudioInputStream stream;
public AudioFormat format;
public DataLine.Info info;
public Clip clip;
privatefinalint BUFFER_SIZE = 128000;
private File soundFile;
private AudioInputStream audioStream;
private AudioFormat audioFormat;
private SourceDataLine sourceLine;
publicvoidloadFile(File file){
this.file = file;
}
publicvoidplay(){
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 = newbyte[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();
}
}
}
Spreading Main.java will not generate interfaces with NetBeans tools there, just some interesting points:
Main.java
publicMain(){
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);
}
}
After initializing the components, you need to assign events to the buttons:
- Events rendered in separate classes.
- For event assignment for 96 buttons, the Reflection API is used, which assigns events in the loop by name (name + i).
SampleEvent.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.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
*/publicclassSampleEventimplementsMouseListener{
publicint fieldNum;
Main frame;
publicSampleEvent(int fieldNum, Main frame){
this.fieldNum = fieldNum;
this.frame = frame;
}
publicvoidmouseClicked(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);
}
}
}
@OverridepublicvoidmousePressed(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseReleased(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseEntered(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseExited(MouseEvent e){
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
PadEvent.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.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
*/publicclassPadEventimplementsMouseListener{
publicint pudNum;
Main frame;
publicPadEvent(int pudNum,Main frame){
this.pudNum = pudNum;
this.frame = frame;
}
@OverridepublicvoidmouseClicked(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 padif(!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);
}
}
@OverridepublicvoidmousePressed(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseReleased(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseEntered(MouseEvent e){
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@OverridepublicvoidmouseExited(MouseEvent e){
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
Of course, like any alpha version of the program, errors occur:
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.javaCl3) 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)The error occurs as I understand it after the multi-assignment and keystrokes due to the busy line.
I think the further development of the program will be towards:
- Change the playback of wav files to midi.
- Additions of notes.
- Volume control on the track.
UPDATE:
1. Replaced algogitm playback.
2. Cleaned up unnecessary pieces of code.
3. Improved synchronization.
4. Added speed control.
5. Added saving playback speed in the project.
6. Stop translates playback to the beginning.
7. You can see where the loop is playing.