From 5527c5a5babb049ac98519a6df804b1a36e0c8fe Mon Sep 17 00:00:00 2001 From: Nico Brailovsky Date: Sun, 6 Jun 2021 12:49:03 +0100 Subject: [PATCH 1/6] Initial proof of concept playing a simple melody. --- .../java/com/nicobrailo/pianoli/Piano.java | 18 ++++++++++++++++-- .../com/nicobrailo/pianoli/PianoCanvas.java | 7 +++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nicobrailo/pianoli/Piano.java b/app/src/main/java/com/nicobrailo/pianoli/Piano.java index 6f03647..3b7395e 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Piano.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Piano.java @@ -6,7 +6,9 @@ import android.util.Log; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; class Piano { private static final double KEYS_FLAT_HEIGHT_RATIO = 0.55; @@ -25,8 +27,10 @@ class Piano { private boolean[] key_pressed; private static SoundPool KeySound = null; private int[] KeySoundIdx; + private List melody = null; + private int melody_idx; - Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset) { + Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset, final List melody) { keys_height = screen_size_y; keys_flats_height = (int) (screen_size_y * KEYS_FLAT_HEIGHT_RATIO); @@ -41,6 +45,9 @@ class Piano { key_pressed = new boolean[keys_count]; Arrays.fill(key_pressed, false); selectSoundset(context, soundset); + + this.melody = melody; + this.melody_idx = 0; } int get_keys_flat_width() { @@ -167,12 +174,19 @@ void selectSoundset(final Context context, String soundSetName) { } } - private void play_sound(final int key_idx) { + private void play_sound(int key_idx) { if (key_idx < 0 || key_idx >= KeySoundIdx.length) { Log.d("PianOli::Piano", "This shouldn't happen: Sound out of range, key" + key_idx); return; } + if (this.melody != null) { + key_idx = this.melody.get(this.melody_idx++); + if (this.melody_idx >= this.melody.size()) { + this.melody_idx = 0; + } + } + KeySound.play(KeySoundIdx[key_idx], 1, 1, 1, 0, 1f); } diff --git a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java index 79b9ef8..218399f 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java +++ b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java @@ -19,6 +19,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.ColorUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -81,7 +83,7 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) { screen_size_x = screen_size.x; screen_size_y = screen_size.y; final String soundset = Preferences.selectedSoundSet(context); - this.piano = new Piano(context, screen_size_x, screen_size_y, soundset); + this.piano = new Piano(context, screen_size_x, screen_size_y, soundset, null); this.bevelWidth = this.piano.get_keys_width() * BEVEL_RATIO; this.appConfigHandler = new AppConfigTrigger(ctx); @@ -90,7 +92,8 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) { } public void selectSoundset(final Context context, final String selected_soundset) { - this.piano = new Piano(context, screen_size_x, screen_size_y, selected_soundset); + this.piano = new Piano(context, screen_size_x, screen_size_y, selected_soundset, + Arrays.asList(0, 2, 4, 0, 4, 0, 4)); } public void setConfigRequestCallback(AppConfigTrigger.AppConfigCallback cb) { From 09e11f05155d6c142a1a4d1f6fd75e25fb079a52 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 17 Jun 2021 22:44:37 +1000 Subject: [PATCH 2/6] Add multiple melodies and cycle through. Includes a parser which lets songs be specified using a relatively human readable form of sheet music (C, C#, D2, F#2, etc) - though not sure that it supports # properly yet. A work in progress, things to fix: * Some of the songs don't seem quite right (adapted from here: https://noobnotes.net/song-collections/childrens-nursery-rhymes/). --- .../nicobrailo/pianoli/AllSongsMelody.java | 48 +++++++ .../java/com/nicobrailo/pianoli/Melody.java | 7 + .../java/com/nicobrailo/pianoli/Piano.java | 64 ++++++++- .../com/nicobrailo/pianoli/PianoCanvas.java | 5 +- .../nicobrailo/pianoli/SingleSongMelody.java | 132 ++++++++++++++++++ 5 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java create mode 100644 app/src/main/java/com/nicobrailo/pianoli/Melody.java create mode 100644 app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java diff --git a/app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java b/app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java new file mode 100644 index 0000000..555c46b --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java @@ -0,0 +1,48 @@ +package com.nicobrailo.pianoli; + +/** + * Cycles through a collection of {@link SingleSongMelody}'s. Each time a melody is completed, the + * next is started. When the final melody is finished, it will return to the first note of the + * first melody again. + */ +public class AllSongsMelody implements Melody { + + public static final Melody[] songs = new Melody[] { + SingleSongMelody.twinkle_twinkle_little_star, + SingleSongMelody.insy_winsy_spider, + SingleSongMelody.im_a_little_teapot, + }; + + private int song_idx = 0; + + @Override + public void reset() { + songs[song_idx].reset(); + song_idx = 0; + songs[0].reset(); + } + + /** + * Cycle through all available {@link SingleSongMelody} songs, and when the last note of the + * last melody is hit, go back to the first again. + */ + @Override + public String nextNote() { + if (!songs[song_idx].hasNextNote()) { + songs[song_idx].reset(); + song_idx = (song_idx + 1) % songs.length; + songs[song_idx].reset(); + } + + return songs[song_idx].nextNote(); + + } + + @Override + public boolean hasNextNote() { + // If we are not on the last song, then there is definitely more notes to be played before + // we should be reset(). If we are on the last song, then just ask that song if it has any + // notes left. + return song_idx < songs.length - 1 || songs[song_idx].hasNextNote(); + } +} diff --git a/app/src/main/java/com/nicobrailo/pianoli/Melody.java b/app/src/main/java/com/nicobrailo/pianoli/Melody.java new file mode 100644 index 0000000..e0edcb5 --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/Melody.java @@ -0,0 +1,7 @@ +package com.nicobrailo.pianoli; + +interface Melody { + String nextNote(); + boolean hasNextNote(); + void reset(); +} diff --git a/app/src/main/java/com/nicobrailo/pianoli/Piano.java b/app/src/main/java/com/nicobrailo/pianoli/Piano.java index 3b7395e..5ce78f6 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Piano.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Piano.java @@ -6,9 +6,9 @@ import android.util.Log; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashMap; +import java.util.Map; class Piano { private static final double KEYS_FLAT_HEIGHT_RATIO = 0.55; @@ -27,10 +27,10 @@ class Piano { private boolean[] key_pressed; private static SoundPool KeySound = null; private int[] KeySoundIdx; - private List melody = null; + private Melody melody = null; private int melody_idx; - Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset, final List melody) { + Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset, final Melody melody) { keys_height = screen_size_y; keys_flats_height = (int) (screen_size_y * KEYS_FLAT_HEIGHT_RATIO); @@ -127,6 +127,54 @@ Key get_area_for_flat_key(int key_idx) { return new Key(x_i, x_i + keys_flat_width, 0, keys_flats_height); } + private static Map note_to_key_idx = new HashMap<>(); + + static { + note_to_key_idx.put("C1", 0); + note_to_key_idx.put("C#1", 1); + note_to_key_idx.put("D1", 2); + note_to_key_idx.put("D#1", 3); + note_to_key_idx.put("E1", 4); + + note_to_key_idx.put("F1", 6); + note_to_key_idx.put("F#1", 7); + note_to_key_idx.put("G1", 8); + note_to_key_idx.put("G#1", 9); + note_to_key_idx.put("A1", 10); + note_to_key_idx.put("A#1", 11); + note_to_key_idx.put("B1", 12); + + note_to_key_idx.put("C2", 14); + note_to_key_idx.put("C#2", 15); + note_to_key_idx.put("D2", 16); + note_to_key_idx.put("D#2", 17); + note_to_key_idx.put("E2", 18); + + note_to_key_idx.put("F2", 20); + note_to_key_idx.put("F#2", 21); + note_to_key_idx.put("G2", 22); + note_to_key_idx.put("G#2", 23); + note_to_key_idx.put("A2", 24); + note_to_key_idx.put("A#2", 25); + note_to_key_idx.put("B2", 26); + } + + int get_key_idx_from_note(String note) { + + Integer key_idx = note_to_key_idx.get(note); + if (key_idx == null) { + Log.w("PianOli::Piano", "Could not find a key corresponding to the note \"" + note + "\"."); + + // 5 is designated as the special sound T.raw.no_note, so the app wont crash, but it wont + // play a noise either. + return 5; + } + + return key_idx; + } + + + void selectSoundset(final Context context, String soundSetName) { if (KeySound != null) { KeySound.release(); @@ -181,10 +229,12 @@ private void play_sound(int key_idx) { } if (this.melody != null) { - key_idx = this.melody.get(this.melody_idx++); - if (this.melody_idx >= this.melody.size()) { - this.melody_idx = 0; + if (!this.melody.hasNextNote()) { + this.melody.reset(); } + + String note = this.melody.nextNote(); + key_idx = get_key_idx_from_note(note); } KeySound.play(KeySoundIdx[key_idx], 1, 1, 1, 0, 1f); diff --git a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java index 218399f..dd3c17f 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java +++ b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java @@ -19,8 +19,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.ColorUtils; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -93,7 +91,8 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) { public void selectSoundset(final Context context, final String selected_soundset) { this.piano = new Piano(context, screen_size_x, screen_size_y, selected_soundset, - Arrays.asList(0, 2, 4, 0, 4, 0, 4)); + new AllSongsMelody() + ); } public void setConfigRequestCallback(AppConfigTrigger.AppConfigCallback cb) { diff --git a/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java b/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java new file mode 100644 index 0000000..6cfd6a3 --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java @@ -0,0 +1,132 @@ +package com.nicobrailo.pianoli; + +import java.util.Locale; +import java.util.NoSuchElementException; + +public class SingleSongMelody implements Melody { + + public static final SingleSongMelody im_a_little_teapot = SingleSongMelody.fromString( + "C D E F G C2 " + + // I’m a little teapot + + "A C2 G " + + // Short and stout + + "F F F E E " + + // Here is my handle + + "D D D C " + + // Here is my spout + + "C D E F G C2 " + + // When I get all steamed up + + "A C2 G " + + // Hear me shout + + "C2 A G G " + + // “Tip me over + + "F E D C " + // And pour me out!” + ); + + public static final SingleSongMelody twinkle_twinkle_little_star = SingleSongMelody.fromString( + "C C G G A A G " + + // Twinkle, twinkle, little star + + "F F E E D D C " + + // How I wonder what you are! + + "G G F F E E D " + + // Up above the world so high, + + "G G F F E E D " + + // Like a diamond in the sky... + + "C C G G A A G " + + // Twinkle, twinkle, little star + + "F F E E D D C" + // How I wonder what you are! + ); + + public static final SingleSongMelody insy_winsy_spider = SingleSongMelody.fromString( + "G1 C2 C2 C2 D2 E2 E2 " + + // "Insy-winsy spider... + + "E2 D2 C2 D2 E2 C2 " + + // "... climbed up the water spout. + + "E2 E2 F2 G2 " + + // Down came the rain... + + "G2 F2 E2 F2 G2 E2 " + + // ... and washed the spider out. + + "C2 C2 D2 E2 " + + // Out came the sun... + + "E2 D2 C2 D2 E2 C2 " + + // ... and dried up all the rain. + + "G1 G1 C2 C2 C2 D2 E2 E2 " + + // Insy-winsy spider... + + "E2 D2 C2 D2 E2 C2" + // ... climbed up the spout again. + ); + + public static final SingleSongMelody[] all = new SingleSongMelody[] { + twinkle_twinkle_little_star, + insy_winsy_spider + }; + + /** + * A somewhat-robust string to melody parser. + * Allows melodies to be specified as a string of notes, where the notes are: "A", "B1", "C#1", "G2", etc. + * + * Notes are separated by whitespace. + * + * Notes in the first octave can leave off the octave designation and it will be automatically + * appended (i.e. "C" will become "C1"). This makes it simpler to write songs that fall within a single octave. + */ + static SingleSongMelody fromString(String melody) { + String[] notes = melody.trim().toUpperCase(Locale.ENGLISH).split("\\s+"); + + for (int i = 0; i < notes.length; i ++) { + if (!notes[i].matches(".*\\d$")) { + notes[i] = notes[i] + "1"; + } + } + return new SingleSongMelody(notes); + } + + private int melody_idx = 0; + private final String[] notes; + + SingleSongMelody(String[] notes) { + this.notes = notes; + } + + @Override + public String nextNote() { + if (!hasNextNote()) { + throw new NoSuchElementException(); + } + String note = notes[melody_idx]; + melody_idx ++; + return note; + } + + @Override + public void reset() { + melody_idx = 0; + } + + @Override + public boolean hasNextNote() { + return melody_idx < notes.length; + } + +} From 0533ba015f32137249afa09aecd58f29abef2696 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 11 Apr 2023 22:23:32 +1000 Subject: [PATCH 3/6] Allow melodies to be enabled/chosen through the settings screen. --- app/build.gradle | 8 ++++ .../java/com/nicobrailo/pianoli/Melody.java | 1 + ...gsMelody.java => MultipleSongsMelody.java} | 37 ++++++++++++------- .../java/com/nicobrailo/pianoli/Piano.java | 9 +++-- .../com/nicobrailo/pianoli/PianoCanvas.java | 6 +-- .../com/nicobrailo/pianoli/Preferences.java | 34 +++++++++++++++++ .../nicobrailo/pianoli/SettingsFragment.java | 37 ++++++++++++++++++- .../nicobrailo/pianoli/SingleSongMelody.java | 21 ++++++++--- app/src/main/res/values/donottranslate.xml | 14 +++++++ app/src/main/res/values/strings.xml | 7 ++++ app/src/main/res/xml/root_preferences.xml | 13 +++++++ 11 files changed, 159 insertions(+), 28 deletions(-) rename app/src/main/java/com/nicobrailo/pianoli/{AllSongsMelody.java => MultipleSongsMelody.java} (55%) create mode 100644 app/src/main/res/values/donottranslate.xml diff --git a/app/build.gradle b/app/build.gradle index 545c89e..bdcef73 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,4 +43,12 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.android.support:design:28.0.0' implementation 'androidx.preference:preference:1.2.0' + implementation "androidx.annotation:annotation:1.6.0" +} + +// Not sure why this started happening all of a sudden, but the buidl was failing +// doe to an error similar to this StackOverflow post: +// https://stackoverflow.com/questions/75274720/a-failure-occurred-while-executing-appcheckdebugduplicateclasses/75315276#75315276 +configurations.implementation { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' } diff --git a/app/src/main/java/com/nicobrailo/pianoli/Melody.java b/app/src/main/java/com/nicobrailo/pianoli/Melody.java index e0edcb5..177fb99 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Melody.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Melody.java @@ -4,4 +4,5 @@ interface Melody { String nextNote(); boolean hasNextNote(); void reset(); + String id(); } diff --git a/app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java b/app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java similarity index 55% rename from app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java rename to app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java index 555c46b..2bb2f36 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/AllSongsMelody.java +++ b/app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java @@ -1,25 +1,34 @@ package com.nicobrailo.pianoli; +import androidx.annotation.NonNull; + +import java.util.List; + /** * Cycles through a collection of {@link SingleSongMelody}'s. Each time a melody is completed, the * next is started. When the final melody is finished, it will return to the first note of the * first melody again. */ -public class AllSongsMelody implements Melody { +public class MultipleSongsMelody implements Melody { - public static final Melody[] songs = new Melody[] { - SingleSongMelody.twinkle_twinkle_little_star, - SingleSongMelody.insy_winsy_spider, - SingleSongMelody.im_a_little_teapot, - }; + private final List songs; + + public MultipleSongsMelody(@NonNull List songs) { + this.songs = songs; + } private int song_idx = 0; @Override public void reset() { - songs[song_idx].reset(); + songs.get(song_idx).reset(); song_idx = 0; - songs[0].reset(); + songs.get(0).reset(); + } + + @Override + public String id() { + return "all_songs"; } /** @@ -28,13 +37,13 @@ public void reset() { */ @Override public String nextNote() { - if (!songs[song_idx].hasNextNote()) { - songs[song_idx].reset(); - song_idx = (song_idx + 1) % songs.length; - songs[song_idx].reset(); + if (!songs.get(song_idx).hasNextNote()) { + songs.get(song_idx).reset(); + song_idx = (song_idx + 1) % songs.size(); + songs.get(song_idx).reset(); } - return songs[song_idx].nextNote(); + return songs.get(song_idx).nextNote(); } @@ -43,6 +52,6 @@ public boolean hasNextNote() { // If we are not on the last song, then there is definitely more notes to be played before // we should be reset(). If we are on the last song, then just ask that song if it has any // notes left. - return song_idx < songs.length - 1 || songs[song_idx].hasNextNote(); + return song_idx < songs.size() - 1 || songs.get(song_idx).hasNextNote(); } } diff --git a/app/src/main/java/com/nicobrailo/pianoli/Piano.java b/app/src/main/java/com/nicobrailo/pianoli/Piano.java index 5ce78f6..2b477d4 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Piano.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Piano.java @@ -28,9 +28,8 @@ class Piano { private static SoundPool KeySound = null; private int[] KeySoundIdx; private Melody melody = null; - private int melody_idx; - Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset, final Melody melody) { + Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset) { keys_height = screen_size_y; keys_flats_height = (int) (screen_size_y * KEYS_FLAT_HEIGHT_RATIO); @@ -46,8 +45,10 @@ class Piano { Arrays.fill(key_pressed, false); selectSoundset(context, soundset); - this.melody = melody; - this.melody_idx = 0; + if (Preferences.areMelodiesEnabled(context)) { + this.melody = new MultipleSongsMelody(Preferences.selectedMelodies(context)); + this.melody.reset(); + } } int get_keys_flat_width() { diff --git a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java index dd3c17f..79b9ef8 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java +++ b/app/src/main/java/com/nicobrailo/pianoli/PianoCanvas.java @@ -81,7 +81,7 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) { screen_size_x = screen_size.x; screen_size_y = screen_size.y; final String soundset = Preferences.selectedSoundSet(context); - this.piano = new Piano(context, screen_size_x, screen_size_y, soundset, null); + this.piano = new Piano(context, screen_size_x, screen_size_y, soundset); this.bevelWidth = this.piano.get_keys_width() * BEVEL_RATIO; this.appConfigHandler = new AppConfigTrigger(ctx); @@ -90,9 +90,7 @@ public PianoCanvas(Context context, AttributeSet as, int defStyle) { } public void selectSoundset(final Context context, final String selected_soundset) { - this.piano = new Piano(context, screen_size_x, screen_size_y, selected_soundset, - new AllSongsMelody() - ); + this.piano = new Piano(context, screen_size_x, screen_size_y, selected_soundset); } public void setConfigRequestCallback(AppConfigTrigger.AppConfigCallback cb) { diff --git a/app/src/main/java/com/nicobrailo/pianoli/Preferences.java b/app/src/main/java/com/nicobrailo/pianoli/Preferences.java index f17df02..4745bbc 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Preferences.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Preferences.java @@ -4,11 +4,45 @@ import android.preference.PreferenceManager; import android.util.Log; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + public class Preferences { private static final String TAG = "Preferences"; private static final String DEFAULT_SOUNDSET = "piano"; private final static String PREF_SELECTED_SOUND_SET = "selectedSoundSet"; + private final static String PREF_SELECTED_MELODIES = "selectedMelodies"; + private final static String PREF_ENABLE_MELODIES = "enableMelodies"; + + /** + * If none are selected, then we play all melodies. + * This is counter intuitive from a user perspective ("Why is it playing all the + * melodies when I deselected them all!"), however it is probably more counter intuitive + * than the alternative which is "Why did it not play any melodies when I selected + * 'enable melodies'?"). + */ + public static List selectedMelodies(Context context) { + final String[] defaultMelodies = context.getResources().getStringArray(R.array.default_selected_melodies); + Set defaultMelodiesSet = new HashSet<>(); + Collections.addAll(defaultMelodiesSet, defaultMelodies); + final Set selectedMelodies = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(PREF_SELECTED_MELODIES, defaultMelodiesSet); + + final ArrayList melodies = new ArrayList<>(selectedMelodies.size()); + for (Melody melody : SingleSongMelody.all) { + if (selectedMelodies.isEmpty() || selectedMelodies.contains(melody.id())) { + melodies.add(melody); + } + } + return melodies; + } + + public static boolean areMelodiesEnabled(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_ENABLE_MELODIES, false); + } /** * The sound set is the name of the folder in assets/sounds/soundset_[NAME] diff --git a/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java b/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java index b327ad2..3191625 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java +++ b/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.preference.ListPreference; +import androidx.preference.MultiSelectListPreference; import androidx.preference.PreferenceFragmentCompat; import java.io.IOException; @@ -28,12 +29,12 @@ public SettingsFragment() { public void onCreatePreferences(Bundle savedzInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); loadSounds(); + loadMelodies(); } public void onAttach (@NonNull Context context) { super.onAttach(context); this.availableSoundsets = getAvailableSoundsets(context); - loadSounds(); } private ArrayList getAvailableSoundsets(Context context) { @@ -68,6 +69,39 @@ private ArrayList getAvailableSoundsets(Context context) { return filtList; } + void loadMelodies() { + MultiSelectListPreference melodies = findPreference("selectedMelodies"); + if (melodies != null) { + + boolean enabled = getPreferenceManager().getSharedPreferences().getBoolean("enableMelodies", false); + melodies.setEnabled(enabled); + + findPreference("enableMelodies").setOnPreferenceChangeListener((preference, newValue) -> { + melodies.setEnabled((Boolean)newValue); + return true; + }); + + String[] melodyEntries = new String[SingleSongMelody.all.length]; + String[] melodyEntryValues = new String[SingleSongMelody.all.length]; + + // Ideally we'd also call setDefaultValue() here too and pass a Set + // containing each melody. However, the system invokes the "persist default valeus" + // before we get here, and thus it never gets respected. Instead that is hardcoded + // in a string-array and referenced directly in root_preferences.xml. + + for (int i = 0; i < SingleSongMelody.all.length; i ++) { + Melody melody = SingleSongMelody.all[i]; + melodyEntryValues[i] = melody.id(); + + int stringId = getResources().getIdentifier("melody_" + melody.id(), "string", requireContext().getPackageName()); + melodyEntries[i] = stringId > 0 ? getString(stringId) : melody.id(); + } + + melodies.setEntries(melodyEntries); + melodies.setEntryValues(melodyEntryValues); + } + } + void loadSounds() { ListPreference soundsets = findPreference("selectedSoundSet"); if (soundsets != null) { @@ -85,4 +119,5 @@ void loadSounds() { soundsets.setEntryValues(soundsetEntryValues); } } + } diff --git a/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java b/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java index 6cfd6a3..82b39af 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java +++ b/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java @@ -6,6 +6,7 @@ public class SingleSongMelody implements Melody { public static final SingleSongMelody im_a_little_teapot = SingleSongMelody.fromString( + "im_a_little_teapot", "C D E F G C2 " + // I’m a little teapot @@ -32,6 +33,7 @@ public class SingleSongMelody implements Melody { ); public static final SingleSongMelody twinkle_twinkle_little_star = SingleSongMelody.fromString( + "twinkle_twinkle_little_star", "C C G G A A G " + // Twinkle, twinkle, little star @@ -52,6 +54,7 @@ public class SingleSongMelody implements Melody { ); public static final SingleSongMelody insy_winsy_spider = SingleSongMelody.fromString( + "insy_winsy_spider", "G1 C2 C2 C2 D2 E2 E2 " + // "Insy-winsy spider... @@ -77,9 +80,10 @@ public class SingleSongMelody implements Melody { // ... climbed up the spout again. ); - public static final SingleSongMelody[] all = new SingleSongMelody[] { + public static final Melody[] all = new Melody[] { twinkle_twinkle_little_star, - insy_winsy_spider + insy_winsy_spider, + im_a_little_teapot, }; /** @@ -91,7 +95,7 @@ public class SingleSongMelody implements Melody { * Notes in the first octave can leave off the octave designation and it will be automatically * appended (i.e. "C" will become "C1"). This makes it simpler to write songs that fall within a single octave. */ - static SingleSongMelody fromString(String melody) { + static SingleSongMelody fromString(String id, String melody) { String[] notes = melody.trim().toUpperCase(Locale.ENGLISH).split("\\s+"); for (int i = 0; i < notes.length; i ++) { @@ -99,13 +103,15 @@ static SingleSongMelody fromString(String melody) { notes[i] = notes[i] + "1"; } } - return new SingleSongMelody(notes); + return new SingleSongMelody(id, notes); } private int melody_idx = 0; + private final String id; private final String[] notes; - SingleSongMelody(String[] notes) { + SingleSongMelody(String id, String[] notes) { + this.id = id; this.notes = notes; } @@ -124,6 +130,11 @@ public void reset() { melody_idx = 0; } + @Override + public String id() { + return id; + } + @Override public boolean hasNextNote() { return melody_idx < notes.length; diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml new file mode 100644 index 0000000..55ad073 --- /dev/null +++ b/app/src/main/res/values/donottranslate.xml @@ -0,0 +1,14 @@ + + + + + twinkle_twinkle_little_star + im_a_little_teapot + insy_winsy_spider + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9a2f84..99080d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,4 +12,11 @@ Piano Sine Wave Vibraphone + Twinkle, Twinkle, Little Star + I\'m a Little Teapot + Insy Winsy Spider + Touching a key plays the next note in a melody + Auto play melodies + Available melodies + List of melodies to cycle through diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index d8b6bb8..184285d 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -9,6 +9,19 @@ app:title="@string/sound_set" app:useSimpleSummaryProvider="true" app:iconSpaceReserved="false" /> + + From 58dff1b22ad5a06aac8bfdac362c3d92ac7a5ad9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 12 Apr 2023 21:39:36 +1000 Subject: [PATCH 4/6] Seperate the melody from the playing of the melody. I wasn't happy with the previous implementation where the static information such as "These notes constitute these melodies" was tied up with the state related to which note the melody was currently up to. This pulls the two things apart so the static notes always stay static, and the "player" related logic is in a different class hierarchy. --- .../java/com/nicobrailo/pianoli/Melody.java | 8 - .../java/com/nicobrailo/pianoli/Piano.java | 12 +- .../com/nicobrailo/pianoli/Preferences.java | 7 +- .../nicobrailo/pianoli/SettingsFragment.java | 20 ++- .../nicobrailo/pianoli/SingleSongMelody.java | 143 ------------------ .../nicobrailo/pianoli/melodies/Melody.java | 122 +++++++++++++++ .../pianoli/melodies/MelodyPlayer.java | 7 + .../MultipleSongsMelodyPlayer.java} | 23 ++- .../melodies/SingleSongMelodyPlayer.java | 35 +++++ 9 files changed, 201 insertions(+), 176 deletions(-) delete mode 100644 app/src/main/java/com/nicobrailo/pianoli/Melody.java delete mode 100644 app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java create mode 100644 app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java create mode 100644 app/src/main/java/com/nicobrailo/pianoli/melodies/MelodyPlayer.java rename app/src/main/java/com/nicobrailo/pianoli/{MultipleSongsMelody.java => melodies/MultipleSongsMelodyPlayer.java} (63%) create mode 100644 app/src/main/java/com/nicobrailo/pianoli/melodies/SingleSongMelodyPlayer.java diff --git a/app/src/main/java/com/nicobrailo/pianoli/Melody.java b/app/src/main/java/com/nicobrailo/pianoli/Melody.java deleted file mode 100644 index 177fb99..0000000 --- a/app/src/main/java/com/nicobrailo/pianoli/Melody.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.nicobrailo.pianoli; - -interface Melody { - String nextNote(); - boolean hasNextNote(); - void reset(); - String id(); -} diff --git a/app/src/main/java/com/nicobrailo/pianoli/Piano.java b/app/src/main/java/com/nicobrailo/pianoli/Piano.java index 2b477d4..8c7a1fc 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Piano.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Piano.java @@ -5,6 +5,10 @@ import android.media.SoundPool; import android.util.Log; +import com.nicobrailo.pianoli.melodies.Melody; +import com.nicobrailo.pianoli.melodies.MelodyPlayer; +import com.nicobrailo.pianoli.melodies.MultipleSongsMelodyPlayer; + import java.io.IOException; import java.util.Arrays; import java.util.HashMap; @@ -24,10 +28,10 @@ class Piano { private final int keys_height; private final int keys_flats_height; private final int keys_count; - private boolean[] key_pressed; + private final boolean[] key_pressed; private static SoundPool KeySound = null; private int[] KeySoundIdx; - private Melody melody = null; + private MelodyPlayer melody = null; Piano(final Context context, int screen_size_x, int screen_size_y, final String soundset) { keys_height = screen_size_y; @@ -46,7 +50,7 @@ class Piano { selectSoundset(context, soundset); if (Preferences.areMelodiesEnabled(context)) { - this.melody = new MultipleSongsMelody(Preferences.selectedMelodies(context)); + this.melody = new MultipleSongsMelodyPlayer(Preferences.selectedMelodies(context)); this.melody.reset(); } } @@ -128,7 +132,7 @@ Key get_area_for_flat_key(int key_idx) { return new Key(x_i, x_i + keys_flat_width, 0, keys_flats_height); } - private static Map note_to_key_idx = new HashMap<>(); + private static final Map note_to_key_idx = new HashMap<>(); static { note_to_key_idx.put("C1", 0); diff --git a/app/src/main/java/com/nicobrailo/pianoli/Preferences.java b/app/src/main/java/com/nicobrailo/pianoli/Preferences.java index 4745bbc..8e83360 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Preferences.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Preferences.java @@ -4,6 +4,9 @@ import android.preference.PreferenceManager; import android.util.Log; +import com.nicobrailo.pianoli.melodies.Melody; +import com.nicobrailo.pianoli.melodies.SingleSongMelodyPlayer; + import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -32,8 +35,8 @@ public static List selectedMelodies(Context context) { final Set selectedMelodies = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(PREF_SELECTED_MELODIES, defaultMelodiesSet); final ArrayList melodies = new ArrayList<>(selectedMelodies.size()); - for (Melody melody : SingleSongMelody.all) { - if (selectedMelodies.isEmpty() || selectedMelodies.contains(melody.id())) { + for (Melody melody : Melody.all) { + if (selectedMelodies.isEmpty() || selectedMelodies.contains(melody.getId())) { melodies.add(melody); } } diff --git a/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java b/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java index 3191625..8437388 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java +++ b/app/src/main/java/com/nicobrailo/pianoli/SettingsFragment.java @@ -1,6 +1,7 @@ package com.nicobrailo.pianoli; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.AssetManager; import android.util.Log; @@ -12,6 +13,8 @@ import androidx.preference.MultiSelectListPreference; import androidx.preference.PreferenceFragmentCompat; +import com.nicobrailo.pianoli.melodies.Melody; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -81,20 +84,21 @@ void loadMelodies() { return true; }); - String[] melodyEntries = new String[SingleSongMelody.all.length]; - String[] melodyEntryValues = new String[SingleSongMelody.all.length]; + String[] melodyEntries = new String[Melody.all.length]; + String[] melodyEntryValues = new String[Melody.all.length]; // Ideally we'd also call setDefaultValue() here too and pass a Set // containing each melody. However, the system invokes the "persist default valeus" // before we get here, and thus it never gets respected. Instead that is hardcoded // in a string-array and referenced directly in root_preferences.xml. - for (int i = 0; i < SingleSongMelody.all.length; i ++) { - Melody melody = SingleSongMelody.all[i]; - melodyEntryValues[i] = melody.id(); + for (int i = 0; i < Melody.all.length; i ++) { + Melody melody = Melody.all[i]; + melodyEntryValues[i] = melody.getId(); - int stringId = getResources().getIdentifier("melody_" + melody.id(), "string", requireContext().getPackageName()); - melodyEntries[i] = stringId > 0 ? getString(stringId) : melody.id(); + @SuppressLint("DiscouragedApi") + int stringId = getResources().getIdentifier("melody_" + melody.getId(), "string", requireContext().getPackageName()); + melodyEntries[i] = stringId > 0 ? getString(stringId) : melody.getId(); } melodies.setEntries(melodyEntries); @@ -111,6 +115,8 @@ void loadSounds() { soundsetEntryValues[i] = availableSoundsets.get(i); String name = SettingsActivity.SOUNDSET_DIR_PREFIX + availableSoundsets.get(i); + + @SuppressLint("DiscouragedApi") int stringId = getResources().getIdentifier(name, "string", requireContext().getPackageName()); soundsetEntries[i] = stringId > 0 ? getString(stringId) : availableSoundsets.get(i); } diff --git a/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java b/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java deleted file mode 100644 index 82b39af..0000000 --- a/app/src/main/java/com/nicobrailo/pianoli/SingleSongMelody.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.nicobrailo.pianoli; - -import java.util.Locale; -import java.util.NoSuchElementException; - -public class SingleSongMelody implements Melody { - - public static final SingleSongMelody im_a_little_teapot = SingleSongMelody.fromString( - "im_a_little_teapot", - "C D E F G C2 " + - // I’m a little teapot - - "A C2 G " + - // Short and stout - - "F F F E E " + - // Here is my handle - - "D D D C " + - // Here is my spout - - "C D E F G C2 " + - // When I get all steamed up - - "A C2 G " + - // Hear me shout - - "C2 A G G " + - // “Tip me over - - "F E D C " - // And pour me out!” - ); - - public static final SingleSongMelody twinkle_twinkle_little_star = SingleSongMelody.fromString( - "twinkle_twinkle_little_star", - "C C G G A A G " + - // Twinkle, twinkle, little star - - "F F E E D D C " + - // How I wonder what you are! - - "G G F F E E D " + - // Up above the world so high, - - "G G F F E E D " + - // Like a diamond in the sky... - - "C C G G A A G " + - // Twinkle, twinkle, little star - - "F F E E D D C" - // How I wonder what you are! - ); - - public static final SingleSongMelody insy_winsy_spider = SingleSongMelody.fromString( - "insy_winsy_spider", - "G1 C2 C2 C2 D2 E2 E2 " + - // "Insy-winsy spider... - - "E2 D2 C2 D2 E2 C2 " + - // "... climbed up the water spout. - - "E2 E2 F2 G2 " + - // Down came the rain... - - "G2 F2 E2 F2 G2 E2 " + - // ... and washed the spider out. - - "C2 C2 D2 E2 " + - // Out came the sun... - - "E2 D2 C2 D2 E2 C2 " + - // ... and dried up all the rain. - - "G1 G1 C2 C2 C2 D2 E2 E2 " + - // Insy-winsy spider... - - "E2 D2 C2 D2 E2 C2" - // ... climbed up the spout again. - ); - - public static final Melody[] all = new Melody[] { - twinkle_twinkle_little_star, - insy_winsy_spider, - im_a_little_teapot, - }; - - /** - * A somewhat-robust string to melody parser. - * Allows melodies to be specified as a string of notes, where the notes are: "A", "B1", "C#1", "G2", etc. - * - * Notes are separated by whitespace. - * - * Notes in the first octave can leave off the octave designation and it will be automatically - * appended (i.e. "C" will become "C1"). This makes it simpler to write songs that fall within a single octave. - */ - static SingleSongMelody fromString(String id, String melody) { - String[] notes = melody.trim().toUpperCase(Locale.ENGLISH).split("\\s+"); - - for (int i = 0; i < notes.length; i ++) { - if (!notes[i].matches(".*\\d$")) { - notes[i] = notes[i] + "1"; - } - } - return new SingleSongMelody(id, notes); - } - - private int melody_idx = 0; - private final String id; - private final String[] notes; - - SingleSongMelody(String id, String[] notes) { - this.id = id; - this.notes = notes; - } - - @Override - public String nextNote() { - if (!hasNextNote()) { - throw new NoSuchElementException(); - } - String note = notes[melody_idx]; - melody_idx ++; - return note; - } - - @Override - public void reset() { - melody_idx = 0; - } - - @Override - public String id() { - return id; - } - - @Override - public boolean hasNextNote() { - return melody_idx < notes.length; - } - -} diff --git a/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java b/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java new file mode 100644 index 0000000..9d05403 --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java @@ -0,0 +1,122 @@ +package com.nicobrailo.pianoli.melodies; + +import java.util.Locale; + +public class Melody { + + public static final Melody im_a_little_teapot = fromString( + "im_a_little_teapot", + "C D E F G C2 " + + // I’m a little teapot + + "A C2 G " + + // Short and stout + + "F F F E E " + + // Here is my handle + + "D D D C " + + // Here is my spout + + "C D E F G C2 " + + // When I get all steamed up + + "A C2 G " + + // Hear me shout + + "C2 A G G " + + // “Tip me over + + "F E D C " + // And pour me out!” + ); + + public static final Melody twinkle_twinkle_little_star = fromString( + "twinkle_twinkle_little_star", + "C C G G A A G " + + // Twinkle, twinkle, little star + + "F F E E D D C " + + // How I wonder what you are! + + "G G F F E E D " + + // Up above the world so high, + + "G G F F E E D " + + // Like a diamond in the sky... + + "C C G G A A G " + + // Twinkle, twinkle, little star + + "F F E E D D C" + // How I wonder what you are! + ); + + public static final Melody insy_winsy_spider = fromString( + "insy_winsy_spider", + "G1 C2 C2 C2 D2 E2 E2 " + + // "Insy-winsy spider... + + "E2 D2 C2 D2 E2 C2 " + + // "... climbed up the water spout. + + "E2 E2 F2 G2 " + + // Down came the rain... + + "G2 F2 E2 F2 G2 E2 " + + // ... and washed the spider out. + + "C2 C2 D2 E2 " + + // Out came the sun... + + "E2 D2 C2 D2 E2 C2 " + + // ... and dried up all the rain. + + "G1 G1 C2 C2 C2 D2 E2 E2 " + + // Insy-winsy spider... + + "E2 D2 C2 D2 E2 C2" + // ... climbed up the spout again. + ); + + public static final Melody[] all = new Melody[] { + twinkle_twinkle_little_star, + insy_winsy_spider, + im_a_little_teapot, + }; + /** + * A somewhat-robust string to melody parser. + * Allows melodies to be specified as a string of notes, where the notes are: "A", "B1", "C#1", "G2", etc. + * + * Notes are separated by whitespace. + * + * Notes in the first octave can leave off the octave designation and it will be automatically + * appended (i.e. "C" will become "C1"). This makes it simpler to write songs that fall within a single octave. + */ + static Melody fromString(String id, String plainTextNotes) { + String[] notes = plainTextNotes.trim().toUpperCase(Locale.ENGLISH).split("\\s+"); + + for (int i = 0; i < notes.length; i ++) { + if (!notes[i].matches(".*\\d$")) { + notes[i] = notes[i] + "1"; + } + } + return new Melody(id, notes); + } + + private final String id; + private final String[] notes; + + public Melody(String id, String[] notes) { + this.id = id; + this.notes = notes; + } + + public String getId() { + return id; + } + + public String[] getNotes() { + return notes; + } +} diff --git a/app/src/main/java/com/nicobrailo/pianoli/melodies/MelodyPlayer.java b/app/src/main/java/com/nicobrailo/pianoli/melodies/MelodyPlayer.java new file mode 100644 index 0000000..c3ed6d8 --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/melodies/MelodyPlayer.java @@ -0,0 +1,7 @@ +package com.nicobrailo.pianoli.melodies; + +public interface MelodyPlayer { + String nextNote(); + boolean hasNextNote(); + void reset(); +} diff --git a/app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java b/app/src/main/java/com/nicobrailo/pianoli/melodies/MultipleSongsMelodyPlayer.java similarity index 63% rename from app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java rename to app/src/main/java/com/nicobrailo/pianoli/melodies/MultipleSongsMelodyPlayer.java index 2bb2f36..cb73897 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/MultipleSongsMelody.java +++ b/app/src/main/java/com/nicobrailo/pianoli/melodies/MultipleSongsMelodyPlayer.java @@ -1,20 +1,24 @@ -package com.nicobrailo.pianoli; +package com.nicobrailo.pianoli.melodies; import androidx.annotation.NonNull; +import java.util.ArrayList; import java.util.List; /** - * Cycles through a collection of {@link SingleSongMelody}'s. Each time a melody is completed, the + * Cycles through a collection of {@link SingleSongMelodyPlayer}'s. Each time a melody is completed, the * next is started. When the final melody is finished, it will return to the first note of the * first melody again. */ -public class MultipleSongsMelody implements Melody { +public class MultipleSongsMelodyPlayer implements MelodyPlayer { - private final List songs; + private final List songs; - public MultipleSongsMelody(@NonNull List songs) { - this.songs = songs; + public MultipleSongsMelodyPlayer(@NonNull List songs) { + this.songs = new ArrayList<>(songs.size()); + for (Melody melody : songs) { + this.songs.add(new SingleSongMelodyPlayer(melody)); + } } private int song_idx = 0; @@ -26,13 +30,8 @@ public void reset() { songs.get(0).reset(); } - @Override - public String id() { - return "all_songs"; - } - /** - * Cycle through all available {@link SingleSongMelody} songs, and when the last note of the + * Cycle through all available {@link SingleSongMelodyPlayer} songs, and when the last note of the * last melody is hit, go back to the first again. */ @Override diff --git a/app/src/main/java/com/nicobrailo/pianoli/melodies/SingleSongMelodyPlayer.java b/app/src/main/java/com/nicobrailo/pianoli/melodies/SingleSongMelodyPlayer.java new file mode 100644 index 0000000..9d744fb --- /dev/null +++ b/app/src/main/java/com/nicobrailo/pianoli/melodies/SingleSongMelodyPlayer.java @@ -0,0 +1,35 @@ +package com.nicobrailo.pianoli.melodies; + +import java.util.Locale; +import java.util.NoSuchElementException; + +public class SingleSongMelodyPlayer implements MelodyPlayer { + + private int melody_idx = 0; + private Melody melody; + + SingleSongMelodyPlayer(Melody melody) { + this.melody = melody; + } + + @Override + public String nextNote() { + if (!hasNextNote()) { + throw new NoSuchElementException(); + } + String note = melody.getNotes()[melody_idx]; + melody_idx ++; + return note; + } + + @Override + public void reset() { + melody_idx = 0; + } + + @Override + public boolean hasNextNote() { + return melody_idx < melody.getNotes().length; + } + +} From da4a14c48a38148241a697cb2bc2e49130e1ba3f Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 12 Apr 2023 12:59:24 +1000 Subject: [PATCH 5/6] Bump build tools / dependencies / etc. * Migrate to AndroidX as per gradle lint recommendations (used Android Studio -> Refactor -> Migrate to AndroidX function) * Bump Android Gradle Plugin as per Android Studio recommendation. * Bump all dependencies to the latest supported versions. * Replace jcenter with mavenCentral due to jcenter being nonexistent/depricated. * Bump SDK versions to 33. --- app/build.gradle | 27 +++++++++++++++--------- app/src/main/AndroidManifest.xml | 3 +-- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bdcef73..4a5073d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,16 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { applicationId "com.nicobrailo.pianoli" minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 19 versionName "1.19" } + buildTypes { release { minifyEnabled false @@ -28,20 +29,26 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - - lintOptions { - disable 'GoogleAppIndexingWarning' + packagingOptions { + jniLibs { + excludes += ['META-INF/*'] + } + resources { + excludes += ['META-INF/*'] + } } - packagingOptions { - exclude 'META-INF/*' + namespace 'com.nicobrailo.pianoli' + + lint { + disable 'MissingTranslation', 'GoogleAppIndexingWarning' } } dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.android.support:design:28.0.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.preference:preference:1.2.0' implementation "androidx.annotation:annotation:1.6.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cdcce52..ba77b2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + Date: Wed, 10 May 2023 07:32:25 +1000 Subject: [PATCH 6/6] Add Waltzing Matilda as a melody, but don't default to playing it. Leave it as an optional extra because it is a lot longer than other melodies and also because it may not be as familiar. Requires addition of accented notes to the melody parser. --- .../java/com/nicobrailo/pianoli/Piano.java | 20 ++++++ .../nicobrailo/pianoli/melodies/Melody.java | 65 +++++++++++++++++-- app/src/main/res/values/donottranslate.xml | 4 ++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nicobrailo/pianoli/Piano.java b/app/src/main/java/com/nicobrailo/pianoli/Piano.java index 8c7a1fc..5df729c 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/Piano.java +++ b/app/src/main/java/com/nicobrailo/pianoli/Piano.java @@ -137,30 +137,50 @@ Key get_area_for_flat_key(int key_idx) { static { note_to_key_idx.put("C1", 0); note_to_key_idx.put("C#1", 1); + note_to_key_idx.put("Db1", 1); + note_to_key_idx.put("D♭1", 1); note_to_key_idx.put("D1", 2); note_to_key_idx.put("D#1", 3); + note_to_key_idx.put("Eb1", 3); + note_to_key_idx.put("E♭1", 3); note_to_key_idx.put("E1", 4); note_to_key_idx.put("F1", 6); note_to_key_idx.put("F#1", 7); + note_to_key_idx.put("Gb1", 7); + note_to_key_idx.put("G♭1", 7); note_to_key_idx.put("G1", 8); note_to_key_idx.put("G#1", 9); + note_to_key_idx.put("Ab1", 9); + note_to_key_idx.put("A♭1", 9); note_to_key_idx.put("A1", 10); note_to_key_idx.put("A#1", 11); + note_to_key_idx.put("Bb1", 11); + note_to_key_idx.put("B♭1", 11); note_to_key_idx.put("B1", 12); note_to_key_idx.put("C2", 14); note_to_key_idx.put("C#2", 15); + note_to_key_idx.put("Db2", 15); + note_to_key_idx.put("D♭2", 15); note_to_key_idx.put("D2", 16); note_to_key_idx.put("D#2", 17); + note_to_key_idx.put("Eb2", 17); + note_to_key_idx.put("E♭2", 17); note_to_key_idx.put("E2", 18); note_to_key_idx.put("F2", 20); note_to_key_idx.put("F#2", 21); + note_to_key_idx.put("Gb2", 21); + note_to_key_idx.put("G♭2", 21); note_to_key_idx.put("G2", 22); note_to_key_idx.put("G#2", 23); + note_to_key_idx.put("Ab2", 23); + note_to_key_idx.put("A♭2", 23); note_to_key_idx.put("A2", 24); note_to_key_idx.put("A#2", 25); + note_to_key_idx.put("Bb2", 25); + note_to_key_idx.put("B♭2", 25); note_to_key_idx.put("B2", 26); } diff --git a/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java b/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java index 9d05403..0adf524 100644 --- a/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java +++ b/app/src/main/java/com/nicobrailo/pianoli/melodies/Melody.java @@ -4,6 +4,57 @@ public class Melody { + public static final Melody waltzing_matilda = fromString( + "waltzing_matilda", + "A A A A G G " + + // Once a jolly swagman` + + "F A F D E F " + + // camped by a billabong + + "C F A C2 C2 C2 " + + // under the shade of a + + "C2 C2 C2 C2 " + + // coolibah tree + + "F G A A A G G " + + // And he sang as he watched and + + "F G A F D E F " + + // waited till his Billy boiled + + "C F A C2 Bb A " + + // You'll come a waltzing ma- + + "G G G F " + + // -tilda with me. + + "C2 C2 C2 C2 A " + + // Waltzing Matilda, + + "F2 F2 E2 D2 C2 " + + // Waltzing Matilda, + + "C2 C2 C2 D2 C2 C2 " + + // You'll come a waltzing Mat- + + "C2 Bb A G F G " + + // -tilda with me, And he + + "A A A G G " + + // Sang as he watched and + + "F G A F D E F " + + // waited till his Billy boiled, + + "C F A C2 Bb A " + + // You'll come a waltzing Ma- + + "G G G F" + // -tilda with me. + ); + public static final Melody im_a_little_teapot = fromString( "im_a_little_teapot", "C D E F G C2 " + @@ -79,24 +130,26 @@ public class Melody { // ... climbed up the spout again. ); - public static final Melody[] all = new Melody[] { + public static final Melody[] all = new Melody[]{ twinkle_twinkle_little_star, insy_winsy_spider, im_a_little_teapot, + waltzing_matilda }; + /** * A somewhat-robust string to melody parser. - * Allows melodies to be specified as a string of notes, where the notes are: "A", "B1", "C#1", "G2", etc. - * + * Allows melodies to be specified as a string of notes, where the notes are: "A", "B1", "C#1", "Bb1", "G2", etc. + *

* Notes are separated by whitespace. - * + *

* Notes in the first octave can leave off the octave designation and it will be automatically * appended (i.e. "C" will become "C1"). This makes it simpler to write songs that fall within a single octave. */ static Melody fromString(String id, String plainTextNotes) { - String[] notes = plainTextNotes.trim().toUpperCase(Locale.ENGLISH).split("\\s+"); + String[] notes = plainTextNotes.trim().split("\\s+"); - for (int i = 0; i < notes.length; i ++) { + for (int i = 0; i < notes.length; i++) { if (!notes[i].matches(".*\\d$")) { notes[i] = notes[i] + "1"; } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 55ad073..f709012 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -10,5 +10,9 @@ twinkle_twinkle_little_star im_a_little_teapot insy_winsy_spider + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99080d8..0b00748 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Twinkle, Twinkle, Little Star I\'m a Little Teapot Insy Winsy Spider + Waltzing Matilda Touching a key plays the next note in a melody Auto play melodies Available melodies