diff --git a/.gitignore b/.gitignore
index ee3e9496..2a935b3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,4 +73,6 @@ fastlane/readme.md
/projectFilesBackup
*.dm
-*.patch
\ No newline at end of file
+*.patch
+
+*.parsedEvents.xml
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56e..b86273d9 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 4328739a..5490b6be 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d4cbafbc..a8b902ab 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -51,7 +51,7 @@
-
+
diff --git a/.idea/other.xml b/.idea/other.xml
deleted file mode 100644
index 94c96f63..00000000
--- a/.idea/other.xml
+++ /dev/null
@@ -1,318 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..16660f1d
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index f5830cb8..de1d8795 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'project-report'
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
android {
compileSdk 34
@@ -12,8 +13,8 @@ android {
multiDexEnabled true
targetSdkVersion 34
resourceConfigurations += ['en', 'fr', 'pt', 'de', 'es', 'it']
- versionCode 591
- versionName "1.74"
+ versionCode 592
+ versionName "1.75"
}
signingConfigs {
debug {
@@ -46,11 +47,13 @@ android {
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
+ sourceCompatibility JavaVersion.VERSION_21
+ targetCompatibility JavaVersion.VERSION_21
}
sourceSets {
- main.java.srcDirs += 'src/main/kotlin/'
+ main.kotlin.srcDirs += 'src/main/kotlin/'
+ test.kotlin.srcDirs += 'src/test/kotlin'
+ test.resources.srcDirs += 'src/test/data'
}
namespace 'com.stevenfrew.beatprompter'
buildFeatures {
@@ -60,38 +63,41 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation('com.google.api-client:google-api-client-android:1.32.1')
- implementation('com.google.apis:google-api-services-drive:v3-rev20211107-1.32.1')
- implementation 'androidx.multidex:multidex:2.0.1'
- implementation 'com.google.android.gms:play-services-auth:21.2.0'
- implementation 'com.google.android.gms:play-services-plus:17.0.0'
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.media:media:1.7.0'
- implementation 'androidx.appcompat:appcompat:1.7.0'
- implementation 'androidx.browser:browser:1.8.0'
- implementation 'com.google.android.material:material:1.12.0'
- implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0'
- implementation 'com.github.apl-devs:appintro:v4.2.2'
- implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
- implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
- implementation 'commons-io:commons-io:2.13.0'
- // Include the sdk as a dependency
+ implementation(libs.google.api.client.android)
+ implementation(libs.google.api.services.drive)
+ implementation libs.androidx.multidex
+ implementation libs.play.services.auth
+ implementation libs.play.services.plus
+ implementation libs.androidx.legacy.support.v4
+ implementation libs.androidx.media
+ implementation libs.androidx.appcompat
+ implementation libs.androidx.browser
+ implementation libs.material
+ implementation libs.hsv.alpha.color.picker.android
+ implementation libs.appintro
+ implementation libs.dropbox.core.sdk
+ implementation libs.rxjava
+ implementation libs.commons.io
+ implementation libs.gson
+ implementation libs.adal
+ implementation libs.kotlin.stdlib.jdk7
+ implementation libs.kotlin.reflect
+ implementation libs.kotlinx.coroutines.core
+ implementation libs.kotlinx.coroutines.android
+ implementation libs.listenablefuture
+ implementation libs.androidx.media3.exoplayer
+ implementation libs.androidx.preference.ktx
+ implementation libs.androidx.lifecycle.viewmodel.android
+
+ // OneDrive
+ implementation "com.microsoft.services.msa:msa-auth:0.8.6@aar"
implementation('com.onedrive.sdk:onedrive-sdk-android:1.3.1@aar') {
transitive = false
}
- // Include the gson dependency
- implementation "com.google.code.gson:gson:2.10.1"
-
- implementation "com.microsoft.services.msa:msa-auth:0.8.6@aar"
- //noinspection GradleDependency
- implementation "com.microsoft.aad:adal:1.16.3"
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
- implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
- implementation 'androidx.media3:media3-exoplayer:1.4.1'
- implementation 'androidx.preference:preference-ktx:1.2.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-android:2.8.6'
+ testImplementation libs.junit
+ testImplementation libs.kotlin.test
+ testImplementation libs.junit.jupiter
+ testImplementation libs.io.mockk
+ androidTestImplementation libs.androidx.junit
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/BeatPrompter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/BeatPrompter.kt
index df815d68..44e5d5e3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/BeatPrompter.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/BeatPrompter.kt
@@ -2,83 +2,58 @@ package com.stevenfrew.beatprompter
import android.app.Application
import android.content.Context
-import android.content.SharedPreferences
-import android.content.res.AssetManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
-import androidx.preference.PreferenceManager
import com.stevenfrew.beatprompter.comm.ConnectionNotificationTask
import com.stevenfrew.beatprompter.comm.bluetooth.Bluetooth
import com.stevenfrew.beatprompter.comm.midi.ClockSignalGeneratorTask
import com.stevenfrew.beatprompter.comm.midi.Midi
+import com.stevenfrew.beatprompter.preferences.AndroidPreferences
+import com.stevenfrew.beatprompter.preferences.Preferences
import com.stevenfrew.beatprompter.song.load.SongLoadQueueWatcherTask
+import com.stevenfrew.beatprompter.util.AndroidUtils
+import com.stevenfrew.beatprompter.util.ApplicationContextResources
import com.stevenfrew.beatprompter.util.GlobalAppResources
+import com.stevenfrew.beatprompter.util.PlatformUtils
class BeatPrompter : Application() {
- override fun attachBaseContext(base: Context) {
- super.attachBaseContext(base)
- MultiDex.install(this)
- }
-
- override fun onCreate() {
- super.onCreate()
- appResources = object : GlobalAppResources {
- override fun getString(resID: Int): String = applicationContext.getString(resID)
-
- override fun getString(resID: Int, vararg args: Any): String =
- applicationContext.getString(resID, *args)
-
- override fun getStringSet(resID: Int): Set =
- applicationContext.resources.getStringArray(resID).toSet()
-
- override val preferences: SharedPreferences
- get() = PreferenceManager.getDefaultSharedPreferences(applicationContext)
-
- override val privatePreferences: SharedPreferences
- get() = applicationContext.getSharedPreferences(SHARED_PREFERENCES_ID, Context.MODE_PRIVATE)
-
- override val assetManager: AssetManager
- get() = applicationContext.assets
- }
- applyPreferenceDefaults()
- AppCompatDelegate.setDefaultNightMode(if (Preferences.darkMode) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
- Midi.initialize(applicationContext)
- Bluetooth.initialize(applicationContext)
- songLoaderTaskThread.start()
- midiClockOutTaskThread.start()
- connectionNotificationTaskThread.start()
- Task.resumeTask(SongLoadQueueWatcherTask, songLoaderTaskThread)
- }
-
- private fun applyPreferenceDefaults() {
- PreferenceManager.setDefaultValues(applicationContext, R.xml.preferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.fontsizepreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.colorpreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.filepreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.midipreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.bluetoothpreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.permissionpreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.songdisplaypreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.audiopreferences, true)
- PreferenceManager.setDefaultValues(applicationContext, R.xml.songlistpreferences, true)
- }
-
- companion object {
- const val APP_NAME = "BeatPrompter"
- private const val SHARED_PREFERENCES_ID = "beatPrompterSharedPreferences"
- lateinit var appResources: GlobalAppResources
- private val debugMessages = mutableListOf()
-
- private val songLoaderTaskThread = Thread(SongLoadQueueWatcherTask)
- private val connectionNotificationTaskThread = Thread(ConnectionNotificationTask)
- val midiClockOutTaskThread =
- Thread(ClockSignalGeneratorTask).also { it.priority = Thread.MAX_PRIORITY }
-
- fun addDebugMessage(message: String) {
- if (BuildConfig.DEBUG) debugMessages.add(message)
- }
-
- val debugLog: String
- get() = debugMessages.takeLast(100).joinToString("\n")
- }
+ override fun attachBaseContext(base: Context) {
+ super.attachBaseContext(base)
+ MultiDex.install(this)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ platformUtils = AndroidUtils(resources)
+ appResources = ApplicationContextResources(resources)
+ preferences = AndroidPreferences(appResources, applicationContext)
+
+ AppCompatDelegate.setDefaultNightMode(if (preferences.darkMode) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ Midi.initialize(applicationContext)
+ Bluetooth.initialize(applicationContext)
+ songLoaderTaskThread.start()
+ midiClockOutTaskThread.start()
+ connectionNotificationTaskThread.start()
+ Task.resumeTask(SongLoadQueueWatcherTask, songLoaderTaskThread)
+ }
+
+ companion object {
+ const val APP_NAME = "BeatPrompter"
+ lateinit var appResources: GlobalAppResources
+ lateinit var preferences: Preferences
+ lateinit var platformUtils: PlatformUtils
+ private val debugMessages = mutableListOf()
+
+ private val songLoaderTaskThread = Thread(SongLoadQueueWatcherTask)
+ private val connectionNotificationTaskThread = Thread(ConnectionNotificationTask)
+ val midiClockOutTaskThread =
+ Thread(ClockSignalGeneratorTask).also { it.priority = Thread.MAX_PRIORITY }
+
+ fun addDebugMessage(message: String) {
+ if (BuildConfig.DEBUG) debugMessages.add(message)
+ }
+
+ val debugLog: String
+ get() = debugMessages.takeLast(100).joinToString("\n")
+ }
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/Logger.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/Logger.kt
index c16ade36..3e725b1c 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/Logger.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/Logger.kt
@@ -42,12 +42,6 @@ object Logger {
fun logLoader(message: String, warn: Boolean = false, t: Throwable? = null) =
log(TAG_LOAD, message, warn, t)
- fun logLoader(message: String, t: Throwable) =
- log(TAG_LOAD, message, false, t)
-
- fun logLoader(message: () -> String, t: Throwable) =
- log(TAG_LOAD, message, false, t)
-
fun logComms(message: () -> String, warn: Boolean = false, t: Throwable? = null) =
log(TAG_COMMS, message, warn, t)
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/Task.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/Task.kt
index 2bbb8280..21c59076 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/Task.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/Task.kt
@@ -24,9 +24,9 @@ abstract class Task(private var running: Boolean) : Runnable {
}
override fun run() {
- Log.d(TASKTAG, "Task initialising.")
+ Log.d(TASK_TAG, "Task initialising.")
initialise()
- Log.d(TASKTAG, "Task starting.")
+ Log.d(TASK_TAG, "Task starting.")
while (!shouldStop) {
if (isRunning) {
doWork()
@@ -34,11 +34,11 @@ abstract class Task(private var running: Boolean) : Runnable {
try {
Thread.sleep(500)
} catch (ie: InterruptedException) {
- Log.d(TASKTAG, "Thread sleep (while paused) was interrupted.", ie)
+ Log.d(TASK_TAG, "Thread sleep (while paused) was interrupted.", ie)
}
}
}
- Log.d(TASKTAG, "Task ended.")
+ Log.d(TASK_TAG, "Task ended.")
}
private fun pause(): Boolean {
@@ -64,7 +64,7 @@ abstract class Task(private var running: Boolean) : Runnable {
abstract fun doWork()
companion object {
- private const val TASKTAG = "task"
+ private const val TASK_TAG = "task"
private fun changeTaskState(task: Task?, thread: Thread?, fn: (Task) -> Boolean): Boolean =
if (task != null) {
@@ -84,7 +84,7 @@ abstract class Task(private var running: Boolean) : Runnable {
try {
thread?.join()
} catch (ie: InterruptedException) {
- Log.d(TASKTAG, "Task interrupted while waiting for join.", ie)
+ Log.d(TASK_TAG, "Task interrupted while waiting for join.", ie)
}
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/audio/ExoPlayerAudioPlayer.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/audio/ExoPlayerAudioPlayer.kt
index 92576502..b76a8792 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/audio/ExoPlayerAudioPlayer.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/audio/ExoPlayerAudioPlayer.kt
@@ -17,7 +17,7 @@ import java.io.File
class ExoPlayerAudioPlayer private constructor(
context: Context,
uri: Uri,
- vol: Int,
+ private var currentVolume: Int,
looping: Boolean
) : AudioPlayer {
@OptIn(UnstableApi::class)
@@ -25,7 +25,7 @@ class ExoPlayerAudioPlayer private constructor(
setSeekParameters(SeekParameters.EXACT)
setMediaItem(MediaItem.fromUri(uri))
seekTo(0)
- volume = 0.01f * vol
+ volume = 0.01f * currentVolume
repeatMode = if (looping) ExoPlayer.REPEAT_MODE_ALL else ExoPlayer.REPEAT_MODE_OFF
prepare()
}
@@ -59,8 +59,9 @@ class ExoPlayerAudioPlayer private constructor(
get() = internalPlayer.duration
override var volume: Int
- get() = (internalPlayer.volume * 100.0).toInt()
+ get() = currentVolume
set(value) {
+ currentVolume = value
internalPlayer.volume = value * 0.01f
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/Cache.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/Cache.kt
index bbabf51d..b1ba8e80 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/Cache.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/Cache.kt
@@ -9,7 +9,6 @@ import androidx.fragment.app.Fragment
import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.BuildConfig
import com.stevenfrew.beatprompter.Logger
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.parse.AudioFileParser
import com.stevenfrew.beatprompter.cache.parse.ImageFileParser
@@ -17,6 +16,7 @@ import com.stevenfrew.beatprompter.cache.parse.InvalidBeatPrompterFileException
import com.stevenfrew.beatprompter.cache.parse.MidiAliasFileParser
import com.stevenfrew.beatprompter.cache.parse.SetListFileParser
import com.stevenfrew.beatprompter.cache.parse.SongInfoParser
+import com.stevenfrew.beatprompter.cache.parse.SupportFileResolver
import com.stevenfrew.beatprompter.events.EventRouter
import com.stevenfrew.beatprompter.events.Events
import com.stevenfrew.beatprompter.storage.CacheFolder
@@ -54,6 +54,14 @@ object Cache {
}
}
+ internal val supportFileResolver: SupportFileResolver = object : SupportFileResolver {
+ override fun getMappedAudioFiles(filename: String): List =
+ cachedCloudItems.getMappedAudioFiles(filename)
+
+ override fun getMappedImageFiles(filename: String): List =
+ cachedCloudItems.getMappedImageFiles(filename)
+ }
+
private const val XML_DATABASE_FILE_NAME = "bpdb.xml"
private const val XML_DATABASE_FILE_ROOT_ELEMENT_TAG = "beatprompterDatabase"
private const val TEMPORARY_SET_LIST_FILENAME = "temporary_setlist.txt"
@@ -106,7 +114,7 @@ object Cache {
}
val songFilesFolder: String
- val useExternalStorage = Preferences.useExternalStorage
+ val useExternalStorage = BeatPrompter.preferences.useExternalStorage
val externalFilesDir = context.getExternalFilesDir(null)
songFilesFolder = if (useExternalStorage && externalFilesDir != null)
externalFilesDir.absolutePath
@@ -283,7 +291,7 @@ object Cache {
fun clearCache(report: Boolean) {
// Clear both cache folders
- val cacheFolder = getCacheFolderForStorage(Preferences.storageSystem)
+ val cacheFolder = getCacheFolderForStorage(BeatPrompter.preferences.storageSystem)
cacheFolder.clear()
cachedCloudItems.clear()
writeDatabase()
@@ -296,13 +304,13 @@ object Cache {
}
private val cloudPath: String
- get() = Preferences.cloudPath
+ get() = BeatPrompter.preferences.cloudPath
private val includeSubFolders: Boolean
- get() = Preferences.includeSubFolders
+ get() = BeatPrompter.preferences.includeSubFolders
fun canPerformCloudSync(): Boolean =
- Preferences.storageSystem !== StorageType.Demo && cloudPath.isNotBlank()
+ BeatPrompter.preferences.storageSystem !== StorageType.Demo && cloudPath.isNotBlank()
fun performFullCloudSync(parentFragment: Fragment): Boolean =
performCloudSync(null, false, parentFragment)
@@ -324,7 +332,7 @@ object Cache {
val context = parentFragment.requireContext()
if (fileToUpdate == null)
clearTemporarySetList(context)
- val cs = Storage.getInstance(Preferences.storageSystem, parentFragment)
+ val cs = Storage.getInstance(BeatPrompter.preferences.storageSystem, parentFragment)
val cloudPath = cloudPath
return if (cloudPath.isBlank()) {
Toast.makeText(
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/SongFile.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/SongFile.kt
index 349e36cb..ecbb3ef3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/SongFile.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/SongFile.kt
@@ -1,13 +1,12 @@
package com.stevenfrew.beatprompter.cache
import com.stevenfrew.beatprompter.BeatPrompter
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.parse.FileParseError
+import com.stevenfrew.beatprompter.chord.KeySignatureDefinition
import com.stevenfrew.beatprompter.midi.SongTrigger
import com.stevenfrew.beatprompter.midi.TriggerType
import com.stevenfrew.beatprompter.song.ScrollingMode
-import com.stevenfrew.beatprompter.song.chord.KeySignatureDefinition
import com.stevenfrew.beatprompter.util.normalize
import org.w3c.dom.Document
import org.w3c.dom.Element
@@ -57,6 +56,12 @@ class SongFile(
fun matchesTrigger(trigger: SongTrigger): Boolean =
songSelectTrigger == trigger || programChangeTrigger == trigger
+ val defaultVariation: String
+ get() =
+ BeatPrompter.preferences.preferredVariation.let {
+ if (variations.contains(it)) it else variations.firstOrNull() ?: ""
+ }
+
override fun writeToXML(doc: Document, element: Element) {
super.writeToXML(doc, element)
element.setAttribute(TITLE_ATTRIBUTE, title)
@@ -86,7 +91,7 @@ class SongFile(
val keySignature: String?
get() = KeySignatureDefinition.getKeySignature(key, firstChord)
- ?.getDisplayString(Preferences.displayUnicodeAccidentals)
+ ?.getDisplayString(BeatPrompter.preferences.displayUnicodeAccidentals)
companion object {
private var thePrefix = "${BeatPrompter.appResources.getString(R.string.lowerCaseThe)} "
@@ -182,7 +187,7 @@ class SongFile(
firstChord,
listOf()
)
- } catch (numberFormatException: NumberFormatException) {
+ } catch (_: NumberFormatException) {
// Attribute is garbage, we'll need to actually examine the file.
null
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/FileParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/FileParser.kt
index 9f340ad5..5a1f4ec4 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/FileParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/FileParser.kt
@@ -7,9 +7,10 @@ import org.w3c.dom.Element
* Base class for all file parsers.
*/
abstract class FileParser(protected val cachedCloudFile: CachedFile) {
- protected val errors = mutableListOf()
+ private val errorList = mutableListOf()
+ val errors: List get() = errorList
abstract fun parse(element: Element? = null): TFileResult
- fun addError(error: FileParseError) = errors.add(error)
+ fun addError(error: FileParseError) = errorList.add(error)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/MidiAliasFileParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/MidiAliasFileParser.kt
index bbddbd61..483ffa2a 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/MidiAliasFileParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/MidiAliasFileParser.kt
@@ -55,7 +55,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
.firstOrNull()
?.also {
if (aliasSetName != null)
- errors.add(FileParseError(it, R.string.midi_alias_set_name_defined_multiple_times))
+ addError(FileParseError(it, R.string.midi_alias_set_name_defined_multiple_times))
else
aliasSetName = it.aliasSetName
}
@@ -79,7 +79,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
private fun startNewAlias(aliasNameTag: MidiAliasNameTag) {
if (aliasSetName.isNullOrBlank())
- errors.add(FileParseError(aliasNameTag, R.string.no_midi_alias_set_name_defined))
+ addError(FileParseError(aliasNameTag, R.string.no_midi_alias_set_name_defined))
else
if (currentAliasName == null)
currentAliasName = aliasNameTag.aliasName
@@ -87,7 +87,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
finishCurrentAlias()
currentAliasName = aliasNameTag.aliasName
if (currentAliasName.isNullOrBlank()) {
- errors.add(FileParseError(aliasNameTag, R.string.midi_alias_without_a_name))
+ addError(FileParseError(aliasNameTag, R.string.midi_alias_without_a_name))
currentAliasName = null
}
}
@@ -95,10 +95,10 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
private fun addInstructionToCurrentAlias(instructionTag: MidiAliasInstructionTag) {
if (aliasSetName.isNullOrBlank())
- errors.add(FileParseError(instructionTag, R.string.no_midi_alias_set_name_defined))
+ addError(FileParseError(instructionTag, R.string.no_midi_alias_set_name_defined))
else {
if (currentAliasName == null)
- errors.add(FileParseError(instructionTag, R.string.no_midi_alias_name_defined))
+ addError(FileParseError(instructionTag, R.string.no_midi_alias_name_defined))
else
currentAliasComponents.add(createAliasComponent(instructionTag))
}
@@ -116,7 +116,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
if (currentAliasComponents.isNotEmpty()) {
val hasArguments = currentAliasComponents.any { it.parameterCount > 0 }
if (hasArguments && withMidiSet) {
- errors.add(FileParseError(R.string.cannot_use_with_midi_with_parameters))
+ addError(FileParseError(R.string.cannot_use_with_midi_with_parameters))
}
aliases.add(
Alias(
@@ -132,7 +132,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
withMidiContinue = false
withMidiStop = false
} else
- errors.add(FileParseError(R.string.midi_alias_has_no_components, currentAliasName!!))
+ addError(FileParseError(R.string.midi_alias_has_no_components, currentAliasName!!))
}
private fun createAliasComponent(tag: MidiAliasInstructionTag): AliasComponent {
@@ -144,7 +144,7 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
val aliasValue = TagParsingUtility.parseMIDIValue(paramBit, paramCounter, paramBits.size)
componentArgs.add(aliasValue)
} catch (mte: MalformedTagException) {
- errors.add(FileParseError(tag, mte))
+ addError(FileParseError(tag, mte))
}
}
val channelArgs = componentArgs.filterIsInstance()
@@ -152,12 +152,12 @@ class MidiAliasFileParser(cachedCloudFile: CachedFile) :
0 -> null
1 -> channelArgs.first().also {
if (componentArgs.last() != it)
- errors.add(FileParseError(tag, R.string.channel_must_be_last_parameter))
+ addError(FileParseError(tag, R.string.channel_must_be_last_parameter))
componentArgs.remove(it)
}
else -> {
- errors.add(FileParseError(tag, R.string.multiple_channel_args))
+ addError(FileParseError(tag, R.string.multiple_channel_args))
null
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SetListFileParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SetListFileParser.kt
index 6b1555d1..81665818 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SetListFileParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SetListFileParser.kt
@@ -12,29 +12,29 @@ import com.stevenfrew.beatprompter.set.SetListEntry
* Parser for set list files.
*/
class SetListFileParser(cachedCloudFile: CachedFile) :
- TextFileParser(cachedCloudFile, true, DirectiveFinder) {
- private var setName: String = ""
- private val setListEntries = mutableListOf()
+ TextFileParser(cachedCloudFile, true, DirectiveFinder) {
+ private var setName: String = ""
+ private val setListEntries = mutableListOf()
- override fun parseLine(line: TextFileLine): Boolean {
- val setNameTag = line
- .tags
- .asSequence()
- .filterIsInstance()
- .firstOrNull()
- if (setNameTag != null) {
- if (setName.isNotBlank())
- errors.add(FileParseError(setNameTag, R.string.set_name_defined_multiple_times))
- else
- setName = setNameTag.setName
- } else if (line.lineWithNoTags.isNotEmpty())
- setListEntries.add(SetListEntry(line.lineWithNoTags))
- return true
- }
+ override fun parseLine(line: TextFileLine): Boolean {
+ val setNameTag = line
+ .tags
+ .asSequence()
+ .filterIsInstance()
+ .firstOrNull()
+ if (setNameTag != null) {
+ if (setName.isNotBlank())
+ addError(FileParseError(setNameTag, R.string.set_name_defined_multiple_times))
+ else
+ setName = setNameTag.setName
+ } else if (line.lineWithNoTags.isNotEmpty())
+ setListEntries.add(SetListEntry(line.lineWithNoTags))
+ return true
+ }
- override fun getResult(): SetListFile {
- if (setName.isBlank())
- throw InvalidBeatPrompterFileException(R.string.no_set_name_defined)
- return SetListFile(cachedCloudFile, setName, setListEntries, errors)
- }
+ override fun getResult(): SetListFile {
+ if (setName.isBlank())
+ throw InvalidBeatPrompterFileException(R.string.no_set_name_defined)
+ return SetListFile(cachedCloudFile, setName, setListEntries, errors)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongFileParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongFileParser.kt
index c61621df..7e18aebb 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongFileParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongFileParser.kt
@@ -66,6 +66,17 @@ abstract class SongFileParser(
tagSequence.filterIsInstance().firstOrNull()
val variationInclusionStartTag =
tagSequence.filterIsInstance().firstOrNull()
+ val namedVariations =
+ variationInclusionStartTag?.variations ?: variationExclusionStartTag?.variations ?: listOf()
+ val unknownNamedVariations = namedVariations.subtract(variations.toSet())
+ if (unknownNamedVariations.any())
+ addError(
+ FileParseError(
+ line.lineNumber,
+ R.string.unknownVariations,
+ unknownNamedVariations.joinToString(", ")
+ )
+ )
if (variationExclusionStartTag != null)
variationExclusions.add(variationExclusionStartTag.variations)
if (variationInclusionStartTag != null)
@@ -102,7 +113,7 @@ abstract class SongFileParser(
variationAudioTags[it] = mutableListOf()
}
} else
- errors.add(FileParseError(line.lineNumber, R.string.variationsAlreadyDefined))
+ addError(FileParseError(line.lineNumber, R.string.variationsAlreadyDefined))
}
// Each audio file defined on a line now maps to a variation.
@@ -147,7 +158,7 @@ abstract class SongFileParser(
thisScrollBeatTotalOffset += scrollBeatTagDiff
if ((beatsPerBarInThisLine != 0) && (thisScrollBeatTotalOffset < -beatsPerBarInThisLine || thisScrollBeatTotalOffset >= beatsPerBarInThisLine)) {
- errors.add(FileParseError(line.lineNumber, R.string.scrollbeatOffTheMap))
+ addError(FileParseError(line.lineNumber, R.string.scrollbeatOffTheMap))
thisScrollBeatTotalOffset = 0
}
@@ -160,7 +171,7 @@ abstract class SongFileParser(
if (allowModeChange && beatModeTags.size == 1)
if (beatStartTags.isNotEmpty())
if (ongoingBeatInfo.bpm == 0.0) {
- errors.add(FileParseError(beatStartTags.first(), R.string.beatstart_with_no_bpm))
+ addError(FileParseError(beatStartTags.first(), R.string.beatstart_with_no_bpm))
lastLineBeatInfo.scrollMode
} else
ScrollingMode.Beat
@@ -217,7 +228,7 @@ abstract class SongFileParser(
variationAudioTags[filename] = mutableListOf()
return listOf(filename)
}
- errors.add(FileParseError(lineNumber, R.string.tooManyAudioTags))
+ addError(FileParseError(lineNumber, R.string.tooManyAudioTags))
return null
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongInfoParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongInfoParser.kt
index e6204dec..d45009dd 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongInfoParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongInfoParser.kt
@@ -36,9 +36,9 @@ import com.stevenfrew.beatprompter.cache.parse.tag.song.TagTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.TimeTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.TitleTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.VariationsTag
+import com.stevenfrew.beatprompter.chord.Chord
import com.stevenfrew.beatprompter.midi.SongTrigger
import com.stevenfrew.beatprompter.song.ScrollingMode
-import com.stevenfrew.beatprompter.song.chord.Chord
import org.w3c.dom.Element
@ParseTags(
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongParser.kt
index 66d5cfd6..2ee00c1f 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SongParser.kt
@@ -3,14 +3,10 @@ package com.stevenfrew.beatprompter.cache.parse
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PointF
-import android.graphics.Rect
-import android.graphics.Typeface
import android.os.Handler
import com.stevenfrew.beatprompter.BeatPrompter
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.AudioFile
-import com.stevenfrew.beatprompter.cache.Cache
import com.stevenfrew.beatprompter.cache.parse.tag.song.ArtistTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.AudioTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.BarMarkerTag
@@ -35,6 +31,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.song.LegacyTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.MidiEventTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.MidiProgramChangeTriggerTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.MidiSongSelectTriggerTag
+import com.stevenfrew.beatprompter.cache.parse.tag.song.NoChordsTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.PauseTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.RatingTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.ScrollBeatModifierTag
@@ -49,17 +46,18 @@ import com.stevenfrew.beatprompter.cache.parse.tag.song.TimeTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.TitleTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.TransposeTag
import com.stevenfrew.beatprompter.cache.parse.tag.song.VariationsTag
+import com.stevenfrew.beatprompter.chord.ChordMap
import com.stevenfrew.beatprompter.comm.midi.message.MidiMessage
import com.stevenfrew.beatprompter.events.Events
import com.stevenfrew.beatprompter.graphics.DisplaySettings
import com.stevenfrew.beatprompter.graphics.LineGraphic
+import com.stevenfrew.beatprompter.graphics.Rect
import com.stevenfrew.beatprompter.graphics.ScreenString
import com.stevenfrew.beatprompter.midi.BeatBlock
import com.stevenfrew.beatprompter.midi.EventOffsetType
import com.stevenfrew.beatprompter.midi.TriggerOutputContext
import com.stevenfrew.beatprompter.song.ScrollingMode
import com.stevenfrew.beatprompter.song.Song
-import com.stevenfrew.beatprompter.song.chord.ChordMap
import com.stevenfrew.beatprompter.song.event.AudioEvent
import com.stevenfrew.beatprompter.song.event.BaseEvent
import com.stevenfrew.beatprompter.song.event.BeatEvent
@@ -68,7 +66,7 @@ import com.stevenfrew.beatprompter.song.event.CommentEvent
import com.stevenfrew.beatprompter.song.event.EndEvent
import com.stevenfrew.beatprompter.song.event.LineEvent
import com.stevenfrew.beatprompter.song.event.LinkedEvent
-import com.stevenfrew.beatprompter.song.event.MIDIEvent
+import com.stevenfrew.beatprompter.song.event.MidiEvent
import com.stevenfrew.beatprompter.song.event.PauseEvent
import com.stevenfrew.beatprompter.song.event.StartEvent
import com.stevenfrew.beatprompter.song.line.ImageLine
@@ -87,1269 +85,1296 @@ import kotlin.math.min
import kotlin.math.roundToInt
@ParseTags(
- ImageTag::class,
- PauseTag::class,
- SendMIDIClockTag::class,
- CommentTag::class,
- CountTag::class,
- StartOfHighlightTag::class,
- EndOfHighlightTag::class,
- BarMarkerTag::class,
- BarsTag::class,
- BeatsPerMinuteTag::class,
- BeatsPerBarTag::class,
- BarsPerLineTag::class,
- ScrollBeatModifierTag::class,
- ScrollBeatTag::class,
- BeatStartTag::class,
- BeatStopTag::class,
- AudioTag::class,
- MidiEventTag::class,
- ChordTag::class,
- StartOfVariationExclusionTag::class,
- EndOfVariationExclusionTag::class,
- StartOfVariationInclusionTag::class,
- EndOfVariationInclusionTag::class,
- StartOfChorusTag::class,
- VariationsTag::class,
- EndOfChorusTag::class,
- TransposeTag::class,
- ChordMapTag::class
+ ImageTag::class,
+ PauseTag::class,
+ SendMIDIClockTag::class,
+ CommentTag::class,
+ CountTag::class,
+ StartOfHighlightTag::class,
+ EndOfHighlightTag::class,
+ BarMarkerTag::class,
+ BarsTag::class,
+ BeatsPerMinuteTag::class,
+ BeatsPerBarTag::class,
+ BarsPerLineTag::class,
+ ScrollBeatModifierTag::class,
+ ScrollBeatTag::class,
+ BeatStartTag::class,
+ BeatStopTag::class,
+ AudioTag::class,
+ MidiEventTag::class,
+ ChordTag::class,
+ StartOfVariationExclusionTag::class,
+ EndOfVariationExclusionTag::class,
+ StartOfVariationInclusionTag::class,
+ EndOfVariationInclusionTag::class,
+ StartOfChorusTag::class,
+ VariationsTag::class,
+ EndOfChorusTag::class,
+ TransposeTag::class,
+ ChordMapTag::class,
+ NoChordsTag::class
)
@IgnoreTags(
- LegacyTag::class,
- TimeTag::class,
- MidiSongSelectTriggerTag::class,
- MidiProgramChangeTriggerTag::class,
- TitleTag::class,
- ArtistTag::class,
- KeyTag::class,
- RatingTag::class,
- TagTag::class,
- FilterOnlyTag::class
+ LegacyTag::class,
+ TimeTag::class,
+ MidiSongSelectTriggerTag::class,
+ MidiProgramChangeTriggerTag::class,
+ TitleTag::class,
+ ArtistTag::class,
+ KeyTag::class,
+ RatingTag::class,
+ TagTag::class,
+ FilterOnlyTag::class
)
/**
* Song file parser. This returns the full information for playing the song.
*/
class SongParser(
- private val songLoadInfo: SongLoadInfo,
- private val songLoadCancelEvent: SongLoadCancelEvent,
- private val songLoadHandler: Handler
+ private val songLoadInfo: SongLoadInfo,
+ private val supportFileResolver: SupportFileResolver,
+ private val songLoadCancelEvent: SongLoadCancelEvent? = null,
+ private val songLoadHandler: Handler? = null
) : SongFileParser(
- songLoadInfo.songFile,
- songLoadInfo.initialScrollMode,
- songLoadInfo.mixedModeActive,
- songLoadInfo.variation,
- true
+ songLoadInfo.songFile,
+ songLoadInfo.initialScrollMode,
+ songLoadInfo.mixedModeActive,
+ songLoadInfo.variation,
+ true
) {
- private val metronomeContext: MetronomeContext
- private val customCommentsUser: String
- private val showChords: Boolean
- private val showKey: Boolean
- private val showBpm: ShowBPMContext
- private val triggerContext: TriggerOutputContext
- private val nativeDeviceSettings: DisplaySettings
- private val initialMidiMessages = mutableListOf()
- private var stopAddingStartupItems = false
- private val startScreenComments = mutableListOf()
- private val events = mutableListOf()
- private val lines = LineList()
- private val rolloverBeats = mutableListOf()
- private val beatBlocks = mutableListOf()
- private val paint = Paint()
- private val font = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
- private val defaultHighlightColor: Int
- private val timePerBar: Long
- private val flatAudioFiles: List
-
- private var songHeight = 0
- private var midiBeatCounter: Int = 0
- private var lastBeatBlock: BeatBlock? = null
- private var beatsToAdjust: Int = 0
- private var currentBeat: Int = 0
- private var countIn: Int
- private var sendMidiClock = false
- private var songTime: Long = 0
- private var defaultMidiOutputChannel: Byte
- private var isInChorusSection = false
- private var pendingAudioTag: AudioTag? = null
- private var audioTagIndex: Int = 0
- private var chordMap: ChordMap? = if (songLoadInfo.songFile.firstChord != null) ChordMap(
- songLoadInfo.songFile.chords.toSet(),
- songLoadInfo.songFile.firstChord,
- songLoadInfo.songFile.key
- ).transpose(songLoadInfo.transposeShift) else null
-
- init {
- // All songFile info parsing errors count as our errors too.
- errors.addAll(songLoadInfo.songFile.errors)
-
- sendMidiClock = Preferences.sendMIDIClock
- countIn = Preferences.defaultCountIn
- metronomeContext = Preferences.metronomeContext
- defaultHighlightColor = Preferences.defaultHighlightColor
- customCommentsUser = Preferences.customCommentsUser
- showChords = Preferences.showChords
- triggerContext = Preferences.sendMIDITriggerOnStart
- val defaultMIDIOutputChannelPrefValue = Preferences.defaultMIDIOutputChannel
- defaultMidiOutputChannel = MidiMessage.getChannelFromBitmask(defaultMIDIOutputChannelPrefValue)
- showKey = Preferences.showKey && !songLoadInfo.songFile.key.isNullOrBlank()
- showBpm =
- if (songLoadInfo.songFile.bpm > 0.0) Preferences.showBPMContext else ShowBPMContext.No
-
- // Figure out the screen size
- nativeDeviceSettings = translateSourceDeviceSettingsToNative(
- songLoadInfo.sourceDisplaySettings,
- songLoadInfo.nativeDisplaySettings
- )
-
- // Start the progress message dialog
- songLoadHandler.obtainMessage(
- Events.SONG_LOAD_LINE_PROCESSED,
- 0, songLoadInfo.songFile.lines
- ).sendToTarget()
-
- val selectedVariation = songLoadInfo.variation
- val audioFilenamesForThisVariation =
- songLoadInfo.songFile.audioFiles[selectedVariation] ?: listOf()
- flatAudioFiles = audioFilenamesForThisVariation.mapNotNull {
- Cache.cachedCloudItems.getMappedAudioFiles(it).firstOrNull()
- }
- val lengthOfBackingTrack = flatAudioFiles.firstOrNull()?.duration ?: 0L
- var songTime =
- if (songLoadInfo.songFile.duration == Utils.TRACK_AUDIO_LENGTH_VALUE)
- lengthOfBackingTrack
- else
- songLoadInfo.songFile.duration
- if (songTime > 0 && songLoadInfo.songFile.totalPauseDuration > songTime) {
- errors.add(FileParseError(R.string.pauseLongerThanSong))
- ongoingBeatInfo = SongBeatInfo(scrollMode = ScrollingMode.Manual)
- currentLineBeatInfo = LineBeatInfo(ongoingBeatInfo)
- songTime = 0
- }
-
- timePerBar =
- if (songTime > 0L)
- (songTime.toDouble() / songLoadInfo.songFile.bars).toLong()
- else
- 0
- }
-
- override fun parseLine(line: TextFileLine): Boolean {
- if (songLoadCancelEvent.isCancelled)
- throw SongLoadCancelledException()
- if (!super.parseLine(line))
- return false
-
- val chordTags = line.tags.filterIsInstance()
- val nonChordTags = line.tags.filter { it !is ChordTag }
- val chordsFound = chordTags.isNotEmpty()
- val chordsFoundButNotShowingThem = !showChords && chordsFound
- val tags = if (showChords) line.tags.toList() else nonChordTags
- val tagSequence = tags.asSequence()
-
- val transposeTags = tagSequence.filterIsInstance()
- transposeTags.forEach {
- try {
- chordMap = chordMap?.transpose(it.value)
- } catch (e: Exception) {
- errors.add(FileParseError(it, e))
- }
- }
-
- val chordMapTags = tagSequence.filterIsInstance()
- chordMapTags.forEach {
- chordMap = chordMap?.addChordMapping(it.from, it.to)
- }
-
- var workLine = line.lineWithNoTags
-
- // Generate clicking beats if the metronome is on.
- // The "on when no track" logic will be performed during song playback.
- val metronomeOn =
- metronomeContext === MetronomeContext.On || metronomeContext === MetronomeContext.OnWhenNoTrack
-
- var imageTag = tagSequence.filterIsInstance().firstOrNull()
-
- val startOfChorusTag = tagSequence.filterIsInstance().firstOrNull()
- val thisLineIsInChorus = startOfChorusTag != null || isInChorusSection
- val endOfChorusTag = tagSequence.filterIsInstance().firstOrNull()
- isInChorusSection =
- if (endOfChorusTag != null) {
- if (startOfChorusTag != null)
- endOfChorusTag.position < startOfChorusTag.position
- else
- false
- } else
- startOfChorusTag != null || isInChorusSection
-
- if (!sendMidiClock)
- sendMidiClock = tags.any { it is SendMIDIClockTag }
-
- if (!stopAddingStartupItems)
- countIn = tags.filterIsInstance().firstOrNull()?.count ?: countIn
-
- tags
- .filterIsInstance()
- .map {
- Song.Comment(
- it.comment,
- it.audience,
- nativeDeviceSettings.screenSize,
- paint,
- font
- )
- }
- .filter { it.isIntendedFor(customCommentsUser) }
- .forEach {
- if (stopAddingStartupItems)
- events.add(CommentEvent(songTime, it))
- else
- startScreenComments.add(it)
- }
-
- // If a line has a number of bars defined, we really should treat it as a line, even if
- // is blank.
- val shorthandBarTag = tagSequence
- .filterIsInstance()
- .firstOrNull()
- val barsTag = tagSequence
- .filterIsInstance()
- .firstOrNull()
- // Contains only tags? Or contains nothing? Don't use it as a blank line.
- // BUT! If there are bar indicators of any kind, use the blank line.
- val isLineContent = (workLine.isNotEmpty()
- || chordsFoundButNotShowingThem
- || chordsFound
- || imageTag != null
- || shorthandBarTag != null
- || (barsTag != null && barsTag.bars > 0))
-
- val pauseTag = tagSequence
- .filterIsInstance()
- .firstOrNull()
-
- val isLineContentOrPause = isLineContent || pauseTag != null
-
- tags
- .filterIsInstance()
- .forEach {
- if (stopAddingStartupItems || isLineContentOrPause)
- events.add(it.toMIDIEvent(songTime))
- else {
- initialMidiMessages.addAll(it.messages)
- if (it.offset.amount != 0)
- errors.add(FileParseError(it, R.string.midi_offset_before_first_line))
- }
- }
-
- // An audio tag is only processed when there is actual line content.
- // But the audio tag can be defined on a line without content.
- // In which case it is "pending", to be applied when line content is found.
- pendingAudioTag =
- if (tags.filterIsInstance().any() && !songLoadInfo.noAudio) getVariationAudioTag(
- audioTagIndex++
- ) else pendingAudioTag
-
- if (isLineContentOrPause) {
- // We definitely have a line!
- // So now is when we want to create the count-in (if any)
- if (countIn > 0) {
- val countInEvents = generateCountInEvents(
- countIn,
- metronomeContext === MetronomeContext.DuringCountIn || metronomeOn
- )
- events.addAll(countInEvents.mEvents)
- songTime = countInEvents.mBlockEndTime
- countIn = 0
- }
-
- // If there is a beatstart, we add a StartEvent. This functions as a "current event" that
- // the song can be set to, then advanced from. The "current event" is not processed when we
- // "press play" to start the song (it is expected that it ALREADY has been processed).
- // StartEvents function as simply dummy starting-point "current" events.
- val beatStartTag = tagSequence.filterIsInstance().firstOrNull()
- if (beatStartTag != null)
- events.add(StartEvent(songTime))
-
- pendingAudioTag?.also {
- // Make sure file exists.
- val mappedTracks =
- Cache.cachedCloudItems.getMappedAudioFiles(it.normalizedFilename)
- if (mappedTracks.isEmpty())
- errors.add(FileParseError(it, R.string.cannotFindAudioFile, it.normalizedFilename))
- else if (mappedTracks.size > 1)
- errors.add(
- FileParseError(
- it,
- R.string.multipleFilenameMatches,
- it.normalizedFilename
- )
- )
- else {
- val audioFile = mappedTracks.first()
- if (!audioFile.file.exists())
- errors.add(
- FileParseError(
- it,
- R.string.cannotFindAudioFile,
- it.normalizedFilename
- )
- )
- else
- events.add(
- AudioEvent(
- songTime,
- audioFile,
- it.volume,
- !stopAddingStartupItems
- )
- )
- }
- }
- // Clear the pending tag.
- pendingAudioTag = null
-
- // Any comments or MIDI events from here will be part of the song,
- // rather than startup events.
- stopAddingStartupItems = true
-
- if (imageTag != null && (workLine.isNotBlank() || chordsFound))
- errors.add(FileParseError(line.lineNumber, R.string.text_found_with_image))
-
- // Measuring a blank line will result in a 0x0 measurement, so we
- // need to have SOMETHING to measure. A nice wee "down arrow" should look OK.
- if (workLine.isBlank() && (!chordsFound || chordsFoundButNotShowingThem))
- workLine = "▼"
-
- // Generate pause events if required (may return null)
- val pauseEvents = generatePauseEvents(songTime, pauseTag)
- val paused = pauseEvents?.any() == true
- if (paused || currentLineBeatInfo.scrollMode !== ScrollingMode.Beat)
- rolloverBeats.clear()
-
- if (isLineContent) {
- // First line should always have a time of zero, so that if the user scrolls
- // back to the start of the song, it still picks up any count-in beat events.
- val lineStartTime = if (lines.isEmpty()) 0L else songTime
-
- // If the first line is a pause event, we need to adjust the total line time accordingly
- // to include any count-in
- val addToPause = if (lines.isEmpty()) songTime else 0L
-
- // Generate beat events (may return null in smooth mode)
- pauseEvents?.maxOf { it.eventTime }
- val beatEvents = if (paused || currentLineBeatInfo.scrollMode === ScrollingMode.Smooth)
- EventBlock(listOf(), pauseEvents?.maxOf { it.eventTime } ?: 0)
- else
- generateBeatEvents(songTime, metronomeOn)
-
- // Calculate how long this line will last for
- val lineDuration = calculateLineDuration(
- pauseTag,
- addToPause,
- lineStartTime,
- beatEvents
- )
-
- // Calculate the start and stop scroll times for this line
- val startAndStopScrollTimes = calculateStartAndStopScrollTimes(
- pauseTag,
- lineStartTime + addToPause,
- lineDuration,
- beatEvents,
- songLoadInfo.audioLatency
- )
-
- // Create the line
- var lineObj: Line? = null
- if (imageTag != null) {
- val imageFiles =
- Cache.cachedCloudItems.getMappedImageFiles(imageTag.filename)
- if (imageFiles.isNotEmpty())
- try {
- lineObj = ImageLine(
- imageFiles.first(),
- imageTag.scalingMode,
- lineStartTime,
- lineDuration,
- currentLineBeatInfo.scrollMode,
- nativeDeviceSettings,
- songHeight,
- thisLineIsInChorus,
- startAndStopScrollTimes
- )
- } catch (t: Throwable) {
- // Bitmap loading could cause error here. Even OutOfMemory!
- errors.add(FileParseError(imageTag, t))
- }
- else {
- workLine = BeatPrompter.appResources.getString(R.string.missing_image_file_warning)
- errors.add(FileParseError(imageTag, R.string.missing_image_file_warning))
- imageTag = null
- }
- }
- if (imageTag == null)
- lineObj = TextLine(
- workLine,
- tags,
- lineStartTime,
- lineDuration,
- currentLineBeatInfo.scrollMode,
- nativeDeviceSettings,
- lines
- .filterIsInstance()
- .lastOrNull()?.trailingHighlightColor,
- songHeight,
- thisLineIsInChorus,
- startAndStopScrollTimes,
- chordMap,
- songLoadCancelEvent
- )
-
- if (lineObj != null) {
- lines.add(lineObj)
- events.add(LineEvent(lineObj.lineTime, lineObj))
-
- songHeight += lineObj.measurements.lineHeight
-
- // If a pause is going to be generated, then we don't need beats.
- if (pauseEvents == null) {
- // Otherwise, add any generated beats
- if (beatEvents != null) {
- events.addAll(beatEvents.mEvents)
- songTime = beatEvents.mBlockEndTime
- }
- // Otherwise, forget it, just bump up the song time
- else
- songTime += lineDuration
- }
- }
- }
- // Now add the pause events to the song (if required).
- if (pauseEvents != null && pauseTag != null) {
- events.addAll(pauseEvents)
- songTime += pauseTag.duration
- }
- }
- if (!isLineContent || currentLineBeatInfo.scrollMode !== ScrollingMode.Beat)
- // If there is no actual line data (or if the line is a manual mode line), then the scroll beat offset never took effect.
- // Clear it so that the next line (which MIGHT be a proper line) doesn't take it into account.
- currentLineBeatInfo = LineBeatInfo(
- currentLineBeatInfo.beats,
- currentLineBeatInfo.bpl,
- currentLineBeatInfo.bpb,
- currentLineBeatInfo.bpm,
- currentLineBeatInfo.scrollBeat,
- currentLineBeatInfo.lastScrollBeatTotalOffset,
- 0, currentLineBeatInfo.scrollMode
- )
-
- songLoadHandler.obtainMessage(
- Events.SONG_LOAD_LINE_PROCESSED,
- line.lineNumber, songLoadInfo.songFile.lines
- ).sendToTarget()
- return true
- }
-
- private fun getVariationAudioTag(index: Int): AudioTag? {
- val tags = variationAudioTags[variation]
- if ((tags?.count() ?: 0) > index)
- return tags!![index]
- return null
- }
-
- override fun getResult(): Song {
- // Song has no lines? Make a dummy line so we don't have to check for null everywhere in the code.
- if (lines.isEmpty())
- throw InvalidBeatPrompterFileException(R.string.no_lines_in_song_file)
-
- val lineSequence = lines.asSequence()
- val smoothMode = lineSequence.filter { it.scrollMode == ScrollingMode.Smooth }.any()
-
- val startScreenStrings = createStartScreenStrings()
- val totalStartScreenTextHeight = startScreenStrings.first.sumOf { it.height }
-
- // In smooth scrolling mode, the display will start scrolling immediately.
- // This is an essential feature of smooth scrolling mode, yet causes a problem: the first line
- // will almost immediately become obscured, just as you are performing it.
- // To combat this, there will an initial blank "buffer zone", created by offsetting the graphical
- // display by a number of pixels.
- val smoothScrollOffset =
- if (smoothMode)
- // Obviously this will only be required if the song cannot fit entirely onscreen.
- if (songHeight > nativeDeviceSettings.usableScreenHeight)
- min(lineSequence.map { it.measurements.lineHeight }.maxByOrNull { it }
- ?: 0, (nativeDeviceSettings.screenSize.height() / 3.0).toInt())
- else
- 0
- else
- 0
-
- // Get all required audio info ...
- val audioEvents = events.filterIsInstance()
-
- // Allocate graphics objects.
- val maxGraphicsRequired = getMaximumGraphicsRequired(nativeDeviceSettings.screenSize.height())
- val lineGraphics = CircularGraphicsList()
- repeat(maxGraphicsRequired) {
- lineGraphics.add(LineGraphic(getBiggestLineSize(it, maxGraphicsRequired)))
- }
-
- // There may be no lines! So we have to check ...
- if (lineGraphics.isNotEmpty()) {
- var graphic: LineGraphic = lineGraphics.first()
- lines.forEach { line ->
- repeat(line.measurements.lines) {
- line.allocateGraphic(graphic)
- graphic = graphic.nextGraphic
- }
- }
- }
-
- val beatCounterHeight = nativeDeviceSettings.beatCounterRect.height()
- val maxSongTitleWidth = nativeDeviceSettings.screenSize.width() * 0.9f
- val maxSongTitleHeight = beatCounterHeight * 0.9f
- val vMargin = (beatCounterHeight - maxSongTitleHeight) / 2.0f
- val songTitleHeader = ScreenString.create(
- songLoadInfo.songFile.title,
- paint,
- maxSongTitleWidth.toInt(),
- maxSongTitleHeight.toInt(),
- Utils.makeHighlightColour(Color.BLACK, 0x80.toByte()),
- font,
- false
- )
- val extraMargin = (maxSongTitleHeight - songTitleHeader.height) / 2.0f
- val x = ((nativeDeviceSettings.screenSize.width() - songTitleHeader.width) / 2.0).toFloat()
- val y = beatCounterHeight - (extraMargin + songTitleHeader.descenderOffset.toFloat() + vMargin)
- val songTitleHeaderLocation = PointF(x, y)
-
- // First of all, find beat events that have the "click" flag set and
- // add a click event (necessary because we want to offset the click by
- // the audio latency without offsetting the beat).
- val eventsWithClicks = generateClickEvents(events)
- // Now offset any MIDI events that have an offset.
- val midiOffsetEventList = offsetMIDIEvents(eventsWithClicks, errors)
- // And offset non-audio events by the audio latency offset.
- val audioLatencyCompensatedEventList =
- compensateForAudioLatency(midiOffsetEventList, Utils.milliToNano(songLoadInfo.audioLatency))
-
- // OK, now sort all events by time, and type within time
- val sortedEventList = sortEvents(audioLatencyCompensatedEventList).toMutableList()
-
- // Songs need a "first event" to have as their "current event". Without this, the initial
- // "current event" could be the EndEvent!
- sortedEventList.add(0, StartEvent())
-
- // Now we need to figure out which lines should NOT scroll offscreen.
- val noScrollLines = mutableListOf()
- val lastLineIsBeat = lines.lastOrNull()?.scrollMode == ScrollingMode.Beat
- if (lastLineIsBeat) {
- noScrollLines.add(lines.last())
- // Why was I removing this? It breaks highlighting the last line ...
- // sortedEventList.removeAt(sortedEventList.indexOfLast { it is LineEvent })
- } else if (smoothMode) {
- var availableScreenHeight = nativeDeviceSettings.usableScreenHeight - smoothScrollOffset
- val lineEvents = sortedEventList.filterIsInstance()
- for (lineEvent in lineEvents.reversed()) {
- availableScreenHeight -= lineEvent.line.measurements.lineHeight
- if (availableScreenHeight >= 0) {
- noScrollLines.add(lineEvent.line)
- sortedEventList.remove(lineEvent)
- } else
- break
- }
- }
-
- // To generate the EndEvent, we need to know the time that the
- // song ends. This could be the time of the final generated event,
- // but there might still be an audio file playing, so find out
- // when the last track ends ...
- val lastAudioEndTime = sortedEventList
- .asSequence()
- .filterIsInstance()
- .map { it.audioFile.duration + it.eventTime }
- .maxOrNull()
- sortedEventList.add(EndEvent(max(lastAudioEndTime ?: 0L, songTime)))
-
- // Now build the final event list.
- val firstEvent = LinkedEvent(sortedEventList)
-
- // Calculate the last position that we can scroll to.
- val scrollEndPixel = calculateScrollEndPixel(smoothMode, smoothScrollOffset)
-
- if ((triggerContext == TriggerOutputContext.Always)
- || (triggerContext == TriggerOutputContext.ManualStartOnly && !songLoadInfo.wasStartedByMidiTrigger)
- ) {
- initialMidiMessages.addAll(
- songLoadInfo.songFile.programChangeTrigger.getMIDIMessages(
- defaultMidiOutputChannel
- )
- )
- initialMidiMessages.addAll(
- songLoadInfo.songFile.songSelectTrigger.getMIDIMessages(
- defaultMidiOutputChannel
- )
- )
- }
-
- return Song(
- songLoadInfo.songFile,
- nativeDeviceSettings,
- firstEvent,
- lines,
- audioEvents,
- initialMidiMessages,
- beatBlocks,
- sendMidiClock,
- startScreenStrings.first,
- startScreenStrings.second,
- totalStartScreenTextHeight,
- songLoadInfo.wasStartedByBandLeader,
- songLoadInfo.nextSong,
- smoothScrollOffset,
- songHeight,
- scrollEndPixel,
- noScrollLines,
- nativeDeviceSettings.beatCounterRect,
- songTitleHeader,
- songTitleHeaderLocation,
- songLoadInfo.loadId,
- songLoadInfo.audioLatency
- )
- }
-
- private fun calculateScrollEndPixel(smoothMode: Boolean, smoothScrollOffset: Int): Int {
- val manualDisplayEnd = max(0, songHeight - nativeDeviceSettings.usableScreenHeight)
- val beatDisplayEnd =
- lines.lastOrNull { it.scrollMode === ScrollingMode.Beat }?.songPixelPosition
- return if (smoothMode)
- manualDisplayEnd + smoothScrollOffset//+smoothScrollEndOffset
- else if (beatDisplayEnd != null)
- if (beatDisplayEnd + nativeDeviceSettings.usableScreenHeight > songHeight)
- beatDisplayEnd
- else
- manualDisplayEnd
- else
- manualDisplayEnd
- }
-
- private fun getBiggestLineSize(index: Int, modulus: Int): Rect {
- var maxHeight = 0
- var maxWidth = 0
- var lineCount = 0
- lines.forEach {
- for (lh in it.measurements.graphicHeights) {
- if (lineCount % modulus == index) {
- maxHeight = max(maxHeight, lh)
- maxWidth = max(maxWidth, it.measurements.lineWidth)
- }
- ++lineCount
- }
- }
- return Rect(0, 0, maxWidth - 1, maxHeight - 1)
- }
-
- private fun getMaximumGraphicsRequired(screenHeight: Int): Int {
- var maxLines = 0
- repeat(lines.size) { start ->
- var heightCounter = 0
- var lineCounter = 0
- for (f in start until lines.size) {
- if (heightCounter < screenHeight) {
- // Assume height of first line to be 1 pixel
- // This is the state of affairs when the top line is almost
- // scrolled offscreen, but not quite.
- var lineHeight = 1
- if (lineCounter > 0)
- lineHeight = lines[f].measurements.lineHeight
- heightCounter += lineHeight
- lineCounter += lines[f].measurements.lines
- }
- }
- maxLines = max(maxLines, lineCounter)
- }
- return maxLines
- }
-
- private fun generateBeatEvents(startTime: Long, click: Boolean): EventBlock? {
- if (currentLineBeatInfo.scrollMode === ScrollingMode.Smooth)
- return null
- var eventTime = startTime
- val beatEvents = mutableListOf()
- var beatThatWeWillScrollOn = 0
- val currentTimePerBeat = Utils.nanosecondsPerBeat(currentLineBeatInfo.bpm)
- val rolloverBeatCount = rolloverBeats.size
- var rolloverBeatsApplied = 0
- // We have N beats to adjust.
- // For the previous N beat events, set the BPB to the new BPB.
- if (beatsToAdjust > 0)
- events.filterIsInstance().takeLast(beatsToAdjust).forEach {
- it.bpb = currentLineBeatInfo.bpb
- }
- beatsToAdjust = 0
-
- var currentLineBeat = 0
- while (currentLineBeat < currentLineBeatInfo.beats) {
- val beatsRemaining = currentLineBeatInfo.beats - currentLineBeat
- beatThatWeWillScrollOn = if (beatsRemaining > currentLineBeatInfo.bpb)
- -1
- else
- (currentBeat + (beatsRemaining - 1)) % currentLineBeatInfo.bpb
- var rolloverBPB = 0
- var rolloverBeatLength: Long = 0
- val beatEvent = if (rolloverBeats.isEmpty())
- BeatEvent(
- eventTime,
- currentLineBeatInfo.bpm,
- currentLineBeatInfo.bpb,
- currentBeat,
- click,
- beatThatWeWillScrollOn
- )
- else {
- val rolloverBeatEvent = rolloverBeats.removeAt(0)
- val modifiedRolloverBeatEvent = BeatEvent(
- rolloverBeatEvent.eventTime,
- rolloverBeatEvent.bpm,
- rolloverBeatEvent.bpb,
- rolloverBeatEvent.beat,
- rolloverBeatEvent.click,
- beatThatWeWillScrollOn
- )
- rolloverBPB = modifiedRolloverBeatEvent.bpb
- rolloverBeatsApplied += 1
- rolloverBeatLength = Utils.nanosecondsPerBeat(modifiedRolloverBeatEvent.bpm)
- modifiedRolloverBeatEvent
- }
- beatEvents.add(beatEvent)
- val beatTimeLength = if (rolloverBeatLength == 0L) currentTimePerBeat else rolloverBeatLength
- val nanoPerBeat = beatTimeLength / 4.0
- // generate MIDI beats.
- if (lastBeatBlock == null || nanoPerBeat != lastBeatBlock!!.nanoPerBeat) {
- lastBeatBlock = BeatBlock(beatEvent.eventTime, midiBeatCounter++, nanoPerBeat)
- beatBlocks.add(lastBeatBlock!!)
- }
-
- eventTime += beatTimeLength
- currentBeat++
- if (currentBeat == (if (rolloverBPB > 0) rolloverBPB else currentLineBeatInfo.bpb))
- currentBeat = 0
- ++currentLineBeat
- }
-
- val beatsThisLine = currentLineBeatInfo.beats - rolloverBeatCount + rolloverBeatsApplied
- val simpleBeatsThisLine =
- (currentLineBeatInfo.bpb * currentLineBeatInfo.bpl) - currentLineBeatInfo.lastScrollBeatTotalOffset
- if (beatsThisLine > simpleBeatsThisLine) {
- // We need to store some information so that the next line can adjust the rollover beats.
- beatsToAdjust = currentLineBeatInfo.beats - simpleBeatsThisLine
- } else if (beatsThisLine < simpleBeatsThisLine) {
- // We need to generate a few beats to store for the next line to use.
- rolloverBeats.clear()
- var rolloverCurrentBeat = currentBeat
- var rolloverCurrentTime = eventTime
- for (f in beatsThisLine until simpleBeatsThisLine) {
- rolloverBeats.add(
- BeatEvent(
- rolloverCurrentTime,
- currentLineBeatInfo.bpm,
- currentLineBeatInfo.bpb,
- rolloverCurrentBeat++,
- click,
- beatThatWeWillScrollOn
- )
- )
- rolloverCurrentTime += currentTimePerBeat
- if (rolloverCurrentBeat == currentLineBeatInfo.bpb)
- rolloverCurrentBeat = 0
- }
- }
- return EventBlock(beatEvents, eventTime)
- }
-
- private fun generatePauseEvents(startTime: Long, pauseTag: PauseTag?): List? {
- if (pauseTag == null)
- return null
- // pauseTime is in milliseconds.
- // We don't want to generate thousands of events, so let's say every 1/10th of a second.
- var eventTime = startTime
- val pauseEvents = mutableListOf()
- val deciSeconds = ceil(Utils.nanoToMilli(pauseTag.duration).toDouble() / 100.0).toInt()
- val remainder = pauseTag.duration - Utils.milliToNano(deciSeconds * 100)
- val oneDeciSecondInNanoseconds = Utils.milliToNano(100)
- eventTime += remainder
- repeat(deciSeconds) {
- val pauseEvent = PauseEvent(eventTime, deciSeconds, it)
- pauseEvents.add(pauseEvent)
- eventTime += oneDeciSecondInNanoseconds
- }
- return pauseEvents
- }
-
- private fun generateCountInEvents(countBars: Int, click: Boolean): EventBlock {
- val countInEvents = mutableListOf()
- var startTime = 0L
- if (countBars > 0) {
- if (currentLineBeatInfo.bpm > 0.0) {
- val countbpm = currentLineBeatInfo.bpm
- val countbpb = currentLineBeatInfo.bpb
- val nanoPerBeat = Utils.nanosecondsPerBeat(countbpm)
- repeat(countBars) { bar ->
- repeat(countbpb) { beat ->
- countInEvents.add(
- BeatEvent(
- startTime,
- currentLineBeatInfo.bpm,
- currentLineBeatInfo.bpb,
- beat,
- click,
- if (bar == countBars - 1) countbpb - 1 else -1
- )
- )
- startTime += nanoPerBeat
- }
- }
- }
- }
- return EventBlock(countInEvents, startTime)
- }
-
- /**
- * Based on the difference in screen size/resolution/orientation, we will alter the min/max font size of our native settings.
- */
- private fun translateSourceDeviceSettingsToNative(
- sourceSettings: DisplaySettings,
- nativeSettings: DisplaySettings
- ): DisplaySettings {
- val sourceScreenSize = sourceSettings.screenSize
- val sourceRatio = sourceScreenSize.width().toDouble() / sourceScreenSize.height().toDouble()
- val screenWillRotate = nativeSettings.orientation != sourceSettings.orientation
- val nativeScreenSize = if (screenWillRotate)
- Rect(0, 0, nativeSettings.screenSize.height(), nativeSettings.screenSize.width())
- else
- nativeSettings.screenSize
- val nativeRatio = nativeScreenSize.width().toDouble() / nativeScreenSize.height().toDouble()
- val minRatio = min(nativeRatio, sourceRatio)
- val maxRatio = max(nativeRatio, sourceRatio)
- val ratioMultiplier = minRatio / maxRatio
- var minimumFontSize = sourceSettings.minimumFontSize
- var maximumFontSize = sourceSettings.maximumFontSize
- minimumFontSize *= ratioMultiplier.toFloat()
- maximumFontSize *= ratioMultiplier.toFloat()
- if (minimumFontSize > maximumFontSize) {
- errors.add(FileParseError(0, R.string.fontSizesAllMessedUp))
- maximumFontSize = minimumFontSize
- }
- return DisplaySettings(
- sourceSettings.orientation,
- minimumFontSize,
- maximumFontSize,
- nativeScreenSize,
- sourceSettings.showBeatCounter
- )
- }
-
- private fun createStartScreenStrings(): Pair, ScreenString?> {
- // As for the start screen display (title/artist/comments/"press go"),
- // the title should take up no more than 20% of the height, the artist
- // no more than 10%, also 10% for the "press go" message.
- // The rest of the space is allocated for the comments and error messages,
- // each line no more than 10% of the screen height.
- val startScreenStrings = mutableListOf()
- var availableScreenHeight = nativeDeviceSettings.screenSize.height()
- var nextSongString: ScreenString? = null
- val boldFont = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
- if (songLoadInfo.nextSong.isNotBlank()) {
- // OK, we have a next song title to display.
- // This should take up no more than 15% of the screen.
- // But that includes a border, so use 13 percent for the text.
- val eightPercent = (nativeDeviceSettings.screenSize.height() * 0.13).toInt()
- val nextSong = songLoadInfo.nextSong
- val fullString = ">>> $nextSong >>>"
- nextSongString = ScreenString.create(
- fullString,
- paint,
- nativeDeviceSettings.screenSize.width(),
- eightPercent,
- Color.BLACK,
- boldFont,
- true
- )
- availableScreenHeight -= (nativeDeviceSettings.screenSize.height() * 0.15f).toInt()
- }
- val tenPercent = (availableScreenHeight / 10.0).toInt()
- val twentyPercent = (availableScreenHeight / 5.0).toInt()
- startScreenStrings.add(
- ScreenString.create(
- songLoadInfo.songFile.title,
- paint,
- nativeDeviceSettings.screenSize.width(),
- twentyPercent,
- Color.YELLOW,
- boldFont,
- true
- )
- )
- if (songLoadInfo.songFile.artist.isNotBlank())
- startScreenStrings.add(
- ScreenString.create(
- songLoadInfo.songFile.artist,
- paint,
- nativeDeviceSettings.screenSize.width(),
- tenPercent,
- Color.YELLOW,
- boldFont,
- true
- )
- )
- val commentLines = mutableListOf()
- for (c in startScreenComments)
- commentLines.add(c.mText)
- val nonBlankCommentLines = mutableListOf()
- for (commentLine in commentLines)
- if (commentLine.trim().isNotEmpty())
- nonBlankCommentLines.add(commentLine.trim())
- val uniqueErrors = errors.asSequence().distinct().sortedBy { it.lineNumber }.toList()
- var errorCount = uniqueErrors.size
- var messages = min(errorCount, 6) + nonBlankCommentLines.size
- val showBPM = showBpm != ShowBPMContext.No
- if (showBPM)
- ++messages
- if (showKey)
- ++messages
- if (messages > 0) {
- val remainingScreenSpace = nativeDeviceSettings.screenSize.height() - twentyPercent * 2
- var spacePerMessageLine = floor((remainingScreenSpace / messages).toDouble()).toInt()
- spacePerMessageLine = min(spacePerMessageLine, tenPercent)
- var errorCounter = 0
- for (error in uniqueErrors) {
- startScreenStrings.add(
- ScreenString.create(
- error.toString(),
- paint,
- nativeDeviceSettings.screenSize.width(),
- spacePerMessageLine,
- Color.RED,
- font,
- false
- )
- )
- ++errorCounter
- --errorCount
- if (errorCounter == 5 && errorCount > 0) {
- startScreenStrings.add(
- ScreenString.create(
- String.format(
- BeatPrompter.appResources.getString(R.string.otherErrorCount),
- errorCount
- ),
- paint,
- nativeDeviceSettings.screenSize.width(),
- spacePerMessageLine,
- Color.RED,
- font,
- false
- )
- )
- break
- }
- }
- for (nonBlankComment in nonBlankCommentLines)
- startScreenStrings.add(
- ScreenString.create(
- nonBlankComment,
- paint,
- nativeDeviceSettings.screenSize.width(),
- spacePerMessageLine,
- Color.WHITE,
- font,
- false
- )
- )
- if (showKey) {
- val keyString =
- BeatPrompter.appResources.getString(R.string.keyPrefix) + ": " + songLoadInfo.songFile.key
- startScreenStrings.add(
- ScreenString.create(
- keyString,
- paint,
- nativeDeviceSettings.screenSize.width(),
- spacePerMessageLine,
- Color.CYAN,
- font,
- false
- )
- )
- }
- if (showBpm != ShowBPMContext.No) {
- val rounded =
- showBpm == ShowBPMContext.Rounded || songLoadInfo.songFile.bpm == songLoadInfo.songFile.bpm.toInt()
- .toDouble()
- var bpmString = BeatPrompter.appResources.getString(R.string.bpmPrefix) + ": "
- bpmString += if (rounded)
- songLoadInfo.songFile.bpm.roundToInt()
- else
- songLoadInfo.songFile.bpm
- startScreenStrings.add(
- ScreenString.create(
- bpmString,
- paint,
- nativeDeviceSettings.screenSize.width(),
- spacePerMessageLine,
- Color.CYAN,
- font,
- false
- )
- )
- }
- }
- if (songLoadInfo.songLoadMode !== ScrollingMode.Manual)
- startScreenStrings.add(
- ScreenString.create(
- BeatPrompter.appResources.getString(R.string.tapTwiceToStart),
- paint,
- nativeDeviceSettings.screenSize.width(),
- tenPercent,
- Color.GREEN,
- boldFont,
- true
- )
- )
- return startScreenStrings to nextSongString
- }
-
- private fun calculateStartAndStopScrollTimes(
- pauseTag: PauseTag?,
- lineStartTime: Long,
- lineDuration: Long,
- currentBeatEvents: EventBlock?,
- audioLatency: Int
- ): Pair {
- // Calculate when this line should start scrolling
- val startScrollTime =
- when (currentLineBeatInfo.scrollMode) {
- // Smooth mode? Start scrolling instantly.
- ScrollingMode.Smooth -> songTime
- else ->
- // Pause line? Start scrolling after 95% of the pause has elapsed.
- if (pauseTag != null)
- lineStartTime + (pauseTag.duration * 0.95).toLong()
- // Beat line? Start scrolling on the last beat.
- else
- currentBeatEvents!!.mEvents.lastOrNull()?.eventTime ?: songTime
- // (Manual mode ignores these scroll values)
- }
- // Calculate when the line should stop scrolling
- val stopScrollTime =
- when (currentLineBeatInfo.scrollMode) {
- // Smooth mode? It should stop scrolling once the allocated time has elapsed.
- ScrollingMode.Smooth -> songTime + lineDuration
- else ->
- // Pause line? It should stop scrolling when the pause has ran out
- if (pauseTag != null)
- lineStartTime + pauseTag.duration
- // Beat line? It should stop scrolling after the final beat
- else
- currentBeatEvents!!.mBlockEndTime
- // (Manual mode ignores these values)
- }
-
- // Events are going to be offset later to compensate for audio latency.
- // Lines, however, won't be. So we need to compensate NOW.
- val audioLatencyOffset = Utils.milliToNano(audioLatency)
- return Pair(
- startScrollTime + audioLatencyOffset,
- stopScrollTime + audioLatencyOffset
- )
- }
-
- private fun calculateLineDuration(
- pauseTag: PauseTag?,
- addToPause: Long,
- lineStartTime: Long,
- currentBeatEvents: EventBlock?
- ): Long {
- // Calculate how long this line will last for.
- return when {
- // Pause line? Lasts as long as the pause!
- pauseTag != null -> pauseTag.duration + addToPause
- // Smooth line? We've counted the bars, so do the sums.
- currentLineBeatInfo.scrollMode == ScrollingMode.Smooth && timePerBar > 0 -> timePerBar * currentLineBeatInfo.bpl
- // Beat line? The result of generateBeatEvents will contain the time
- // that the beats end, so subtract the start time from that to get our duration.
- else -> currentBeatEvents!!.mBlockEndTime - lineStartTime
- // (Manual mode ignores these scroll values)
- }
- }
-
- /**
- * An "event block" is simply a list of events, in chronological order, and a time that marks the point
- * at which the block ends. Note that the end time is not necessarily the same as the time of the last
- * event. For example, a block of five beat events (where each beat last n nanoseconds) will contain
- * five events with the times of n*0, n*1, n*2, n*3, n*4, and the end time will be n*5, as a "beat event"
- * actually covers the duration of the beat.
- */
- private data class EventBlock(val mEvents: List, val mBlockEndTime: Long)
-
- private class LineList : ArrayList() {
- override fun add(element: Line): Boolean {
- val lastOrNull = lastOrNull()
- lastOrNull?.nextLine = element
- element.previousLine = lastOrNull
- return super.add(element)
- }
- }
-
- private class CircularGraphicsList : ArrayList() {
- override fun add(element: LineGraphic): Boolean {
- lastOrNull()?.nextGraphic = element
- val result = super.add(element)
- last().nextGraphic = first()
- return result
- }
- }
-
- private fun sortEvents(eventList: List): List {
- // Sort all events by time, and by type within that.
- return eventList.sortedWith { e1, e2 ->
- when {
- e1.eventTime > e2.eventTime -> 1
- e1.eventTime < e2.eventTime -> -1
- else -> {
- // StartEvents must appear before anything else, as their
- // function is a "starting point" for song processing to
- // continue from.
- if (e1 is StartEvent && e2 is StartEvent)
- 0
- else if (e1 is StartEvent)
- -1
- else if (e2 is StartEvent)
- 1
- // MIDI events are most important. We want to
- // these first at any given time for maximum MIDI
- // responsiveness
- else if (e1 is MIDIEvent && e2 is MIDIEvent)
- 0
- else if (e1 is MIDIEvent)
- -1
- else if (e2 is MIDIEvent)
- 1
- // AudioEvents are next-most important. We want to process
- // these first at any given time for maximum audio
- // responsiveness
- else if (e1 is AudioEvent && e2 is AudioEvent)
- 0
- else if (e1 is AudioEvent)
- -1
- else if (e2 is AudioEvent)
- 1
- // Now LineEvents for maximum visual responsiveness
- else if (e1 is LineEvent && e2 is LineEvent)
- 0
- else if (e1 is LineEvent)
- -1
- else if (e2 is LineEvent)
- 1
- // Remaining order doesn't really matter
- else
- 0
- }
- }
- }
- }
-
- companion object {
- /**
- * Each MIDIEvent might have an offset. Process that here.
- */
- private fun offsetMIDIEvents(
- events: List,
- errors: MutableList
- ): List {
- val beatEvents =
- events.asSequence().filterIsInstance().sortedBy { it.eventTime }.toList()
- return events.map {
- if (it is MIDIEvent)
- offsetMIDIEvent(it, beatEvents, errors)
- else
- it
- }
- }
-
- /**
- * Each MIDIEvent might have an offset. Process that here.
- */
- private fun offsetMIDIEvent(
- midiEvent: MIDIEvent,
- beatEvents: List,
- errors: MutableList
- ): MIDIEvent =
- if (midiEvent.offset.amount != 0) {
- // OK, this event needs moved.
- var newTime: Long = -1
- if (midiEvent.offset.offsetType === EventOffsetType.Milliseconds) {
- val offset = Utils.milliToNano(midiEvent.offset.amount)
- newTime = midiEvent.eventTime + offset
- } else {
- // Offset by beat count.
- val beatCount = midiEvent.offset.amount
- val beatsBeforeOrAfterThisMIDIEvent = beatEvents.filter {
- if (beatCount >= 0)
- it.eventTime > midiEvent.eventTime
- else
- it.eventTime < midiEvent.eventTime
- }
- val beatsInOrder =
- if (beatCount < 0)
- beatsBeforeOrAfterThisMIDIEvent.reversed()
- else
- beatsBeforeOrAfterThisMIDIEvent
- val beatWeWant = beatsInOrder.asSequence().take(beatCount.absoluteValue).lastOrNull()
- if (beatWeWant != null)
- newTime = beatWeWant.eventTime
- }
- if (newTime < 0) {
- errors.add(
- FileParseError(
- midiEvent.offset.sourceFileLineNumber,
- R.string.midi_offset_is_before_start_of_song
- )
- )
- newTime = 0
- }
- MIDIEvent(newTime, midiEvent.messages)
- } else
- midiEvent
-
- private fun BaseEvent.shouldCompensateForAudioLatency(lineEventFound: Boolean): Boolean =
- !(this is AudioEvent || this is StartEvent || this is ClickEvent || (this is LineEvent && !lineEventFound))
-
- private fun compensateForAudioLatency(
- events: List,
- nanoseconds: Long
- ): List {
- // First line event should NOT be offset.
- var lineEventFound = false
- return events.map {
- (if (it.shouldCompensateForAudioLatency(lineEventFound))
- it.offset(nanoseconds)
- else
- it).also { event ->
- lineEventFound = lineEventFound || event is LineEvent
- }
- }
- }
-
- private fun generateClickEvents(
- events: List
- ): List =
- events.flatMap {
- if (it is BeatEvent && it.click)
- listOf(it, ClickEvent(it.eventTime))
- else
- listOf(it)
- }
- }
+ private val metronomeContext: MetronomeContext
+ private val customCommentsUser: String
+ private var showChords: Boolean
+ private val showKey: Boolean
+ private val showBpm: ShowBPMContext
+ private val triggerContext: TriggerOutputContext
+ private val nativeDeviceSettings: DisplaySettings
+ private val initialMidiMessages = mutableListOf()
+ private var stopAddingStartupItems = false
+ private val startScreenComments = mutableListOf()
+ private val events = mutableListOf()
+ private val lines = LineList()
+ private val rolloverBeats = mutableListOf()
+ private val beatBlocks = mutableListOf()
+ private val paint = Paint()
+ private val defaultHighlightColor: Int
+ private val timePerBar: Long
+ private val flatAudioFiles: List
+
+ private var songHeight = 0
+ private var midiBeatCounter: Int = 0
+ private var lastBeatBlock: BeatBlock? = null
+ private var beatsToAdjust: Int = 0
+ private var currentBeat: Int = 0
+ private var countIn: Int
+ private var sendMidiClock = false
+ private var songTime: Long = 0
+ private var defaultMidiOutputChannel: Byte
+ private var isInChorusSection = false
+ private var pendingAudioTag: AudioTag? = null
+ private var audioTagIndex: Int = 0
+ private var chordMap: ChordMap? = if (songLoadInfo.songFile.firstChord != null) ChordMap(
+ songLoadInfo.songFile.chords.toSet(),
+ songLoadInfo.songFile.firstChord,
+ songLoadInfo.songFile.key
+ ).transpose(songLoadInfo.transposeShift) else null
+
+ init {
+ // All songFile info parsing errors count as our errors too.
+ songLoadInfo.songFile.errors.forEach { addError(it) }
+
+ sendMidiClock = BeatPrompter.preferences.sendMIDIClock
+ countIn = BeatPrompter.preferences.defaultCountIn
+ metronomeContext = BeatPrompter.preferences.metronomeContext
+ defaultHighlightColor = BeatPrompter.preferences.defaultHighlightColor
+ customCommentsUser = BeatPrompter.preferences.customCommentsUser
+ showChords = BeatPrompter.preferences.showChords
+ triggerContext = BeatPrompter.preferences.sendMIDITriggerOnStart
+ val defaultMIDIOutputChannelPrefValue = BeatPrompter.preferences.defaultMIDIOutputChannel
+ defaultMidiOutputChannel = MidiMessage.getChannelFromBitmask(defaultMIDIOutputChannelPrefValue)
+ showKey = BeatPrompter.preferences.showKey && !songLoadInfo.songFile.key.isNullOrBlank()
+ showBpm =
+ if (songLoadInfo.songFile.bpm > 0.0) BeatPrompter.preferences.showBPMContext else ShowBPMContext.No
+
+ // Figure out the screen size
+ nativeDeviceSettings = translateSourceDeviceSettingsToNative(
+ songLoadInfo.sourceDisplaySettings,
+ songLoadInfo.nativeDisplaySettings
+ )
+
+ // Start the progress message dialog
+ songLoadHandler?.obtainMessage(
+ Events.SONG_LOAD_LINE_PROCESSED,
+ 0, songLoadInfo.songFile.lines
+ )?.sendToTarget()
+
+ val selectedVariation = songLoadInfo.variation
+ val audioFilenamesForThisVariation =
+ songLoadInfo.songFile.audioFiles[selectedVariation] ?: listOf()
+ flatAudioFiles = audioFilenamesForThisVariation.mapNotNull {
+ supportFileResolver.getMappedAudioFiles(it).firstOrNull()
+ }
+ val lengthOfBackingTrack = flatAudioFiles.firstOrNull()?.duration ?: 0L
+ var songTime =
+ if (songLoadInfo.songFile.duration == Utils.TRACK_AUDIO_LENGTH_VALUE)
+ lengthOfBackingTrack
+ else
+ songLoadInfo.songFile.duration
+ if (songTime > 0 && songLoadInfo.songFile.totalPauseDuration > songTime) {
+ addError(FileParseError(R.string.pauseLongerThanSong))
+ ongoingBeatInfo = SongBeatInfo(scrollMode = ScrollingMode.Manual)
+ currentLineBeatInfo = LineBeatInfo(ongoingBeatInfo)
+ songTime = 0
+ }
+
+ timePerBar =
+ if (songTime > 0L)
+ (songTime.toDouble() / songLoadInfo.songFile.bars).toLong()
+ else
+ 0
+ }
+
+ override fun parseLine(line: TextFileLine): Boolean {
+ if (songLoadCancelEvent?.isCancelled == true)
+ throw SongLoadCancelledException()
+ if (!super.parseLine(line))
+ return false
+
+ showChords = showChords and !line.tags.filterIsInstance().any()
+ val chordTags = line.tags.filterIsInstance()
+ val nonChordTags = line.tags.filter { it !is ChordTag }
+ val chordsFound = chordTags.isNotEmpty()
+ val chordsFoundButNotShowingThem = !showChords && chordsFound
+ val tags = if (showChords) line.tags.toList() else nonChordTags
+ val tagSequence = tags.asSequence()
+
+ val transposeTags = tagSequence.filterIsInstance()
+ transposeTags.forEach {
+ try {
+ chordMap = chordMap?.transpose(it.value)
+ } catch (e: Exception) {
+ addError(FileParseError(it, e))
+ }
+ }
+
+ val chordMapTags = tagSequence.filterIsInstance()
+ chordMapTags.forEach {
+ chordMap = chordMap?.addChordMapping(it.from, it.to)
+ }
+
+ var workLine = line.lineWithNoTags
+
+ // Generate clicking beats if the metronome is on.
+ // The "on when no track" logic will be performed during song playback.
+ val metronomeOn =
+ metronomeContext === MetronomeContext.On || metronomeContext === MetronomeContext.OnWhenNoTrack
+
+ var imageTag = tagSequence.filterIsInstance().firstOrNull()
+
+ val startOfChorusTag = tagSequence.filterIsInstance().firstOrNull()
+ val thisLineIsInChorus = startOfChorusTag != null || isInChorusSection
+ val endOfChorusTag = tagSequence.filterIsInstance().firstOrNull()
+ isInChorusSection =
+ if (endOfChorusTag != null) {
+ if (startOfChorusTag != null)
+ endOfChorusTag.position < startOfChorusTag.position
+ else
+ false
+ } else
+ startOfChorusTag != null || isInChorusSection
+
+ if (!sendMidiClock)
+ sendMidiClock = tags.any { it is SendMIDIClockTag }
+
+ if (!stopAddingStartupItems)
+ countIn = tags.filterIsInstance().firstOrNull()?.count ?: countIn
+
+ tags
+ .filterIsInstance()
+ .map {
+ Song.Comment(
+ it.comment,
+ it.audience,
+ it.color
+ ?: if (stopAddingStartupItems) BeatPrompter.preferences.commentColor else DEFAULT_START_SCREEN_COMMENT_COLOR,
+ Rect(nativeDeviceSettings.screenSize),
+ paint
+ )
+ }
+ .filter { it.isIntendedFor(customCommentsUser) }
+ .forEach {
+ if (stopAddingStartupItems)
+ events.add(CommentEvent(songTime, it))
+ else
+ startScreenComments.add(it)
+ }
+
+ // If a line has a number of bars defined, we really should treat it as a line, even if
+ // is blank.
+ val shorthandBarTag = tagSequence
+ .filterIsInstance()
+ .firstOrNull()
+ val barsTag = tagSequence
+ .filterIsInstance()
+ .firstOrNull()
+ // Contains only tags? Or contains nothing? Don't use it as a blank line.
+ // BUT! If there are bar indicators of any kind, use the blank line.
+ val isLineContent = (workLine.isNotEmpty()
+ || chordsFoundButNotShowingThem
+ || chordsFound
+ || imageTag != null
+ || shorthandBarTag != null
+ || (barsTag != null && barsTag.bars > 0))
+
+ val pauseTag = tagSequence
+ .filterIsInstance()
+ .firstOrNull()
+
+ val isLineContentOrPause = isLineContent || pauseTag != null
+
+ tags
+ .filterIsInstance()
+ .forEach {
+ if (stopAddingStartupItems || isLineContentOrPause)
+ events.add(it.toMIDIEvent(songTime))
+ else {
+ initialMidiMessages.addAll(it.messages)
+ if (it.offset.amount != 0)
+ addError(FileParseError(it, R.string.midi_offset_before_first_line))
+ }
+ }
+
+ // An audio tag is only processed when there is actual line content.
+ // But the audio tag can be defined on a line without content.
+ // In which case it is "pending", to be applied when line content is found.
+ pendingAudioTag =
+ if (tags.filterIsInstance().any() && !songLoadInfo.noAudio) getVariationAudioTag(
+ audioTagIndex++
+ ) else pendingAudioTag
+
+ if (isLineContentOrPause) {
+ // We definitely have a line!
+ // So now is when we want to create the count-in (if any)
+ if (countIn > 0) {
+ val countInEvents = generateCountInEvents(
+ countIn,
+ metronomeContext === MetronomeContext.DuringCountIn || metronomeOn
+ )
+ events.addAll(countInEvents.mEvents)
+ songTime = countInEvents.mBlockEndTime
+ countIn = 0
+ }
+
+ // If there is a beatstart, we add a StartEvent. This functions as a "current event" that
+ // the song can be set to, then advanced from. The "current event" is not processed when we
+ // "press play" to start the song (it is expected that it ALREADY has been processed).
+ // StartEvents function as simply dummy starting-point "current" events.
+ val beatStartTag = tagSequence.filterIsInstance().firstOrNull()
+ if (beatStartTag != null)
+ events.add(StartEvent(songTime))
+
+ pendingAudioTag?.also {
+ // Make sure file exists.
+ val mappedTracks =
+ supportFileResolver.getMappedAudioFiles(it.normalizedFilename)
+ if (mappedTracks.isEmpty())
+ addError(FileParseError(it, R.string.cannotFindAudioFile, it.normalizedFilename))
+ else if (mappedTracks.size > 1)
+ addError(
+ FileParseError(
+ it,
+ R.string.multipleFilenameMatches,
+ it.normalizedFilename
+ )
+ )
+ else {
+ val audioFile = mappedTracks.first()
+ if (!audioFile.file.exists())
+ addError(
+ FileParseError(
+ it,
+ R.string.cannotFindAudioFile,
+ it.normalizedFilename
+ )
+ )
+ else
+ events.add(
+ AudioEvent(
+ songTime,
+ audioFile,
+ it.volume,
+ !stopAddingStartupItems
+ )
+ )
+ }
+ }
+ // Clear the pending tag.
+ pendingAudioTag = null
+
+ // Any comments or MIDI events from here will be part of the song,
+ // rather than startup events.
+ stopAddingStartupItems = true
+
+ if (imageTag != null && (workLine.isNotBlank() || chordsFound))
+ addError(FileParseError(line.lineNumber, R.string.text_found_with_image))
+
+ // Measuring a blank line will result in a 0x0 measurement, so we
+ // need to have SOMETHING to measure. A nice wee "down arrow" should look OK.
+ if (workLine.isBlank() && (!chordsFound || chordsFoundButNotShowingThem))
+ workLine = "▼"
+
+ // Generate pause events if required (may return null)
+ val pauseEvents = generatePauseEvents(songTime, pauseTag)
+ val paused = pauseEvents?.any() == true
+ if (paused || currentLineBeatInfo.scrollMode !== ScrollingMode.Beat)
+ rolloverBeats.clear()
+
+ if (isLineContent) {
+ // First line should always have a time of zero, so that if the user scrolls
+ // back to the start of the song, it still picks up any count-in beat events.
+ val lineStartTime = if (lines.isEmpty) 0L else songTime
+
+ // If the first line is a pause event, we need to adjust the total line time accordingly
+ // to include any count-in
+ val addToPause = if (lines.isEmpty) songTime else 0L
+
+ // Generate beat events (may return null in smooth mode)
+ pauseEvents?.maxOf { it.eventTime }
+ val beatEvents = if (paused || currentLineBeatInfo.scrollMode === ScrollingMode.Smooth)
+ EventBlock(listOf(), pauseEvents?.maxOf { it.eventTime } ?: 0)
+ else
+ generateBeatEvents(songTime, metronomeOn)
+
+ // Calculate how long this line will last for
+ val lineDuration = calculateLineDuration(
+ pauseTag,
+ addToPause,
+ lineStartTime,
+ beatEvents
+ )
+
+ // Calculate the start and stop scroll times for this line
+ val startAndStopScrollTimes = calculateStartAndStopScrollTimes(
+ pauseTag,
+ lineStartTime + addToPause,
+ lineDuration,
+ beatEvents,
+ songLoadInfo.audioLatency
+ )
+
+ // Create the line
+ var lineObj: Line? = null
+ if (imageTag != null) {
+ val imageFiles =
+ supportFileResolver.getMappedImageFiles(imageTag.filename)
+ if (imageFiles.isNotEmpty())
+ try {
+ lineObj = ImageLine(
+ imageFiles.first(),
+ imageTag.scalingMode,
+ lineStartTime,
+ lineDuration,
+ currentLineBeatInfo.scrollMode,
+ nativeDeviceSettings,
+ songHeight,
+ thisLineIsInChorus,
+ startAndStopScrollTimes
+ )
+ } catch (t: Throwable) {
+ // Bitmap loading could cause error here. Even OutOfMemory!
+ addError(FileParseError(imageTag, t))
+ }
+ else {
+ workLine = BeatPrompter.appResources.getString(R.string.missing_image_file_warning)
+ addError(FileParseError(imageTag, R.string.missing_image_file_warning))
+ imageTag = null
+ }
+ }
+ if (imageTag == null)
+ lineObj = TextLine(
+ workLine,
+ tags,
+ lineStartTime,
+ lineDuration,
+ currentLineBeatInfo.scrollMode,
+ nativeDeviceSettings,
+ lines
+ .filterIsInstance()
+ .lastOrNull()?.trailingHighlightColor,
+ songHeight,
+ thisLineIsInChorus,
+ startAndStopScrollTimes,
+ chordMap,
+ songLoadCancelEvent
+ )
+
+ if (lineObj != null) {
+ lines.add(lineObj)
+ events.add(LineEvent(lineObj.lineTime, lineObj))
+
+ songHeight += lineObj.measurements.lineHeight
+
+ // If a pause is going to be generated, then we don't need beats.
+ if (pauseEvents == null) {
+ // Otherwise, add any generated beats
+ if (beatEvents?.mEvents?.isNotEmpty() == true) {
+ events.addAll(beatEvents.mEvents)
+ songTime = beatEvents.mBlockEndTime
+ }
+ // Otherwise, forget it, just bump up the song time
+ else
+ songTime += lineDuration
+ }
+ }
+ }
+ // Now add the pause events to the song (if required).
+ if (pauseEvents != null && pauseTag != null) {
+ events.addAll(pauseEvents)
+ songTime += pauseTag.duration
+ }
+ }
+ if (!isLineContent || currentLineBeatInfo.scrollMode !== ScrollingMode.Beat)
+ // If there is no actual line data (or if the line is a manual mode line), then the scroll beat offset never took effect.
+ // Clear it so that the next line (which MIGHT be a proper line) doesn't take it into account.
+ currentLineBeatInfo = LineBeatInfo(
+ currentLineBeatInfo.beats,
+ currentLineBeatInfo.bpl,
+ currentLineBeatInfo.bpb,
+ currentLineBeatInfo.bpm,
+ currentLineBeatInfo.scrollBeat,
+ currentLineBeatInfo.lastScrollBeatTotalOffset,
+ 0, currentLineBeatInfo.scrollMode
+ )
+
+ songLoadHandler?.obtainMessage(
+ Events.SONG_LOAD_LINE_PROCESSED,
+ line.lineNumber, songLoadInfo.songFile.lines
+ )?.sendToTarget()
+ return true
+ }
+
+ private fun getVariationAudioTag(index: Int): AudioTag? {
+ val tags = variationAudioTags[variation]
+ if ((tags?.count() ?: 0) > index)
+ return tags!![index]
+ return null
+ }
+
+ override fun getResult(): Song {
+ // Song has no lines? Make a dummy line so we don't have to check for null everywhere in the code.
+ if (lines.isEmpty)
+ throw InvalidBeatPrompterFileException(R.string.no_lines_in_song_file)
+
+ val lineSequence = lines.asSequence()
+ val smoothMode = lineSequence.filter { it.scrollMode == ScrollingMode.Smooth }.any()
+
+ val startScreenStrings = createStartScreenStrings()
+ val totalStartScreenTextHeight = startScreenStrings.first.sumOf { it.height }
+
+ // In smooth scrolling mode, the display will start scrolling immediately.
+ // This is an essential feature of smooth scrolling mode, yet causes a problem: the first line
+ // will almost immediately become obscured, just as you are performing it.
+ // To combat this, there will an initial blank "buffer zone", created by offsetting the graphical
+ // display by a number of pixels.
+ val smoothScrollOffset =
+ if (smoothMode)
+ // Obviously this will only be required if the song cannot fit entirely onscreen.
+ if (songHeight > nativeDeviceSettings.usableScreenHeight)
+ min(lineSequence.map { it.measurements.lineHeight }.maxByOrNull { it }
+ ?: 0, (nativeDeviceSettings.screenSize.height / 3.0).toInt())
+ else
+ 0
+ else
+ 0
+
+ // Get all required audio info ...
+ val audioEvents = events.filterIsInstance()
+
+ // Allocate graphics objects.
+ val maxGraphicsRequired = getMaximumGraphicsRequired(nativeDeviceSettings.screenSize.height)
+ val lineGraphics = CircularGraphicsList()
+ repeat(maxGraphicsRequired) {
+ lineGraphics.add(LineGraphic(getBiggestLineSize(it, maxGraphicsRequired)))
+ }
+
+ // There may be no lines! So we have to check ...
+ if (lineGraphics.isNotEmpty()) {
+ var graphic: LineGraphic = lineGraphics.first()
+ lines.forEach { line ->
+ repeat(line.measurements.lines) {
+ line.allocateGraphic(graphic)
+ graphic = graphic.nextGraphic
+ }
+ }
+ }
+
+ val beatCounterHeight = nativeDeviceSettings.beatCounterRect.height
+ val maxSongTitleWidth = nativeDeviceSettings.screenSize.width * 0.9f
+ val maxSongTitleHeight = beatCounterHeight * 0.9f
+ val vMargin = (beatCounterHeight - maxSongTitleHeight) / 2.0f
+ val songTitleHeader = ScreenString.create(
+ songLoadInfo.songFile.title,
+ paint,
+ maxSongTitleWidth.toInt(),
+ maxSongTitleHeight.toInt(),
+ Utils.makeHighlightColour(Color.BLACK, 0x80.toByte()),
+ false
+ )
+ val extraMargin = (maxSongTitleHeight - songTitleHeader.height) / 2.0f
+ val x = ((nativeDeviceSettings.screenSize.width - songTitleHeader.width) / 2.0).toFloat()
+ val y = beatCounterHeight - (extraMargin + songTitleHeader.descenderOffset.toFloat() + vMargin)
+ val songTitleHeaderLocation = PointF(x, y)
+
+ // First of all, find beat events that have the "click" flag set and
+ // add a click event (necessary because we want to offset the click by
+ // the audio latency without offsetting the beat).
+ val eventsWithClicks = generateClickEvents(events)
+ // Now offset any MIDI events that have an offset.
+ val midiOffsetEventList = offsetMIDIEvents(eventsWithClicks)
+ // And offset non-audio events by the audio latency offset.
+ val audioLatencyCompensatedEventList =
+ compensateForAudioLatency(midiOffsetEventList, Utils.milliToNano(songLoadInfo.audioLatency))
+
+ // OK, now sort all events by time, and type within time
+ val sortedEventList = sortEvents(audioLatencyCompensatedEventList).toMutableList()
+
+ // Songs need a "first event" to have as their "current event". Without this, the initial
+ // "current event" could be the EndEvent!
+ sortedEventList.add(0, StartEvent())
+
+ // Now we need to figure out which lines should NOT scroll offscreen.
+ val noScrollLines = mutableListOf()
+ val lastLineIsBeat = lines.lastOrNull()?.scrollMode == ScrollingMode.Beat
+ if (lastLineIsBeat) {
+ noScrollLines.add(lines.last())
+ // Why was I removing this? It breaks highlighting the last line ...
+ // sortedEventList.removeAt(sortedEventList.indexOfLast { it is LineEvent })
+ } else if (smoothMode) {
+ var availableScreenHeight = nativeDeviceSettings.usableScreenHeight - smoothScrollOffset
+ val lineEvents = sortedEventList.filterIsInstance()
+ for (lineEvent in lineEvents.reversed()) {
+ availableScreenHeight -= lineEvent.line.measurements.lineHeight
+ if (availableScreenHeight >= 0) {
+ noScrollLines.add(lineEvent.line)
+ sortedEventList.remove(lineEvent)
+ } else
+ break
+ }
+ }
+
+ // To generate the EndEvent, we need to know the time that the
+ // song ends. This could be the time of the final generated event,
+ // but there might still be an audio file playing, so find out
+ // when the last track ends ...
+ val lastAudioEndTime = sortedEventList
+ .asSequence()
+ .filterIsInstance()
+ .map { it.audioFile.duration + it.eventTime }
+ .maxOrNull()
+ sortedEventList.add(EndEvent(max(lastAudioEndTime ?: 0L, songTime)))
+
+ // Now build the final event list.
+ val firstEvent = LinkedEvent(sortedEventList)
+
+ // Calculate the last position that we can scroll to.
+ val scrollEndPixel = calculateScrollEndPixel(smoothMode, smoothScrollOffset)
+
+ if ((triggerContext == TriggerOutputContext.Always)
+ || (triggerContext == TriggerOutputContext.ManualStartOnly && !songLoadInfo.wasStartedByMidiTrigger)
+ ) {
+ initialMidiMessages.addAll(
+ songLoadInfo.songFile.programChangeTrigger.getMIDIMessages(
+ defaultMidiOutputChannel
+ )
+ )
+ initialMidiMessages.addAll(
+ songLoadInfo.songFile.songSelectTrigger.getMIDIMessages(
+ defaultMidiOutputChannel
+ )
+ )
+ }
+
+ return Song(
+ songLoadInfo.songFile,
+ nativeDeviceSettings,
+ firstEvent,
+ lines,
+ audioEvents,
+ initialMidiMessages,
+ beatBlocks,
+ sendMidiClock,
+ startScreenStrings.first,
+ startScreenStrings.second,
+ totalStartScreenTextHeight,
+ songLoadInfo.wasStartedByBandLeader,
+ songLoadInfo.nextSong,
+ smoothScrollOffset,
+ songHeight,
+ scrollEndPixel,
+ noScrollLines,
+ nativeDeviceSettings.beatCounterRect,
+ songTitleHeader,
+ songTitleHeaderLocation,
+ songLoadInfo.loadId,
+ songLoadInfo.audioLatency
+ )
+ }
+
+ private fun calculateScrollEndPixel(smoothMode: Boolean, smoothScrollOffset: Int): Int {
+ val manualDisplayEnd = max(0, songHeight - nativeDeviceSettings.usableScreenHeight)
+ val beatDisplayEnd =
+ lines.lastOrNull { it.scrollMode === ScrollingMode.Beat }?.songPixelPosition
+ return if (smoothMode)
+ manualDisplayEnd + smoothScrollOffset//+smoothScrollEndOffset
+ else if (beatDisplayEnd != null)
+ if (beatDisplayEnd + nativeDeviceSettings.usableScreenHeight > songHeight)
+ beatDisplayEnd
+ else
+ manualDisplayEnd
+ else
+ manualDisplayEnd
+ }
+
+ private fun getBiggestLineSize(index: Int, modulus: Int): Rect {
+ var maxHeight = 0
+ var maxWidth = 0
+ var lineCount = 0
+ lines.forEach {
+ for (lh in it.measurements.graphicHeights) {
+ if (lineCount % modulus == index) {
+ maxHeight = max(maxHeight, lh)
+ maxWidth = max(maxWidth, it.measurements.lineWidth)
+ }
+ ++lineCount
+ }
+ }
+ return Rect(0, 0, maxWidth - 1, maxHeight - 1)
+ }
+
+ private fun getMaximumGraphicsRequired(screenHeight: Int): Int {
+ var maxLines = 0
+ repeat(lines.size) { start ->
+ var heightCounter = 0
+ var lineCounter = 0
+ for (f in start until lines.size) {
+ if (heightCounter < screenHeight) {
+ // Assume height of first line to be 1 pixel
+ // This is the state of affairs when the top line is almost
+ // scrolled offscreen, but not quite.
+ var lineHeight = 1
+ if (lineCounter > 0)
+ lineHeight = lines[f].measurements.lineHeight
+ heightCounter += lineHeight
+ lineCounter += lines[f].measurements.lines
+ }
+ }
+ maxLines = max(maxLines, lineCounter)
+ }
+ return maxLines
+ }
+
+ private fun generateBeatEvents(startTime: Long, click: Boolean): EventBlock? {
+ if (currentLineBeatInfo.scrollMode === ScrollingMode.Smooth)
+ return null
+ var eventTime = startTime
+ val beatEvents = mutableListOf()
+ var beatThatWeWillScrollOn = 0
+ val currentTimePerBeat = Utils.nanosecondsPerBeat(currentLineBeatInfo.bpm)
+ // If the previous line scrolled early, there will still be some beats left over,
+ // so they need to be added to this beat event group.
+ val rolloverBeatCount = rolloverBeats.size
+ var rolloverBeatsApplied = 0
+ // We have N beats to adjust, because the BPB has changed on this line, and the
+ // previous line scrolled late, meaning that one or more beat events from the previous
+ // beat-event group will have incorrect BPB values.
+ // If we don't adjust the BPB of the previous beats to the BPB from THIS line, they
+ // will not appear correctly.
+ // So ... for the previous N beat events, set the BPB to the new BPB.
+ if (beatsToAdjust > 0) {
+ events.filterIsInstance().takeLast(beatsToAdjust).forEach {
+ it.bpb = currentLineBeatInfo.bpb
+ }
+ beatsToAdjust = 0
+ }
+
+ // Okay, we can now create the beat events for this line.
+ (0 until currentLineBeatInfo.beats).forEach { beatIndex ->
+ currentLineBeatInfo.beats - beatIndex
+ val beatsRemaining = currentLineBeatInfo.beats - beatIndex
+ // Calculate the scroll beat.
+ // If the number of beats remaining in this line is greater than the BPB,
+ // then the scroll beat won't be happening in the bar that this beat is in.
+ beatThatWeWillScrollOn = if (beatsRemaining > currentLineBeatInfo.bpb) -1 else
+ // Otherwise the scrollbeat will be coming up ...
+ ((currentLineBeatInfo.bpb * 2 + currentLineBeatInfo.scrollBeatTotalOffset - 1) % currentLineBeatInfo.bpb).let {
+ // If the scrollbeat is the current beat, we don't want to display the marker on this beat.
+ if (it == currentBeat)
+ -2
+ else
+ it
+ }
+ // If we have a non-negative value for this beatThatWeWillScrollOn, we can retroactively
+ // set beat events with this to improve visibility.
+
+ if (beatThatWeWillScrollOn >= 0) {
+ // Find the previous beat event, with a beat counter at least equal to the scrollbeat,
+ // with a -2 value for willScrollOnBeat
+ (if (beatEvents.isEmpty()) events.filterIsInstance() else beatEvents)
+ .takeLastWhile { it.willScrollOnBeat == -2 }
+ .forEach { it.willScrollOnBeat = beatThatWeWillScrollOn }
+ }
+ var rolloverBPB = 0
+ var rolloverBeatLength = 0L
+ // Create the next beat event. If there are rollover beats (caused by the
+ // previous line scrolling early), "use them up" first before creating
+ // new beat events for this line.
+ val beatEvent = if (rolloverBeats.isEmpty())
+ BeatEvent(
+ eventTime,
+ currentLineBeatInfo.bpm,
+ currentLineBeatInfo.bpb,
+ currentBeat,
+ click,
+ beatThatWeWillScrollOn
+ )
+ else {
+ // Take a rollover beat from the pile.
+ val rolloverBeatEvent = rolloverBeats.removeAt(0)
+ // Update it with the new beatThatWeWillScrollOn value.
+ val modifiedRolloverBeatEvent = BeatEvent(
+ rolloverBeatEvent.eventTime,
+ rolloverBeatEvent.bpm,
+ rolloverBeatEvent.bpb,
+ rolloverBeatEvent.beat,
+ rolloverBeatEvent.click,
+ beatThatWeWillScrollOn
+ )
+ rolloverBPB = modifiedRolloverBeatEvent.bpb
+ rolloverBeatLength = Utils.nanosecondsPerBeat(rolloverBeatEvent.bpm)
+ rolloverBeatsApplied += 1
+ modifiedRolloverBeatEvent
+ }
+ beatEvents.add(beatEvent)
+ val beatTimeLength = if (rolloverBeatLength == 0L) currentTimePerBeat else rolloverBeatLength
+
+ // generate MIDI beat blocks.
+ val nanoPerBeat = beatTimeLength / 4.0
+ if (lastBeatBlock == null || nanoPerBeat != lastBeatBlock!!.nanoPerBeat) {
+ lastBeatBlock = BeatBlock(beatEvent.eventTime, midiBeatCounter++, nanoPerBeat)
+ beatBlocks.add(lastBeatBlock!!)
+ }
+
+ eventTime += beatTimeLength
+
+ // Keep track of the current beat number.
+ currentBeat++
+ if (currentBeat == (if (rolloverBPB > 0) rolloverBPB else currentLineBeatInfo.bpb))
+ currentBeat = 0
+ }
+
+ val beatsThisLine = currentLineBeatInfo.beats - rolloverBeatCount + rolloverBeatsApplied
+ val simpleBeatsThisLine =
+ (currentLineBeatInfo.bpb * currentLineBeatInfo.bpl) - currentLineBeatInfo.lastScrollBeatTotalOffset
+ if (beatsThisLine > simpleBeatsThisLine) {
+ // This line is scrolling EARLY.
+ // We need to store some information so that the next line can adjust the rollover beats.
+ beatsToAdjust = currentLineBeatInfo.beats - simpleBeatsThisLine
+ } else if (beatsThisLine < simpleBeatsThisLine) {
+ // This line is scrolling LATE.
+ // We need to generate a few beats to store for the next line to use.
+ rolloverBeats.clear()
+ var rolloverCurrentBeat = currentBeat
+ var rolloverCurrentTime = eventTime
+ repeat(simpleBeatsThisLine - beatsThisLine) {
+ rolloverBeats.add(
+ BeatEvent(
+ rolloverCurrentTime,
+ currentLineBeatInfo.bpm,
+ currentLineBeatInfo.bpb,
+ rolloverCurrentBeat++,
+ click,
+ beatThatWeWillScrollOn
+ )
+ )
+ rolloverCurrentTime += currentTimePerBeat
+ if (rolloverCurrentBeat == currentLineBeatInfo.bpb)
+ rolloverCurrentBeat = 0
+ }
+ }
+ return EventBlock(beatEvents, eventTime)
+ }
+
+ private fun generatePauseEvents(startTime: Long, pauseTag: PauseTag?): List? {
+ if (pauseTag == null)
+ return null
+ // pauseTime is in milliseconds.
+ // We don't want to generate thousands of events, so let's say every 1/10th of a second.
+ var eventTime = startTime
+ val pauseEvents = mutableListOf()
+ val deciSeconds = ceil(Utils.nanoToMilli(pauseTag.duration).toDouble() / 100.0).toInt()
+ val remainder = pauseTag.duration - Utils.milliToNano(deciSeconds * 100)
+ val oneDeciSecondInNanoseconds = Utils.milliToNano(100)
+ eventTime += remainder
+ repeat(deciSeconds) {
+ val pauseEvent = PauseEvent(eventTime, deciSeconds, it)
+ pauseEvents.add(pauseEvent)
+ eventTime += oneDeciSecondInNanoseconds
+ }
+ return pauseEvents
+ }
+
+ private fun generateCountInEvents(countBars: Int, click: Boolean): EventBlock {
+ val countInEvents = mutableListOf()
+ var startTime = 0L
+ if (countBars > 0) {
+ if (currentLineBeatInfo.bpm > 0.0) {
+ val countbpm = currentLineBeatInfo.bpm
+ val countbpb = currentLineBeatInfo.bpb
+ val nanoPerBeat = Utils.nanosecondsPerBeat(countbpm)
+ repeat(countBars) { bar ->
+ repeat(countbpb) { beat ->
+ countInEvents.add(
+ BeatEvent(
+ startTime,
+ currentLineBeatInfo.bpm,
+ currentLineBeatInfo.bpb,
+ beat,
+ click,
+ if (bar == countBars - 1) countbpb - 1 else -1
+ )
+ )
+ startTime += nanoPerBeat
+ }
+ }
+ }
+ }
+ return EventBlock(countInEvents, startTime)
+ }
+
+ /**
+ * Based on the difference in screen size/resolution/orientation, we will alter the min/max font size of our native settings.
+ */
+ private fun translateSourceDeviceSettingsToNative(
+ sourceSettings: DisplaySettings,
+ nativeSettings: DisplaySettings
+ ): DisplaySettings {
+ val sourceScreenSize = sourceSettings.screenSize
+ val sourceRatio = sourceScreenSize.width.toDouble() / sourceScreenSize.height.toDouble()
+ val screenWillRotate = nativeSettings.orientation != sourceSettings.orientation
+ val nativeScreenSize = if (screenWillRotate)
+ Rect(0, 0, nativeSettings.screenSize.height, nativeSettings.screenSize.width)
+ else
+ nativeSettings.screenSize
+ val nativeRatio = nativeScreenSize.width.toDouble() / nativeScreenSize.height.toDouble()
+ val minRatio = min(nativeRatio, sourceRatio)
+ val maxRatio = max(nativeRatio, sourceRatio)
+ val ratioMultiplier = minRatio / maxRatio
+ var minimumFontSize = sourceSettings.minimumFontSize
+ var maximumFontSize = sourceSettings.maximumFontSize
+ minimumFontSize *= ratioMultiplier.toFloat()
+ maximumFontSize *= ratioMultiplier.toFloat()
+ if (minimumFontSize > maximumFontSize) {
+ addError(FileParseError(0, R.string.fontSizesAllMessedUp))
+ maximumFontSize = minimumFontSize
+ }
+ return DisplaySettings(
+ sourceSettings.orientation,
+ minimumFontSize,
+ maximumFontSize,
+ nativeScreenSize,
+ sourceSettings.showBeatCounter
+ )
+ }
+
+ private fun createStartScreenStrings(): Pair, ScreenString?> {
+ // As for the start screen display (title/artist/comments/"press go"),
+ // the title should take up no more than 20% of the height, the artist
+ // no more than 10%, also 10% for the "press go" message.
+ // The rest of the space is allocated for the comments and error messages,
+ // each line no more than 10% of the screen height.
+ val startScreenStrings = mutableListOf()
+ var availableScreenHeight = nativeDeviceSettings.screenSize.height
+ var nextSongString: ScreenString? = null
+ if (songLoadInfo.nextSong.isNotBlank()) {
+ // OK, we have a next song title to display.
+ // This should take up no more than 15% of the screen.
+ // But that includes a border, so use 13 percent for the text.
+ val eightPercent = (nativeDeviceSettings.screenSize.height * 0.13).toInt()
+ val nextSong = songLoadInfo.nextSong
+ val fullString = ">>> $nextSong >>>"
+ nextSongString = ScreenString.create(
+ fullString,
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ eightPercent,
+ Color.BLACK,
+ true
+ )
+ availableScreenHeight -= (nativeDeviceSettings.screenSize.height * 0.15f).toInt()
+ }
+ val tenPercent = (availableScreenHeight / 10.0).toInt()
+ val twentyPercent = (availableScreenHeight / 5.0).toInt()
+ startScreenStrings.add(
+ ScreenString.create(
+ songLoadInfo.songFile.title,
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ twentyPercent,
+ START_SCREEN_TITLE_COLOR,
+ true
+ )
+ )
+ if (songLoadInfo.songFile.artist.isNotBlank())
+ startScreenStrings.add(
+ ScreenString.create(
+ songLoadInfo.songFile.artist,
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ tenPercent,
+ START_SCREEN_ARTIST_COLOR,
+ true
+ )
+ )
+ val nonBlankCommentLines = startScreenComments.filter { it.text.trim().isNotEmpty() }
+ val uniqueErrors = errors.asSequence().distinct().sortedBy { it.lineNumber }.toList()
+ var errorCount = uniqueErrors.size
+ var messages = min(errorCount, 6) + nonBlankCommentLines.size
+ val showBPM = showBpm != ShowBPMContext.No
+ if (showBPM)
+ ++messages
+ if (showKey)
+ ++messages
+ if (messages > 0) {
+ val remainingScreenSpace = nativeDeviceSettings.screenSize.height - twentyPercent * 2
+ var spacePerMessageLine = floor((remainingScreenSpace / messages).toDouble()).toInt()
+ spacePerMessageLine = min(spacePerMessageLine, tenPercent)
+ var errorCounter = 0
+ for (error in uniqueErrors) {
+ startScreenStrings.add(
+ ScreenString.create(
+ error.toString(),
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ spacePerMessageLine,
+ START_SCREEN_ERROR_COLOR,
+ false
+ )
+ )
+ ++errorCounter
+ --errorCount
+ if (errorCounter == 5 && errorCount > 0) {
+ startScreenStrings.add(
+ ScreenString.create(
+ String.format(
+ BeatPrompter.appResources.getString(R.string.otherErrorCount),
+ errorCount
+ ),
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ spacePerMessageLine,
+ START_SCREEN_ERROR_COLOR,
+ false
+ )
+ )
+ break
+ }
+ }
+ for (nonBlankComment in nonBlankCommentLines)
+ startScreenStrings.add(
+ ScreenString.create(
+ nonBlankComment.text.trim(),
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ spacePerMessageLine,
+ nonBlankComment.textColor,
+ false
+ )
+ )
+ if (showKey) {
+ val keyString =
+ BeatPrompter.appResources.getString(R.string.keyPrefix) + ": " + songLoadInfo.songFile.key
+ startScreenStrings.add(
+ ScreenString.create(
+ keyString,
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ spacePerMessageLine,
+ START_SCREEN_KEY_COLOR,
+ false
+ )
+ )
+ }
+ if (showBpm != ShowBPMContext.No) {
+ val rounded =
+ showBpm == ShowBPMContext.Rounded || songLoadInfo.songFile.bpm == songLoadInfo.songFile.bpm.toInt()
+ .toDouble()
+ var bpmString = BeatPrompter.appResources.getString(R.string.bpmPrefix) + ": "
+ bpmString += if (rounded)
+ songLoadInfo.songFile.bpm.roundToInt()
+ else
+ songLoadInfo.songFile.bpm
+ startScreenStrings.add(
+ ScreenString.create(
+ bpmString,
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ spacePerMessageLine,
+ START_SCREEN_BPM_COLOR,
+ false
+ )
+ )
+ }
+ }
+ if (songLoadInfo.songLoadMode !== ScrollingMode.Manual)
+ startScreenStrings.add(
+ ScreenString.create(
+ BeatPrompter.appResources.getString(R.string.tapTwiceToStart),
+ paint,
+ nativeDeviceSettings.screenSize.width,
+ tenPercent,
+ START_SCREEN_TAP_TO_START_COLOR,
+ true
+ )
+ )
+ return startScreenStrings to nextSongString
+ }
+
+ private fun calculateStartAndStopScrollTimes(
+ pauseTag: PauseTag?,
+ lineStartTime: Long,
+ lineDuration: Long,
+ currentBeatEvents: EventBlock?,
+ audioLatency: Int
+ ): Pair {
+ // Calculate when this line should start scrolling
+ val startScrollTime =
+ when (currentLineBeatInfo.scrollMode) {
+ // Smooth mode? Start scrolling instantly.
+ ScrollingMode.Smooth -> songTime
+ else ->
+ // Pause line? Start scrolling after 95% of the pause has elapsed.
+ if (pauseTag != null)
+ lineStartTime + (pauseTag.duration * 0.95).toLong()
+ // Beat line? Start scrolling on the last beat.
+ else
+ currentBeatEvents!!.mEvents.lastOrNull()?.eventTime ?: songTime
+ // (Manual mode ignores these scroll values)
+ }
+ // Calculate when the line should stop scrolling
+ val stopScrollTime =
+ when (currentLineBeatInfo.scrollMode) {
+ // Smooth mode? It should stop scrolling once the allocated time has elapsed.
+ ScrollingMode.Smooth -> songTime + lineDuration
+ else ->
+ // Pause line? It should stop scrolling when the pause has ran out
+ if (pauseTag != null)
+ lineStartTime + pauseTag.duration
+ // Beat line? It should stop scrolling after the final beat
+ else
+ currentBeatEvents!!.mBlockEndTime
+ // (Manual mode ignores these values)
+ }
+
+ // Events are going to be offset later to compensate for audio latency.
+ // Lines, however, won't be. So we need to compensate NOW.
+ val audioLatencyOffset = Utils.milliToNano(audioLatency)
+ return Pair(
+ startScrollTime + audioLatencyOffset,
+ stopScrollTime + audioLatencyOffset
+ )
+ }
+
+ private fun calculateLineDuration(
+ pauseTag: PauseTag?,
+ addToPause: Long,
+ lineStartTime: Long,
+ currentBeatEvents: EventBlock?
+ ): Long {
+ // Calculate how long this line will last for.
+ return when {
+ // Pause line? Lasts as long as the pause!
+ pauseTag != null -> pauseTag.duration + addToPause
+ // Smooth line? We've counted the bars, so do the sums.
+ currentLineBeatInfo.scrollMode == ScrollingMode.Smooth && timePerBar > 0 -> timePerBar * currentLineBeatInfo.bpl
+ // Beat line? The result of generateBeatEvents will contain the time
+ // that the beats end, so subtract the start time from that to get our duration.
+ else -> currentBeatEvents!!.mBlockEndTime - lineStartTime
+ // (Manual mode ignores these scroll values)
+ }
+ }
+
+ /**
+ * An "event block" is simply a list of events, in chronological order, and a time that marks the point
+ * at which the block ends. Note that the end time is not necessarily the same as the time of the last
+ * event. For example, a block of five beat events (where each beat last n nanoseconds) will contain
+ * five events with the times of n*0, n*1, n*2, n*3, n*4, and the end time will be n*5, as a "beat event"
+ * actually covers the duration of the beat.
+ */
+ private data class EventBlock(val mEvents: List, val mBlockEndTime: Long)
+
+ private class LineList : ArrayList() {
+ override fun add(element: Line): Boolean {
+ val lastOrNull = lastOrNull()
+ lastOrNull?.nextLine = element
+ element.previousLine = lastOrNull
+ return super.add(element)
+ }
+ }
+
+ private class CircularGraphicsList : ArrayList() {
+ override fun add(element: LineGraphic): Boolean {
+ lastOrNull()?.nextGraphic = element
+ val result = super.add(element)
+ last().nextGraphic = first()
+ return result
+ }
+ }
+
+ private fun sortEvents(eventList: List): List {
+ // Sort all events by time, and by type within that.
+ return eventList.sortedWith { e1, e2 ->
+ when {
+ e1.eventTime > e2.eventTime -> 1
+ e1.eventTime < e2.eventTime -> -1
+ else -> {
+ // StartEvents must appear before anything else, as their
+ // function is a "starting point" for song processing to
+ // continue from.
+ if (e1 is StartEvent && e2 is StartEvent)
+ 0
+ else if (e1 is StartEvent)
+ -1
+ else if (e2 is StartEvent)
+ 1
+ // MIDI events are most important. We want to
+ // these first at any given time for maximum MIDI
+ // responsiveness
+ else if (e1 is MidiEvent && e2 is MidiEvent)
+ 0
+ else if (e1 is MidiEvent)
+ -1
+ else if (e2 is MidiEvent)
+ 1
+ // AudioEvents are next-most important. We want to process
+ // these first at any given time for maximum audio
+ // responsiveness
+ else if (e1 is AudioEvent && e2 is AudioEvent)
+ 0
+ else if (e1 is AudioEvent)
+ -1
+ else if (e2 is AudioEvent)
+ 1
+ // Now LineEvents for maximum visual responsiveness
+ else if (e1 is LineEvent && e2 is LineEvent)
+ 0
+ else if (e1 is LineEvent)
+ -1
+ else if (e2 is LineEvent)
+ 1
+ // Remaining order doesn't really matter
+ else
+ 0
+ }
+ }
+ }
+ }
+
+ /**
+ * Each MIDIEvent might have an offset. Process that here.
+ */
+ private fun offsetMIDIEvents(
+ events: List
+ ): List {
+ val beatEvents =
+ events.asSequence().filterIsInstance().sortedBy { it.eventTime }.toList()
+ return events.map {
+ if (it is MidiEvent)
+ offsetMIDIEvent(it, beatEvents)
+ else
+ it
+ }
+ }
+
+ /**
+ * Each MIDIEvent might have an offset. Process that here.
+ */
+ private fun offsetMIDIEvent(
+ midiEvent: MidiEvent,
+ beatEvents: List
+ ): MidiEvent =
+ if (midiEvent.offset.amount != 0) {
+ // OK, this event needs moved.
+ var newTime: Long = -1
+ if (midiEvent.offset.offsetType === EventOffsetType.Milliseconds) {
+ val offset = Utils.milliToNano(midiEvent.offset.amount)
+ newTime = midiEvent.eventTime + offset
+ } else {
+ // Offset by beat count.
+ val beatCount = midiEvent.offset.amount
+ val beatsBeforeOrAfterThisMIDIEvent = beatEvents.filter {
+ if (beatCount >= 0)
+ it.eventTime > midiEvent.eventTime
+ else
+ it.eventTime < midiEvent.eventTime
+ }
+ val beatsInOrder =
+ if (beatCount < 0)
+ beatsBeforeOrAfterThisMIDIEvent.reversed()
+ else
+ beatsBeforeOrAfterThisMIDIEvent
+ val beatWeWant = beatsInOrder.asSequence().take(beatCount.absoluteValue).lastOrNull()
+ if (beatWeWant != null)
+ newTime = beatWeWant.eventTime
+ }
+ if (newTime < 0) {
+ addError(
+ FileParseError(
+ midiEvent.offset.sourceFileLineNumber,
+ R.string.midi_offset_is_before_start_of_song
+ )
+ )
+ newTime = 0
+ }
+ MidiEvent(newTime, midiEvent.messages)
+ } else
+ midiEvent
+
+ companion object {
+ private const val DEFAULT_START_SCREEN_COMMENT_COLOR = Color.WHITE
+ private const val START_SCREEN_ERROR_COLOR = Color.RED
+ private const val START_SCREEN_TITLE_COLOR = Color.YELLOW
+ private const val START_SCREEN_ARTIST_COLOR = Color.YELLOW
+ private const val START_SCREEN_BPM_COLOR = Color.CYAN
+ private const val START_SCREEN_KEY_COLOR = Color.CYAN
+ private const val START_SCREEN_TAP_TO_START_COLOR = Color.GREEN
+
+ private fun BaseEvent.shouldCompensateForAudioLatency(lineEventFound: Boolean): Boolean =
+ !(this is AudioEvent || this is StartEvent || this is ClickEvent || (this is LineEvent && !lineEventFound))
+
+ private fun compensateForAudioLatency(
+ events: List,
+ nanoseconds: Long
+ ): List {
+ // First line event should NOT be offset.
+ var lineEventFound = false
+ return events.map {
+ (if (it.shouldCompensateForAudioLatency(lineEventFound))
+ it.offset(nanoseconds)
+ else
+ it).also { event ->
+ lineEventFound = lineEventFound || event is LineEvent
+ }
+ }
+ }
+
+ private fun generateClickEvents(
+ events: List
+ ): List =
+ events.flatMap {
+ if (it is BeatEvent && it.click)
+ listOf(it, ClickEvent(it.eventTime))
+ else
+ listOf(it)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SupportFileResolver.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SupportFileResolver.kt
new file mode 100644
index 00000000..ca16fa1b
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/SupportFileResolver.kt
@@ -0,0 +1,9 @@
+package com.stevenfrew.beatprompter.cache.parse
+
+import com.stevenfrew.beatprompter.cache.AudioFile
+import com.stevenfrew.beatprompter.cache.ImageFile
+
+interface SupportFileResolver {
+ fun getMappedAudioFiles(filename: String): List
+ fun getMappedImageFiles(filename: String): List
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/TextFileParser.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/TextFileParser.kt
index d1aab793..3bee2687 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/TextFileParser.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/TextFileParser.kt
@@ -25,117 +25,117 @@ import kotlin.reflect.full.primaryConstructor
* Base class for text file parsers.
*/
abstract class TextFileParser(
- cachedCloudFile: CachedFile,
- private val reportUnexpectedTags: Boolean,
- private vararg val tagFinders: TagFinder
+ cachedCloudFile: CachedFile,
+ private val reportUnexpectedTags: Boolean,
+ private vararg val tagFinders: TagFinder
) : FileParser(cachedCloudFile) {
- override fun parse(element: Element?): TFileResult {
- val tagParseHelper = TagParsingUtility.getTagParsingHelper(this)
- var lineNumber = 0
- val fileTags = mutableSetOf>()
- val livePairings = mutableSetOf, KClass>>()
- cachedCloudFile.file.forEachLine { strLine ->
- ++lineNumber
- val txt = strLine.trim().removeControlCharacters()
- // Ignore empty lines and comments
- if (txt.isNotEmpty() && !txt.startsWith('#')) {
- val textLine = TextFileLine(txt, lineNumber, tagParseHelper, this)
- val lineTags = mutableSetOf>()
- textLine.tags.forEach { tag ->
- val tagClass = tag::class
- val isOncePerFile = tagClass.findAnnotation() != null
- val isOncePerLine = tagClass.findAnnotation() != null
- val startedByAnnotation = tagClass.findAnnotation()
- val endedByAnnotation = tagClass.findAnnotation()
- val lineExclusiveTags = tagClass.annotations.filterIsInstance()
- val alreadyUsedInFile = fileTags.contains(tagClass)
- val alreadyUsedInLine = lineTags.contains(tagClass)
- fileTags.add(tagClass)
- lineTags.add(tagClass)
- if (isOncePerFile && alreadyUsedInFile)
- errors.add(FileParseError(tag, R.string.tag_used_multiple_times_in_file, tag.name))
- if (isOncePerLine && alreadyUsedInLine)
- errors.add(FileParseError(tag, R.string.tag_used_multiple_times_in_line, tag.name))
- if (startedByAnnotation != null) {
- val startedByClass = startedByAnnotation.startedBy
- val startedByEndedByAnnotation = startedByClass.findAnnotation()!!
- val endedByClass = startedByEndedByAnnotation.endedBy
- if (!livePairings.remove(startedByClass to endedByClass))
- errors.add(
- FileParseError(
- tag,
- R.string.ending_tag_found_before_starting_tag,
- tag.name
- )
- )
- } else if (endedByAnnotation != null) {
- val endedByClass = endedByAnnotation.endedBy
- val endedByStartedByAnnotation = endedByClass.findAnnotation()!!
- val startedByClass = endedByStartedByAnnotation.startedBy
- val pairing = startedByClass to endedByClass
- if (livePairings.contains(pairing))
- errors.add(
- FileParseError(
- tag,
- R.string.starting_tag_found_after_starting_tag,
- tag.name
- )
- )
- else
- livePairings.add(pairing)
- }
- lineExclusiveTags.forEach {
- if (lineTags.contains(it.cannotShareWith))
- errors.add(
- FileParseError(
- tag, R.string.tag_cant_share_line_with,
- tag.name,
- it.cannotShareWith.findAnnotation()!!.names.first()
- )
- )
- }
- }
- parseLine(textLine)
- }
- }
- return getResult()
- }
+ override fun parse(element: Element?): TFileResult {
+ val tagParseHelper = TagParsingUtility.getTagParsingHelper(this)
+ var lineNumber = 0
+ val fileTags = mutableSetOf>()
+ val livePairings = mutableSetOf, KClass>>()
+ cachedCloudFile.file.forEachLine { strLine ->
+ ++lineNumber
+ val txt = strLine.trim().removeControlCharacters()
+ // Ignore empty lines and comments
+ if (txt.isNotEmpty() && !txt.startsWith('#')) {
+ val textLine = TextFileLine(txt, lineNumber, tagParseHelper, this)
+ val lineTags = mutableSetOf>()
+ textLine.tags.forEach { tag ->
+ val tagClass = tag::class
+ val isOncePerFile = tagClass.findAnnotation() != null
+ val isOncePerLine = tagClass.findAnnotation() != null
+ val startedByAnnotation = tagClass.findAnnotation()
+ val endedByAnnotation = tagClass.findAnnotation()
+ val lineExclusiveTags = tagClass.annotations.filterIsInstance()
+ val alreadyUsedInFile = fileTags.contains(tagClass)
+ val alreadyUsedInLine = lineTags.contains(tagClass)
+ fileTags.add(tagClass)
+ lineTags.add(tagClass)
+ if (isOncePerFile && alreadyUsedInFile)
+ addError(FileParseError(tag, R.string.tag_used_multiple_times_in_file, tag.name))
+ if (isOncePerLine && alreadyUsedInLine)
+ addError(FileParseError(tag, R.string.tag_used_multiple_times_in_line, tag.name))
+ if (startedByAnnotation != null) {
+ val startedByClass = startedByAnnotation.startedBy
+ val startedByEndedByAnnotation = startedByClass.findAnnotation()!!
+ val endedByClass = startedByEndedByAnnotation.endedBy
+ if (!livePairings.remove(startedByClass to endedByClass))
+ addError(
+ FileParseError(
+ tag,
+ R.string.ending_tag_found_before_starting_tag,
+ tag.name
+ )
+ )
+ } else if (endedByAnnotation != null) {
+ val endedByClass = endedByAnnotation.endedBy
+ val endedByStartedByAnnotation = endedByClass.findAnnotation()!!
+ val startedByClass = endedByStartedByAnnotation.startedBy
+ val pairing = startedByClass to endedByClass
+ if (livePairings.contains(pairing))
+ addError(
+ FileParseError(
+ tag,
+ R.string.starting_tag_found_after_starting_tag,
+ tag.name
+ )
+ )
+ else
+ livePairings.add(pairing)
+ }
+ lineExclusiveTags.forEach {
+ if (lineTags.contains(it.cannotShareWith))
+ addError(
+ FileParseError(
+ tag, R.string.tag_cant_share_line_with,
+ tag.name,
+ it.cannotShareWith.findAnnotation()!!.names.first()
+ )
+ )
+ }
+ }
+ parseLine(textLine)
+ }
+ }
+ return getResult()
+ }
- abstract fun getResult(): TFileResult
+ abstract fun getResult(): TFileResult
- abstract fun parseLine(line: TextFileLine): Boolean
+ abstract fun parseLine(line: TextFileLine): Boolean
- fun parseTag(
- foundTag: FoundTag,
- lineNumber: Int,
- parseHelper: TagParsingHelper
- ): Tag? {
- // Should we ignore this tag?
- if (!parseHelper.ignoreTagNames.contains(foundTag.name)) {
- // Nope, better parse it!
- val tagClass = parseHelper.nameToClassMap[foundTag.type to foundTag.name]
- ?: parseHelper.noNameToClassMap[foundTag.type]
- if (tagClass != null) {
- // We found a match!
- // Construct a tag of this class
- try {
- tagClass.primaryConstructor?.apply {
- return if (parameters.size == 4)
- call(foundTag.name, lineNumber, foundTag.start, foundTag.value)
- else
- call(foundTag.name, lineNumber, foundTag.start)
- }
- } catch (ite: InvocationTargetException) {
- throw ite.targetException
- }
- } else if (reportUnexpectedTags)
- throw MalformedTagException(R.string.unexpected_tag_in_file, foundTag.name)
- }
- return null
- }
+ fun parseTag(
+ foundTag: FoundTag,
+ lineNumber: Int,
+ parseHelper: TagParsingHelper
+ ): Tag? {
+ // Should we ignore this tag?
+ if (!parseHelper.ignoreTagNames.contains(foundTag.name)) {
+ // Nope, better parse it!
+ val tagClass = parseHelper.nameToClassMap[foundTag.type to foundTag.name]
+ ?: parseHelper.noNameToClassMap[foundTag.type]
+ if (tagClass != null) {
+ // We found a match!
+ // Construct a tag of this class
+ try {
+ tagClass.primaryConstructor?.apply {
+ return if (parameters.size == 4)
+ call(foundTag.name, lineNumber, foundTag.start, foundTag.value)
+ else
+ call(foundTag.name, lineNumber, foundTag.start)
+ }
+ } catch (ite: InvocationTargetException) {
+ throw ite.targetException
+ }
+ } else if (reportUnexpectedTags)
+ throw MalformedTagException(R.string.unexpected_tag_in_file, foundTag.name)
+ }
+ return null
+ }
- fun findFirstTag(text: String): FoundTag? =
- tagFinders
- .mapNotNull { it.findTag(text) }
- .minByOrNull { it.start }
+ fun findFirstTag(text: String): FoundTag? =
+ tagFinders
+ .mapNotNull { it.findTag(text) }
+ .minByOrNull { it.start }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/TagParsingUtility.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/TagParsingUtility.kt
index bb59e374..b41f7006 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/TagParsingUtility.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/TagParsingUtility.kt
@@ -1,6 +1,5 @@
package com.stevenfrew.beatprompter.cache.parse.tag
-import android.graphics.Color
import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.parse.TextFileParser
@@ -38,7 +37,7 @@ object TagParsingUtility {
else if (it > max)
throw MalformedTagException(R.string.intValueTooHigh, max, it)
}
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(R.string.intValueUnreadable, value)
}
@@ -62,7 +61,7 @@ object TagParsingUtility {
)
)
})
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(
BeatPrompter.appResources.getString(
R.string.durationValueUnreadable,
@@ -91,7 +90,7 @@ object TagParsingUtility {
)
)
}
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(
BeatPrompter.appResources.getString(
R.string.doubleValueUnreadable,
@@ -102,11 +101,11 @@ object TagParsingUtility {
fun parseColourValue(value: String): Int =
try {
- Color.parseColor(value)
- } catch (iae: IllegalArgumentException) {
+ BeatPrompter.platformUtils.parseColor(value)
+ } catch (_: IllegalArgumentException) {
try {
- Color.parseColor("#$value")
- } catch (iae2: IllegalArgumentException) {
+ BeatPrompter.platformUtils.parseColor("#$value")
+ } catch (_: IllegalArgumentException) {
throw MalformedTagException(
BeatPrompter.appResources.getString(
R.string.colorValueUnreadable,
@@ -128,7 +127,7 @@ object TagParsingUtility {
try {
// Arguments are one-based in the alias files.
ArgumentValue(Integer.parseInt(withoutQuestion) - 1)
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(BeatPrompter.appResources.getString(R.string.not_a_valid_argument_index))
}
}
@@ -156,7 +155,7 @@ object TagParsingUtility {
}
} catch (valueEx: ValueException) {
throw MalformedTagException(valueEx.message!!)
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(BeatPrompter.appResources.getString(R.string.not_a_valid_byte_value))
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/set/SetNameTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/set/SetNameTag.kt
index 06ee13f5..04b7a799 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/set/SetNameTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/set/SetNameTag.kt
@@ -10,7 +10,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@TagName("set")
@TagType(Type.Directive)
/**
- * Tag that defines the name of a setlist.
+ * Tag that defines the name of a set list.
*/
class SetNameTag internal constructor(
name: String,
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/AudioTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/AudioTag.kt
index 60cd677a..27294578 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/AudioTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/AudioTag.kt
@@ -1,6 +1,6 @@
package com.stevenfrew.beatprompter.cache.parse.tag.song
-import com.stevenfrew.beatprompter.Preferences
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.parse.tag.MalformedTagException
import com.stevenfrew.beatprompter.cache.parse.tag.TagName
@@ -28,7 +28,7 @@ class AudioTag internal constructor(
init {
val bits = value.splitAndTrim(":")
- val defaultTrackVolume = Preferences.defaultTrackVolume
+ val defaultTrackVolume = BeatPrompter.preferences.defaultTrackVolume
filename = File(bits[0]).name
normalizedFilename = filename.normalize()
volume =
@@ -41,10 +41,12 @@ class AudioTag internal constructor(
companion object {
fun parseVolume(value: String, defaultTrackVolume: Int): Int =
try {
- value.toInt().takeIf { it in 0..100 }?.let {
- (defaultTrackVolume.toDouble() * (it.toDouble() / 100.0)).toInt()
+ val absolute = value.startsWith('=')
+ val factor = if (absolute) 100.0 else defaultTrackVolume.toDouble()
+ value.trim('=').toDouble().takeIf { it in 0.0..100.0 }?.let {
+ (factor * (it / 100.0)).toInt()
} ?: throw MalformedTagException(R.string.badAudioVolume)
- } catch (nfe: NumberFormatException) {
+ } catch (_: NumberFormatException) {
throw MalformedTagException(R.string.badAudioVolume)
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/ChordMapTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/ChordMapTag.kt
index 85b86154..314c424a 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/ChordMapTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/ChordMapTag.kt
@@ -7,11 +7,12 @@ import com.stevenfrew.beatprompter.cache.parse.tag.Tag
import com.stevenfrew.beatprompter.cache.parse.tag.TagName
import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
-import com.stevenfrew.beatprompter.song.chord.Chord
-import com.stevenfrew.beatprompter.song.chord.IChord
-import com.stevenfrew.beatprompter.song.chord.UnknownChord
+import com.stevenfrew.beatprompter.chord.Chord
+import com.stevenfrew.beatprompter.chord.IChord
+import com.stevenfrew.beatprompter.chord.InvalidChordException
+import com.stevenfrew.beatprompter.chord.UnknownChord
-@TagName("chord_map")
+@TagName("chord_map", "chordmap")
@TagType(Type.Directive)
/**
* Transposes the chord map by the given amount.
@@ -30,6 +31,10 @@ class ChordMapTag internal constructor(
if ((bits.size != 2) || (bits.any { it.isBlank() }))
throw MalformedTagException(BeatPrompter.appResources.getString(R.string.chordMapTagMustContainTwoValues))
from = bits[0]
- to = Chord.parse(bits[1]) ?: UnknownChord(bits[1])
+ to = try {
+ Chord.parse(bits[1])
+ } catch (_: InvalidChordException) {
+ UnknownChord(bits[1])
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/CommentTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/CommentTag.kt
index 9a52d573..8c4cb29c 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/CommentTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/CommentTag.kt
@@ -2,6 +2,7 @@ package com.stevenfrew.beatprompter.cache.parse.tag.song
import com.stevenfrew.beatprompter.cache.parse.tag.OncePerLine
import com.stevenfrew.beatprompter.cache.parse.tag.TagName
+import com.stevenfrew.beatprompter.cache.parse.tag.TagParsingUtility
import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.ValueTag
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@@ -21,21 +22,28 @@ class CommentTag internal constructor(
) : ValueTag(name, lineNumber, position, value) {
val audience: List
val comment: String
+ val color: Int?
init {
- val bits = value.splitAndTrim(AUDIENCE_END_MARKER)
- if (bits.size > 1) {
- audience = bits[0].splitAndTrim(AUDIENCE_SEPARATOR)
- comment = bits[1]
+ val colorBits = value.splitAndTrim(COLOR_SEPARATOR)
+ color = if (colorBits.size > 1)
+ TagParsingUtility.parseColourValue(colorBits[1])
+ else
+ null
+ val audienceBits = colorBits[0].splitAndTrim(AUDIENCE_END_MARKER)
+ if (audienceBits.size > 1) {
+ audience = audienceBits[0].splitAndTrim(AUDIENCE_SEPARATOR)
+ comment = audienceBits[1]
} else {
audience = listOf()
- comment = value
+ comment = colorBits[0]
}
}
companion object {
const val AUDIENCE_END_MARKER = "|||||"
const val AUDIENCE_SEPARATOR = "@"
+ const val COLOR_SEPARATOR = "###"
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfChorusTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfChorusTag.kt
index 76ff0049..40378b13 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfChorusTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfChorusTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@StartedBy(StartOfChorusTag::class)
-@TagName("eoc", "end_of_chorus")
+@TagName("eoc", "endofchorus", "end_of_chorus")
@TagType(Type.Directive)
/**
* Tag that defines the end of a highlighted block of text.
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfHighlightTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfHighlightTag.kt
index 4d540d27..5d341bf7 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfHighlightTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfHighlightTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@StartedBy(StartOfHighlightTag::class)
-@TagName("eoh")
+@TagName("eoh", "end_of_highlight", "endofhighlight")
@TagType(Type.Directive)
/**
* Tag that defines the end of a highlighted block of text.
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationExclusionTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationExclusionTag.kt
index 1c21b869..db108493 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationExclusionTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationExclusionTag.kt
@@ -8,7 +8,14 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@StartedBy(StartOfVariationExclusionTag::class)
-@TagName("varxend", "varexend", "varxstop", "varexstop", "end_of_variation_exclusion")
+@TagName(
+ "varxend",
+ "varexend",
+ "varxstop",
+ "varexstop",
+ "end_of_variation_exclusion",
+ "endofvariationexclusion"
+)
@TagType(Type.Directive)
@OncePerLine
/**
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationInclusionTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationInclusionTag.kt
index db7dc654..0c19b13b 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationInclusionTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/EndOfVariationInclusionTag.kt
@@ -8,7 +8,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@StartedBy(StartOfVariationInclusionTag::class)
-@TagName("varend", "varstop", "end_of_variation")
+@TagName("varend", "varstop", "end_of_variation", "endofvariation")
@TagType(Type.Directive)
@OncePerLine
/**
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/FilterOnlyTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/FilterOnlyTag.kt
index 9e72c9f2..cc250bb1 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/FilterOnlyTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/FilterOnlyTag.kt
@@ -5,7 +5,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagName
import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
-@TagName("filter_only")
+@TagName("filter_only", "filteronly")
@TagType(Type.Directive)
/**
* Tag that instructs the app to NOT list this song file in the main song list. It will only be
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiEventTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiEventTag.kt
index 7b676d11..4b12b5c4 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiEventTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiEventTag.kt
@@ -1,6 +1,6 @@
package com.stevenfrew.beatprompter.cache.parse.tag.song
-import com.stevenfrew.beatprompter.Preferences
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.Cache
import com.stevenfrew.beatprompter.cache.parse.tag.MalformedTagException
@@ -15,7 +15,7 @@ import com.stevenfrew.beatprompter.midi.alias.Alias
import com.stevenfrew.beatprompter.midi.alias.ChannelValue
import com.stevenfrew.beatprompter.midi.alias.ResolutionException
import com.stevenfrew.beatprompter.midi.alias.Value
-import com.stevenfrew.beatprompter.song.event.MIDIEvent
+import com.stevenfrew.beatprompter.song.event.MidiEvent
import com.stevenfrew.beatprompter.util.splitAndTrim
@TagType(Type.Directive)
@@ -40,7 +40,7 @@ class MidiEventTag internal constructor(
offset = parsedEvent.second
}
- fun toMIDIEvent(time: Long): MIDIEvent = MIDIEvent(time, messages, offset)
+ fun toMIDIEvent(time: Long): MidiEvent = MidiEvent(time, messages, offset)
companion object {
const val MIDI_SEND_TAG = "midi_send"
@@ -63,7 +63,7 @@ class MidiEventTag internal constructor(
val (params, channelValue) =
separateParametersFromChannel(
firstPassParamValues,
- MidiMessage.getChannelFromBitmask(Preferences.defaultMIDIOutputChannel)
+ MidiMessage.getChannelFromBitmask(BeatPrompter.preferences.defaultMIDIOutputChannel)
)
try {
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiProgramChangeTriggerTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiProgramChangeTriggerTag.kt
index a77e097a..db582dcf 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiProgramChangeTriggerTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiProgramChangeTriggerTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
import com.stevenfrew.beatprompter.midi.TriggerType
@OncePerFile
-@TagName("midi_program_change_trigger")
+@TagName("midi_program_change_trigger", "midiprogramchangetrigger")
@TagType(Type.Directive)
/**
* Tag that defines a MIDI program change event that, if received, will cause this song to be
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiSongSelectTriggerTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiSongSelectTriggerTag.kt
index 74003fc6..f2ce08c5 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiSongSelectTriggerTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/MidiSongSelectTriggerTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
import com.stevenfrew.beatprompter.midi.TriggerType
@OncePerFile
-@TagName("midi_song_select_trigger")
+@TagName("midi_song_select_trigger", "midisongselecttrigger")
@TagType(Type.Directive)
/**
* Tag that defines a MIDI song select event that, if received, will cause this song to be
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/NoChordsTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/NoChordsTag.kt
new file mode 100644
index 00000000..3f871243
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/NoChordsTag.kt
@@ -0,0 +1,19 @@
+package com.stevenfrew.beatprompter.cache.parse.tag.song
+
+import com.stevenfrew.beatprompter.cache.parse.tag.OncePerFile
+import com.stevenfrew.beatprompter.cache.parse.tag.Tag
+import com.stevenfrew.beatprompter.cache.parse.tag.TagName
+import com.stevenfrew.beatprompter.cache.parse.tag.TagType
+import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
+
+@OncePerFile
+@TagName("no_chords", "nochords")
+@TagType(Type.Directive)
+/**
+ * Tag that instructs the app to ignore chords from here onwards.
+ */
+class NoChordsTag internal constructor(
+ name: String,
+ lineNumber: Int,
+ position: Int
+) : Tag(name, lineNumber, position)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/SendMIDIClockTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/SendMIDIClockTag.kt
index 02aff080..eb644424 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/SendMIDIClockTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/SendMIDIClockTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@OncePerFile
-@TagName("send_midi_clock")
+@TagName("send_midi_clock", "sendmidiclock")
@TagType(Type.Directive)
/**
* Tag that instructs the app to output MIDI clock signals at the same tempo as the song.
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfChorusTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfChorusTag.kt
index 2f95a81d..5b7e06ef 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfChorusTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfChorusTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@EndedBy(EndOfChorusTag::class)
-@TagName("soc", "start_of_chorus")
+@TagName("soc", "start_of_chorus", "startofchorus")
@TagType(Type.Directive)
/**
* Tag that defines the start of a block of highlighted text.
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfHighlightTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfHighlightTag.kt
index a32e35a3..5c7b854e 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfHighlightTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfHighlightTag.kt
@@ -1,13 +1,13 @@
package com.stevenfrew.beatprompter.cache.parse.tag.song
-import com.stevenfrew.beatprompter.Preferences
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.cache.parse.tag.EndedBy
import com.stevenfrew.beatprompter.cache.parse.tag.TagName
import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@EndedBy(EndOfHighlightTag::class)
-@TagName("soh")
+@TagName("soh", "start_of_highlight", "startofhighlight")
@TagType(Type.Directive)
/**
* Tag that defines the start of a block of highlighted text.
@@ -25,6 +25,9 @@ class StartOfHighlightTag internal constructor(
) {
companion object {
fun getDefaultHighlightColorString(): String =
- "#${((Preferences.defaultHighlightColor and 0x00FFFFFF).toString(16).padStart(6, '0'))}"
+ "#${
+ ((BeatPrompter.preferences.defaultHighlightColor and 0x00FFFFFF).toString(16)
+ .padStart(6, '0'))
+ }"
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationExclusionTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationExclusionTag.kt
index 8bb00d13..95eac034 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationExclusionTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationExclusionTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@EndedBy(EndOfVariationExclusionTag::class)
-@TagName("varxstart", "varexstart", "start_of_variation_exclusion")
+@TagName("varxstart", "varexstart", "start_of_variation_exclusion", "startofvariationexclusion")
@TagType(Type.Directive)
@OncePerLine
/**
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationInclusionTag.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationInclusionTag.kt
index c44a0a64..d3ad38ba 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationInclusionTag.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/cache/parse/tag/song/StartOfVariationInclusionTag.kt
@@ -7,7 +7,7 @@ import com.stevenfrew.beatprompter.cache.parse.tag.TagType
import com.stevenfrew.beatprompter.cache.parse.tag.find.Type
@EndedBy(EndOfVariationInclusionTag::class)
-@TagName("varstart", "start_of_variation")
+@TagName("varstart", "start_of_variation", "startofvariation")
@TagType(Type.Directive)
@OncePerLine
/**
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Chord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Chord.kt
new file mode 100644
index 00000000..621d14fa
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Chord.kt
@@ -0,0 +1,137 @@
+package com.stevenfrew.beatprompter.chord
+
+import java.util.regex.Pattern
+
+/**
+ * Represents a musical chord. For example, Am7/C would have:
+ *
+ * root: A
+ * suffix: m7
+ * bass: C
+ */
+class Chord(
+ val root: Note,
+ val suffix: String? = null,
+ val bass: Note? = null
+) : IChord {
+ companion object {
+ /**
+ * The rank for each possible chord, and also the sharp-only version.
+ * Rank is the distance in semitones from C.
+ */
+ val CHORD_RANKS: Map = mapOf(
+ Note.BSharp to 0,
+ Note.C to 0,
+ Note.CSharp to 1,
+ Note.DFlat to 1,
+ Note.D to 2,
+ Note.DSharp to 3,
+ Note.EFlat to 3,
+ Note.E to 4,
+ Note.FFlat to 4,
+ Note.ESharp to 5,
+ Note.F to 5,
+ Note.FSharp to 6,
+ Note.GFlat to 6,
+ Note.G to 7,
+ Note.GSharp to 8,
+ Note.AFlat to 8,
+ Note.A to 9,
+ Note.ASharp to 10,
+ Note.BFlat to 10,
+ Note.CFlat to 11,
+ Note.B to 11
+ )
+
+ private const val REGEX_ROOT_GROUP_NAME = "root"
+ private const val REGEX_SUFFIX_GROUP_NAME = "suffix"
+ private const val REGEX_BASS_GROUP_NAME = "bass"
+
+ private val ACCIDENTALS = listOf('b', '♭', '#', '♯', '♮')
+
+ // Regex for recognizing chords
+ private val MINOR_SUFFIXES = listOf("m", "mmaj", "mM", "min", "minor")
+ private val NOT_MINOR_SUFFIXES =
+ listOf("M", "maj", "major", "dim", "sus", "dom", "aug", "Ø", "ø", "°", "Δ", "∆", "\\+", "-")
+
+ private val TRIAD_PATTERN =
+ "(${NOT_MINOR_SUFFIXES.joinToString("|")}|${MINOR_SUFFIXES.joinToString("|")})"
+ private val ADDED_TONE_PATTERN =
+ "(\\(?([\\/\\.\\+]|add)?[${ACCIDENTALS.joinToString()}]?\\d+[\\+-]?\\)?)"
+ private val SUFFIX_PATTERN =
+ "(?<$REGEX_SUFFIX_GROUP_NAME>$TRIAD_PATTERN?\\d*\\(?$TRIAD_PATTERN?$ADDED_TONE_PATTERN*\\)?)"
+ private val BASS_PATTERN =
+ "(\\/(?<$REGEX_BASS_GROUP_NAME>[A-G](${ACCIDENTALS.joinToString("|")})?))?"
+
+ private val ROOT_PATTERN = "(?<$REGEX_ROOT_GROUP_NAME>[A-G](${ACCIDENTALS.joinToString("|")})?)"
+
+ private val CHORD_REGEX = "^$ROOT_PATTERN$SUFFIX_PATTERN$BASS_PATTERN$"
+
+ private val CHORD_REGEX_PATTERN = Pattern.compile(CHORD_REGEX)
+
+ fun parse(chord: String): Chord {
+ val result = CHORD_REGEX_PATTERN.matcher(chord)
+ if (result.find()) {
+ // For Oreo
+ /* val root = result.group(REGEX_ROOT_GROUP_NAME)
+ val suffix = result.group(REGEX_SUFFIX_GROUP_NAME)
+ val bass = result.group(REGEX_BASS_GROUP_NAME)*/
+ val root = result.group(1)
+ val suffix = result.group(3)
+ val bass = result.group(9)
+ if (!root.isNullOrBlank())
+ try {
+ return Chord(
+ Note.parse(root),
+ if (suffix.isNullOrBlank()) null else suffix,
+ if (bass.isNullOrBlank()) null else Note.parse(bass)
+ )
+ } catch (ine: InvalidNoteException) {
+ throw InvalidChordException(chord, ine)
+ }
+ }
+ throw InvalidChordException(chord)
+ }
+
+ fun isChord(token: String): Boolean = CHORD_REGEX_PATTERN.matcher(token).matches()
+
+ internal fun isMinorSuffix(suffix: String?) =
+ (suffix?.startsWith("m") == true || suffix?.startsWith("min") == true) && !suffix.startsWith("maj")
+ }
+
+ override fun toDisplayString(
+ alwaysUseSharps: Boolean,
+ useUnicodeAccidentals: Boolean,
+ majorOrMinorRootOnly: Boolean
+ ): String {
+ val replacedSuffix = suffix?.let {
+ ChordUtils.replaceAccidentals(it, useUnicodeAccidentals)
+ } ?: ""
+ val secondPart =
+ if (majorOrMinorRootOnly)
+ ""
+ else if (bass != null)
+ "$replacedSuffix/${
+ bass.toDisplayString(alwaysUseSharps, useUnicodeAccidentals, false, replacedSuffix)
+ }"
+ else
+ replacedSuffix
+ return "${
+ root.toDisplayString(
+ alwaysUseSharps,
+ useUnicodeAccidentals,
+ majorOrMinorRootOnly,
+ replacedSuffix
+ )
+ }${secondPart}"
+ }
+
+ override fun transpose(transpositionMap: Map): IChord =
+ Chord(
+ transpositionMap[root] ?: root,
+ suffix,
+ transpositionMap[bass]
+ )
+
+ val isMinor = isMinorSuffix(suffix)
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordMap.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordMap.kt
new file mode 100644
index 00000000..8fc080c3
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordMap.kt
@@ -0,0 +1,103 @@
+package com.stevenfrew.beatprompter.chord
+
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.R
+
+class ChordMap private constructor(
+ private val chordMap: Map,
+ private val key: KeySignature,
+ private val alwaysUseSharps: Boolean = false,
+ private val useUnicodeAccidentals: Boolean = false
+) : Map {
+ constructor(chordStrings: Set, firstChord: String, key: String? = null) : this(
+ chordStrings.associateWith {
+ try {
+ Chord.parse(it)
+ } catch (_: InvalidChordException) {
+ UnknownChord(it)
+ }
+ },
+ KeySignatureDefinition.getKeySignature(key, firstChord)
+ ?: throw Exception("Could not determine key signature"),
+ BeatPrompter.preferences.alwaysDisplaySharpChords,
+ BeatPrompter.preferences.displayUnicodeAccidentals,
+ )
+
+ fun transpose(amount: String): ChordMap =
+ try {
+ val shiftAmount = amount.toInt()
+ if (shiftAmount < NUMBER_OF_KEYS && shiftAmount > -NUMBER_OF_KEYS)
+ shift(shiftAmount)
+ else
+ throw Exception(BeatPrompter.appResources.getString(R.string.excessiveTransposeMagnitude))
+ } catch (nfe: NumberFormatException) {
+ // Must be a key then!
+ toKey(amount)
+ }
+
+ fun transpose(amount: Int): ChordMap = shift(amount)
+
+ private fun shift(semitones: Int): ChordMap {
+ if (semitones == 0)
+ return this
+ val newKey =
+ key.shift(semitones)
+ ?: throw Exception(
+ BeatPrompter.appResources.getString(
+ R.string.couldNotShiftKey,
+ key,
+ semitones
+ )
+ )
+ val newChords = transposeChords(key, newKey)
+ return ChordMap(newChords, newKey, alwaysUseSharps, useUnicodeAccidentals)
+ }
+
+ private fun toKey(toKey: String): ChordMap {
+ val newKey =
+ Chord.parse(toKey).let { KeySignatureDefinition.valueOf(it) }
+ ?: throw Exception(BeatPrompter.appResources.getString(R.string.failedToParseKey, toKey))
+ val newChords = transposeChords(key, newKey)
+ return ChordMap(newChords, newKey, alwaysUseSharps, useUnicodeAccidentals)
+ }
+
+ /**
+ * Transposes the given parsed text (by the parse() function) to another key.
+ */
+ private fun transposeChords(
+ fromKey: KeySignature,
+ toKey: KeySignature
+ ): Map {
+ val transpositionMap = fromKey.createTranspositionMap(toKey)
+ return chordMap.map {
+ it.key to it.value.transpose(transpositionMap)
+ }.toMap()
+ }
+
+ fun getChordDisplayString(chord: String): String? =
+ get(chord)?.toDisplayString(alwaysUseSharps, useUnicodeAccidentals)
+
+ fun addChordMapping(fromChord: String, toChord: IChord): ChordMap {
+ val mutableMap = toMutableMap()
+ mutableMap[fromChord] = toChord
+ return ChordMap(mutableMap, key, alwaysUseSharps, useUnicodeAccidentals)
+ }
+
+ override val entries: Set>
+ get() = chordMap.entries
+ override val keys: Set
+ get() = chordMap.keys
+ override val size: Int
+ get() = chordMap.size
+ override val values: Collection
+ get() = chordMap.values
+
+ override fun isEmpty(): Boolean = chordMap.isEmpty()
+ override fun get(key: String): IChord? = chordMap[key]
+ override fun containsValue(value: IChord): Boolean = chordMap.containsValue(value)
+ override fun containsKey(key: String): Boolean = chordMap.containsKey(key)
+
+ companion object {
+ const val NUMBER_OF_KEYS = 12
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordUtils.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordUtils.kt
new file mode 100644
index 00000000..cc2e84b7
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/ChordUtils.kt
@@ -0,0 +1,7 @@
+package com.stevenfrew.beatprompter.chord
+
+object ChordUtils {
+ fun replaceAccidentals(str: String, unicode: Boolean): String =
+ if (unicode) str.replace('b', '♭').replace('#', '♯') else str.replace('♭', 'b')
+ .replace('♯', '#')
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/IChord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/IChord.kt
new file mode 100644
index 00000000..7733bc13
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/IChord.kt
@@ -0,0 +1,11 @@
+package com.stevenfrew.beatprompter.chord
+
+interface IChord {
+ fun toDisplayString(
+ alwaysUseSharps: Boolean = false,
+ useUnicodeAccidentals: Boolean = false,
+ majorOrMinorRootOnly: Boolean = false
+ ): String
+
+ fun transpose(transpositionMap: Map): IChord
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidChordException.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidChordException.kt
new file mode 100644
index 00000000..e2d38a87
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidChordException.kt
@@ -0,0 +1,22 @@
+package com.stevenfrew.beatprompter.chord
+
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.R
+
+/**
+ * Chord parsing exception.
+ */
+internal class InvalidChordException : Exception {
+ constructor(chord: String) : super(getMessage(chord))
+ constructor(chord: String, invalidNoteException: InvalidNoteException) : super(
+ getMessage(chord), invalidNoteException
+ )
+
+ companion object {
+ fun getMessage(chord: String): String =
+ BeatPrompter.appResources.getString(
+ R.string.failedToParseChord,
+ chord
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidNoteException.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidNoteException.kt
new file mode 100644
index 00000000..0ae0cdee
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/InvalidNoteException.kt
@@ -0,0 +1,15 @@
+package com.stevenfrew.beatprompter.chord
+
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.R
+
+/**
+ * Note parsing exception.
+ */
+internal class InvalidNoteException(note: String) :
+ Exception(
+ BeatPrompter.appResources.getString(
+ R.string.failedToParseNote,
+ note
+ )
+ )
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignature.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignature.kt
new file mode 100644
index 00000000..8e2ffcb7
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignature.kt
@@ -0,0 +1,25 @@
+package com.stevenfrew.beatprompter.chord
+
+import com.stevenfrew.beatprompter.chord.ChordMap.Companion.NUMBER_OF_KEYS
+
+class KeySignature(
+ val chord: IChord,
+ keySignatureDefinition: KeySignatureDefinition
+) : KeySignatureDefinition(keySignatureDefinition) {
+ fun getDisplayString(useUnicodeAccidentals: Boolean): String =
+ chord.toDisplayString(false, useUnicodeAccidentals, true)
+
+ /**
+ * Finds the key that is a specified number of semitones above/below the current
+ * key.
+ */
+ fun shift(
+ semitones: Int
+ ): KeySignature? {
+ val newRank = (rank + semitones + NUMBER_OF_KEYS) % NUMBER_OF_KEYS
+ val newKeySignatureDefinition = forRank(newRank)
+ val transpositionMap = newKeySignatureDefinition?.let { createTranspositionMap(it) }
+ val transposedChord = transpositionMap?.let { chord.transpose(it) }
+ return transposedChord?.let { KeySignature(it, newKeySignatureDefinition) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignatureDefinition.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignatureDefinition.kt
new file mode 100644
index 00000000..0fefd7f6
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeySignatureDefinition.kt
@@ -0,0 +1,180 @@
+package com.stevenfrew.beatprompter.chord
+
+import com.stevenfrew.beatprompter.chord.Chord.Companion.CHORD_RANKS
+import com.stevenfrew.beatprompter.chord.ChordMap.Companion.NUMBER_OF_KEYS
+
+open class KeySignatureDefinition(
+ val majorKey: Note,
+ val relativeMinor: Note?,
+ private val keyType: KeyType,
+ val rank: Int,
+ val chromaticScale: List
+) {
+ constructor(copy: KeySignatureDefinition) : this(
+ copy.majorKey,
+ copy.relativeMinor,
+ copy.keyType,
+ copy.rank,
+ copy.chromaticScale
+ )
+
+ companion object {
+ // Chromatic scale starting from C using flats only.
+ private val FLAT_SCALE =
+ listOf(
+ Note.C,
+ Note.DFlat,
+ Note.D,
+ Note.EFlat,
+ Note.E,
+ Note.F,
+ Note.GFlat,
+ Note.G,
+ Note.AFlat,
+ Note.A,
+ Note.BFlat,
+ Note.CFlat
+ )
+
+ // Chromatic scale starting from C using sharps only.
+ private val SHARP_SCALE =
+ listOf(
+ Note.C,
+ Note.CSharp,
+ Note.D,
+ Note.DSharp,
+ Note.E,
+ Note.F,
+ Note.FSharp,
+ Note.G,
+ Note.GSharp,
+ Note.A,
+ Note.ASharp,
+ Note.B
+ )
+
+ // Chromatic scale for F# major which includes E#.
+ private val F_SHARP_SCALE = SHARP_SCALE.map { if (it == Note.F) Note.ESharp else it }
+ private val C_SHARP_SCALE =
+ F_SHARP_SCALE.map { if (it == Note.C) Note.BSharp else it }
+ private val G_FLAT_SCALE = FLAT_SCALE.map { if (it == Note.B) Note.CFlat else it }
+ private val C_FLAT_SCALE = G_FLAT_SCALE.map { if (it == Note.E) Note.FFlat else it }
+
+ private val cKeySignature =
+ KeySignatureDefinition(Note.C, Note.A, KeyType.SHARP, 0, SHARP_SCALE)
+ private val dFlatKeySignature =
+ KeySignatureDefinition(Note.DFlat, Note.BFlat, KeyType.FLAT, 1, FLAT_SCALE)
+ private val dKeySignature =
+ KeySignatureDefinition(Note.D, Note.B, KeyType.SHARP, 2, SHARP_SCALE)
+ private val eFlatKeySignature =
+ KeySignatureDefinition(Note.EFlat, Note.C, KeyType.FLAT, 3, FLAT_SCALE)
+ private val eKeySignature =
+ KeySignatureDefinition(Note.E, Note.CSharp, KeyType.SHARP, 4, SHARP_SCALE)
+ private val fKeySignature = KeySignatureDefinition(Note.F, Note.D, KeyType.FLAT, 5, FLAT_SCALE)
+ private val gFlatKeySignature =
+ KeySignatureDefinition(Note.GFlat, Note.EFlat, KeyType.FLAT, 6, G_FLAT_SCALE)
+ private val fSharpKeySignature =
+ KeySignatureDefinition(Note.FSharp, Note.DSharp, KeyType.SHARP, 6, F_SHARP_SCALE)
+ private val gKeySignature =
+ KeySignatureDefinition(Note.G, Note.E, KeyType.SHARP, 7, SHARP_SCALE)
+ private val aFlatKeySignature =
+ KeySignatureDefinition(Note.AFlat, Note.F, KeyType.FLAT, 8, FLAT_SCALE)
+ private val aKeySignature =
+ KeySignatureDefinition(Note.A, Note.FSharp, KeyType.SHARP, 9, SHARP_SCALE)
+ private val bFlatKeySignature =
+ KeySignatureDefinition(Note.BFlat, Note.G, KeyType.FLAT, 10, FLAT_SCALE)
+ private val bKeySignature =
+ KeySignatureDefinition(Note.B, Note.GSharp, KeyType.SHARP, 11, SHARP_SCALE)
+ private val cSharpKeySignature =
+ KeySignatureDefinition(Note.CSharp, Note.ASharp, KeyType.SHARP, 1, C_SHARP_SCALE)
+ private val cFlatKeySignature =
+ KeySignatureDefinition(Note.CFlat, Note.AFlat, KeyType.FLAT, 11, C_FLAT_SCALE)
+ private val dSharpKeySignature =
+ KeySignatureDefinition(Note.DSharp, null, KeyType.SHARP, 3, SHARP_SCALE)
+ private val gSharpKeySignature =
+ KeySignatureDefinition(Note.GSharp, null, KeyType.SHARP, 8, SHARP_SCALE)
+
+ /** Enum for each key signature. */
+ private val keySignatures = mapOf(
+ Note.C to cKeySignature,
+ Note.DFlat to dFlatKeySignature,
+ Note.D to dKeySignature,
+ Note.EFlat to eFlatKeySignature,
+ Note.E to eKeySignature,
+ Note.F to fKeySignature,
+ Note.GFlat to gFlatKeySignature,
+ Note.FSharp to fSharpKeySignature,
+ Note.G to gKeySignature,
+ Note.AFlat to aFlatKeySignature,
+ Note.A to aKeySignature,
+ Note.BFlat to bFlatKeySignature,
+ Note.B to bKeySignature,
+ Note.CSharp to cSharpKeySignature,
+ Note.CFlat to cFlatKeySignature,
+ Note.DSharp to dSharpKeySignature,
+ Note.GSharp to gSharpKeySignature,
+ )
+
+ private val majorKeySignatureMap = keySignatures.map {
+ it.value.majorKey to it.value
+ }.toMap()
+
+ private val minorKeySignatureMap = keySignatures.map {
+ it.value.relativeMinor to it.value
+ }.toMap()
+
+ private val rankMap = keySignatures.map {
+ it.value.rank to it.value
+ }.reversed().toMap()
+
+ /**
+ * Returns the enum constant with the specific name or returns null if the
+ * key signature is not valid.
+ */
+ fun valueOf(chord: Chord): KeySignature? {
+ val isMinor = chord.isMinor
+ val lookupMap = if (isMinor) minorKeySignatureMap else majorKeySignatureMap
+ val foundSignature = lookupMap[chord.root]
+ if (foundSignature != null)
+ return KeySignature(chord, foundSignature)
+
+ // If all else fails, try to find any key with this chord in it.
+ for (signatureKvp in keySignatures) {
+ if (signatureKvp.value.chromaticScale.contains(chord.root))
+ return KeySignature(chord, signatureKvp.value)
+ }
+ return null
+ }
+
+ internal fun forRank(rank: Int): KeySignatureDefinition? = rankMap[rank]
+
+ private fun getKeySignature(chordName: String?): KeySignature? =
+ chordName?.let {
+ try {
+ valueOf(Chord.parse(it))
+ } catch (_: InvalidChordException) {
+ null
+ }
+ }
+
+ fun getKeySignature(key: String?, firstChord: String?): KeySignature? =
+ getKeySignature(key) ?: getKeySignature(firstChord)
+ }
+
+ /**
+ * Given the current key and the number of semitones to transpose, returns a
+ * mapping from each note to a transposed note.
+ */
+ internal fun createTranspositionMap(
+ newKey: KeySignatureDefinition
+ ): Map {
+ val semitones = semitonesTo(newKey)
+ val scale: List = newKey.chromaticScale
+ return CHORD_RANKS.map { it.key to scale[(it.value + semitones + NUMBER_OF_KEYS) % NUMBER_OF_KEYS] }
+ .toMap()
+ }
+
+ /** Finds the number of semitones between the given keys. */
+ private fun semitonesTo(other: KeySignatureDefinition): Int =
+ other.rank - rank
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeyType.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeyType.kt
new file mode 100644
index 00000000..d6b06490
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/KeyType.kt
@@ -0,0 +1,3 @@
+package com.stevenfrew.beatprompter.chord
+
+enum class KeyType { FLAT, SHARP }
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Note.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Note.kt
new file mode 100644
index 00000000..c97ade6b
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/Note.kt
@@ -0,0 +1,115 @@
+package com.stevenfrew.beatprompter.chord
+
+enum class Note {
+ A,
+ AFlat,
+ ASharp,
+ B,
+ BFlat,
+ BSharp,
+ C,
+ CFlat,
+ CSharp,
+ D,
+ DFlat,
+ DSharp,
+ E,
+ EFlat,
+ ESharp,
+ F,
+ FFlat,
+ FSharp,
+ G,
+ GFlat,
+ GSharp;
+
+ private fun makeSharp(): Note =
+ when (this) {
+ A -> A
+ AFlat -> GSharp
+ ASharp -> ASharp
+ B -> B
+ BFlat -> ASharp
+ BSharp -> BSharp
+ C -> C
+ CFlat -> B
+ CSharp -> CSharp
+ D -> D
+ DFlat -> CSharp
+ DSharp -> DSharp
+ E -> E
+ EFlat -> DSharp
+ ESharp -> ESharp
+ F -> F
+ FFlat -> E
+ FSharp -> FSharp
+ G -> G
+ GFlat -> FSharp
+ GSharp -> GSharp
+ }
+
+ fun toDisplayString(
+ alwaysUseSharps: Boolean,
+ useUnicodeAccidentals: Boolean,
+ majorOrMinorRootOnly: Boolean,
+ suffix: String?
+ ): String {
+ val possiblySharpenedRoot = if (alwaysUseSharps) makeSharp() else this
+ val asString = when (possiblySharpenedRoot) {
+ A -> "A"
+ AFlat -> "Ab"
+ ASharp -> "A#"
+ B -> "B"
+ BFlat -> "Bb"
+ BSharp -> "B#"
+ C -> "C"
+ CFlat -> "Cb"
+ CSharp -> "C#"
+ D -> "D"
+ DFlat -> "Db"
+ DSharp -> "D#"
+ E -> "E"
+ EFlat -> "Eb"
+ ESharp -> "E#"
+ F -> "F"
+ FFlat -> "Fb"
+ FSharp -> "F#"
+ G -> "G"
+ GFlat -> "Gb"
+ GSharp -> "G#"
+ }
+ val asMajorOrMinorString =
+ if (majorOrMinorRootOnly && Chord.isMinorSuffix(suffix)) "${asString}m" else asString
+ return asMajorOrMinorString.let {
+ ChordUtils.replaceAccidentals(it, useUnicodeAccidentals)
+ }
+ }
+
+ companion object {
+ fun parse(note: String): Note =
+ when (note.replace("♭", "b").replace("♯", "#").replace("♮", "")) {
+ "A" -> A
+ "Ab" -> AFlat
+ "A#" -> ASharp
+ "B" -> B
+ "Bb" -> BFlat
+ "B#" -> BSharp
+ "C" -> C
+ "Cb" -> CFlat
+ "C#" -> CSharp
+ "D" -> D
+ "Db" -> DFlat
+ "D#" -> DSharp
+ "E" -> E
+ "Eb" -> EFlat
+ "E#" -> ESharp
+ "F" -> F
+ "Fb" -> FFlat
+ "F#" -> FSharp
+ "G" -> G
+ "Gb" -> GFlat
+ "G#" -> GSharp
+ else -> throw InvalidNoteException("Unknown note: $note")
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/UnknownChord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/UnknownChord.kt
new file mode 100644
index 00000000..06c31d02
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/chord/UnknownChord.kt
@@ -0,0 +1,11 @@
+package com.stevenfrew.beatprompter.chord
+
+class UnknownChord(private val chord: String) : IChord {
+ override fun toDisplayString(
+ alwaysUseSharps: Boolean,
+ useUnicodeAccidentals: Boolean,
+ majorOrMinorRootOnly: Boolean
+ ): String = ChordUtils.replaceAccidentals(chord, useUnicodeAccidentals)
+
+ override fun transpose(transpositionMap: Map): IChord = this
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Message.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Message.kt
index 6f6305b2..a2c057fd 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Message.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Message.kt
@@ -1,6 +1,6 @@
package com.stevenfrew.beatprompter.comm
-open class Message(val bytes: ByteArray) {
+open class Message(val type: MessageType, val bytes: ByteArray) {
val length: Int
get() = bytes.size
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/MessageType.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/MessageType.kt
new file mode 100644
index 00000000..fdb09291
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/MessageType.kt
@@ -0,0 +1,9 @@
+package com.stevenfrew.beatprompter.comm
+
+enum class MessageType {
+ // MIDI message (Native, USB, or BlueTooth)
+ Midi,
+
+ // Band message (always Bluetooth)
+ Band
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Sender.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Sender.kt
index 23a3a8b6..97b39f6d 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Sender.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/Sender.kt
@@ -2,4 +2,5 @@ package com.stevenfrew.beatprompter.comm
interface Sender : Communicator {
fun send(messages: List)
+ val messageType: MessageType
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/SenderTask.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/SenderTask.kt
index 856c20e3..cc1b05ec 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/SenderTask.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/SenderTask.kt
@@ -11,13 +11,17 @@ class SenderTask(private val messageQueue: MessageQueue) : Task(false) {
try {
// This take() will block if the queue is empty
val messages = messageQueue.getMessages()
+ val groupedMessages = messages.groupBy { it.type }
synchronized(sendersLock) {
for (f in senders.size - 1 downTo 0) {
// Sanity check in case a dead sender was removed.
if (f < senders.size) {
try {
- senders[f].send(messages)
+ // Only send the types of messages that it is expecting.
+ groupedMessages[senders[f].messageType]?.also {
+ senders[f].send(it)
+ }
} catch (commException: Exception) {
// Problem with the I/O? This sender is now dead to us.
Logger.logComms("Sender threw an exception. Assuming it to be dead.")
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/BandBluetoothController.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/BandBluetoothController.kt
index 545afba1..56d9d9a3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/BandBluetoothController.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/BandBluetoothController.kt
@@ -8,7 +8,6 @@ import android.content.IntentFilter
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.Logger
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.comm.CommunicationType
import com.stevenfrew.beatprompter.comm.ConnectionDescriptor
@@ -78,7 +77,7 @@ object BandBluetoothController : CoroutineScope {
when (key) {
bluetoothModeKey -> {
Logger.logComms("Bluetooth mode changed.")
- if (Preferences.bluetoothMode === BluetoothMode.None)
+ if (BeatPrompter.preferences.bluetoothMode === BluetoothMode.None)
onStopBluetooth(senderTask, receiverTasks)
else
onStartBluetooth(context, bluetoothAdapter, senderTask, receiverTasks)
@@ -86,14 +85,14 @@ object BandBluetoothController : CoroutineScope {
bandLeaderDeviceKey -> {
Logger.logComms("Band leader device changed.")
- if (Preferences.bluetoothMode === BluetoothMode.Client) {
+ if (BeatPrompter.preferences.bluetoothMode === BluetoothMode.Client) {
shutDownBluetoothClient(receiverTasks)
startBluetoothWatcherThreads(context, bluetoothAdapter, senderTask, receiverTasks)
}
}
}
}
- Preferences.registerOnSharedPreferenceChangeListener(prefsListener)
+ BeatPrompter.preferences.registerOnSharedPreferenceChangeListener(prefsListener)
this.prefsListener = prefsListener
onBluetoothActivation(context, bluetoothAdapter, senderTask, receiverTasks)
@@ -114,7 +113,7 @@ object BandBluetoothController : CoroutineScope {
receiverTasks: ReceiverTasks
) {
Logger.logComms("Bluetooth is on.")
- if (Preferences.bluetoothMode !== BluetoothMode.None)
+ if (BeatPrompter.preferences.bluetoothMode !== BluetoothMode.None)
onStartBluetooth(context, bluetoothAdapter, senderTask, receiverTasks)
}
@@ -203,12 +202,12 @@ object BandBluetoothController : CoroutineScope {
) {
if (bluetoothAdapter.isEnabled) {
synchronized(bluetoothThreadsLock) {
- when (Preferences.bluetoothMode) {
+ when (BeatPrompter.preferences.bluetoothMode) {
BluetoothMode.Client -> {
shutDownBluetoothServer(senderTask)
if (connectToBandLeaderThread == null) {
Bluetooth.getPairedDevices(context)
- .firstOrNull { it.address == Preferences.bandLeaderDevice }
+ .firstOrNull { it.address == BeatPrompter.preferences.bandLeaderDevice }
?.also {
try {
Logger.logComms({ "Starting Bluetooth client thread, looking to connect with '${it.name}'." })
@@ -255,7 +254,7 @@ object BandBluetoothController : CoroutineScope {
* new connection.
*/
private fun handleConnectionFromClient(socket: BluetoothSocket, senderTask: SenderTask) {
- if (Preferences.bluetoothMode === BluetoothMode.Server)
+ if (BeatPrompter.preferences.bluetoothMode === BluetoothMode.Server)
try {
Logger.logComms({ "Client connection opened with '${socket.remoteDevice.name}'" })
senderTask.addSender(
@@ -281,7 +280,7 @@ object BandBluetoothController : CoroutineScope {
*/
private fun setServerConnection(socket: BluetoothSocket, receiverTasks: ReceiverTasks) {
try {
- if (Preferences.bluetoothMode === BluetoothMode.Client) {
+ if (BeatPrompter.preferences.bluetoothMode === BluetoothMode.Client) {
Logger.logComms({ "Server connection opened with '${socket.remoteDevice.name}'" })
receiverTasks.addReceiver(
socket.remoteDevice.address,
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/Sender.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/Sender.kt
index 35b36f47..2b290490 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/Sender.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/Sender.kt
@@ -3,6 +3,7 @@ package com.stevenfrew.beatprompter.comm.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.BluetoothSocket
import com.stevenfrew.beatprompter.comm.CommunicationType
+import com.stevenfrew.beatprompter.comm.MessageType
import com.stevenfrew.beatprompter.comm.SenderBase
@SuppressLint("MissingPermission") // The method that uses this constructor checks for SecurityException.
@@ -16,5 +17,7 @@ class Sender(private val clientSocket: BluetoothSocket, type: CommunicationType)
)
)
+ override val messageType: MessageType = MessageType.Band
+
override fun close() = clientSocket.close()
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/BluetoothMessage.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/BluetoothMessage.kt
index 5ad509a0..e5ddf9f5 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/BluetoothMessage.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/BluetoothMessage.kt
@@ -1,8 +1,9 @@
package com.stevenfrew.beatprompter.comm.bluetooth.message
import com.stevenfrew.beatprompter.comm.Message
+import com.stevenfrew.beatprompter.comm.MessageType
-open class BluetoothMessage(bytes: ByteArray) : Message(bytes) {
+open class BluetoothMessage(bytes: ByteArray) : Message(MessageType.Band, bytes) {
companion object {
internal const val CHOOSE_SONG_MESSAGE_ID: Byte = 0
internal const val TOGGLE_START_STOP_MESSAGE_ID: Byte = 1
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/ChooseSongMessage.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/ChooseSongMessage.kt
index e45cb78c..8137034e 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/ChooseSongMessage.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/bluetooth/message/ChooseSongMessage.kt
@@ -1,7 +1,7 @@
package com.stevenfrew.beatprompter.comm.bluetooth.message
-import android.graphics.Rect
import com.stevenfrew.beatprompter.Logger
+import com.stevenfrew.beatprompter.graphics.Rect
import com.stevenfrew.beatprompter.song.load.SongChoiceInfo
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -12,88 +12,88 @@ import java.io.ObjectOutputStream
* OutgoingMessage that is sent/received when a song is chosen.
*/
class ChooseSongMessage(
- bytes: ByteArray,
- val choiceInfo: SongChoiceInfo
+ bytes: ByteArray,
+ val choiceInfo: SongChoiceInfo
) : BluetoothMessage(bytes) {
- constructor(choiceInfo: SongChoiceInfo) : this(asBytes(choiceInfo), choiceInfo)
+ constructor(choiceInfo: SongChoiceInfo) : this(asBytes(choiceInfo), choiceInfo)
- companion object {
- private fun asBytes(choiceInfo: SongChoiceInfo): ByteArray =
- ByteArrayOutputStream().apply {
- write(byteArrayOf(CHOOSE_SONG_MESSAGE_ID), 0, 1)
- ObjectOutputStream(this).apply {
- writeObject(choiceInfo.normalizedTitle)
- writeObject(choiceInfo.normalizedArtist)
- writeObject(choiceInfo.variation)
- writeBoolean(choiceInfo.isBeatScroll)
- writeBoolean(choiceInfo.isSmoothScroll)
- writeInt(choiceInfo.orientation)
- writeFloat(choiceInfo.minFontSize)
- writeFloat(choiceInfo.maxFontSize)
- writeInt(choiceInfo.screenSize.width())
- writeInt(choiceInfo.screenSize.height())
- writeBoolean(choiceInfo.noAudio)
- writeInt(choiceInfo.audioLatency)
- writeInt(choiceInfo.transposeShift)
- flush()
- close()
- }
- }.toByteArray()
+ companion object {
+ private fun asBytes(choiceInfo: SongChoiceInfo): ByteArray =
+ ByteArrayOutputStream().apply {
+ write(byteArrayOf(CHOOSE_SONG_MESSAGE_ID), 0, 1)
+ ObjectOutputStream(this).apply {
+ writeObject(choiceInfo.normalizedTitle)
+ writeObject(choiceInfo.normalizedArtist)
+ writeObject(choiceInfo.variation)
+ writeBoolean(choiceInfo.isBeatScroll)
+ writeBoolean(choiceInfo.isSmoothScroll)
+ writeInt(choiceInfo.orientation)
+ writeFloat(choiceInfo.minFontSize)
+ writeFloat(choiceInfo.maxFontSize)
+ writeInt(choiceInfo.screenSize.width)
+ writeInt(choiceInfo.screenSize.height)
+ writeBoolean(choiceInfo.noAudio)
+ writeInt(choiceInfo.audioLatency)
+ writeInt(choiceInfo.transposeShift)
+ flush()
+ close()
+ }
+ }.toByteArray()
- internal fun fromBytes(bytes: ByteArray): ChooseSongMessage {
- try {
- ByteArrayInputStream(bytes).apply {
- val dataRead = read(ByteArray(1))
- if (dataRead == 1) {
- val availableStart = available()
- val songChoiceInfo =
- ObjectInputStream(this).run {
- val title = readObject() as String
- val artist = readObject() as String
- val track = readObject() as String
- val beatScroll = readBoolean()
- val smoothScroll = readBoolean()
- val orientation = readInt()
- val minFontSize = readFloat()
- val maxFontSize = readFloat()
- val screenWidth = readInt()
- val screenHeight = readInt()
- val noAudio = readBoolean()
- var audioLatency = 0
- var transposeShift = 0
- try {
- audioLatency = readInt()
- transposeShift = readInt()
- } catch (e: Exception) {
- // Old versions will not send these last two items of data.
- // Try to cope.
- }
- SongChoiceInfo(
- title,
- artist,
- track,
- orientation,
- beatScroll,
- smoothScroll,
- minFontSize,
- maxFontSize,
- Rect(0, 0, screenWidth, screenHeight),
- noAudio,
- audioLatency,
- transposeShift
- )
- }
- val availableEnd = available()
- val messageLength = 1 + (availableStart - availableEnd)
- close()
- Logger.logLoader({ "Received Bluetooth request to load \"${songChoiceInfo.normalizedTitle}\"" })
- return ChooseSongMessage(bytes.copyOfRange(0, messageLength), songChoiceInfo)
- }
- }
- } catch (e: Exception) {
- Logger.logComms({ "Couldn't read ChooseSongMessage data, assuming not enough data" }, e)
- }
- throw NotEnoughDataException()
- }
- }
+ internal fun fromBytes(bytes: ByteArray): ChooseSongMessage {
+ try {
+ ByteArrayInputStream(bytes).apply {
+ val dataRead = read(ByteArray(1))
+ if (dataRead == 1) {
+ val availableStart = available()
+ val songChoiceInfo =
+ ObjectInputStream(this).run {
+ val title = readObject() as String
+ val artist = readObject() as String
+ val variation = readObject() as String
+ val beatScroll = readBoolean()
+ val smoothScroll = readBoolean()
+ val orientation = readInt()
+ val minFontSize = readFloat()
+ val maxFontSize = readFloat()
+ val screenWidth = readInt()
+ val screenHeight = readInt()
+ val noAudio = readBoolean()
+ var audioLatency = 0
+ var transposeShift = 0
+ try {
+ audioLatency = readInt()
+ transposeShift = readInt()
+ } catch (e: Exception) {
+ // Old versions will not send these last two items of data.
+ // Try to cope.
+ }
+ SongChoiceInfo(
+ title,
+ artist,
+ variation,
+ orientation,
+ beatScroll,
+ smoothScroll,
+ minFontSize,
+ maxFontSize,
+ Rect(0, 0, screenWidth, screenHeight),
+ noAudio,
+ audioLatency,
+ transposeShift
+ )
+ }
+ val availableEnd = available()
+ val messageLength = 1 + (availableStart - availableEnd)
+ close()
+ Logger.logLoader({ "Received Bluetooth request to load \"${songChoiceInfo.normalizedTitle}\"" })
+ return ChooseSongMessage(bytes.copyOfRange(0, messageLength), songChoiceInfo)
+ }
+ }
+ } catch (e: Exception) {
+ Logger.logComms({ "Couldn't read ChooseSongMessage data, assuming not enough data" }, e)
+ }
+ throw NotEnoughDataException()
+ }
+ }
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/BluetoothMidiController.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/BluetoothMidiController.kt
index 3869412b..6094530d 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/BluetoothMidiController.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/BluetoothMidiController.kt
@@ -10,7 +10,6 @@ import android.media.midi.MidiManager
import android.media.midi.MidiManager.OnDeviceOpenedListener
import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.Logger
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.comm.CommunicationType
import com.stevenfrew.beatprompter.comm.ReceiverTasks
@@ -74,14 +73,14 @@ object BluetoothMidiController {
Logger.logComms("Bluetooth MIDI connection types or target devices changed ... restarting connections.")
Logger.logComms("Removing all Bluetooth MIDI senders & receivers.")
onBluetoothStopped(senderTask, receiverTasks)
- if (Preferences.midiConnectionTypes.contains(ConnectionType.Bluetooth)) {
+ if (BeatPrompter.preferences.midiConnectionTypes.contains(ConnectionType.Bluetooth)) {
Logger.logComms("Bluetooth is still a selected MIDI connection type ... attempting Bluetooth MIDI connections.")
attemptBluetoothMidiConnections(bluetoothAdapter, manager, listener)
}
}
}
}
- Preferences.registerOnSharedPreferenceChangeListener(prefsListener)
+ BeatPrompter.preferences.registerOnSharedPreferenceChangeListener(prefsListener)
this.prefsListener = prefsListener
attemptBluetoothMidiConnections(bluetoothAdapter, manager, listener)
}
@@ -102,7 +101,7 @@ object BluetoothMidiController {
manager: MidiManager,
listener: OnDeviceOpenedListener
) =
- Preferences.bluetoothMidiDevices.run {
+ BeatPrompter.preferences.bluetoothMidiDevices.run {
Bluetooth.getPairedDevices(bluetoothAdapter).filter { contains(it.address) }
.forEach { attemptMidiConnection(manager, it, listener) }
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Midi.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Midi.kt
index a0a608c4..97dc74e8 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Midi.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Midi.kt
@@ -1,8 +1,8 @@
package com.stevenfrew.beatprompter.comm.midi
import android.content.Context
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.Logger
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.Task
import com.stevenfrew.beatprompter.cache.Cache
import com.stevenfrew.beatprompter.comm.Message
@@ -57,7 +57,7 @@ object Midi {
val messages = it.resolve(
Cache.cachedCloudItems.midiAliases,
byteArrayOf(),
- MidiMessage.getChannelFromBitmask(Preferences.defaultMIDIOutputChannel)
+ MidiMessage.getChannelFromBitmask(BeatPrompter.preferences.defaultMIDIOutputChannel)
)
messages.forEach { msg -> tryPutMessage(msg, it.name) }
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/MidiSenderBase.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/MidiSenderBase.kt
new file mode 100644
index 00000000..cedecfa4
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/MidiSenderBase.kt
@@ -0,0 +1,12 @@
+package com.stevenfrew.beatprompter.comm.midi
+
+import com.stevenfrew.beatprompter.comm.CommunicationType
+import com.stevenfrew.beatprompter.comm.MessageType
+import com.stevenfrew.beatprompter.comm.SenderBase
+
+abstract class MidiSenderBase(
+ name: String,
+ type: CommunicationType
+) : SenderBase(name, type) {
+ override val messageType: MessageType = MessageType.Midi
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeMidiController.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeMidiController.kt
index ac38f0dd..a049b1a3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeMidiController.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeMidiController.kt
@@ -4,7 +4,7 @@ import android.content.Context
import android.content.pm.PackageManager
import android.media.midi.MidiDeviceInfo
import android.media.midi.MidiManager
-import com.stevenfrew.beatprompter.Preferences
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.comm.CommunicationType
import com.stevenfrew.beatprompter.comm.ReceiverTasks
import com.stevenfrew.beatprompter.comm.SenderTask
@@ -37,7 +37,7 @@ object NativeMidiController {
}
private fun addNativeDevice(nativeDeviceInfo: MidiDeviceInfo, manager: MidiManager) {
- if (Preferences.midiConnectionTypes.contains(ConnectionType.Native))
+ if (BeatPrompter.preferences.midiConnectionTypes.contains(ConnectionType.Native))
manager.openDevice(nativeDeviceInfo, deviceListener, null)
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeReceiver.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeReceiver.kt
index 27e96779..1a51e1d9 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeReceiver.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeReceiver.kt
@@ -11,6 +11,7 @@ import kotlin.math.min
class NativeReceiver(
// Annoyingly, if we don't keep a hold of the MIDI device reference for Bluetooth MIDI, then
// it automatically closes. So I'm storing it here.
+ // ⚠️⚠️⚠️⚠️ SO WHATEVER YOU DO, DO NOT REMOVE THIS SO-CALLED "UNUSED" PARAMETER!!! ⚠️⚠️⚠️⚠️
private val device: MidiDevice,
private val port: MidiOutputPort,
name: String,
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeSender.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeSender.kt
index 074c8097..406be42b 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeSender.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/NativeSender.kt
@@ -3,16 +3,16 @@ package com.stevenfrew.beatprompter.comm.midi
import android.media.midi.MidiDevice
import android.media.midi.MidiInputPort
import com.stevenfrew.beatprompter.comm.CommunicationType
-import com.stevenfrew.beatprompter.comm.SenderBase
class NativeSender(
// Annoyingly, if we don't keep a hold of the MIDI device reference for Bluetooth MIDI, then
// it automatically closes. So I'm storing it here.
+ // ⚠️⚠️⚠️⚠️ SO WHATEVER YOU DO, DO NOT REMOVE THIS SO-CALLED "UNUSED" PARAMETER!!! ⚠️⚠️⚠️⚠️
private val device: MidiDevice,
private val port: MidiInputPort,
name: String,
type: CommunicationType
-) : SenderBase(name, type) {
+) : MidiSenderBase(name, type) {
override fun sendMessageData(bytes: ByteArray, length: Int) = port.send(bytes, 0, length)
override fun close() = port.close()
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Receiver.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Receiver.kt
index 122a1f18..58779911 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Receiver.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/Receiver.kt
@@ -2,7 +2,6 @@ package com.stevenfrew.beatprompter.comm.midi
import android.content.SharedPreferences
import com.stevenfrew.beatprompter.BeatPrompter
-import com.stevenfrew.beatprompter.Preferences
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.comm.CommunicationType
import com.stevenfrew.beatprompter.comm.ReceiverBase
@@ -107,7 +106,7 @@ abstract class Receiver(name: String, type: CommunicationType) : ReceiverBase(na
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
init {
- Preferences.registerOnSharedPreferenceChangeListener(this)
+ BeatPrompter.preferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
@@ -121,7 +120,7 @@ abstract class Receiver(name: String, type: CommunicationType) : ReceiverBase(na
private var incomingChannels = getIncomingChannelsPrefValue()
private val incomingChannelsLock = Any()
- private fun getIncomingChannelsPrefValue(): Int = Preferences.incomingMIDIChannels
+ private fun getIncomingChannelsPrefValue(): Int = BeatPrompter.preferences.incomingMIDIChannels
private fun getIncomingChannels(): Int =
synchronized(incomingChannelsLock) {
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbBroadcastReceiver.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbBroadcastReceiver.kt
index c9b474fa..c1412c23 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbBroadcastReceiver.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbBroadcastReceiver.kt
@@ -38,6 +38,7 @@ internal class UsbBroadcastReceiver(
UsbMidiController.ACTION_USB_PERMISSION -> {
synchronized(this) {
getDeviceFromIntent(intent)?.apply {
+ val displayName = if (productName.isNullOrBlank()) deviceName else productName!!
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
val midiInterface = getUsbDeviceMidiInterface()
if (midiInterface != null) {
@@ -60,7 +61,7 @@ internal class UsbBroadcastReceiver(
)
ConnectionNotificationTask.addConnection(
ConnectionDescriptor(
- deviceName,
+ displayName,
CommunicationType.UsbMidi
)
)
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbMidiController.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbMidiController.kt
index b849d581..1f994725 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbMidiController.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbMidiController.kt
@@ -9,7 +9,7 @@ import android.content.pm.PackageManager
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
-import com.stevenfrew.beatprompter.Preferences
+import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.comm.ReceiverTasks
import com.stevenfrew.beatprompter.comm.SenderTask
import com.stevenfrew.beatprompter.util.getUsbDeviceMidiInterface
@@ -49,7 +49,7 @@ object UsbMidiController {
}
internal fun attemptUsbMidiConnection(manager: UsbManager, permissionIntent: PendingIntent) {
- if (Preferences.midiConnectionTypes.contains(ConnectionType.USBOnTheGo)) {
+ if (BeatPrompter.preferences.midiConnectionTypes.contains(ConnectionType.USBOnTheGo)) {
val list = manager.deviceList
if (list != null && list.size > 0) {
val devObjects = list.values
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbSender.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbSender.kt
index c1ab9941..3499f7ba 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbSender.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/UsbSender.kt
@@ -4,7 +4,6 @@ import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import com.stevenfrew.beatprompter.comm.CommunicationType
import com.stevenfrew.beatprompter.comm.Message
-import com.stevenfrew.beatprompter.comm.SenderBase
import com.stevenfrew.beatprompter.comm.midi.message.UsbMidiMessage
class UsbSender(
@@ -12,7 +11,7 @@ class UsbSender(
private val endpoint: UsbEndpoint,
name: String,
type: CommunicationType
-) : SenderBase(name, type) {
+) : MidiSenderBase(name, type) {
override fun close() = connection.close()
override fun sendMessageData(bytes: ByteArray, length: Int) {
connection.bulkTransfer(endpoint, bytes, length, 5000)
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/message/MidiMessage.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/message/MidiMessage.kt
index c1086552..ca012633 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/message/MidiMessage.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/comm/midi/message/MidiMessage.kt
@@ -1,10 +1,11 @@
package com.stevenfrew.beatprompter.comm.midi.message
import com.stevenfrew.beatprompter.comm.Message
+import com.stevenfrew.beatprompter.comm.MessageType
import kotlin.experimental.and
import kotlin.experimental.or
-open class MidiMessage(bytes: ByteArray) : Message(bytes) {
+open class MidiMessage(bytes: ByteArray) : Message(MessageType.Midi, bytes) {
constructor(byte: Byte) : this(byteArrayOf(byte))
constructor(byte1: Byte, byte2: Byte) : this(byteArrayOf(byte1, byte2))
constructor(byte1: Byte, byte2: Byte, byte3: Byte) : this(byteArrayOf(byte1, byte2, byte3))
@@ -42,10 +43,4 @@ open class MidiMessage(bytes: ByteArray) : Message(bytes) {
return 0
}
}
-
- private fun isSystemCommonMessage(message: Byte): Boolean =
- bytes.isNotEmpty() && bytes[0] == message
-
- private fun isChannelVoiceMessage(message: Byte): Boolean =
- bytes.isNotEmpty() && (bytes[0] and 0xF0.toByte() == message)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ColorRect.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ColorRect.kt
index fbecdb34..590b2390 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ColorRect.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ColorRect.kt
@@ -6,4 +6,7 @@ data class ColorRect(
val right: Int,
val bottom: Int,
val color: Int
-)
+) {
+ val height = bottom - top
+ val width = right - left
+}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/DisplaySettings.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/DisplaySettings.kt
index a5dc46f7..440e779e 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/DisplaySettings.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/DisplaySettings.kt
@@ -1,6 +1,5 @@
package com.stevenfrew.beatprompter.graphics
-import android.graphics.Rect
import com.stevenfrew.beatprompter.song.load.SongChoiceInfo
class DisplaySettings internal constructor(
@@ -8,24 +7,24 @@ class DisplaySettings internal constructor(
val minimumFontSize: Float,
val maximumFontSize: Float,
val screenSize: Rect,
- val showBeatCounter: Boolean
+ val showBeatCounter: Boolean = true
) {
internal constructor(choiceInfo: SongChoiceInfo)
: this(
choiceInfo.orientation,
choiceInfo.minFontSize,
choiceInfo.maxFontSize,
- choiceInfo.screenSize,
+ Rect(choiceInfo.screenSize),
choiceInfo.isBeatScroll || choiceInfo.isSmoothScroll
)
// Top 5% of screen is used for beat counter
private val beatCounterHeight =
if (showBeatCounter)
- (screenSize.height() / 20.0).toInt()
+ (screenSize.height / 20.0).toInt()
else
0
- val beatCounterRect = Rect(0, 0, screenSize.width(), beatCounterHeight)
- val usableScreenHeight = screenSize.height() - beatCounterRect.height()
+ val beatCounterRect = Rect(0, 0, screenSize.width, beatCounterHeight)
+ val usableScreenHeight = screenSize.height - beatCounterRect.height
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/LineGraphic.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/LineGraphic.kt
index f8f6a0e1..59d87094 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/LineGraphic.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/LineGraphic.kt
@@ -1,7 +1,7 @@
package com.stevenfrew.beatprompter.graphics
-import android.graphics.Bitmap
-import android.graphics.Rect
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.graphics.bitmaps.Bitmap
import com.stevenfrew.beatprompter.song.line.Line
class LineGraphic(private val size: Rect) {
@@ -18,10 +18,9 @@ class LineGraphic(private val size: Rect) {
}
private fun createBitmap(): Bitmap =
- Bitmap.createBitmap(
- size.width(),
- size.height(),
- Bitmap.Config.ARGB_8888
+ BeatPrompter.platformUtils.bitmapFactory.createBitmap(
+ size.width,
+ size.height
)
fun recycle() {
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/Rect.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/Rect.kt
new file mode 100644
index 00000000..8fb7c5ba
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/Rect.kt
@@ -0,0 +1,15 @@
+package com.stevenfrew.beatprompter.graphics
+
+data class Rect(
+ val left: Int,
+ val top: Int,
+ val right: Int,
+ val bottom: Int
+) {
+ constructor(rect: android.graphics.Rect) : this(rect.left, rect.top, rect.right, rect.bottom)
+ constructor(rect: Rect) : this(rect.left, rect.top, rect.right, rect.bottom)
+ constructor() : this(0, 0, 0, 0)
+
+ val height = bottom - top
+ val width = right - left
+}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenComment.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenComment.kt
index c8ddbfc1..3d13065d 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenComment.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenComment.kt
@@ -4,52 +4,51 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PointF
-import android.graphics.Rect
import android.graphics.RectF
-import android.graphics.Typeface
-import com.stevenfrew.beatprompter.util.Utils
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.util.inflate
class ScreenComment(
private val text: String,
screenSize: Rect,
- paint: Paint,
- font: Typeface
+ paint: Paint
) {
private val screenString: ScreenString
private val textDrawLocation: PointF
private val popupRect: RectF
init {
- val maxCommentBoxHeight = (screenSize.height() / 4.0).toInt()
- val maxTextWidth = (screenSize.width() * 0.9).toInt()
+ val maxCommentBoxHeight = (screenSize.height / 4.0).toInt()
+ val maxTextWidth = (screenSize.width * 0.9).toInt()
val maxTextHeight = (maxCommentBoxHeight * 0.9).toInt()
screenString =
- ScreenString.create(text, paint, maxTextWidth, maxTextHeight, Color.BLACK, font, false)
+ ScreenString.create(text, paint, maxTextWidth, maxTextHeight, Color.BLACK)
val rectWidth = (screenString.width * 1.1).toFloat()
val rectHeight = (screenString.height * 1.1).toFloat()
val heightDiff = ((rectHeight - screenString.height) / 2.0).toFloat()
- val rectX = ((screenSize.width() - rectWidth) / 2.0).toFloat()
- val rectY = screenSize.height() - rectHeight - 10
+ val rectX = ((screenSize.width - rectWidth) / 2.0).toFloat()
+ val rectY = screenSize.height - rectHeight - (screenSize.height * 0.05).toInt()
val textWidth = screenString.width
- val textX = ((screenSize.width() - textWidth) / 2.0).toFloat()
+ val textX = ((screenSize.width - textWidth) / 2.0).toFloat()
val textY = rectY + rectHeight - (screenString.descenderOffset + heightDiff)
popupRect = RectF(rectX, rectY, rectX + rectWidth, rectY + rectHeight)
textDrawLocation = PointF(textX, textY)
}
fun draw(canvas: Canvas, paint: Paint, textColor: Int) {
+ val backgroundColor = BeatPrompter.preferences.backgroundColor
+ val outline =
+ if ((Color.red(backgroundColor) + Color.green(backgroundColor) + Color.blue(backgroundColor)) / 3 > 127) Color.BLACK else Color.WHITE
+
paint.apply {
- textSize = screenString.fontSize * Utils.FONT_SCALING
+ BeatPrompter.platformUtils.fontManager.setTextSize(this, screenString.fontSize)
flags = Paint.ANTI_ALIAS_FLAG
- color = Color.BLACK
+ color = outline
}
canvas.drawRect(popupRect, paint)
- paint.color = Color.WHITE
+ paint.color = backgroundColor
canvas.drawRect(
- popupRect.left + 1,
- popupRect.top + 1,
- popupRect.right - 1,
- popupRect.bottom - 1,
+ popupRect.inflate(-1),
paint
)
paint.color = textColor
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenString.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenString.kt
index 7f69273f..66cc2827 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenString.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/ScreenString.kt
@@ -1,210 +1,47 @@
package com.stevenfrew.beatprompter.graphics
import android.graphics.Paint
-import android.graphics.Rect
-import android.graphics.Typeface
-import com.stevenfrew.beatprompter.util.Utils
-import kotlin.math.ceil
-import kotlin.math.floor
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.graphics.fonts.FontManager
import kotlin.math.max
class ScreenString private constructor(
- internal val text: String,
- internal val fontSize: Float,
- internal val color: Int,
+ val text: String,
+ val fontSize: Float,
+ val color: Int,
width: Int,
height: Int,
- internal val typeface: Typeface,
- val descenderOffset: Int
+ val descenderOffset: Int,
+ val bold: Boolean
) {
val width = max(0, width)
val height = max(0, height)
companion object {
- private const val MARGIN_PIXELS = 10
- private const val MASKING = true
- private const val MASKING_STRING = "X"
- private const val DOUBLE_MASKING_STRING = MASKING_STRING + MASKING_STRING
-
- private val boldDoubleXWidth = IntArray(Utils.MAXIMUM_FONT_SIZE - Utils.MINIMUM_FONT_SIZE + 1)
- private val regularDoubleXWidth =
- IntArray(Utils.MAXIMUM_FONT_SIZE - Utils.MINIMUM_FONT_SIZE + 1)
-
- init {
- for (f in Utils.MINIMUM_FONT_SIZE..Utils.MAXIMUM_FONT_SIZE) {
- regularDoubleXWidth[f - Utils.MINIMUM_FONT_SIZE] = -1
- boldDoubleXWidth[f - Utils.MINIMUM_FONT_SIZE] =
- regularDoubleXWidth[f - Utils.MINIMUM_FONT_SIZE]
- }
- }
-
- private fun getTextRect(
- str: String,
- paint: Paint, r: Rect
- ) {
- val measureWidth = paint.measureText(str)
- paint.getTextBounds(str, 0, str.length, r)
- r.left = 0
- r.right = ceil(measureWidth.toDouble()).toInt()
- }
-
- private val doubleXRect = Rect()
- private fun getDoubleXStringLength(
- paint: Paint,
- fontSize: Float,
- bold: Boolean
- ): Int {
- val intFontSize = (fontSize.toInt() - Utils.MINIMUM_FONT_SIZE).let {
- // This should never happen, but let's check anyway.
- when {
- it < 0 -> 0
- it >= boldDoubleXWidth.size -> boldDoubleXWidth.size - 1
- else -> it
- }
- }
-
- return (if (bold) boldDoubleXWidth else regularDoubleXWidth)[intFontSize].let {
- if (it == -1) {
- getTextRect(DOUBLE_MASKING_STRING, paint, doubleXRect)
- val newSize = doubleXRect.width()
- (if (bold) boldDoubleXWidth else regularDoubleXWidth)[intFontSize] = newSize
- newSize
- } else
- it
- }
- }
-
- private val stringWidthRect = Rect()
- internal fun getStringWidth(
- paint: Paint,
- strIn: String?,
- face: Typeface,
- fontSize: Float
- ): Int {
- if (strIn.isNullOrEmpty())
- return 0
- paint.typeface = face
- paint.textSize = fontSize * Utils.FONT_SCALING
- val str = if (MASKING) "$MASKING_STRING$strIn$MASKING_STRING" else strIn
- getTextRect(str, paint, stringWidthRect)
- return stringWidthRect.width() - if (MASKING) getDoubleXStringLength(
- paint,
- fontSize,
- false
- ) else 0
- }
-
- internal fun getBestFontSize(
- text: String,
- paint: Paint,
- minimumFontSize: Float,
- maximumFontSize: Float,
- maxWidth: Int,
- maxHeight: Int,
- face: Typeface
- ): Int =
- getBestFontSize(
- text,
- paint,
- minimumFontSize,
- maximumFontSize,
- maxWidth,
- maxHeight,
- face,
- false
- )
-
fun create(
text: String,
paint: Paint,
maxWidth: Int,
maxHeight: Int,
color: Int,
- face: Typeface,
- bold: Boolean
+ bold: Boolean = false
): ScreenString {
- val fontSize = getBestFontSize(
+ val (fontSize, bestFontSizeRect) = BeatPrompter.platformUtils.fontManager.getBestFontSize(
text,
paint,
- Utils.MINIMUM_FONT_SIZE.toFloat(),
- Utils.MAXIMUM_FONT_SIZE.toFloat(),
maxWidth,
maxHeight,
- face,
bold
)
return ScreenString(
text,
fontSize.toFloat(),
color,
- bestFontSizeRect.width(),
- bestFontSizeRect.height() + MARGIN_PIXELS,
- face,
- bestFontSizeRect.bottom
+ bestFontSizeRect.width,
+ bestFontSizeRect.height + FontManager.MARGIN_PIXELS,
+ bestFontSizeRect.bottom,
+ bold
)
}
-
- private val measureRect = Rect()
- var mMeasuredWidth = 0
- var mMeasuredHeight = 0
- var mMeasuredDescenderOffset = 0
- fun measure(
- text: String,
- paint: Paint,
- fontSize: Float,
- face: Typeface
- ) {
- paint.typeface = face
- paint.textSize = fontSize * Utils.FONT_SCALING
- val measureText = if (MASKING) "$MASKING_STRING$text$MASKING_STRING" else text
- getTextRect(measureText, paint, measureRect)
- if (MASKING)
- measureRect.right -= getDoubleXStringLength(paint, fontSize, false)
- mMeasuredWidth = max(0, measureRect.width())
- mMeasuredHeight = max(0, measureRect.height() + MARGIN_PIXELS)
- mMeasuredDescenderOffset = measureRect.bottom
- }
-
- private val bestFontSizeRect = Rect()
- private fun getBestFontSize(
- textIn: String,
- paint: Paint,
- minimumFontSize: Float,
- maximumFontSize: Float,
- maxWidth: Int,
- maxHeight: Int,
- face: Typeface,
- bold: Boolean
- ): Int {
- if (maxWidth <= 0)
- return 0
- var hi = maximumFontSize
- var lo = minimumFontSize
- val threshold = 0.5f // How close we have to be
-
- val text = if (MASKING) "$MASKING_STRING$textIn$MASKING_STRING" else textIn
- paint.typeface = face
- while (hi - lo > threshold) {
- val size = ((hi + lo) / 2.0).toFloat()
- val intSize = floor(size.toDouble()).toInt()
- paint.textSize = intSize * Utils.FONT_SCALING
- getTextRect(text, paint, bestFontSizeRect)
- val widthXX =
- (if (MASKING) getDoubleXStringLength(paint, intSize.toFloat(), bold) else 0).toFloat()
- if (bestFontSizeRect.width() - widthXX >= maxWidth || maxHeight != -1 && bestFontSizeRect.height() >= maxHeight - MARGIN_PIXELS)
- hi = size // too big
- else
- lo = size // too small
- }
- // Use lo so that we undershoot rather than overshoot
- val sizeToUse = floor(lo.toDouble()).toInt()
- paint.textSize = sizeToUse * Utils.FONT_SCALING
- getTextRect(text, paint, bestFontSizeRect)
- if (MASKING) {
- val widthXX = getDoubleXStringLength(paint, sizeToUse.toFloat(), bold).toFloat()
- bestFontSizeRect.right -= widthXX.toInt()
- }
- return sizeToUse
- }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmap.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmap.kt
new file mode 100644
index 00000000..4490011a
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmap.kt
@@ -0,0 +1,11 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+class AndroidBitmap(internal val androidBitmap: android.graphics.Bitmap) : Bitmap {
+ override val isRecycled: Boolean
+ get() = androidBitmap.isRecycled
+
+ override fun recycle() = androidBitmap.recycle()
+ override fun toCanvas(): BitmapCanvas = AndroidBitmapCanvas(androidBitmap)
+ override val width: Int = androidBitmap.width
+ override val height: Int = androidBitmap.height
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapCanvas.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapCanvas.kt
new file mode 100644
index 00000000..a40a2040
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapCanvas.kt
@@ -0,0 +1,36 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.Rect
+
+class AndroidBitmapCanvas(bitmap: android.graphics.Bitmap) : BitmapCanvas {
+ private val canvas = Canvas(bitmap)
+ override fun drawBitmap(bitmap: Bitmap, x: Float, y: Float, paint: Paint) =
+ canvas.drawBitmap((bitmap as AndroidBitmap).androidBitmap, x, y, paint)
+
+ override fun drawBitmap(bitmap: Bitmap, srcRect: Rect, destRect: Rect, paint: Paint) =
+ canvas.drawBitmap((bitmap as AndroidBitmap).androidBitmap, srcRect, destRect, paint)
+
+ override fun drawColor(color: Int, mode: PorterDuff.Mode) =
+ canvas.drawColor(color, mode)
+
+ override fun drawText(text: String, x: Float, y: Float, paint: Paint) =
+ canvas.drawText(text, x, y, paint)
+
+ override fun clipRect(left: Int, top: Int, right: Int, bottom: Int) {
+ canvas.clipRect(left, top, right, bottom)
+ }
+
+ override fun drawRect(rect: Rect, paint: Paint) =
+ canvas.drawRect(rect, paint)
+
+ override fun save() {
+ canvas.save()
+ }
+
+ override fun restore() {
+ canvas.restore()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapFactory.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapFactory.kt
new file mode 100644
index 00000000..2d6a1531
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/AndroidBitmapFactory.kt
@@ -0,0 +1,22 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+object AndroidBitmapFactory : BitmapFactory {
+ override fun createBitmap(width: Int, height: Int): Bitmap =
+ AndroidBitmap(
+ android.graphics.Bitmap.createBitmap(
+ width,
+ height,
+ android.graphics.Bitmap.Config.ARGB_8888
+ )
+ )
+
+ override fun createBitmap(path: String): Bitmap =
+ AndroidBitmap(
+ android.graphics.BitmapFactory.decodeFile(
+ path,
+ DEFAULT_OPTIONS
+ )
+ )
+
+ private val DEFAULT_OPTIONS = android.graphics.BitmapFactory.Options()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/Bitmap.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/Bitmap.kt
new file mode 100644
index 00000000..2375a1d0
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/Bitmap.kt
@@ -0,0 +1,9 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+interface Bitmap {
+ val isRecycled: Boolean
+ fun recycle()
+ fun toCanvas(): BitmapCanvas
+ val width: Int
+ val height: Int
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapCanvas.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapCanvas.kt
new file mode 100644
index 00000000..5476f212
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapCanvas.kt
@@ -0,0 +1,16 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.Rect
+
+interface BitmapCanvas {
+ fun drawBitmap(bitmap: Bitmap, x: Float, y: Float, paint: Paint)
+ fun drawBitmap(bitmap: Bitmap, srcRect: Rect, destRect: Rect, paint: Paint)
+ fun drawColor(color: Int, mode: PorterDuff.Mode)
+ fun drawText(text: String, x: Float, y: Float, paint: Paint)
+ fun clipRect(left: Int, top: Int, right: Int, bottom: Int)
+ fun drawRect(rect: Rect, paint: Paint)
+ fun save()
+ fun restore()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapFactory.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapFactory.kt
new file mode 100644
index 00000000..6f31a44d
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/bitmaps/BitmapFactory.kt
@@ -0,0 +1,6 @@
+package com.stevenfrew.beatprompter.graphics.bitmaps
+
+interface BitmapFactory {
+ fun createBitmap(width: Int, height: Int): Bitmap
+ fun createBitmap(path: String): Bitmap
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/AndroidFontManager.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/AndroidFontManager.kt
new file mode 100644
index 00000000..b3183a77
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/AndroidFontManager.kt
@@ -0,0 +1,162 @@
+package com.stevenfrew.beatprompter.graphics.fonts
+
+import android.graphics.Paint
+import android.graphics.Typeface
+import com.stevenfrew.beatprompter.graphics.Rect
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.max
+
+class AndroidFontManager(
+ override val minimumFontSize: Float,
+ override val maximumFontSize: Float,
+ override val fontScaling: Float
+) : FontManager {
+ companion object {
+ private val NORMAL_TYPEFACE = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
+ private val BOLD_TYPEFACE = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
+
+ private const val MASKING = true
+ private const val MASKING_STRING = "X"
+ private const val DOUBLE_MASKING_STRING = MASKING_STRING + MASKING_STRING
+ }
+
+ private val boldDoubleXWidth: Array> = Array(
+ ceil(maximumFontSize).toInt() - floor(minimumFontSize).toInt() + 1,
+ init = { _ -> -1 to Rect() }
+ )
+ private val regularDoubleXWidth: Array> = Array(
+ ceil(maximumFontSize).toInt() - floor(minimumFontSize).toInt() + 1,
+ init = { _ -> -1 to Rect() }
+ )
+
+ private fun getTextRect(
+ str: String,
+ paint: Paint,
+ fontSize: Float
+ ): Rect {
+ setTextSize(paint, fontSize)
+ val measureWidth = paint.measureText(str)
+ val androidRect = android.graphics.Rect()
+ paint.getTextBounds(str, 0, str.length, androidRect)
+ androidRect.left = 0
+ androidRect.right = ceil(measureWidth.toDouble()).toInt()
+ return Rect(androidRect)
+ }
+
+ private fun getDoubleXStringLength(
+ paint: Paint,
+ fontSize: Float,
+ bold: Boolean
+ ): Pair {
+ val intFontSize = (fontSize - minimumFontSize).let {
+ // This should never happen, but let's check anyway.
+ when {
+ it < 0 -> 0
+ it >= boldDoubleXWidth.size -> boldDoubleXWidth.size - 1
+ else -> it
+ }.toInt()
+ }
+
+ return (if (bold) boldDoubleXWidth else regularDoubleXWidth)[intFontSize].let {
+ if (it.first == -1) {
+ val doubleXRect = getTextRect(DOUBLE_MASKING_STRING, paint, fontSize)
+ val newSize = doubleXRect.width to doubleXRect
+ (if (bold) boldDoubleXWidth else regularDoubleXWidth)[intFontSize] = newSize
+ newSize
+ } else
+ it
+ }
+ }
+
+ override fun getStringWidth(
+ paint: Paint,
+ strIn: String,
+ fontSize: Float,
+ bold: Boolean
+ ): Pair {
+ if (strIn.isEmpty())
+ return 0 to Rect()
+ setTypeface(paint, bold)
+ val str = if (MASKING) "$MASKING_STRING$strIn$MASKING_STRING" else strIn
+ val stringWidthRect = getTextRect(str, paint, fontSize)
+ return (stringWidthRect.width - if (MASKING) getDoubleXStringLength(
+ paint,
+ fontSize,
+ bold
+ ).first else 0) to stringWidthRect
+ }
+
+ override fun measure(
+ text: String,
+ paint: Paint,
+ fontSize: Float,
+ bold: Boolean
+ ): TextMeasurement {
+ setTypeface(paint, bold)
+ val measureText = if (MASKING) "$MASKING_STRING$text$MASKING_STRING" else text
+ var measureRect = getTextRect(measureText, paint, fontSize)
+ if (MASKING)
+ measureRect = Rect(
+ measureRect.left,
+ measureRect.top,
+ measureRect.right - getDoubleXStringLength(paint, fontSize, bold).first,
+ measureRect.bottom
+ )
+ val measuredWidth = max(0, measureRect.width)
+ val measuredHeight = max(0, measureRect.height + FontManager.MARGIN_PIXELS)
+ val measuredDescenderOffset = measureRect.bottom
+ return TextMeasurement(measureRect, measuredWidth, measuredHeight, measuredDescenderOffset)
+ }
+
+ override fun setTypeface(paint: Paint, bold: Boolean) {
+ paint.typeface = if (bold) BOLD_TYPEFACE else NORMAL_TYPEFACE
+ }
+
+ override fun setTextSize(paint: Paint, size: Float) {
+ paint.textSize = size * fontScaling
+ }
+
+ override fun getBestFontSize(
+ text: String,
+ paint: Paint,
+ maxWidth: Int,
+ maxHeight: Int,
+ bold: Boolean,
+ minimumFontSize: Float?,
+ maximumFontSize: Float?
+ ): Pair {
+ if (maxWidth <= 0)
+ return 0 to Rect()
+ var hi = maximumFontSize ?: this.maximumFontSize
+ var lo = minimumFontSize ?: this.minimumFontSize
+ val threshold = 0.5f // How close we have to be
+
+ val maskedText = if (MASKING) "$MASKING_STRING$text$MASKING_STRING" else text
+ setTypeface(paint, bold)
+ while (hi - lo > threshold) {
+ val size = ((hi + lo) / 2.0).toFloat()
+ val intSize = floor(size.toDouble()).toInt()
+ val bestFontSizeRect = getTextRect(maskedText, paint, intSize.toFloat())
+ val widthXX =
+ (if (MASKING) getDoubleXStringLength(paint, intSize.toFloat(), bold).first else 0).toFloat()
+ if (bestFontSizeRect.width - widthXX >= maxWidth || maxHeight != -1 && bestFontSizeRect.height >= maxHeight - FontManager.MARGIN_PIXELS)
+ hi = size // too big
+ else
+ lo = size // too small
+ }
+ // Use lo so that we undershoot rather than overshoot
+ val sizeToUse = floor(lo.toDouble()).toInt()
+ var bestFontSizeRect = getTextRect(maskedText, paint, sizeToUse.toFloat())
+ if (MASKING) {
+ val widthXX = getDoubleXStringLength(paint, sizeToUse.toFloat(), bold).first.toFloat()
+ bestFontSizeRect = Rect(
+ bestFontSizeRect.left,
+ bestFontSizeRect.top,
+ bestFontSizeRect.right - widthXX.toInt(),
+ bestFontSizeRect.bottom
+ )
+ }
+ return sizeToUse to bestFontSizeRect
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/FontManager.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/FontManager.kt
new file mode 100644
index 00000000..35433705
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/FontManager.kt
@@ -0,0 +1,48 @@
+package com.stevenfrew.beatprompter.graphics.fonts
+
+import android.graphics.Paint
+import com.stevenfrew.beatprompter.graphics.Rect
+
+interface FontManager {
+ fun getStringWidth(
+ paint: Paint,
+ strIn: String,
+ fontSize: Float,
+ bold: Boolean = false
+ ): Pair
+
+ fun getBestFontSize(
+ text: String,
+ paint: Paint,
+ maxWidth: Int,
+ maxHeight: Int,
+ bold: Boolean = false,
+ minimumFontSize: Float? = null,
+ maximumFontSize: Float? = null,
+ ): Pair
+
+ fun measure(
+ text: String,
+ paint: Paint,
+ fontSize: Float,
+ bold: Boolean = false
+ ): TextMeasurement
+
+ fun setTypeface(
+ paint: Paint,
+ bold: Boolean = false
+ )
+
+ fun setTextSize(
+ paint: Paint,
+ size: Float
+ )
+
+ val maximumFontSize: Float
+ val minimumFontSize: Float
+ val fontScaling: Float
+
+ companion object {
+ const val MARGIN_PIXELS = 10
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/TextMeasurement.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/TextMeasurement.kt
new file mode 100644
index 00000000..85656449
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/graphics/fonts/TextMeasurement.kt
@@ -0,0 +1,10 @@
+package com.stevenfrew.beatprompter.graphics.fonts
+
+import com.stevenfrew.beatprompter.graphics.Rect
+
+data class TextMeasurement(
+ val rect: Rect = Rect(),
+ var width: Int = 0,
+ var height: Int = 0,
+ var descenderOffset: Int = 0
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/Preferences.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AbstractPreferences.kt
similarity index 64%
rename from app/src/main/kotlin/com/stevenfrew/beatprompter/Preferences.kt
rename to app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AbstractPreferences.kt
index ad517a55..c79e25ff 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/Preferences.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AbstractPreferences.kt
@@ -1,8 +1,8 @@
-package com.stevenfrew.beatprompter
+package com.stevenfrew.beatprompter.preferences
import android.content.SharedPreferences
-import android.graphics.Color
import androidx.appcompat.app.AppCompatDelegate
+import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.audio.AudioPlayerType
import com.stevenfrew.beatprompter.cache.parse.ShowBPMContext
import com.stevenfrew.beatprompter.comm.bluetooth.BluetoothMode
@@ -12,38 +12,42 @@ import com.stevenfrew.beatprompter.storage.StorageType
import com.stevenfrew.beatprompter.ui.SongView
import com.stevenfrew.beatprompter.ui.pref.MetronomeContext
import com.stevenfrew.beatprompter.ui.pref.SortingPreference
+import com.stevenfrew.beatprompter.util.GlobalAppResources
-object Preferences {
- val midiConnectionTypes: Set
+abstract class AbstractPreferences(
+ private val appResources: GlobalAppResources
+) : Preferences {
+
+ override val midiConnectionTypes: Set
get() = try {
getStringSetPreference(
R.string.pref_midiConnectionTypes_key,
- BeatPrompter.appResources.getStringSet(R.array.pref_midiConnectionTypes_defaultValues)
+ appResources.getStringSet(R.array.pref_midiConnectionTypes_defaultValues)
).map { ConnectionType.valueOf(it) }.toSet()
} catch (e: Exception) {
// Backwards compatibility with old shite values from previous app versions.
setOf(ConnectionType.USBOnTheGo)
}
- val alwaysDisplaySharpChords: Boolean
+ override val alwaysDisplaySharpChords: Boolean
get() = getBooleanPreference(
R.string.pref_alwaysDisplaySharpChords_key,
false
)
- val displayUnicodeAccidentals: Boolean
+ override val displayUnicodeAccidentals: Boolean
get() = getBooleanPreference(
R.string.pref_displayUnicodeAccidentals_key,
false
)
- val bluetoothMidiDevices: Set
+ override val bluetoothMidiDevices: Set
get() = getStringSetPreference(
R.string.pref_bluetoothMidiDevices_key,
- BeatPrompter.appResources.getStringSet(R.array.pref_bluetoothMidiDevices_defaultValues)
+ appResources.getStringSet(R.array.pref_bluetoothMidiDevices_defaultValues)
).toSet()
- var darkMode: Boolean
+ override var darkMode: Boolean
get() = getBooleanPreference(
R.string.pref_darkMode_key,
false
@@ -53,30 +57,33 @@ object Preferences {
AppCompatDelegate.setDefaultNightMode(if (value) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
}
- val defaultTrackVolume: Int
+ override val defaultTrackVolume: Int
get() = getIntPreference(
R.string.pref_defaultTrackVolume_key,
R.string.pref_defaultTrackVolume_default,
1
)
- val defaultMIDIOutputChannel: Int
+ override val defaultMIDIOutputChannel: Int
get() = getIntPreference(
R.string.pref_defaultMIDIOutputChannel_key,
R.string.pref_defaultMIDIOutputChannel_default,
0
)
- val defaultHighlightColor: Int
+ override val defaultHighlightColor: Int
get() = getColorPreference(
R.string.pref_highlightColor_key,
R.string.pref_highlightColor_default
)
- val bandLeaderDevice: String
+ override val bandLeaderDevice: String
get() = getStringPreference(R.string.pref_bandLeaderDevice_key, "")
- val bluetoothMode: BluetoothMode
+ override val preferredVariation: String
+ get() = getStringPreference(R.string.pref_preferredVariation_key, "")
+
+ override val bluetoothMode: BluetoothMode
get() = try {
BluetoothMode.valueOf(
getStringPreference(
@@ -89,31 +96,31 @@ object Preferences {
BluetoothMode.None
}
- val incomingMIDIChannels: Int
+ override val incomingMIDIChannels: Int
get() = getIntPreference(R.string.pref_midiIncomingChannels_key, 65535)
- var cloudDisplayPath: String
+ override var cloudDisplayPath: String
get() = getStringPreference(R.string.pref_cloudDisplayPath_key, "")
set(value) = setStringPreference(R.string.pref_cloudDisplayPath_key, value)
- var cloudPath: String
+ override var cloudPath: String
get() = getStringPreference(R.string.pref_cloudPath_key, "")
set(value) = setStringPreference(R.string.pref_cloudPath_key, value)
- val includeSubFolders: Boolean
+ override val includeSubFolders: Boolean
get() = getBooleanPreference(R.string.pref_includeSubfolders_key, false)
- var firstRun: Boolean
+ override var firstRun: Boolean
get() = getBooleanPreference(R.string.pref_firstRun_key, true)
set(value) = setBooleanPreference(R.string.pref_firstRun_key, value)
- val manualMode: Boolean
+ override val manualMode: Boolean
get() = getBooleanPreference(R.string.pref_manualMode_key, false)
- val mute: Boolean
+ override val mute: Boolean
get() = getBooleanPreference(R.string.pref_mute_key, false)
- var sorting: Array
+ override var sorting: Array
get() = try {
val stringPref = getStringPreference(
R.string.pref_sorting_key,
@@ -131,34 +138,34 @@ object Preferences {
setStringPreference(R.string.pref_sorting_key, newString)
}
- val defaultCountIn: Int
+ override val defaultCountIn: Int
get() = getIntPreference(R.string.pref_countIn_key, R.string.pref_countIn_default, 0)
- val audioLatency: Int
+ override val audioLatency: Int
get() = getIntPreference(R.string.pref_audioLatency_key, R.string.pref_countIn_default, 0)
- val sendMIDIClock: Boolean
+ override val sendMIDIClock: Boolean
get() = getBooleanPreference(R.string.pref_sendMidi_key, false)
- val customCommentsUser: String
+ override val customCommentsUser: String
get() = getStringPreference(
R.string.pref_customComments_key,
R.string.pref_customComments_defaultValue
)
- val showChords: Boolean
+ override val showChords: Boolean
get() = getBooleanPreference(
R.string.pref_showChords_key,
R.string.pref_showChords_defaultValue
)
- val showKey: Boolean
+ override val showKey: Boolean
get() = getBooleanPreference(
R.string.pref_showSongKey_key,
R.string.pref_showSongKey_defaultValue
)
- val showBPMContext: ShowBPMContext
+ override val showBPMContext: ShowBPMContext
get() = try {
ShowBPMContext.valueOf(
getStringPreference(
@@ -171,7 +178,7 @@ object Preferences {
ShowBPMContext.No
}
- val sendMIDITriggerOnStart: TriggerOutputContext
+ override val sendMIDITriggerOnStart: TriggerOutputContext
get() = try {
TriggerOutputContext.valueOf(
getStringPreference(
@@ -184,7 +191,7 @@ object Preferences {
TriggerOutputContext.ManualStartOnly
}
- val metronomeContext: MetronomeContext
+ override val metronomeContext: MetronomeContext
get() = try {
MetronomeContext.valueOf(
getStringPreference(
@@ -197,37 +204,37 @@ object Preferences {
MetronomeContext.Off
}
- val lyricColor: Int
+ override val lyricColor: Int
get() = getColorPreference(R.string.pref_lyricColor_key, R.string.pref_lyricColor_default)
- val chordColor: Int
+ override val chordColor: Int
get() = getColorPreference(R.string.pref_chordColor_key, R.string.pref_chordColor_default)
- val chorusHighlightColor: Int
+ override val chorusHighlightColor: Int
get() = getColorPreference(
R.string.pref_chorusSectionHighlightColor_key,
R.string.pref_chorusSectionHighlightColor_default
)
- val annotationColor: Int
+ override val annotationColor: Int
get() = getColorPreference(
R.string.pref_annotationColor_key,
R.string.pref_annotationColor_default
)
- val largePrint: Boolean
+ override val largePrint: Boolean
get() = getBooleanPreference(
R.string.pref_largePrintList_key,
R.string.pref_largePrintList_defaultValue
)
- val proximityScroll: Boolean
+ override val proximityScroll: Boolean
get() = getBooleanPreference(R.string.pref_proximityScroll_key, false)
- val anyOtherKeyPageDown: Boolean
+ override val anyOtherKeyPageDown: Boolean
get() = getBooleanPreference(R.string.pref_anyOtherKeyPageDown_key, false)
- var storageSystem: StorageType
+ override var storageSystem: StorageType
get() = try {
StorageType.valueOf(
getStringPreference(
@@ -241,94 +248,94 @@ object Preferences {
}
set(value) = setStringPreference(R.string.pref_cloudStorageSystem_key, value.name)
- val onlyUseBeatFontSizes: Boolean
+ override val onlyUseBeatFontSizes: Boolean
get() = getBooleanPreference(
R.string.pref_alwaysUseBeatFontPrefs_key,
R.string.pref_alwaysUseBeatFontPrefs_defaultValue
)
private val minimumFontSizeOffset =
- Integer.parseInt(BeatPrompter.appResources.getString(R.string.fontSizeMin))
+ Integer.parseInt(appResources.getString(R.string.fontSizeMin))
- val minimumBeatFontSize: Int
+ override val minimumBeatFontSize: Int
get() = getIntPreference(
R.string.pref_minFontSize_key,
R.string.pref_minFontSize_default,
minimumFontSizeOffset
)
- val maximumBeatFontSize: Int
+ override val maximumBeatFontSize: Int
get() = getIntPreference(
R.string.pref_maxFontSize_key,
R.string.pref_maxFontSize_default,
minimumFontSizeOffset
)
- val minimumSmoothFontSize: Int
+ override val minimumSmoothFontSize: Int
get() = getIntPreference(
R.string.pref_minFontSizeSmooth_key,
R.string.pref_minFontSizeSmooth_default,
minimumFontSizeOffset
)
- val maximumSmoothFontSize: Int
+ override val maximumSmoothFontSize: Int
get() = getIntPreference(
R.string.pref_maxFontSizeSmooth_key,
R.string.pref_maxFontSizeSmooth_default,
minimumFontSizeOffset
)
- val minimumManualFontSize: Int
+ override val minimumManualFontSize: Int
get() = getIntPreference(
R.string.pref_minFontSizeManual_key,
R.string.pref_minFontSizeManual_default,
minimumFontSizeOffset
)
- val maximumManualFontSize: Int
+ override val maximumManualFontSize: Int
get() = getIntPreference(
R.string.pref_maxFontSizeManual_key,
R.string.pref_maxFontSizeManual_default,
minimumFontSizeOffset
)
- val useExternalStorage: Boolean
+ override val useExternalStorage: Boolean
get() = getBooleanPreference(R.string.pref_useExternalStorage_key, false)
- val mimicBandLeaderDisplay: Boolean
+ override val mimicBandLeaderDisplay: Boolean
get() = getBooleanPreference(R.string.pref_mimicBandLeaderDisplay_key, true)
- val playNextSong: String
+ override val playNextSong: String
get() = getStringPreference(
R.string.pref_automaticallyPlayNextSong_key,
R.string.pref_automaticallyPlayNextSong_defaultValue
)
- val showBeatStyleIcons: Boolean
+ override val showBeatStyleIcons: Boolean
get() = getBooleanPreference(
R.string.pref_showBeatStyleIcons_key,
R.string.pref_showBeatStyleIcons_defaultValue
)
- val showKeyInSongList: Boolean
+ override val showKeyInSongList: Boolean
get() = getBooleanPreference(
R.string.pref_showKeyInList_key,
R.string.pref_showKeyInList_defaultValue
)
- val showRatingInSongList: Boolean
+ override val showRatingInSongList: Boolean
get() = getBooleanPreference(
R.string.pref_showRatingInList_key,
R.string.pref_showRatingInList_defaultValue
)
- val showMusicIcon: Boolean
+ override val showMusicIcon: Boolean
get() = getBooleanPreference(
R.string.pref_showMusicIcon_key,
R.string.pref_showMusicIcon_defaultValue
)
- val screenAction: SongView.ScreenAction
+ override val screenAction: SongView.ScreenAction
get() = try {
SongView.ScreenAction.valueOf(
getStringPreference(
@@ -341,7 +348,7 @@ object Preferences {
SongView.ScreenAction.Scroll
}
- val audioPlayer: AudioPlayerType
+ override val audioPlayer: AudioPlayerType
get() = try {
AudioPlayerType.valueOf(
getStringPreference(
@@ -354,29 +361,29 @@ object Preferences {
AudioPlayerType.MediaPlayer
}
- val showScrollIndicator: Boolean
+ override val showScrollIndicator: Boolean
get() = getBooleanPreference(
R.string.pref_showScrollIndicator_key,
R.string.pref_showScrollIndicator_defaultValue
)
- val showSongTitle: Boolean
+ override val showSongTitle: Boolean
get() = getBooleanPreference(
R.string.pref_showSongTitle_key,
R.string.pref_showSongTitle_defaultValue
)
private val commentDisplayTimeOffset =
- Integer.parseInt(BeatPrompter.appResources.getString(R.string.pref_commentDisplayTime_offset))
+ Integer.parseInt(appResources.getString(R.string.pref_commentDisplayTime_offset))
- val commentDisplayTime: Int
+ override val commentDisplayTime: Int
get() = getIntPreference(
R.string.pref_commentDisplayTime_key,
R.string.pref_commentDisplayTime_default,
commentDisplayTimeOffset
)
- val midiTriggerSafetyCatch: SongView.TriggerSafetyCatch
+ override val midiTriggerSafetyCatch: SongView.TriggerSafetyCatch
get() = try {
SongView.TriggerSafetyCatch.valueOf(
getStringPreference(
@@ -389,87 +396,87 @@ object Preferences {
SongView.TriggerSafetyCatch.WhenAtTitleScreenOrPausedOrLastLine
}
- val highlightCurrentLine: Boolean
+ override val highlightCurrentLine: Boolean
get() = getBooleanPreference(
R.string.pref_highlightCurrentLine_key,
R.string.pref_highlightCurrentLine_defaultValue
)
- val showPageDownMarker: Boolean
+ override val showPageDownMarker: Boolean
get() = getBooleanPreference(
R.string.pref_highlightPageDownLine_key,
R.string.pref_highlightPageDownLine_defaultValue
)
- val clearTagsOnFolderChange: Boolean
+ override val clearTagsOnFolderChange: Boolean
get() = getBooleanPreference(
R.string.pref_clearTagFilterOnFolderChange_key,
R.string.pref_highlightPageDownLine_defaultValue
)
- val highlightBeatSectionStart: Boolean
+ override val highlightBeatSectionStart: Boolean
get() = getBooleanPreference(
R.string.pref_highlightBeatSectionStart_key,
R.string.pref_highlightBeatSectionStart_defaultValue
)
- val beatCounterColor: Int
+ override val beatCounterColor: Int
get() = getColorPreference(
R.string.pref_beatCounterColor_key,
R.string.pref_beatCounterColor_default
)
- val commentColor: Int
+ override val commentColor: Int
get() = getColorPreference(
R.string.pref_commentTextColor_key,
R.string.pref_commentTextColor_default
)
- val scrollIndicatorColor: Int
+ override val scrollIndicatorColor: Int
get() = getColorPreference(
R.string.pref_scrollMarkerColor_key,
R.string.pref_scrollMarkerColor_default
)
- val beatSectionStartHighlightColor: Int
+ override val beatSectionStartHighlightColor: Int
get() = getColorPreference(
R.string.pref_beatSectionStartHighlightColor_key,
R.string.pref_beatSectionStartHighlightColor_default
)
- val currentLineHighlightColor: Int
+ override val currentLineHighlightColor: Int
get() = getColorPreference(
R.string.pref_currentLineHighlightColor_key,
R.string.pref_currentLineHighlightColor_default
)
- val pageDownMarkerColor: Int
+ override val pageDownMarkerColor: Int
get() = getColorPreference(
R.string.pref_pageDownScrollHighlightColor_key,
R.string.pref_pageDownScrollHighlightColor_default
)
- val pulseDisplay: Boolean
+ override val pulseDisplay: Boolean
get() = getBooleanPreference(R.string.pref_pulse_key, R.string.pref_pulse_defaultValue)
- val backgroundColor: Int
+ override val backgroundColor: Int
get() = getColorPreference(
R.string.pref_backgroundColor_key,
R.string.pref_backgroundColor_default
)
- val pulseColor: Int
+ override val pulseColor: Int
get() = getColorPreference(R.string.pref_pulseColor_key, R.string.pref_pulseColor_default)
- var dropboxAccessToken: String
+ override var dropboxAccessToken: String
get() = getPrivateStringPreference(R.string.pref_dropboxAccessToken_key, "")
set(value) = setPrivateStringPreference(R.string.pref_dropboxAccessToken_key, value)
- var dropboxRefreshToken: String
+ override var dropboxRefreshToken: String
get() = getPrivateStringPreference(R.string.pref_dropboxRefreshToken_key, "")
set(value) = setPrivateStringPreference(R.string.pref_dropboxRefreshToken_key, value)
- var dropboxExpiryTime: Long
+ override var dropboxExpiryTime: Long
get() = getPrivateLongPreference(R.string.pref_dropboxExpiryTime_key, 0L)
set(value) = setPrivateLongPreference(R.string.pref_dropboxExpiryTime_key, value)
@@ -480,150 +487,62 @@ object Preferences {
): Int {
return getIntPreference(
prefResourceString,
- BeatPrompter.appResources.getString(prefDefaultResourceString).toInt()
+ appResources.getString(prefDefaultResourceString).toInt()
) + offset
}
- private fun getIntPreference(prefResourceString: Int, default: Int): Int {
- return BeatPrompter
- .appResources
- .preferences
- .getInt(BeatPrompter.appResources.getString(prefResourceString), default)
- }
+ protected abstract fun getIntPreference(prefResourceString: Int, default: Int): Int
+ abstract override fun getStringPreference(key: String, default: String): String
+ abstract override fun getStringSetPreference(key: String, default: Set): Set
- fun getStringPreference(key: String, default: String): String =
- BeatPrompter
- .appResources
- .preferences
- .getString(key, default) ?: default
+ @Suppress("SameParameterValue")
+ protected abstract fun getPrivateStringPreference(
+ prefResourceString: Int,
+ default: String
+ ): String
- fun getStringSetPreference(key: String, default: Set): Set =
- BeatPrompter
- .appResources
- .preferences
- .getStringSet(key, default) ?: default
+ protected abstract fun setStringPreference(prefResourceString: Int, value: String)
@Suppress("SameParameterValue")
- private fun getPrivateStringPreference(prefResourceString: Int, default: String): String =
- BeatPrompter
- .appResources
- .privatePreferences
- .getString(
- BeatPrompter.appResources.getString(prefResourceString),
- default
- ) ?: default
-
+ protected abstract fun setPrivateStringPreference(prefResourceString: Int, value: String)
@Suppress("SameParameterValue")
- private fun getPrivateLongPreference(prefResourceString: Int, default: Long): Long {
- return BeatPrompter
- .appResources
- .privatePreferences
- .getLong(
- BeatPrompter.appResources.getString(prefResourceString),
- default
- )
- }
+ protected abstract fun setPrivateLongPreference(prefResourceString: Int, value: Long)
+ abstract override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener)
+ abstract override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener)
- private fun getStringPreference(prefResourceString: Int, default: String): String =
- BeatPrompter
- .appResources
- .preferences
- .getString(
- BeatPrompter.appResources.getString(prefResourceString),
- default
- ) ?: default
-
- private fun getStringSetPreference(prefResourceString: Int, default: Set): Set =
- BeatPrompter
- .appResources
- .preferences
- .getStringSet(
- BeatPrompter.appResources.getString(prefResourceString),
- default
- ) ?: default
+ @Suppress("SameParameterValue")
+ protected abstract fun getPrivateLongPreference(prefResourceString: Int, default: Long): Long
+ protected abstract fun getStringPreference(prefResourceString: Int, default: String): String
+ protected abstract fun getStringSetPreference(
+ prefResourceString: Int,
+ default: Set
+ ): Set
- private fun getStringPreference(
+ protected abstract fun getColorPreference(
prefResourceString: Int,
prefDefaultResourceString: Int
- ): String = BeatPrompter.appResources.getString(prefDefaultResourceString).let {
- getStringPreference(
- prefResourceString,
- it
- )
- }
+ ): Int
- private fun getColorPreference(prefResourceString: Int, prefDefaultResourceString: Int): Int {
- return BeatPrompter
- .appResources
- .preferences
- .getInt(
- BeatPrompter.appResources.getString(prefResourceString),
- Color.parseColor(BeatPrompter.appResources.getString(prefDefaultResourceString))
- )
- }
-
- private fun getBooleanPreference(prefResourceString: Int, default: Boolean): Boolean {
- return BeatPrompter
- .appResources
- .preferences
- .getBoolean(BeatPrompter.appResources.getString(prefResourceString), default)
- }
+ protected abstract fun getBooleanPreference(prefResourceString: Int, default: Boolean): Boolean
@Suppress("SameParameterValue")
- private fun setBooleanPreference(prefResourceString: Int, value: Boolean) {
- BeatPrompter
- .appResources
- .preferences
- .edit()
- .putBoolean(BeatPrompter.appResources.getString(prefResourceString), value)
- .apply()
- }
+ protected abstract fun setBooleanPreference(prefResourceString: Int, value: Boolean)
+
+ private fun getStringPreference(
+ prefResourceString: Int,
+ prefDefaultResourceString: Int
+ ): String = getStringPreference(
+ prefResourceString,
+ appResources.getString(prefDefaultResourceString)
+ )
private fun getBooleanPreference(
prefResourceString: Int,
prefDefaultResourceString: Int
- ): Boolean {
- return getBooleanPreference(
+ ): Boolean =
+ getBooleanPreference(
prefResourceString,
- BeatPrompter.appResources.getString(prefDefaultResourceString).toBoolean()
+ appResources.getString(prefDefaultResourceString).toBoolean()
)
- }
-
- private fun setStringPreference(prefResourceString: Int, value: String) {
- BeatPrompter
- .appResources
- .preferences
- .edit()
- .putString(BeatPrompter.appResources.getString(prefResourceString), value)
- .apply()
- }
-
- @Suppress("SameParameterValue")
- private fun setPrivateStringPreference(prefResourceString: Int, value: String) {
- BeatPrompter
- .appResources
- .privatePreferences
- .edit()
- .putString(BeatPrompter.appResources.getString(prefResourceString), value)
- .apply()
- }
-
- @Suppress("SameParameterValue")
- private fun setPrivateLongPreference(prefResourceString: Int, value: Long) {
- BeatPrompter
- .appResources
- .privatePreferences
- .edit()
- .putLong(BeatPrompter.appResources.getString(prefResourceString), value)
- .apply()
- }
-
- fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
- BeatPrompter.appResources.preferences.registerOnSharedPreferenceChangeListener(listener)
- }
-
- fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
- BeatPrompter.appResources.preferences.unregisterOnSharedPreferenceChangeListener(listener)
- }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AndroidPreferences.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AndroidPreferences.kt
new file mode 100644
index 00000000..96f3755c
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/AndroidPreferences.kt
@@ -0,0 +1,113 @@
+package com.stevenfrew.beatprompter.preferences
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
+import androidx.preference.PreferenceManager
+import com.stevenfrew.beatprompter.BeatPrompter
+import com.stevenfrew.beatprompter.R
+import com.stevenfrew.beatprompter.util.GlobalAppResources
+
+class AndroidPreferences(
+ private val appResources: GlobalAppResources,
+ applicationContext: Context,
+) : AbstractPreferences(appResources) {
+ private val publicPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ private val privatePreferences =
+ applicationContext.getSharedPreferences(SHARED_PREFERENCES_ID, MODE_PRIVATE)
+
+ init {
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.preferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.fontsizepreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.colorpreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.filepreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.midipreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.bluetoothpreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.permissionpreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.songdisplaypreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.audiopreferences, true)
+ PreferenceManager.setDefaultValues(applicationContext, R.xml.songlistpreferences, true)
+ }
+
+ override fun getIntPreference(prefResourceString: Int, default: Int): Int =
+ publicPreferences.getInt(appResources.getString(prefResourceString), default)
+
+ override fun getStringPreference(key: String, default: String): String =
+ publicPreferences.getString(key, default) ?: default
+
+ override fun getStringSetPreference(key: String, default: Set): Set =
+ publicPreferences.getStringSet(key, default) ?: default
+
+ @Suppress("SameParameterValue")
+ override fun getPrivateStringPreference(prefResourceString: Int, default: String): String =
+ privatePreferences.getString(
+ appResources.getString(prefResourceString),
+ default
+ ) ?: default
+
+ @Suppress("SameParameterValue")
+ override fun getPrivateLongPreference(prefResourceString: Int, default: Long): Long =
+ privatePreferences
+ .getLong(
+ appResources.getString(prefResourceString),
+ default
+ )
+
+ override fun getStringPreference(prefResourceString: Int, default: String): String =
+ publicPreferences.getString(
+ appResources.getString(prefResourceString),
+ default
+ ) ?: default
+
+ override fun getStringSetPreference(prefResourceString: Int, default: Set): Set =
+ publicPreferences.getStringSet(
+ appResources.getString(prefResourceString),
+ default
+ ) ?: default
+
+ override fun getColorPreference(prefResourceString: Int, prefDefaultResourceString: Int): Int =
+ publicPreferences.getInt(
+ appResources.getString(prefResourceString),
+ BeatPrompter.platformUtils.parseColor(appResources.getString(prefDefaultResourceString))
+ )
+
+ override fun getBooleanPreference(prefResourceString: Int, default: Boolean): Boolean =
+ publicPreferences.getBoolean(appResources.getString(prefResourceString), default)
+
+ @Suppress("SameParameterValue")
+ override fun setBooleanPreference(prefResourceString: Int, value: Boolean) =
+ publicPreferences
+ .edit()
+ .putBoolean(appResources.getString(prefResourceString), value)
+ .apply()
+
+ override fun setStringPreference(prefResourceString: Int, value: String) =
+ publicPreferences
+ .edit()
+ .putString(appResources.getString(prefResourceString), value)
+ .apply()
+
+ @Suppress("SameParameterValue")
+ override fun setPrivateStringPreference(prefResourceString: Int, value: String) =
+ privatePreferences
+ .edit()
+ .putString(appResources.getString(prefResourceString), value)
+ .apply()
+
+ @Suppress("SameParameterValue")
+ override fun setPrivateLongPreference(prefResourceString: Int, value: Long) =
+ privatePreferences
+ .edit()
+ .putLong(appResources.getString(prefResourceString), value)
+ .apply()
+
+ override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
+ publicPreferences.registerOnSharedPreferenceChangeListener(listener)
+
+ override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
+ publicPreferences.unregisterOnSharedPreferenceChangeListener(listener)
+
+ companion object {
+ private const val SHARED_PREFERENCES_ID = "beatPrompterSharedPreferences"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/Preferences.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/Preferences.kt
new file mode 100644
index 00000000..f9228d9a
--- /dev/null
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/preferences/Preferences.kt
@@ -0,0 +1,93 @@
+package com.stevenfrew.beatprompter.preferences
+
+import android.content.SharedPreferences
+import com.stevenfrew.beatprompter.audio.AudioPlayerType
+import com.stevenfrew.beatprompter.cache.parse.ShowBPMContext
+import com.stevenfrew.beatprompter.comm.bluetooth.BluetoothMode
+import com.stevenfrew.beatprompter.comm.midi.ConnectionType
+import com.stevenfrew.beatprompter.midi.TriggerOutputContext
+import com.stevenfrew.beatprompter.storage.StorageType
+import com.stevenfrew.beatprompter.ui.SongView
+import com.stevenfrew.beatprompter.ui.pref.MetronomeContext
+import com.stevenfrew.beatprompter.ui.pref.SortingPreference
+
+interface Preferences {
+ val midiConnectionTypes: Set
+ val alwaysDisplaySharpChords: Boolean
+ val displayUnicodeAccidentals: Boolean
+ val bluetoothMidiDevices: Set
+ var darkMode: Boolean
+ val defaultTrackVolume: Int
+ val defaultMIDIOutputChannel: Int
+ val defaultHighlightColor: Int
+ val bandLeaderDevice: String
+ val preferredVariation: String
+ val bluetoothMode: BluetoothMode
+ val incomingMIDIChannels: Int
+ var cloudDisplayPath: String
+ var cloudPath: String
+ val includeSubFolders: Boolean
+ var firstRun: Boolean
+ val manualMode: Boolean
+ val mute: Boolean
+ var sorting: Array
+ val defaultCountIn: Int
+ val audioLatency: Int
+ val sendMIDIClock: Boolean
+ val customCommentsUser: String
+ val showChords: Boolean
+ val showKey: Boolean
+ val showBPMContext: ShowBPMContext
+ val sendMIDITriggerOnStart: TriggerOutputContext
+ val metronomeContext: MetronomeContext
+ val lyricColor: Int
+ val chordColor: Int
+ val chorusHighlightColor: Int
+ val annotationColor: Int
+ val largePrint: Boolean
+ val proximityScroll: Boolean
+ val anyOtherKeyPageDown: Boolean
+ var storageSystem: StorageType
+ val onlyUseBeatFontSizes: Boolean
+ val minimumBeatFontSize: Int
+ val maximumBeatFontSize: Int
+ val minimumSmoothFontSize: Int
+ val maximumSmoothFontSize: Int
+ val minimumManualFontSize: Int
+ val maximumManualFontSize: Int
+ val useExternalStorage: Boolean
+ val mimicBandLeaderDisplay: Boolean
+ val playNextSong: String
+ val showBeatStyleIcons: Boolean
+ val showKeyInSongList: Boolean
+ val showRatingInSongList: Boolean
+ val showMusicIcon: Boolean
+ val screenAction: SongView.ScreenAction
+ val audioPlayer: AudioPlayerType
+ val showScrollIndicator: Boolean
+ val showSongTitle: Boolean
+ val commentDisplayTime: Int
+ val midiTriggerSafetyCatch: SongView.TriggerSafetyCatch
+ val highlightCurrentLine: Boolean
+ val showPageDownMarker: Boolean
+ val clearTagsOnFolderChange: Boolean
+ val highlightBeatSectionStart: Boolean
+ val beatCounterColor: Int
+ val commentColor: Int
+ val scrollIndicatorColor: Int
+ val beatSectionStartHighlightColor: Int
+ val currentLineHighlightColor: Int
+ val pageDownMarkerColor: Int
+ val pulseDisplay: Boolean
+ val backgroundColor: Int
+ val pulseColor: Int
+ var dropboxAccessToken: String
+ var dropboxRefreshToken: String
+ var dropboxExpiryTime: Long
+
+ fun getStringPreference(key: String, default: String): String
+ fun getStringSetPreference(key: String, default: Set): Set
+
+ fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener)
+ fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener)
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/Playlist.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/Playlist.kt
index 70fdadc7..f70a563d 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/Playlist.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/Playlist.kt
@@ -6,31 +6,37 @@ import kotlin.random.Random
internal class Playlist private constructor(
val nodes: Array
) {
- private val songFiles: List
- get() = nodes.map { it.songFile }
+ private val songFiles: List>
+ get() = nodes.map { it.songFile to it.variation }
constructor() : this(buildSongList(listOf()))
- constructor(songs: List) : this(buildSongList(songs))
+ constructor(songs: List>) : this(buildSongList(songs))
+
+ fun sortByTitle(): Playlist =
+ Playlist(buildSongList(songFiles.sortedBy { it.first.sortableTitle }))
+
+ fun sortByMode(): Playlist =
+ Playlist(buildSongList(songFiles.sortedBy { it.first.bestScrollingMode }))
- fun sortByTitle(): Playlist = Playlist(buildSongList(songFiles.sortedBy { it.sortableTitle }))
- fun sortByMode(): Playlist = Playlist(buildSongList(songFiles.sortedBy { it.bestScrollingMode }))
fun sortByRating(): Playlist =
- Playlist(buildSongList(songFiles.sortedByDescending { it.rating })) // Sort from best to worst
+ Playlist(buildSongList(songFiles.sortedByDescending { it.first.rating })) // Sort from best to worst
+
+ fun sortByArtist(): Playlist =
+ Playlist(buildSongList(songFiles.sortedBy { it.first.sortableArtist }))
- fun sortByArtist(): Playlist = Playlist(buildSongList(songFiles.sortedBy { it.sortableArtist }))
- fun sortByKey(): Playlist = Playlist(buildSongList(songFiles.sortedBy { it.key }))
+ fun sortByKey(): Playlist = Playlist(buildSongList(songFiles.sortedBy { it.first.key }))
fun sortByDateModified(): Playlist =
- Playlist(buildSongList(songFiles.sortedByDescending { it.lastModified }))
+ Playlist(buildSongList(songFiles.sortedByDescending { it.first.lastModified }))
fun shuffle(): Playlist =
Playlist(buildSongList(songFiles.map { it to Random.Default.nextDouble() }
.sortedBy { it.second }.map { it.first }))
companion object {
- private fun buildSongList(songs: List): Array {
+ private fun buildSongList(songs: List>): Array {
var lastNode: PlaylistNode? = null
return songs.reversed().map {
- val node = PlaylistNode(it, lastNode)
+ val node = PlaylistNode(it.first, it.second, lastNode)
lastNode = node
node
}.reversed().toTypedArray()
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/PlaylistNode.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/PlaylistNode.kt
index 53bc5cc3..200d0f0d 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/PlaylistNode.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/PlaylistNode.kt
@@ -2,4 +2,8 @@ package com.stevenfrew.beatprompter.set
import com.stevenfrew.beatprompter.cache.SongFile
-data class PlaylistNode(val songFile: SongFile,val nextSong:PlaylistNode?=null)
+data class PlaylistNode(
+ val songFile: SongFile,
+ val variation: String? = null,
+ val nextSong: PlaylistNode? = null
+)
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/SetListEntry.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/SetListEntry.kt
index 1c8fdcdd..4e3e8ee3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/set/SetListEntry.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/set/SetListEntry.kt
@@ -9,16 +9,21 @@ import com.stevenfrew.beatprompter.util.splitAndTrim
*/
class SetListEntry private constructor(
private val normalizedTitle: String,
- private val normalizedArtist: String
+ private val normalizedArtist: String,
+ val variation: String
) {
- private constructor(titleAndArtist: Pair)
- : this(titleAndArtist.first, titleAndArtist.second)
+ private constructor(titleAndArtist: Triple)
+ : this(titleAndArtist.first, titleAndArtist.second, titleAndArtist.third)
constructor(songFile: SongFile)
- : this(songFile.normalizedTitle, songFile.normalizedArtist)
+ : this(
+ songFile.normalizedTitle,
+ songFile.normalizedArtist,
+ songFile.defaultVariation
+ )
constructor(setListFileLine: String)
- : this(getTitleAndArtistFromSetListLine(setListFileLine))
+ : this(getInfoFromSetListLine(setListFileLine))
fun matches(songFile: SongFile): SetListMatch =
if (songFile.normalizedTitle.equals(normalizedTitle, true)) {
@@ -32,17 +37,31 @@ class SetListEntry private constructor(
if (normalizedArtist.isBlank()) normalizedTitle
else "$normalizedArtist - $normalizedTitle"
- override fun toString(): String = normalizedTitle + SET_LIST_ENTRY_DELIMITER + normalizedArtist
+ override fun toString(): String = normalizedTitle + SET_LIST_ARTIST_DELIMITER + normalizedArtist
companion object {
- private const val SET_LIST_ENTRY_DELIMITER = "==="
+ private const val SET_LIST_ARTIST_DELIMITER = "==="
+ private const val SET_LIST_VARIATION_DELIMITER = "###"
- private fun getTitleAndArtistFromSetListLine(setListFileLine: String): Pair =
- setListFileLine.splitAndTrim(SET_LIST_ENTRY_DELIMITER).let {
+ private fun splitOnDelimiter(setListFileLine: String, delimiter: String): Pair =
+ setListFileLine.splitAndTrim(delimiter).let {
if (it.size > 1)
- it[0] to it[1]
+ it[0].trim() to it[1].trim()
else
- it[0] to ""
+ it[0].trim() to ""
+ }
+
+ private fun getInfoFromSetListLine(setListFileLine: String): Triple =
+ splitOnDelimiter(setListFileLine, SET_LIST_ARTIST_DELIMITER).let {
+ if (it.second.isNotBlank()) {
+ // Artist was specified.
+ val secondSplit = splitOnDelimiter(it.second, SET_LIST_VARIATION_DELIMITER)
+ Triple(it.first, secondSplit.first, secondSplit.second)
+ } else {
+ // No artist specified, but variation still might be
+ val secondSplit = splitOnDelimiter(it.first, SET_LIST_VARIATION_DELIMITER)
+ Triple(secondSplit.first, it.second, secondSplit.second)
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/Song.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/Song.kt
index 01cf8708..b3d99791 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/Song.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/Song.kt
@@ -3,12 +3,11 @@ package com.stevenfrew.beatprompter.song
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
-import android.graphics.Rect
-import android.graphics.Typeface
import com.stevenfrew.beatprompter.cache.AudioFile
import com.stevenfrew.beatprompter.cache.SongFile
import com.stevenfrew.beatprompter.comm.midi.message.MidiMessage
import com.stevenfrew.beatprompter.graphics.DisplaySettings
+import com.stevenfrew.beatprompter.graphics.Rect
import com.stevenfrew.beatprompter.graphics.ScreenComment
import com.stevenfrew.beatprompter.graphics.ScreenString
import com.stevenfrew.beatprompter.midi.BeatBlock
@@ -61,9 +60,10 @@ class Song(
// Look at events where the time is the SAME as the progress event, or
// the same with audio latency compensation.
while (nextEvent != null) {
- if (nextEvent.event is LineEvent)
+ val nextEventEvent = nextEvent.event
+ if (nextEventEvent is LineEvent)
if (nextEvent.time == latencyCompensatedEventTime) // This is the line
- return nextEvent.event as LineEvent
+ return nextEventEvent
else // Found a line event with a daft time
break
nextEvent = nextEvent.nextEvent
@@ -113,21 +113,21 @@ class Song(
}
class Comment internal constructor(
- var mText: String,
+ var text: String,
audience: List,
+ val textColor: Int,
screenSize: Rect,
- paint: Paint,
- font: Typeface
+ paint: Paint
) {
private val commentAudience = audience
- private val commentGraphic = ScreenComment(mText, screenSize, paint, font)
+ private val commentGraphic = ScreenComment(text, screenSize, paint)
fun isIntendedFor(audience: String): Boolean =
commentAudience.isEmpty() ||
audience.isBlank() ||
audience.lowercase().splitAndTrim(",").intersect(commentAudience.toSet()).any()
- fun draw(canvas: Canvas, paint: Paint, textColor: Int) =
+ fun draw(canvas: Canvas, paint: Paint) =
commentGraphic.draw(canvas, paint, textColor)
}
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/Chord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/Chord.kt
deleted file mode 100644
index 78d36c86..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/Chord.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-import com.stevenfrew.beatprompter.BeatPrompter
-import com.stevenfrew.beatprompter.R
-import java.util.regex.Pattern
-
-/**
- * Represents a musical chord. For example, Am7/C would have:
- *
- * root: A
- * suffix: m7
- * bass: C
- */
-class Chord(
- val root: String,
- private val suffix: String? = null,
- private val bass: String? = null
-) : IChord {
- companion object {
- /**
- * The rank for each possible chord, and also the sharp-only version.
- * Rank is the distance in semitones from C.
- */
- val CHORD_RANKS_AND_SHARPS: Map> = mapOf(
- "B#" to (0 to "B#"),
- "B♯" to (0 to "B♯"),
- "C" to (0 to "C"),
- "C#" to (1 to "C#"),
- "C♯" to (1 to "C♯"),
- "Db" to (1 to "C#"),
- "D♭" to (1 to "C♯"),
- "D" to (2 to "D"),
- "D#" to (3 to "D#"),
- "D♯" to (3 to "D♯"),
- "Eb" to (3 to "D#"),
- "E♭" to (3 to "D♯"),
- "E" to (4 to "E"),
- "Fb" to (4 to "E"),
- "F♭" to (4 to "E"),
- "E#" to (5 to "E#"),
- "E♯" to (5 to "E♯"),
- "F" to (5 to "F"),
- "F#" to (6 to "F#"),
- "F♯" to (6 to "F♯"),
- "Gb" to (6 to "F#"),
- "G♭" to (6 to "F♯"),
- "G" to (7 to "G"),
- "G#" to (8 to "G#"),
- "G♯" to (8 to "G♯"),
- "Ab" to (8 to "G#"),
- "A♭" to (8 to "G♯"),
- "A" to (9 to "A"),
- "A#" to (10 to "A#"),
- "A♯" to (10 to "A♯"),
- "Bb" to (10 to "A#"),
- "B♭" to (10 to "A♯"),
- "Cb" to (11 to "B"),
- "C♭" to (11 to "B"),
- "B" to (11 to "B")
- )
-
- private const val REGEX_ROOT_GROUP_NAME = "root"
- private const val REGEX_SUFFIX_GROUP_NAME = "suffix"
- private const val REGEX_BASS_GROUP_NAME = "bass"
-
- private val ACCIDENTALS = listOf('b', '♭', '#', '♯', '♮')
-
- // Regex for recognizing chords
- val MINOR_SUFFIXES = listOf("m", "mmaj", "mM", "min", "minor")
- private val NOT_MINOR_SUFFIXES =
- listOf("M", "maj", "major", "dim", "sus", "dom", "aug", "Ø", "ø", "°", "Δ", "∆", "\\+", "-")
-
- private val TRIAD_PATTERN =
- "(${NOT_MINOR_SUFFIXES.joinToString("|")}|${MINOR_SUFFIXES.joinToString("|")})"
- private val ADDED_TONE_PATTERN =
- "(\\(?([\\/\\.\\+]|add)?[${ACCIDENTALS.joinToString()}]?\\d+[\\+-]?\\)?)"
- private val SUFFIX_PATTERN =
- "(?<$REGEX_SUFFIX_GROUP_NAME>\\d*\\(?${TRIAD_PATTERN}?${ADDED_TONE_PATTERN}*\\)?)"
- private val BASS_PATTERN =
- "(\\/(?<$REGEX_BASS_GROUP_NAME>[A-G](${ACCIDENTALS.joinToString("|")})?))?"
-
- private val ROOT_PATTERN = "(?<$REGEX_ROOT_GROUP_NAME>[A-G](${ACCIDENTALS.joinToString("|")})?)"
-
- private val CHORD_REGEX = "^${ROOT_PATTERN}${SUFFIX_PATTERN}${BASS_PATTERN}$"
-
- private val CHORD_REGEX_PATTERN = Pattern.compile(CHORD_REGEX)
-
- fun parse(chord: String): Chord? {
- try {
- val result = CHORD_REGEX_PATTERN.matcher(chord)
- if (result.find()) {
- // For Oreo
- /* val root = result.group(REGEX_ROOT_GROUP_NAME)
- val suffix = result.group(REGEX_SUFFIX_GROUP_NAME)
- val bass = result.group(REGEX_BASS_GROUP_NAME)*/
- val root = result.group(1)
- val suffix = result.group(3)
- val bass = result.group(8)
- if (root.isNullOrBlank())
- throw IllegalStateException(
- BeatPrompter.appResources.getString(
- R.string.failedToParseChord,
- chord
- )
- )
- return Chord(root, suffix, bass)
- }
- } catch (e: IllegalStateException) {
- // Chord could not be parsed.
- }
- return null
- }
-
- fun isChord(token: String): Boolean = CHORD_REGEX_PATTERN.matcher(token).matches()
-
- private fun getMinorRootIfSuffixIsMinor(root: String, suffix: String?) =
- if (isMinor(suffix)) "${root}m" else root
-
- private fun isMinorSuffix(suffix: String) =
- (suffix.startsWith("m") || suffix.startsWith("min")) && !suffix.startsWith("maj")
-
- private fun isMinor(suffix: String?) =
- if (suffix.isNullOrBlank()) false else isMinorSuffix(suffix)
- }
-
- private fun makeSharp(chord: String?): String =
- chord?.let { CHORD_RANKS_AND_SHARPS[it]?.second ?: chord } ?: ""
-
- override fun getChordDisplayString(
- alwaysUseSharps: Boolean,
- useUnicodeAccidentals: Boolean,
- majorOrMinorRootOnly: Boolean
- ): String {
- val possiblySharpenedRoot =
- if (alwaysUseSharps) makeSharp(root) else root
- val root = if (majorOrMinorRootOnly) getMinorRootIfSuffixIsMinor(
- possiblySharpenedRoot,
- suffix
- ) else possiblySharpenedRoot
- val bass = if (alwaysUseSharps) makeSharp(bass) else bass
- val secondPart =
- if (majorOrMinorRootOnly)
- ""
- else if (!bass.isNullOrBlank())
- "$suffix/$bass"
- else
- suffix
- val rawChord = root + secondPart
- return rawChord.let {
- if (useUnicodeAccidentals) ChordUtils.useUnicodeAccidentals(it) else it
- }
- }
-
- override fun transpose(transpositionMap: Map): IChord =
- Chord(
- transpositionMap[root] ?: root,
- suffix,
- transpositionMap[bass]
- )
-
- val majorOrMinorRoot
- get() = getMinorRootIfSuffixIsMinor(root, suffix)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordMap.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordMap.kt
deleted file mode 100644
index b9e16fc2..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordMap.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-import com.stevenfrew.beatprompter.BeatPrompter
-import com.stevenfrew.beatprompter.Preferences
-import com.stevenfrew.beatprompter.R
-
-class ChordMap private constructor(
- private val chordMap: Map,
- private val key: KeySignature,
- private val alwaysUseSharps: Boolean = false,
- private val useUnicodeAccidentals: Boolean = false
-) : Map {
- constructor(chordStrings: Set, firstChord: String, key: String? = null) : this(
- chordStrings.associateWith {
- (Chord.parse(it) ?: UnknownChord(it))
- },
- KeySignatureDefinition.getKeySignature(key, firstChord)
- ?: throw Exception("Could not determine key signature"),
- Preferences.alwaysDisplaySharpChords,
- Preferences.displayUnicodeAccidentals,
- )
-
- fun transpose(amount: String): ChordMap =
- try {
- val shiftAmount = amount.toInt()
- if (shiftAmount < NUMBER_OF_KEYS && shiftAmount > -NUMBER_OF_KEYS)
- shift(shiftAmount)
- else
- throw Exception(BeatPrompter.appResources.getString(R.string.excessiveTransposeMagnitude))
- } catch (nfe: NumberFormatException) {
- // Must be a key then!
- toKey(amount)
- }
-
- fun transpose(amount: Int): ChordMap = shift(amount)
-
- private fun shift(semitones: Int): ChordMap {
- if (semitones == 0)
- return this
- val newKey =
- key.shift(semitones)
- ?: throw Exception(
- BeatPrompter.appResources.getString(
- R.string.couldNotShiftKey,
- key,
- semitones
- )
- )
- val newChords = transposeChords(key, newKey)
- return ChordMap(newChords, newKey, alwaysUseSharps, useUnicodeAccidentals)
- }
-
- private fun toKey(toKey: String): ChordMap {
- val newKey =
- Chord.parse(toKey)?.let { KeySignatureDefinition.valueOf(it) }
- ?: throw Exception(BeatPrompter.appResources.getString(R.string.failedToParseKey, toKey))
- val newChords = transposeChords(key, newKey)
- return ChordMap(newChords, newKey, alwaysUseSharps, useUnicodeAccidentals)
- }
-
- /**
- * Transposes the given parsed text (by the parse() function) to another key.
- */
- private fun transposeChords(
- fromKey: KeySignature,
- toKey: KeySignature
- ): Map {
- val transpositionMap = fromKey.createTranspositionMap(toKey)
- return chordMap.map {
- it.key to it.value.transpose(transpositionMap)
- }.toMap()
- }
-
- fun getChordDisplayString(chord: String): String? =
- get(chord)?.getChordDisplayString(alwaysUseSharps, useUnicodeAccidentals)
-
- fun addChordMapping(fromChord: String, toChord: IChord): ChordMap {
- val mutableMap = toMutableMap()
- mutableMap[fromChord] = toChord
- return ChordMap(mutableMap, key, alwaysUseSharps, useUnicodeAccidentals)
- }
-
- override val entries: Set>
- get() = chordMap.entries
- override val keys: Set
- get() = chordMap.keys
- override val size: Int
- get() = chordMap.size
- override val values: Collection
- get() = chordMap.values
-
- override fun isEmpty(): Boolean = chordMap.isEmpty()
- override fun get(key: String): IChord? = chordMap[key]
- override fun containsValue(value: IChord): Boolean = chordMap.containsValue(value)
- override fun containsKey(key: String): Boolean = chordMap.containsKey(key)
-
- companion object {
- const val NUMBER_OF_KEYS = 12
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordUtils.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordUtils.kt
deleted file mode 100644
index 05015bf0..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/ChordUtils.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-object ChordUtils {
- fun useUnicodeAccidentals(str: String): String = str.replace('b', '♭').replace('#', '♯')
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/IChord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/IChord.kt
deleted file mode 100644
index 553a7a01..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/IChord.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-interface IChord {
- fun getChordDisplayString(
- alwaysUseSharps: Boolean,
- useUnicodeAccidentals: Boolean,
- majorOrMinorRootOnly: Boolean = false
- ): String
-
- fun transpose(transpositionMap: Map): IChord
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignature.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignature.kt
deleted file mode 100644
index a3eb4b80..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignature.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-import com.stevenfrew.beatprompter.song.chord.ChordMap.Companion.NUMBER_OF_KEYS
-
-class KeySignature(
- val chord: IChord,
- keySignatureDefinition: KeySignatureDefinition
-) : KeySignatureDefinition(keySignatureDefinition) {
- fun getDisplayString(useUnicodeAccidentals: Boolean): String =
- chord.getChordDisplayString(false, useUnicodeAccidentals, true)
-
- /**
- * Finds the key that is a specified number of semitones above/below the current
- * key.
- */
- fun shift(
- semitones: Int
- ): KeySignature? {
- val newRank = (rank + semitones + NUMBER_OF_KEYS) % NUMBER_OF_KEYS
- val newKeySignatureDefinition = forRank(newRank)
- val transpositionMap = newKeySignatureDefinition?.let { createTranspositionMap(it) }
- val transposedChord = transpositionMap?.let { chord.transpose(it) }
- return transposedChord?.let { KeySignature(it, newKeySignatureDefinition) }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignatureDefinition.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignatureDefinition.kt
deleted file mode 100644
index 86e68d95..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeySignatureDefinition.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-import com.stevenfrew.beatprompter.song.chord.Chord.Companion.CHORD_RANKS_AND_SHARPS
-import com.stevenfrew.beatprompter.song.chord.ChordMap.Companion.NUMBER_OF_KEYS
-
-open class KeySignatureDefinition(
- val majorKey: String,
- val relativeMinor: String,
- private val keyType: KeyType,
- val rank: Int,
- val chromaticScale: List
-) {
- constructor(copy: KeySignatureDefinition) : this(
- copy.majorKey,
- copy.relativeMinor,
- copy.keyType,
- copy.rank,
- copy.chromaticScale
- )
-
- companion object {
- private val MINOR_PATTERN = "(${Chord.MINOR_SUFFIXES})+"
-
- // Chromatic scale starting from C using flats only.
- private val FLAT_SCALE =
- listOf("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "Cb")
-
- // Chromatic scale starting from C using sharps only.
- private val SHARP_SCALE =
- listOf("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
-
- // Chromatic scale for F# major which includes E#.
- private val F_SHARP_SCALE = SHARP_SCALE.map { if (it == "F") "E#" else it }
- private val C_SHARP_SCALE = F_SHARP_SCALE.map { if (it == "C") "B#" else it }
- private val G_FLAT_SCALE = FLAT_SCALE.map { if (it == "B") "Cb" else it }
- private val C_FLAT_SCALE = G_FLAT_SCALE.map { if (it == "E") "Fb" else it }
-
- private val cKeySignature = KeySignatureDefinition("C", "Am", KeyType.SHARP, 0, SHARP_SCALE)
- private val dFlatKeySignature = KeySignatureDefinition("Db", "Bbm", KeyType.FLAT, 1, FLAT_SCALE)
- private val dKeySignature = KeySignatureDefinition("D", "Bm", KeyType.SHARP, 2, SHARP_SCALE)
- private val eFlatKeySignature = KeySignatureDefinition("Eb", "Cm", KeyType.FLAT, 3, FLAT_SCALE)
- private val eKeySignature = KeySignatureDefinition("E", "C#m", KeyType.SHARP, 4, SHARP_SCALE)
- private val fKeySignature = KeySignatureDefinition("F", "Dm", KeyType.FLAT, 5, FLAT_SCALE)
- private val gFlatKeySignature =
- KeySignatureDefinition("Gb", "Ebm", KeyType.FLAT, 6, G_FLAT_SCALE)
- private val fSharpKeySignature =
- KeySignatureDefinition("F#", "D#m", KeyType.SHARP, 6, F_SHARP_SCALE)
- private val gKeySignature = KeySignatureDefinition("G", "Em", KeyType.SHARP, 7, SHARP_SCALE)
- private val aFlatKeySignature = KeySignatureDefinition("Ab", "Fm", KeyType.FLAT, 8, FLAT_SCALE)
- private val aKeySignature = KeySignatureDefinition("A", "F#m", KeyType.SHARP, 9, SHARP_SCALE)
- private val bFlatKeySignature = KeySignatureDefinition("Bb", "Gm", KeyType.FLAT, 10, FLAT_SCALE)
- private val bKeySignature = KeySignatureDefinition("B", "G#m", KeyType.SHARP, 11, SHARP_SCALE)
- private val cSharpKeySignature =
- KeySignatureDefinition("C#", "A#m", KeyType.SHARP, 1, C_SHARP_SCALE)
- private val cFlatKeySignature =
- KeySignatureDefinition("Cb", "Abm", KeyType.FLAT, 11, C_FLAT_SCALE)
- private val dSharpKeySignature = KeySignatureDefinition("D#", "", KeyType.SHARP, 3, SHARP_SCALE)
- private val gSharpKeySignature = KeySignatureDefinition("G#", "", KeyType.SHARP, 8, SHARP_SCALE)
-
- /** Enum for each key signature. */
- private val keySignatures = mapOf(
- "C" to cKeySignature,
- "Db" to dFlatKeySignature,
- "D♭" to dFlatKeySignature,
- "D" to dKeySignature,
- "Eb" to eFlatKeySignature,
- "E♭" to eFlatKeySignature,
- "E" to eKeySignature,
- "F" to fKeySignature,
- "Gb" to gFlatKeySignature,
- "G♭" to gFlatKeySignature,
- "F#" to fSharpKeySignature,
- "F♯" to fSharpKeySignature,
- "G" to gKeySignature,
- "Ab" to aFlatKeySignature,
- "A♭" to aFlatKeySignature,
- "A" to aKeySignature,
- "Bb" to bFlatKeySignature,
- "B♭" to bFlatKeySignature,
- "B" to bKeySignature,
- "C#" to cSharpKeySignature,
- "C♯" to cSharpKeySignature,
- "Cb" to cFlatKeySignature,
- "C♭" to cFlatKeySignature,
- "D#" to dSharpKeySignature,
- "D♯" to dSharpKeySignature,
- "G#" to gSharpKeySignature,
- "G♯" to gSharpKeySignature
- )
-
- private val keySignatureMap = keySignatures.flatMap {
- listOf(it.value.majorKey to it.value, it.value.relativeMinor to it.value)
- }.toMap()
-
- private val rankMap = keySignatures.map {
- it.value.rank to it.value
- }.reversed().toMap()
-
- /**
- * Returns the enum constant with the specific name or returns null if the
- * key signature is not valid.
- */
- fun valueOf(chord: Chord): KeySignature? {
- val foundSignature = this.keySignatureMap[chord.majorOrMinorRoot]
- if (foundSignature != null)
- return KeySignature(chord, foundSignature)
-
- // If all else fails, try to find any key with this chord in it.
- for (signatureKvp in keySignatures) {
- if (signatureKvp.value.chromaticScale.contains(chord.root))
- return KeySignature(chord, signatureKvp.value)
- }
- return null
- }
-
- internal fun forRank(rank: Int): KeySignatureDefinition? = rankMap[rank]
-
- private fun getKeySignature(chordName: String?): KeySignature? =
- chordName?.let { Chord.parse(it)?.let { parsedChord -> valueOf(parsedChord) } }
-
- fun getKeySignature(key: String?, firstChord: String?): KeySignature? =
- getKeySignature(key) ?: getKeySignature(firstChord)
- }
-
- /**
- * Given the current key and the number of semitones to transpose, returns a
- * mapping from each note to a transposed note.
- */
- internal fun createTranspositionMap(
- newKey: KeySignatureDefinition
- ): Map {
- val semitones = semitonesTo(newKey)
- val scale: List = newKey.chromaticScale
- return CHORD_RANKS_AND_SHARPS.map { it.key to scale[(it.value.first + semitones + NUMBER_OF_KEYS) % NUMBER_OF_KEYS] }
- .toMap()
- }
-
- /** Finds the number of semitones between the given keys. */
- private fun semitonesTo(other: KeySignatureDefinition): Int =
- other.rank - rank
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeyType.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeyType.kt
deleted file mode 100644
index 4e47b6ca..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/KeyType.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-enum class KeyType { FLAT, SHARP }
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/UnknownChord.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/UnknownChord.kt
deleted file mode 100644
index 9cb63c9f..00000000
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/chord/UnknownChord.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.stevenfrew.beatprompter.song.chord
-
-class UnknownChord(private val chord: String) : IChord {
- override fun getChordDisplayString(
- alwaysUseSharps: Boolean,
- useUnicodeAccidentals: Boolean,
- majorOrMinorRootOnly: Boolean
- ): String = if (useUnicodeAccidentals) ChordUtils.useUnicodeAccidentals(chord) else chord
-
- override fun transpose(transpositionMap: Map): IChord = this
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/BeatEvent.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/BeatEvent.kt
index 7d3e5d52..bb8451f3 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/BeatEvent.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/BeatEvent.kt
@@ -10,7 +10,7 @@ class BeatEvent(
var bpb: Int,
val beat: Int,
val click: Boolean,
- val willScrollOnBeat: Int
+ var willScrollOnBeat: Int
) : BaseEvent(eventTime) {
override fun offset(nanoseconds: Long): BaseEvent =
BeatEvent(eventTime + nanoseconds, bpm, bpb, beat, click, willScrollOnBeat)
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MIDIEvent.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MidiEvent.kt
similarity index 85%
rename from app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MIDIEvent.kt
rename to app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MidiEvent.kt
index 0968cf5c..1b3afd35 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MIDIEvent.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/event/MidiEvent.kt
@@ -6,11 +6,11 @@ import com.stevenfrew.beatprompter.midi.EventOffset
/**
* A MIDIEvent tells the event processor to chuck some MIDI data out of the USB port.
*/
-class MIDIEvent(
+class MidiEvent(
time: Long,
val messages: List,
val offset: EventOffset = EventOffset(0)
) : BaseEvent(time) {
override fun offset(nanoseconds: Long): BaseEvent =
- MIDIEvent(eventTime + nanoseconds, messages, offset)
+ MidiEvent(eventTime + nanoseconds, messages, offset)
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/ImageLine.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/ImageLine.kt
index 294508ea..ac039be6 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/ImageLine.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/ImageLine.kt
@@ -1,15 +1,13 @@
package com.stevenfrew.beatprompter.song.line
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
import android.graphics.Paint
-import android.graphics.Rect
import com.stevenfrew.beatprompter.BeatPrompter
import com.stevenfrew.beatprompter.R
import com.stevenfrew.beatprompter.cache.ImageFile
import com.stevenfrew.beatprompter.cache.parse.SongParserException
import com.stevenfrew.beatprompter.graphics.DisplaySettings
import com.stevenfrew.beatprompter.graphics.ImageScalingMode
+import com.stevenfrew.beatprompter.graphics.Rect
import com.stevenfrew.beatprompter.song.ScrollingMode
class ImageLine internal constructor(
@@ -33,10 +31,12 @@ class ImageLine internal constructor(
displaySettings
) {
private val bitmap =
- BitmapFactory.decodeFile(mImageFile.file.absolutePath, BitmapFactory.Options())
- private val sourceRect = Rect(0, 0, mImageFile.size.width, mImageFile.size.height)
+ BeatPrompter.platformUtils.bitmapFactory.createBitmap(mImageFile.file.absolutePath)
+ private val sourceRect =
+ android.graphics.Rect(0, 0, mImageFile.size.width, mImageFile.size.height)
private val destinationRect = getDestinationRect(
- bitmap,
+ bitmap.width,
+ bitmap.height,
displaySettings.screenSize,
scalingMode
)
@@ -68,30 +68,28 @@ class ImageLine internal constructor(
companion object {
private fun getDestinationRect(
- bitmap: Bitmap,
+ imageWidth: Int,
+ imageHeight: Int,
screenSize: Rect,
scalingMode: ImageScalingMode
- ): Rect {
- val imageHeight = bitmap.height
- val imageWidth = bitmap.width
-
+ ): android.graphics.Rect {
val needsStretched =
- imageWidth > screenSize.width() || scalingMode === ImageScalingMode.Stretch
+ imageWidth > screenSize.width || scalingMode === ImageScalingMode.Stretch
val scaledImageHeight =
if (needsStretched)
- (imageHeight * (screenSize.width().toDouble() / imageWidth.toDouble())).toInt()
+ (imageHeight * (screenSize.width.toDouble() / imageWidth.toDouble())).toInt()
else
imageHeight
val scaledImageWidth =
if (needsStretched)
- screenSize.width()
+ screenSize.width
else
imageWidth
if (scaledImageHeight > 8192 || scaledImageWidth > 8192)
throw SongParserException(BeatPrompter.appResources.getString(R.string.image_too_large))
- return Rect(0, 0, scaledImageWidth, scaledImageHeight)
+ return android.graphics.Rect(0, 0, scaledImageWidth, scaledImageHeight)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/Line.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/Line.kt
index b5bd0349..5a1e9bf0 100644
--- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/Line.kt
+++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/Line.kt
@@ -1,9 +1,9 @@
package com.stevenfrew.beatprompter.song.line
-import android.graphics.Canvas
import android.graphics.Paint
import com.stevenfrew.beatprompter.graphics.DisplaySettings
import com.stevenfrew.beatprompter.graphics.LineGraphic
+import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapCanvas
import com.stevenfrew.beatprompter.song.ScrollingMode
abstract class Line internal constructor(
@@ -21,7 +21,7 @@ abstract class Line internal constructor(
abstract val measurements: LineMeasurements
protected val graphics =
mutableListOf() // pointer to the allocated graphic, if one exists
- protected val canvasses = mutableListOf