diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index ef0af81..c9daa9e 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -15,7 +15,8 @@ import com.lukas.music.song.note.NoteName -class SongFragment : Fragment(), AdapterView.OnItemSelectedListener { +class SongFragment(val playFragment: PlayFragment) : Fragment(), + AdapterView.OnItemSelectedListener { private lateinit var binding: FragmentSongBinding var displayChordNames = true @@ -38,7 +39,7 @@ binding.keySelection.onItemSelectedListener = this binding.keySelection.setSelection(Song.currentSong.root.noteName.index) binding.addPhraseButton.setOnClickListener { - Song.currentSong.chordProgression.phrases += Phrase() + Song.currentSong.chordProgression += Phrase() updateChords() } updateChords() @@ -47,24 +48,26 @@ fun updateChords() { binding.chords.removeAllViews() - for (phrase in Song.currentSong.chordProgression.phrases) { + for (phrase in Song.currentSong.chordProgression) { val row = TableRow(binding.root.context) - for (chord in phrase.chords) { + for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = layout + card.layoutParams = tableRowLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) - text.layoutParams = layout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + text.layoutParams = tableRowLayout + text.textSize = 20f card.addView(text) row.addView(card) } val button = ImageButton(binding.root.context) button.setOnClickListener { - Song.currentSong.chordProgression.phrases -= phrase + Song.currentSong.chordProgression -= phrase updateChords() } button.setImageResource(android.R.drawable.ic_delete) @@ -72,10 +75,11 @@ row.addView(button) binding.chords.addView(row) } + playFragment.updateChords() } companion object { - val layout = TableRow.LayoutParams( + val tableRowLayout = TableRow.LayoutParams( TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT ) @@ -85,7 +89,7 @@ ) init { - layout.setMargins(10) + tableRowLayout.setMargins(10) } } diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index ef0af81..c9daa9e 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -15,7 +15,8 @@ import com.lukas.music.song.note.NoteName -class SongFragment : Fragment(), AdapterView.OnItemSelectedListener { +class SongFragment(val playFragment: PlayFragment) : Fragment(), + AdapterView.OnItemSelectedListener { private lateinit var binding: FragmentSongBinding var displayChordNames = true @@ -38,7 +39,7 @@ binding.keySelection.onItemSelectedListener = this binding.keySelection.setSelection(Song.currentSong.root.noteName.index) binding.addPhraseButton.setOnClickListener { - Song.currentSong.chordProgression.phrases += Phrase() + Song.currentSong.chordProgression += Phrase() updateChords() } updateChords() @@ -47,24 +48,26 @@ fun updateChords() { binding.chords.removeAllViews() - for (phrase in Song.currentSong.chordProgression.phrases) { + for (phrase in Song.currentSong.chordProgression) { val row = TableRow(binding.root.context) - for (chord in phrase.chords) { + for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = layout + card.layoutParams = tableRowLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) - text.layoutParams = layout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + text.layoutParams = tableRowLayout + text.textSize = 20f card.addView(text) row.addView(card) } val button = ImageButton(binding.root.context) button.setOnClickListener { - Song.currentSong.chordProgression.phrases -= phrase + Song.currentSong.chordProgression -= phrase updateChords() } button.setImageResource(android.R.drawable.ic_delete) @@ -72,10 +75,11 @@ row.addView(button) binding.chords.addView(row) } + playFragment.updateChords() } companion object { - val layout = TableRow.LayoutParams( + val tableRowLayout = TableRow.LayoutParams( TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT ) @@ -85,7 +89,7 @@ ) init { - layout.setMargins(10) + tableRowLayout.setMargins(10) } } diff --git a/app/src/main/java/com/lukas/music/util/Cycle.kt b/app/src/main/java/com/lukas/music/util/Cycle.kt new file mode 100644 index 0000000..7e49239 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/Cycle.kt @@ -0,0 +1,120 @@ +package com.lukas.music.util + +open class Cycle(initialSize: Int = 0) : ArrayList(initialSize) { + + var index = 0 + val stepCallback = mutableListOf<() -> Unit>() + val wraparoundListeners = mutableListOf<() -> Unit>() + val indexBehind: Int + get() = (index - 1 + size) % size + + val currentItem: T? + get() = if (size == 0) null else this[index] + + open fun step(): T? { + if (size == 0) { + return null + } + index++ + if (index >= size) { + index %= size + for (callback in wraparoundListeners) { + callback() + } + } + for (callback in stepCallback) { + callback() + } + return this[index] + } + + open fun reset() { + index = size - 1 + step() + } + + open fun reverse() { + if (size == 0) { + return + } + index = indexBehind + // TODO: back around handlers + for (callback in stepCallback) { + callback() + } + } + + fun lookahead(distance: Int): T { + return this[(index + distance) % size] + } + + fun lookbehind(distance: Int): T { + return lookahead(-distance) + } +} + +open class MetaCycle> : Cycle() { + val miniStepCallback = mutableListOf<() -> Unit>() + + override fun step(): T? { + if (size == 0) { + return null + } + this[index].step() + if (this[index].index == 0) { + super.step() + } + for (callback in miniStepCallback) { + callback() + } + return this[index] + } + + override fun reset() { + this[index].reset() + super.reset() + } + + fun bigStep(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem?.indexBehind ?: return + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } + + override fun reverse() { + currentItem?.reverse() ?: return + if (currentItem!!.index == currentItem!!.size - 1) { + currentItem!!.reset() + super.reverse() + currentItem!!.index = currentItem!!.size - 1 + } + for (callback in miniStepCallback) { + callback() + } + } + + fun bigReverse(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.reset() + index = indexBehind + index = indexBehind + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem!!.indexBehind + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index ef0af81..c9daa9e 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -15,7 +15,8 @@ import com.lukas.music.song.note.NoteName -class SongFragment : Fragment(), AdapterView.OnItemSelectedListener { +class SongFragment(val playFragment: PlayFragment) : Fragment(), + AdapterView.OnItemSelectedListener { private lateinit var binding: FragmentSongBinding var displayChordNames = true @@ -38,7 +39,7 @@ binding.keySelection.onItemSelectedListener = this binding.keySelection.setSelection(Song.currentSong.root.noteName.index) binding.addPhraseButton.setOnClickListener { - Song.currentSong.chordProgression.phrases += Phrase() + Song.currentSong.chordProgression += Phrase() updateChords() } updateChords() @@ -47,24 +48,26 @@ fun updateChords() { binding.chords.removeAllViews() - for (phrase in Song.currentSong.chordProgression.phrases) { + for (phrase in Song.currentSong.chordProgression) { val row = TableRow(binding.root.context) - for (chord in phrase.chords) { + for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = layout + card.layoutParams = tableRowLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) - text.layoutParams = layout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + text.layoutParams = tableRowLayout + text.textSize = 20f card.addView(text) row.addView(card) } val button = ImageButton(binding.root.context) button.setOnClickListener { - Song.currentSong.chordProgression.phrases -= phrase + Song.currentSong.chordProgression -= phrase updateChords() } button.setImageResource(android.R.drawable.ic_delete) @@ -72,10 +75,11 @@ row.addView(button) binding.chords.addView(row) } + playFragment.updateChords() } companion object { - val layout = TableRow.LayoutParams( + val tableRowLayout = TableRow.LayoutParams( TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT ) @@ -85,7 +89,7 @@ ) init { - layout.setMargins(10) + tableRowLayout.setMargins(10) } } diff --git a/app/src/main/java/com/lukas/music/util/Cycle.kt b/app/src/main/java/com/lukas/music/util/Cycle.kt new file mode 100644 index 0000000..7e49239 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/Cycle.kt @@ -0,0 +1,120 @@ +package com.lukas.music.util + +open class Cycle(initialSize: Int = 0) : ArrayList(initialSize) { + + var index = 0 + val stepCallback = mutableListOf<() -> Unit>() + val wraparoundListeners = mutableListOf<() -> Unit>() + val indexBehind: Int + get() = (index - 1 + size) % size + + val currentItem: T? + get() = if (size == 0) null else this[index] + + open fun step(): T? { + if (size == 0) { + return null + } + index++ + if (index >= size) { + index %= size + for (callback in wraparoundListeners) { + callback() + } + } + for (callback in stepCallback) { + callback() + } + return this[index] + } + + open fun reset() { + index = size - 1 + step() + } + + open fun reverse() { + if (size == 0) { + return + } + index = indexBehind + // TODO: back around handlers + for (callback in stepCallback) { + callback() + } + } + + fun lookahead(distance: Int): T { + return this[(index + distance) % size] + } + + fun lookbehind(distance: Int): T { + return lookahead(-distance) + } +} + +open class MetaCycle> : Cycle() { + val miniStepCallback = mutableListOf<() -> Unit>() + + override fun step(): T? { + if (size == 0) { + return null + } + this[index].step() + if (this[index].index == 0) { + super.step() + } + for (callback in miniStepCallback) { + callback() + } + return this[index] + } + + override fun reset() { + this[index].reset() + super.reset() + } + + fun bigStep(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem?.indexBehind ?: return + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } + + override fun reverse() { + currentItem?.reverse() ?: return + if (currentItem!!.index == currentItem!!.size - 1) { + currentItem!!.reset() + super.reverse() + currentItem!!.index = currentItem!!.size - 1 + } + for (callback in miniStepCallback) { + callback() + } + } + + fun bigReverse(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.reset() + index = indexBehind + index = indexBehind + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem!!.indexBehind + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_play.xml b/app/src/main/res/layout/fragment_play.xml index 136de13..e4182bf 100644 --- a/app/src/main/res/layout/fragment_play.xml +++ b/app/src/main/res/layout/fragment_play.xml @@ -1,72 +1,188 @@ - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_margin="10dp" + android:text="@string/unknown_chord" + android:textSize="20sp" + tools:layout_conversion_absoluteHeight="27dp" + tools:layout_conversion_absoluteWidth="258dp" /> + - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index ef0af81..c9daa9e 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -15,7 +15,8 @@ import com.lukas.music.song.note.NoteName -class SongFragment : Fragment(), AdapterView.OnItemSelectedListener { +class SongFragment(val playFragment: PlayFragment) : Fragment(), + AdapterView.OnItemSelectedListener { private lateinit var binding: FragmentSongBinding var displayChordNames = true @@ -38,7 +39,7 @@ binding.keySelection.onItemSelectedListener = this binding.keySelection.setSelection(Song.currentSong.root.noteName.index) binding.addPhraseButton.setOnClickListener { - Song.currentSong.chordProgression.phrases += Phrase() + Song.currentSong.chordProgression += Phrase() updateChords() } updateChords() @@ -47,24 +48,26 @@ fun updateChords() { binding.chords.removeAllViews() - for (phrase in Song.currentSong.chordProgression.phrases) { + for (phrase in Song.currentSong.chordProgression) { val row = TableRow(binding.root.context) - for (chord in phrase.chords) { + for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = layout + card.layoutParams = tableRowLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) - text.layoutParams = layout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + text.layoutParams = tableRowLayout + text.textSize = 20f card.addView(text) row.addView(card) } val button = ImageButton(binding.root.context) button.setOnClickListener { - Song.currentSong.chordProgression.phrases -= phrase + Song.currentSong.chordProgression -= phrase updateChords() } button.setImageResource(android.R.drawable.ic_delete) @@ -72,10 +75,11 @@ row.addView(button) binding.chords.addView(row) } + playFragment.updateChords() } companion object { - val layout = TableRow.LayoutParams( + val tableRowLayout = TableRow.LayoutParams( TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT ) @@ -85,7 +89,7 @@ ) init { - layout.setMargins(10) + tableRowLayout.setMargins(10) } } diff --git a/app/src/main/java/com/lukas/music/util/Cycle.kt b/app/src/main/java/com/lukas/music/util/Cycle.kt new file mode 100644 index 0000000..7e49239 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/Cycle.kt @@ -0,0 +1,120 @@ +package com.lukas.music.util + +open class Cycle(initialSize: Int = 0) : ArrayList(initialSize) { + + var index = 0 + val stepCallback = mutableListOf<() -> Unit>() + val wraparoundListeners = mutableListOf<() -> Unit>() + val indexBehind: Int + get() = (index - 1 + size) % size + + val currentItem: T? + get() = if (size == 0) null else this[index] + + open fun step(): T? { + if (size == 0) { + return null + } + index++ + if (index >= size) { + index %= size + for (callback in wraparoundListeners) { + callback() + } + } + for (callback in stepCallback) { + callback() + } + return this[index] + } + + open fun reset() { + index = size - 1 + step() + } + + open fun reverse() { + if (size == 0) { + return + } + index = indexBehind + // TODO: back around handlers + for (callback in stepCallback) { + callback() + } + } + + fun lookahead(distance: Int): T { + return this[(index + distance) % size] + } + + fun lookbehind(distance: Int): T { + return lookahead(-distance) + } +} + +open class MetaCycle> : Cycle() { + val miniStepCallback = mutableListOf<() -> Unit>() + + override fun step(): T? { + if (size == 0) { + return null + } + this[index].step() + if (this[index].index == 0) { + super.step() + } + for (callback in miniStepCallback) { + callback() + } + return this[index] + } + + override fun reset() { + this[index].reset() + super.reset() + } + + fun bigStep(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem?.indexBehind ?: return + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } + + override fun reverse() { + currentItem?.reverse() ?: return + if (currentItem!!.index == currentItem!!.size - 1) { + currentItem!!.reset() + super.reverse() + currentItem!!.index = currentItem!!.size - 1 + } + for (callback in miniStepCallback) { + callback() + } + } + + fun bigReverse(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.reset() + index = indexBehind + index = indexBehind + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem!!.indexBehind + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_play.xml b/app/src/main/res/layout/fragment_play.xml index 136de13..e4182bf 100644 --- a/app/src/main/res/layout/fragment_play.xml +++ b/app/src/main/res/layout/fragment_play.xml @@ -1,72 +1,188 @@ - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_margin="10dp" + android:text="@string/unknown_chord" + android:textSize="20sp" + tools:layout_conversion_absoluteHeight="27dp" + tools:layout_conversion_absoluteWidth="258dp" /> + - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 66e3f6c..e691a4f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,4 +11,14 @@ #FF039BE5 #FFBDBDBD #FF757575 + #101010 + #202020 + #303030 + #404040 + #505050 + #606060 + #707070 + #808080 + #909090 + #A0A0A0 \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/MainActivity.kt b/app/src/main/java/com/lukas/music/MainActivity.kt index 0b075f1..22042e9 100644 --- a/app/src/main/java/com/lukas/music/MainActivity.kt +++ b/app/src/main/java/com/lukas/music/MainActivity.kt @@ -3,7 +3,6 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.lukas.music.databinding.ActivityMainBinding -import com.lukas.music.instruments.Rhythm import com.lukas.music.ui.fragments.CreditsFragment import com.lukas.music.ui.fragments.InstrumentListFragment import com.lukas.music.ui.fragments.PlayFragment @@ -14,9 +13,10 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val playFragment = PlayFragment() private val tabFragments = listOf( - PlayFragment(), - SongFragment(), + playFragment, + SongFragment(playFragment), InstrumentListFragment(), CreditsFragment(), ) @@ -30,7 +30,6 @@ binding.tabPager.registerOnPageChangeCallback(PageListener(binding.tabLayout)) binding.tabLayout.addOnTabSelectedListener(TabListener(binding.tabPager)) startAudio() - Rhythm.start() supportActionBar?.hide() } diff --git a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt index c449d04..4b47c80 100644 --- a/app/src/main/java/com/lukas/music/instruments/Rhythm.kt +++ b/app/src/main/java/com/lukas/music/instruments/Rhythm.kt @@ -13,11 +13,19 @@ } } - fun start() { - Timer().schedule(0, 500) { - if (on) { - Song.currentSong.step() - } + private val callback: TimerTask.() -> Unit = { + if (on) { + Song.currentSong.step() } } + + private val timer = Timer() + private lateinit var task: TimerTask + + fun setTempo(tempo: Int) { + if (this::task.isInitialized) { + task.cancel() + } + task = timer.schedule((60000 / tempo).toLong(), (60000 / tempo).toLong(), callback) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index 4b23eac..e666dca 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -1,46 +1,38 @@ package com.lukas.music.song -import android.os.Handler -import android.os.Looper -import android.widget.RadioButton -import android.widget.TextView import com.lukas.music.instruments.Instrument -import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordProgression import com.lukas.music.song.note.Note +import com.lukas.music.util.Cycle class Song( var root: Note, - val chordProgression: ChordProgression, val beats: Int -) { - private var beat = 0 - private var chord: Chord = chordProgression.step() - val stepButtons = mutableListOf() - lateinit var chordDisplay: TextView +) : Cycle(beats) { + val chordProgression = ChordProgression() - fun step() { - Handler(Looper.getMainLooper()).post { - stepButtons[beat].isChecked = false - beat++ - if (beat >= beats) { - beat = 0 - chord = chordProgression.step() - } - stepButtons[beat].isChecked = true - // this should not be executed here, but otherwise timing problems show up... - val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) - } - chordDisplay.text = chord.toString(true, root) + init { + for (i in 0 until beats) { + this += i } + wraparoundListeners += { + chordProgression.step() + } + } + + override fun step(): Int { + super.step() + val chord = chordProgression.currentItem?.currentItem ?: return index + val chordNotes = chord.getNotes(root) + for (voice in Instrument.voice) { + voice.step(root, chordNotes) + } + return index } companion object { var currentSong = Song( Note.NOTES[69], - ChordProgression(), 4 ) } diff --git a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt index 2e189b4..f1a5bb2 100644 --- a/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt +++ b/app/src/main/java/com/lukas/music/song/chords/ChordProgression.kt @@ -1,19 +1,15 @@ package com.lukas.music.song.chords -class ChordProgression { - // TODO: special handler for increasing or decreasing measuresPerPhrase - val measuresPerPhrase: Int = 4 - val phrases = mutableListOf(Phrase(), Phrase()) +import com.lukas.music.util.MetaCycle - private var position = 0 - fun step(): Chord { - val phrase = phrases[position] - return phrase.step(this) - } - - operator fun inc(): ChordProgression { - position++ - position %= phrases.size - return this +class ChordProgression : MetaCycle() { + override fun add(element: Phrase): Boolean { + if (size == 0) { + for (callback in stepCallback) { + // first step + callback() + } + } + return super.add(element) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt index 611d1b8..384254f 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Phrase.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Phrase.kt @@ -1,22 +1,11 @@ package com.lukas.music.song.chords -class Phrase { - val chords = mutableListOf( - Chord(0, ChordType.MAJOR), - Chord(5, ChordType.MAJOR), - Chord(2, ChordType.MINOR), - Chord(7, ChordType.MAJOR), - ) +import com.lukas.music.util.Cycle - var position = 0 - fun step(parent: ChordProgression): Chord { - var parent: ChordProgression = parent - val result = chords[position] - position++ - if (position >= chords.size) { - position = 0 - parent++ +class Phrase : Cycle() { + init { + for (i in 0 until 4) { + this += Chord(0, ChordType.MAJOR) } - return result } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index e72a20d..fe25848 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -1,20 +1,24 @@ package com.lukas.music.ui.fragments import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.SeekBar -import android.widget.Space +import android.widget.* +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.lukas.music.R import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song class PlayFragment : Fragment() { - lateinit var binding: FragmentPlayBinding + private lateinit var binding: FragmentPlayBinding + private val beatIndicators = mutableListOf() + private val chordDisplays = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,25 +31,48 @@ if (Rhythm.on) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play ) } - binding.masterVolumeSlider.min = 0 - binding.masterVolumeSlider.max = 100 - binding.masterVolumeSlider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - setMasterVolume(progress.toDouble() / 100.0) - binding.masterVolumeText.text = "Master volume: $progress%" + binding.advancePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigStep(true) + } + binding.reversePhraseButton.setOnClickListener { + Song.currentSong.chordProgression.bigReverse(true) + } + binding.advanceMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.step() + } + binding.reverseMeasureButton.setOnClickListener { + Song.currentSong.chordProgression.currentItem?.let { + chordDisplays[it.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) } + Song.currentSong.chordProgression.reverse() + } + setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + setMasterVolume(it.toDouble() / 100.0) + binding.masterVolumeText.text = "Master volume: $it%" + } + setupSlider(binding.tempoSlider, 50, 150, 90) { + Rhythm.setTempo(it) + binding.tempoText.text = "tempo: ${it}bpm" + } + setupBeatIndicator() + Song.currentSong.stepCallback += { + Handler(Looper.getMainLooper()).post { + beatIndicators[Song.currentSong.indexBehind].isChecked = false + beatIndicators[Song.currentSong.index].isChecked = true + } + } + Song.currentSong.chordProgression.stepCallback += { + Handler(Looper.getMainLooper()).post { updateChords() } + } + Song.currentSong.chordProgression.miniStepCallback += { + Handler(Looper.getMainLooper()).post { updateChordView() } + } + return binding.root + } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - binding.masterVolumeSlider.progress = 100 + private fun setupBeatIndicator() { val layout = RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.MATCH_PARENT @@ -61,12 +88,73 @@ if (i == 0) { child.isChecked = true } - Song.currentSong.stepButtons += child + beatIndicators += child binding.beatIndicator.addView(child) } - Song.currentSong.chordDisplay = binding.currentChord - return binding.root } - external fun setMasterVolume(volume: Double) + private fun updateChordView() { + if (chordDisplays.isEmpty()) { + updateChords() + return + } + chordDisplays[Song.currentSong.chordProgression.currentItem!!.index].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.purple_700) + ) + chordDisplays[Song.currentSong.chordProgression.currentItem!!.indexBehind].setCardBackgroundColor( + ContextCompat.getColor(binding.root.context, R.color.gray_0x40) + ) + } + + private fun setupSlider( + slider: SeekBar, + min: Int, + max: Int, + initialProgress: Int, + callback: (Int) -> Unit + ) { + slider.min = min + slider.max = max + slider.setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + slider.progress = initialProgress + } + + fun updateChords() { + binding.phraseDisplay.removeAllViews() + chordDisplays.clear() + for (chord in Song.currentSong.chordProgression.currentItem ?: return) { + val card = CardView(binding.root.context) + card.layoutParams = SongFragment.tableRowLayout + card.radius = 10f + card.preventCornerOverlap = false + val text = TextView(binding.root.context) + text.text = chord.toString(true, Song.currentSong.root) + text.layoutParams = SongFragment.tableRowLayout + text.textSize = 20f + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + card.addView(text) + binding.phraseDisplay.addView(card) + chordDisplays += card + } + binding.phraseTable.isStretchAllColumns = true + binding.nextChordText.text = + Song.currentSong.chordProgression.lookahead(1)[0].toString(true, Song.currentSong.root) + updateChordView() + } + + private external fun setMasterVolume(volume: Double) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt index ef0af81..c9daa9e 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/SongFragment.kt @@ -15,7 +15,8 @@ import com.lukas.music.song.note.NoteName -class SongFragment : Fragment(), AdapterView.OnItemSelectedListener { +class SongFragment(val playFragment: PlayFragment) : Fragment(), + AdapterView.OnItemSelectedListener { private lateinit var binding: FragmentSongBinding var displayChordNames = true @@ -38,7 +39,7 @@ binding.keySelection.onItemSelectedListener = this binding.keySelection.setSelection(Song.currentSong.root.noteName.index) binding.addPhraseButton.setOnClickListener { - Song.currentSong.chordProgression.phrases += Phrase() + Song.currentSong.chordProgression += Phrase() updateChords() } updateChords() @@ -47,24 +48,26 @@ fun updateChords() { binding.chords.removeAllViews() - for (phrase in Song.currentSong.chordProgression.phrases) { + for (phrase in Song.currentSong.chordProgression) { val row = TableRow(binding.root.context) - for (chord in phrase.chords) { + for (chord in phrase) { val card = CardView(binding.root.context) card.radius = 10f - card.layoutParams = layout + card.layoutParams = tableRowLayout card.setOnClickListener { EditChordFragment(chord, this).showNow(childFragmentManager, "") } val text = TextView(binding.root.context) text.text = chord.toString(displayChordNames, Song.currentSong.root) - text.layoutParams = layout + text.textAlignment = TextView.TEXT_ALIGNMENT_CENTER + text.layoutParams = tableRowLayout + text.textSize = 20f card.addView(text) row.addView(card) } val button = ImageButton(binding.root.context) button.setOnClickListener { - Song.currentSong.chordProgression.phrases -= phrase + Song.currentSong.chordProgression -= phrase updateChords() } button.setImageResource(android.R.drawable.ic_delete) @@ -72,10 +75,11 @@ row.addView(button) binding.chords.addView(row) } + playFragment.updateChords() } companion object { - val layout = TableRow.LayoutParams( + val tableRowLayout = TableRow.LayoutParams( TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT ) @@ -85,7 +89,7 @@ ) init { - layout.setMargins(10) + tableRowLayout.setMargins(10) } } diff --git a/app/src/main/java/com/lukas/music/util/Cycle.kt b/app/src/main/java/com/lukas/music/util/Cycle.kt new file mode 100644 index 0000000..7e49239 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/Cycle.kt @@ -0,0 +1,120 @@ +package com.lukas.music.util + +open class Cycle(initialSize: Int = 0) : ArrayList(initialSize) { + + var index = 0 + val stepCallback = mutableListOf<() -> Unit>() + val wraparoundListeners = mutableListOf<() -> Unit>() + val indexBehind: Int + get() = (index - 1 + size) % size + + val currentItem: T? + get() = if (size == 0) null else this[index] + + open fun step(): T? { + if (size == 0) { + return null + } + index++ + if (index >= size) { + index %= size + for (callback in wraparoundListeners) { + callback() + } + } + for (callback in stepCallback) { + callback() + } + return this[index] + } + + open fun reset() { + index = size - 1 + step() + } + + open fun reverse() { + if (size == 0) { + return + } + index = indexBehind + // TODO: back around handlers + for (callback in stepCallback) { + callback() + } + } + + fun lookahead(distance: Int): T { + return this[(index + distance) % size] + } + + fun lookbehind(distance: Int): T { + return lookahead(-distance) + } +} + +open class MetaCycle> : Cycle() { + val miniStepCallback = mutableListOf<() -> Unit>() + + override fun step(): T? { + if (size == 0) { + return null + } + this[index].step() + if (this[index].index == 0) { + super.step() + } + for (callback in miniStepCallback) { + callback() + } + return this[index] + } + + override fun reset() { + this[index].reset() + super.reset() + } + + fun bigStep(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem?.indexBehind ?: return + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } + + override fun reverse() { + currentItem?.reverse() ?: return + if (currentItem!!.index == currentItem!!.size - 1) { + currentItem!!.reset() + super.reverse() + currentItem!!.index = currentItem!!.size - 1 + } + for (callback in miniStepCallback) { + callback() + } + } + + fun bigReverse(keepSubindex: Boolean = false) { + val subindex = currentItem?.index ?: return + currentItem?.reset() + index = indexBehind + index = indexBehind + currentItem?.index = currentItem!!.size - 1 + step() + if (keepSubindex) { + currentItem?.index = subindex + currentItem?.index = currentItem!!.indexBehind + currentItem?.step() + for (callback in miniStepCallback) { + callback() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_play.xml b/app/src/main/res/layout/fragment_play.xml index 136de13..e4182bf 100644 --- a/app/src/main/res/layout/fragment_play.xml +++ b/app/src/main/res/layout/fragment_play.xml @@ -1,72 +1,188 @@ - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_margin="10dp" + android:text="@string/unknown_chord" + android:textSize="20sp" + tools:layout_conversion_absoluteHeight="27dp" + tools:layout_conversion_absoluteWidth="258dp" /> + - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 66e3f6c..e691a4f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,4 +11,14 @@ #FF039BE5 #FFBDBDBD #FF757575 + #101010 + #202020 + #303030 + #404040 + #505050 + #606060 + #707070 + #808080 + #909090 + #A0A0A0 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ba02a4..c125463 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,9 @@ Chord pitch Chord type Exit this menu + Go to the next measure + Go to the start of the current measure + Go to the next phrase + Go to the start of the current phrase + Next chord not known yet . . . \ No newline at end of file