I'm working on a project, where I need to show the pitches
of the words (from the song) in a GraphView
. I wrote a program
that would get the pitches
from a song and create a .txt file
from it.
Then I wrote a class, that reads the file
and creates a list
from it. So in the end I would have a list
that consists of words
and word
contains pitches
. In the MakePitchesList
you can see what the pitches
output from a song looks like. I have 0,14:23281,61
on every line. The first part of the line is timeOccurance
meaning, when this pitch was heard and the second part is the pitch itself. So in this example the timeOccurance
would be 0,14
and pitch at that given time 23281,61
.
Here are the three classes that make a wordsList
out of a pitch .txt file
.
public class Pitch{
float occuranceTime;
float pitch;
public void setOccuranceTime(float occuranceTime) {
this.occuranceTime = occuranceTime;
}
public float getOccuranceTime() {
return occuranceTime;
}
public void setPitch(float pitch) {
this.pitch = pitch;
}
public float getPitch() {
return pitch;
}
}
public class MakePitchesList {
String[] pitches;
List<Pitch> listOfPitches = new ArrayList<Pitch>();
public List<Pitch> getListOfPitches(){
getPitches();
for (String pitchString: pitches) {
Pitch pitch = new Pitch();
makeListOfPitches(pitch, pitchString);
}
return listOfPitches;
}
public void makeListOfPitches(Pitch pitch, String pitchString){
pitch.setPitch(getPitchesInfo(pitchString, 1));
pitch.setOccuranceTime(getPitchesInfo(pitchString, 0));
listOfPitches.add(pitch);
}
public String[] getPitches() {
pitches = pitchesRaw.split("\\r?\\n");
return pitches;
}
private float getPitchesInfo(String pitch, int position){
String[] frequencyAndTime = pitch.split("\\:");
if(position == 0){
return Float.parseFloat(frequencyAndTime[0].replace(',', '.'));
}
if(position == 1){
return Float.parseFloat(frequencyAndTime[1].replace(',', '.'));
}
else return 0;
}
String pitchesRaw =
"0,14:23281,61\n" +
"0,23:53,65\n" +
"0,37:72,53\n" +
"0,56:86,09\n" +
"0,60:88,58\n" +
"0,65:87,45\n" +
"0,70:87,11\n" +
"0,74:89,56\n" +
"0,79:96,22\n" +
"0,84:23288,24\n" +
"0,88:103,92\n" +
"0,93:107,46\n" +
"0,98:108,02\n" +
"1,02:107,51\n" +
"1,07:104,92\n" +
"1,11:105,94\n" +
"1,16:106,40\n" +
"1,21:104,43\n" +
"1,25:104,93\n" +
"1,30:108,01\n" +
"1,35:316,81\n" +
"1,39:103,98\n" +
"1,44:23297,42\n" +
"1,49:23357,42\n" +
"1,53:23359,74\n" +
"1,58:23393,04\n" +
"1,63:23244,18\n" +
"1,67:23220,51\n" +
"1,72:23250,06\n" +
"1,76:23288,84\n" +
"1,81:23241,81\n" +
"1,86:23295,22\n" +
"1,90:23268,04\n" +
"1,95:23252,78\n" +
"2,00:23224,22\n" +
"2,04:23429,71\n" +
"2,09:23214,58\n" +
"2,14:23240,70\n" +
"2,18:23237,71\n" +
"2,23:23231,22\n" +
"2,28:23222,77\n" +
"2,32:23239,73\n" +
"2,37:23235,98\n" +
"2,41:23222,16\n" +
"2,46:23224,01\n" +
"2,51:23214,26\n" +
"2,55:23223,20\n" +
"2,60:23234,11\n" +
"2,65:23221,65\n" +
"2,69:23213,45\n" +
"2,74:23217,44\n" +
"2,79:23235,93\n" +
"2,83:11122,79\n" +
"2,88:23234,58\n" +
"2,93:23229,52\n" +
"2,97:23255,48\n" +
"3,02:23254,44\n" +
"3,07:23355,41\n" +
"3,44:105,48\n" +
"3,48:115,45\n" +
"3,53:117,78\n" +
"3,58:127,36\n" +
"3,62:131,24\n" +
"3,67:130,33\n" +
"3,72:131,93\n" +
"3,76:127,32\n" +
"3,81:117,18\n" +
"3,85:117,80\n" +
"3,90:117,15\n" +
"3,95:121,04\n" +
"3,99:131,22\n" +
"4,04:130,38\n" +
"4,09:130,34\n" +
"4,13:129,57\n" +
"4,18:120,38\n" +
"4,23:121,06\n" +
"4,32:100,12\n" +
"4,37:23483,16\n" +
"4,41:112,95\n" +
"4,46:23448,04\n" +
"4,50:23396,09\n" +
"4,55:23292,90\n" +
"4,60:117,21\n" +
"4,64:116,58\n" +
"4,69:116,62\n" +
"4,74:119,18\n" +
"4,78:131,19\n" +
"4,83:130,34\n" +
"4,88:129,59\n" +
"4,92:132,64\n" +
"4,97:129,68\n" +
"5,02:132,71\n" +
"5,06:133,57\n" +
"5,11:128,94\n" +
"5,15:131,09\n" +
"5,20:132,75\n" +
"5,25:129,68\n" +
"5,29:131,26\n" +
"5,34:131,22\n" +
"5,39:130,38\n" +
"5,43:146,01\n" +
"5,48:140,43\n" +
"5,57:23450,16\n" +
"5,62:130,46\n" +
"5,67:132,02\n" +
"5,71:23243,22\n" +
"5,76:23456,28\n" +
"5,85:23246,64\n" +
"5,90:23274,97\n" +
"5,94:23310,30\n" +
"5,99:23229,71\n" +
"6,08:23214,33\n" +
"6,13:23221,53\n" +
"6,18:23263,48\n" +
"6,22:23213,17\n" +
"6,27:23235,04\n" +
"6,32:23222,02\n" +
"6,36:23214,90\n" +
"6,41:23230,05\n" +
"6,46:23212,55\n" +
"6,50:23221,33\n" +
"6,55:23226,70\n" +
"6,59:23217,07\n" +
"6,64:23272,07\n" +
"6,69:11102,74\n" +
"6,73:23263,38\n" +
"6,78:23217,53\n" +
"6,97:23243,63\n" +
"7,11:23214,11\n" +
"7,15:23229,58\n" +
"7,20:23225,70\n" +
"7,24:23244,82\n" +
"7,29:23243,09\n" +
"7,34:23249,66\n" +
"7,38:23226,67\n" +
"7,43:23246,31\n" +
"7,48:23258,55\n" +
"7,52:23230,34\n" +
"7,57:23225,60\n" +
"7,62:23280,25\n" +
"7,66:23238,08\n" +
"7,71:23221,47\n" +
"7,85:117,87\n" +
"7,89:117,19\n" +
"7,94:117,21\n" +
"7,99:117,21\n" +
"8,03:116,57\n" +
"8,08:119,10\n" +
"8,13:44,01\n" +
"8,17:129,52\n" +
"8,22:132,72\n" +
"8,27:143,19\n" +
"8,31:141,13\n" +
"8,36:139,35\n" +
"8,45:132,82\n" +
"8,50:129,76\n" +
"8,54:130,43\n" +
"8,68:94,20\n" +
"8,78:132,70\n" +
"8,82:130,43\n" +
"8,87:129,60\n" +
"8,92:130,56\n" +
"8,96:128,92\n" +
"9,01:119,19\n" +
"9,06:118,45\n" +
"9,10:103,41\n" +
"9,15:103,41\n" +
"9,20:103,89\n" +
"9,24:106,46\n" +
"9,29:214,93\n" +
"9,33:23427,95\n" +
"9,38:23356,01\n" +
"9,43:106,41\n" +
"9,47:100,57\n" +
"9,52:106,39\n" +
"9,57:104,40\n" +
"9,61:99,70\n" +
"9,66:106,42\n" +
"9,71:103,50\n" +
"9,75:104,47\n" +
"9,80:106,97\n" +
"9,85:99,68\n" +
"9,89:23454,22\n" +
"9,94:23299,56\n" +
"9,98:23275,30\n" +
"10,03:23222,72\n" +
"10,08:23246,09\n" +
"10,12:23221,14\n" +
"10,17:23240,54\n" +
"10,22:23246,81\n" +
"10,26:23224,74\n" +
"10,31:23249,41\n" +
"10,36:23214,79\n" +
"10,40:23213,46\n" +
"10,45:23259,51\n" +
"10,50:23217,39\n" +
"10,54:23215,36\n" +
"10,59:23224,87\n" +
"10,63:23242,27\n" +
"10,68:23270,82\n" +
"10,73:23243,19\n" +
"10,77:23222,75\n" +
"10,82:23268,78\n" +
"10,87:23321,62\n" +
"10,91:23259,65\n" +
"11,05:23226,24\n" +
"11,10:23222,92\n" +
"11,15:23218,83\n" +
"11,19:23211,71\n" +
"11,24:11112,28\n" +
"11,28:23261,03\n" +
"11,33:23265,31\n" +
"11,38:23245,92\n" +
"11,42:57,09\n" +
"11,61:103,45\n" +
"11,66:103,91\n" +
"11,70:102,02\n" +
"11,75:107,96\n" +
"11,80:105,43\n" +
"11,84:104,46\n" +
"11,89:116,64\n" +
"11,94:115,99\n" +
"11,98:114,77\n" +
"12,03:121,72\n" +
"12,07:123,16\n" +
"12,12:125,12\n" +
"12,17:128,85\n" +
"12,21:120,37\n" +
"12,26:116,52\n" +
"12,31:130,55\n" +
"12,35:131,06\n" +
"12,40:131,89\n" +
"12,45:128,88\n" +
"12,49:23397,75\n" +
"12,59:118,45\n" +
"12,63:116,54\n" +
"12,68:119,70\n" +
"12,72:115,45\n" +
"12,77:115,30\n" +
"12,82:119,86\n" +
"12,86:116,59\n" +
"12,91:114,13\n" +
"12,96:119,04\n" +
"13,00:118,47\n" +
"13,05:115,38\n" +
"13,10:128,92\n";
}
public class MakeWordsList {
List<Pitch> pitches;
public List<List<Pitch>> getWordsList(List<Pitch> pitchList) {
return makeWordsList(pitchList);
}
List<Pitch> oneWord = new ArrayList<>();
List<List<Pitch>> wordList = new ArrayList<>();
public List<List<Pitch>> makeWordsList(List<Pitch> pitchList){
pitches = pitchList;
int pauseCounter = 0;
for (int i = 0; i < pitchList.size(); i++) {
if(pitchList.get(i).getPitch() > 10000){
pauseCounter++;
} else {
if(pauseCounter > 0){
if(pauseCounter >= 5){
wordList.add(oneWord);
oneWord = new ArrayList<>();
}
pauseCounter = 0;
}
oneWord.add(pitchList.get(i));
}
}
if(oneWord.size() > 0){
wordList.add(oneWord);
}
return wordList;
}
}
Now from that wordsList
I create a scrollable GraphView
that on paper would look something like this.
Now I have set the boundaries for the GraphView
: MinX = -0.8; MaxX = 0.4
. This way I'm showing 1 sec at a time on screen. Then I start a thread so it would change the coordinates
of the GraphView
by 0.1 every 100ms, which should add up as 1 every sec.
Here it is in code:
public class MainActivity extends AppCompatActivity {
final static double GRAPH_STARTING_X = -0.8;
final static double GRAPH_ENDING_X = 0.4;
List<java.util.List<Pitch>> wordsList;
private Viewport graphViewPort;
private Handler handler;
private Runnable runnable;
private GraphView graph;
private DataPoint[] points;
private LineGraphSeries<DataPoint> series;
private int seriesNr;
private double orderNr = GRAPH_STARTING_X;
private ImageButton playRecordButton;
private List<Pitch> pitchesList;
private int songIndex;
private MediaPlayer mediaPlayer;
private boolean isPlaying = false;
@SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Get Textview and Button
playRecordButton = (ImageButton) findViewById(R.id.playRecord);
//Initialize pitches and words
MakePitchesList listOfPithes = new MakePitchesList();
MakeWordsList listOfWords = new MakeWordsList();
pitchesList = listOfPithes.getListOfPitches();
wordsList = listOfWords.getWordsList(pitchesList);
//Initialize graph
graph = (GraphView) findViewById(R.id.graph);
initGraph();
//ViewPort
graphViewPort = graph.getViewport();
//Handler
handler = new Handler();
playRecordButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!isPlaying){
playMedia(songIndex);
//Start moving graph
//Moved to PlayMedia - setOnPreparedListen
// drawAndMoveGraph();
isPlaying = true;
} else if(isPlaying){
//Move graph to starting position
resetGraph();
//Stop Playing audio
stopAudio();
isPlaying = false;
}
}
});
}
//Moves the graph every 100ms by 0.1 which results in 1 every second
private void drawAndMoveGraph() {
runnable = new Runnable() {
public void run() {
graphViewPort.setMinX(orderNr);
graphViewPort.setMaxX(orderNr + 1);
graph.invalidate();
if (pitchesList.size() != orderNr) {
orderNr = orderNr + 0.1;
}
handler.postDelayed(this, 100);
}
};
runnable.run();
}
//Load up and paint the graph
private void initGraph() {
for (int i = 0; i < wordsList.size(); i++) {
seriesNr = 0;
points = new DataPoint[wordsList.get(i).size()];
for (Pitch pitch : wordsList.get(i)) {
points[seriesNr] = new DataPoint(pitch.getOccuranceTime(), pitch.getPitch());
seriesNr++;
}
series = new LineGraphSeries<>(points);
series.setThickness(15);
graph.addSeries(series);
}
//VocalTestPoints - Just for vocal testing
PointsGraphSeries<DataPoint> series = new PointsGraphSeries<>(new DataPoint[]{
new DataPoint(0, 50),
new DataPoint(0, 75),
new DataPoint(0, 100),
new DataPoint(0, 125),
new DataPoint(0, 150),
new DataPoint(0, 175),
new DataPoint(0, 200),
new DataPoint(0, 225),
new DataPoint(0, 250),
new DataPoint(0, 275),
});
series.setSize(5);
series.setColor(Color.YELLOW);
graph.addSeries(series);
// set manual X bounds
graph.getViewport().setYAxisBoundsManual(true);
graph.getViewport().setMinY(-50);
graph.getViewport().setMaxY(400);
graph.getViewport().setXAxisBoundsManual(true);
graph.getViewport().setMinX(GRAPH_STARTING_X);
graph.getViewport().setMaxX(GRAPH_ENDING_X);
graph.getViewport().setScrollable(true);
}
// Play the chosen song and start moving the graph
private void playMedia(int songIndex) {
Uri uri = Uri.parse("android.resource://com.example.thermaltakei7.testsinging/" + R.raw.perfect);
mediaPlayer = new MediaPlayer();
//Reset so that the MediaPlayer is not pointing to another data source
mediaPlayer.reset();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mediaPlayer.setDataSource(getApplicationContext(), uri);
} catch (IOException e) {
e.printStackTrace();
}
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();
drawAndMoveGraph();
}
});
}
//Stop audio from playing
private void stopAudio() {
mediaPlayer.stop();
mediaPlayer.release();
}
//Reset graph to its initial position
private void resetGraph() {
handler.removeCallbacks(runnable);
graphViewPort.setMinX(GRAPH_STARTING_X);
graphViewPort.setMaxX(GRAPH_ENDING_X);
graph.invalidate();
orderNr = GRAPH_STARTING_X;
}
}
To sum up what the code does: it gets the wordsList
from the pitches, initializes the graph, starts listening button click. When button is clicked, it starts moving graph as described above and plays audio.
The problem is that the audio and moving graphView
go out of sync. For the first 10sec or so it works like it should but then the lines
start falling behind. I would really appreciate if someone took their time to understand all of this mess.
Maybe picture of the actual final graphView
will also help. .
Blue line is the first incoming word.
The audio's timing and line should meet at "0".
I have also created a small sample that only contains the classes and topics mentioned above. It can be accessed from here: https://github.com/richardboss/testSinging
This code containst a song in its raw folder so it's a bit bigger than it should be, but this was the fastest way to show a demo of what I was trying to say.
Any ideas, suggestions, help is really welcome. Or comments on the code style.