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() + protected val canvasses = mutableListOf() internal abstract fun renderGraphics(paint: Paint) @@ -57,7 +57,7 @@ abstract class Line internal constructor( internal fun allocateGraphic(graphic: LineGraphic) { graphics.add(graphic) - canvasses.add(Canvas(graphic.bitmap)) + canvasses.add(graphic.bitmap.toCanvas()) } internal fun getGraphics(paint: Paint): List { diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/LineSection.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/LineSection.kt index 3810c16f..1d84c474 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/LineSection.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/LineSection.kt @@ -1,12 +1,11 @@ package com.stevenfrew.beatprompter.song.line import android.graphics.Paint -import android.graphics.Typeface +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.cache.parse.tag.Tag import com.stevenfrew.beatprompter.cache.parse.tag.song.EndOfHighlightTag import com.stevenfrew.beatprompter.cache.parse.tag.song.StartOfHighlightTag import com.stevenfrew.beatprompter.graphics.ColorRect -import com.stevenfrew.beatprompter.graphics.ScreenString import kotlin.math.max import kotlin.math.min @@ -37,28 +36,31 @@ class LineSection( val height: Int get() = lineHeight + chordHeight - fun setTextFontSizeAndMeasure(paint: Paint, fontSize: Int, face: Typeface): Int { - ScreenString.measure(lineText, paint, fontSize.toFloat(), face) - lineWidth = ScreenString.mMeasuredWidth + fun setTextFontSizeAndMeasure(paint: Paint, fontSize: Int): Int { + val measurement = + BeatPrompter.platformUtils.fontManager.measure(lineText, paint, fontSize.toFloat()) + lineWidth = measurement.width lineHeight = if (lineText.isBlank()) 0 else - ScreenString.mMeasuredHeight - lineDescenderOffset = ScreenString.mMeasuredDescenderOffset + measurement.height + lineDescenderOffset = measurement.descenderOffset return lineWidth } - fun setChordFontSizeAndMeasure(paint: Paint, fontSize: Int, face: Typeface): Int { - ScreenString.measure(chordText, paint, fontSize.toFloat(), face) - chordWidth = ScreenString.mMeasuredWidth + fun setChordFontSizeAndMeasure(paint: Paint, fontSize: Int): Int { + val measurement = + BeatPrompter.platformUtils.fontManager.measure(chordText, paint, fontSize.toFloat()) + chordWidth = measurement.width chordHeight = if (trimmedChord.isEmpty()) 0 else - ScreenString.mMeasuredHeight - chordDescenderOffset = ScreenString.mMeasuredDescenderOffset + measurement.height + chordDescenderOffset = measurement.descenderOffset chordTrimWidth = if (trimmedChord.length < chordText.length) { - ScreenString.measure(trimmedChord, paint, fontSize.toFloat(), face) - ScreenString.mMeasuredWidth + val newMeasurement = + BeatPrompter.platformUtils.fontManager.measure(trimmedChord, paint, fontSize.toFloat()) + newMeasurement.width } else chordWidth @@ -68,7 +70,6 @@ class LineSection( fun calculateHighlightedSections( paint: Paint, textSize: Float, - face: Typeface, currentHighlightColour: Int? ): Int? { var lookingForEnd = currentHighlightColour != null @@ -80,18 +81,21 @@ class LineSection( val length = min(it.position - sectionPosition, lineText.length) if (it is StartOfHighlightTag && !lookingForEnd) { val strHighlightText = lineText.substring(0, length) - startX = ScreenString.getStringWidth(paint, strHighlightText, face, textSize) + val stringWidth = + BeatPrompter.platformUtils.fontManager.getStringWidth(paint, strHighlightText, textSize) + startX = stringWidth.first startPosition = it.position - sectionPosition highlightColour = it.color lookingForEnd = true } else if (it is EndOfHighlightTag && lookingForEnd) { val strHighlightText = lineText.substring(startPosition, length) - val sectionWidth = ScreenString.getStringWidth(paint, strHighlightText, face, textSize) + val sectionWidth = + BeatPrompter.platformUtils.fontManager.getStringWidth(paint, strHighlightText, textSize) highlightingRectangles.add( ColorRect( startX, chordHeight, - startX + sectionWidth, + startX + sectionWidth.first, chordHeight + lineHeight, highlightColour!! ) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/TextLine.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/TextLine.kt index 37f8fa1d..904f57d1 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/TextLine.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/line/TextLine.kt @@ -3,15 +3,13 @@ package com.stevenfrew.beatprompter.song.line import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.Rect -import android.graphics.Typeface -import com.stevenfrew.beatprompter.Preferences +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.cache.parse.tag.Tag import com.stevenfrew.beatprompter.cache.parse.tag.song.ChordTag +import com.stevenfrew.beatprompter.chord.Chord +import com.stevenfrew.beatprompter.chord.ChordMap import com.stevenfrew.beatprompter.graphics.DisplaySettings -import com.stevenfrew.beatprompter.graphics.ScreenString import com.stevenfrew.beatprompter.song.ScrollingMode -import com.stevenfrew.beatprompter.song.chord.Chord -import com.stevenfrew.beatprompter.song.chord.ChordMap import com.stevenfrew.beatprompter.song.load.SongLoadCancelEvent import com.stevenfrew.beatprompter.song.load.SongLoadCancelledException import com.stevenfrew.beatprompter.util.Utils @@ -32,7 +30,7 @@ class TextLine internal constructor( inChorusSection: Boolean, scrollTimes: Pair, private val chordMap: ChordMap?, - songLoadCancelEvent: SongLoadCancelEvent + songLoadCancelEvent: SongLoadCancelEvent? = null ) : Line( lineTime, lineDuration, @@ -47,7 +45,6 @@ class TextLine internal constructor( private var chordTextSize: Int = 0 // font size to use, pre-measured. private var chordHeight: Int = 0 private var lyricHeight: Int = 0 - private val typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) private val xSplits = mutableListOf() private val lineWidths = mutableListOf() private val chordsDrawn = mutableListOf() @@ -66,41 +63,40 @@ class TextLine internal constructor( init { val paint = Paint() - lyricColor = Preferences.lyricColor - chordColor = Preferences.chordColor - chorusHighlightColor = Utils.makeHighlightColour(Preferences.chorusHighlightColor) - annotationColor = Preferences.annotationColor + lyricColor = BeatPrompter.preferences.lyricColor + chordColor = BeatPrompter.preferences.chordColor + chorusHighlightColor = Utils.makeHighlightColour(BeatPrompter.preferences.chorusHighlightColor) + annotationColor = BeatPrompter.preferences.annotationColor // TODO: Fix this, for god's sake! sections = calculateSections(songLoadCancelEvent) - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() // we have the sections, now fit 'em // Start with an arbitrary size val longestBits = StringBuilder() for (section in sections) { - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() - section.setTextFontSizeAndMeasure(paint, 100, typeface) - section.setChordFontSizeAndMeasure(paint, 100, typeface) + section.setTextFontSizeAndMeasure(paint, 100) + section.setChordFontSizeAndMeasure(paint, 100) if (section.chordWidth > section.lineWidth) longestBits.append(section.chordText) else longestBits.append(section.lineText) } - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() - val maxLongestFontSize = ScreenString.getBestFontSize( + val maxLongestFontSize = BeatPrompter.platformUtils.fontManager.getBestFontSize( longestBits.toString(), paint, - displaySettings.minimumFontSize, - displaySettings.maximumFontSize, - displaySettings.screenSize.width(), + displaySettings.screenSize.width, -1, - typeface - ).toDouble() + minimumFontSize = displaySettings.minimumFontSize, + maximumFontSize = displaySettings.maximumFontSize, + ).first.toDouble() var textFontSize = maxLongestFontSize var chordFontSize = maxLongestFontSize var allTextSmallerThanChords: Boolean @@ -113,22 +109,28 @@ class TextLine internal constructor( textExists = false width = 0 for (section in sections) { - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() textExists = textExists or (section.lineText.isNotEmpty()) val textWidth = - section.setTextFontSizeAndMeasure(paint, floor(textFontSize).toInt(), typeface) + section.setTextFontSizeAndMeasure( + paint, + floor(textFontSize).toInt() + ) val chordWidth = - section.setChordFontSizeAndMeasure(paint, floor(chordFontSize).toInt(), typeface) + section.setChordFontSizeAndMeasure( + paint, + floor(chordFontSize).toInt() + ) if (chordWidth > textWidth) allChordsSmallerThanText = false else if (textWidth > 0 && textWidth > chordWidth) allTextSmallerThanChords = false width += max(textWidth, chordWidth) } - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() - if (width >= displaySettings.screenSize.width()) { + if (width >= displaySettings.screenSize.width) { if (textFontSize >= displaySettings.minimumFontSize + 2 && chordFontSize >= displaySettings.minimumFontSize + 2) { textFontSize -= 2.0 chordFontSize -= 2.0 @@ -140,16 +142,16 @@ class TextLine internal constructor( textFontSize = chordFontSize } } - } while (!songLoadCancelEvent.isCancelled && width >= displaySettings.screenSize.width() && textFontSize > displaySettings.minimumFontSize && chordFontSize > displaySettings.minimumFontSize) - if (songLoadCancelEvent.isCancelled) + } while (songLoadCancelEvent?.isCancelled != true && width >= displaySettings.screenSize.width && textFontSize > displaySettings.minimumFontSize && chordFontSize > displaySettings.minimumFontSize) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() do { var proposedLargerTextFontSize = textFontSize var proposedLargerChordFontSize = chordFontSize - if (allTextSmallerThanChords && textExists && textFontSize <= Utils.MAXIMUM_FONT_SIZE - 2 && proposedLargerTextFontSize <= displaySettings.maximumFontSize - 2) + if (allTextSmallerThanChords && textExists && textFontSize <= BeatPrompter.platformUtils.fontManager.maximumFontSize - 2 && proposedLargerTextFontSize <= displaySettings.maximumFontSize - 2) proposedLargerTextFontSize += 2.0 - else if (allChordsSmallerThanText && chordFontSize <= Utils.MAXIMUM_FONT_SIZE - 2 && proposedLargerChordFontSize <= displaySettings.maximumFontSize - 2) + else if (allChordsSmallerThanText && chordFontSize <= BeatPrompter.platformUtils.fontManager.maximumFontSize - 2 && proposedLargerChordFontSize <= displaySettings.maximumFontSize - 2) proposedLargerChordFontSize += 2.0 else // Nothing we can do. Increasing any size will make things bigger than the screen. @@ -159,18 +161,16 @@ class TextLine internal constructor( val lastWidth = width width = 0 for (section in sections) { - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() val textWidth = section.setTextFontSizeAndMeasure( paint, - floor(proposedLargerTextFontSize).toInt(), - typeface + floor(proposedLargerTextFontSize).toInt() ) val chordWidth = section.setChordFontSizeAndMeasure( paint, - floor(proposedLargerChordFontSize).toInt(), - typeface + floor(proposedLargerChordFontSize).toInt() ) if (chordWidth > textWidth) allChordsSmallerThanText = false @@ -178,17 +178,17 @@ class TextLine internal constructor( allTextSmallerThanChords = false width += max(textWidth, chordWidth) } - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() // If the text still isn't wider than the screen, // or it IS wider than the screen, but hasn't got any wider, // accept the new sizes. - if (width < displaySettings.screenSize.width() || width == lastWidth) { + if (width < displaySettings.screenSize.width || width == lastWidth) { textFontSize = proposedLargerTextFontSize chordFontSize = proposedLargerChordFontSize } - } while (!songLoadCancelEvent.isCancelled && width < displaySettings.screenSize.width() || width == lastWidth) - if (songLoadCancelEvent.isCancelled) + } while (songLoadCancelEvent?.isCancelled != true && width < displaySettings.screenSize.width || width == lastWidth) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() lineTextSize = floor(textFontSize).toInt() @@ -202,10 +202,10 @@ class TextLine internal constructor( lyricHeight = 0 var highlightColor = startingHighlightColor for (section in sections) { - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() - section.setTextFontSizeAndMeasure(paint, lineTextSize, typeface) - section.setChordFontSizeAndMeasure(paint, chordTextSize, typeface) + section.setTextFontSizeAndMeasure(paint, lineTextSize) + section.setChordFontSizeAndMeasure(paint, chordTextSize) lineDescenderOffset = max(lineDescenderOffset, section.lineDescenderOffset) chordDescenderOffset = max(chordDescenderOffset, section.chordDescenderOffset) lyricHeight = max(lyricHeight, section.lineHeight - section.lineDescenderOffset) @@ -219,16 +219,15 @@ class TextLine internal constructor( section.calculateHighlightedSections( paint, lineTextSize.toFloat(), - typeface, highlightColor ) } trailingHighlightColor = highlightColor - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() // Word wrapping time! - if (width > displaySettings.screenSize.width()) { + if (width > displaySettings.screenSize.width) { var bothersomeSection: LineSection? do { // Start from the first section again, but work from off the left hand edge @@ -245,7 +244,7 @@ class TextLine internal constructor( if (pixelSplits.size > 0) lastSplitWasPixelSplit = pixelSplits[pixelSplits.size - 1] for (sec in sections) { - if (songLoadCancelEvent.isCancelled) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() if (totalWidth > 0 && firstOnscreenSection == null) firstOnscreenSection = sec @@ -255,19 +254,19 @@ class TextLine internal constructor( if (startX <= 0 && startX + sec.chordTrimWidth > 0 && lastSplitWasPixelSplit) chordsDrawn = chordsDrawn or sec.hasChord() totalWidth += sec.width - if (startX >= 0 && totalWidth < displaySettings.screenSize.width()) { + if (startX >= 0 && totalWidth < displaySettings.screenSize.width) { // this whole section fits onscreen, no problem. chordsDrawn = chordsDrawn or sec.hasChord() textDrawn = textDrawn or sec.hasText() - } else if (totalWidth >= displaySettings.screenSize.width()) { + } else if (totalWidth >= displaySettings.screenSize.width) { bothersomeSection = sec break } } if (bothersomeSection != null) { val previousSplit = - if (xSplits.size > 0) xSplits[xSplits.size - 1] else displaySettings.screenSize.width() - val leftoverSpaceOnPreviousLine = displaySettings.screenSize.width() - previousSplit + if (xSplits.size > 0) xSplits[xSplits.size - 1] else displaySettings.screenSize.width + val leftoverSpaceOnPreviousLine = displaySettings.screenSize.width - previousSplit val widthWithoutBothersomeSection = totalWidth - bothersomeSection.width var xSplit = 0 var lineWidth = 0 @@ -281,20 +280,23 @@ class TextLine internal constructor( var splitOnLetter = wordCount <= 1 if (!splitOnLetter) { var f = wordCount - 1 - while (f >= 1 && !songLoadCancelEvent.isCancelled) { + while (f >= 1 && songLoadCancelEvent?.isCancelled != true) { val tryThisWithWhitespace = Utils.stitchBits(bits, f) val tryThis = tryThisWithWhitespace.trim() val tryThisWidth = - ScreenString.getStringWidth(paint, tryThis, typeface, lineTextSize.toFloat()) + BeatPrompter.platformUtils.fontManager.getStringWidth( + paint, + tryThis, + lineTextSize.toFloat() + ).first var tryThisWithWhitespaceWidth = tryThisWidth if (tryThisWithWhitespace.length > tryThis.length) - tryThisWithWhitespaceWidth = ScreenString.getStringWidth( + tryThisWithWhitespaceWidth = BeatPrompter.platformUtils.fontManager.getStringWidth( paint, tryThisWithWhitespace, - typeface, lineTextSize.toFloat() - ) - if (tryThisWidth >= bothersomeSection.chordTrimWidth || bothersomeSection.chordTrimWidth + widthWithoutBothersomeSection < displaySettings.screenSize.width()) { + ).first + if (tryThisWidth >= bothersomeSection.chordTrimWidth || bothersomeSection.chordTrimWidth + widthWithoutBothersomeSection < displaySettings.screenSize.width) { val possibleSplitPoint = widthWithoutBothersomeSection + tryThisWidth val possibleSplitPointWithWhitespace = widthWithoutBothersomeSection + tryThisWithWhitespaceWidth @@ -303,7 +305,7 @@ class TextLine internal constructor( // Let's split on letter. splitOnLetter = true break - } else if (possibleSplitPoint < displaySettings.screenSize.width()) { + } else if (possibleSplitPoint < displaySettings.screenSize.width) { // We have a winner! if (bothersomeSection.chordDrawLine == -1) bothersomeSection.chordDrawLine = xSplits.size @@ -311,7 +313,7 @@ class TextLine internal constructor( textDrawn = textDrawn or sectionTextOnscreen xSplit = possibleSplitPointWithWhitespace lineWidth = xSplit - if (tryThisWidth < bothersomeSection.chordTrimWidth && bothersomeSection.chordTrimWidth + widthWithoutBothersomeSection < displaySettings.screenSize.width()) + if (tryThisWidth < bothersomeSection.chordTrimWidth && bothersomeSection.chordTrimWidth + widthWithoutBothersomeSection < displaySettings.screenSize.width) lineWidth = bothersomeSection.chordTrimWidth + widthWithoutBothersomeSection break } @@ -324,7 +326,7 @@ class TextLine internal constructor( // No? Have to split to pixel chordsDrawn = chordsDrawn or sectionChordOnscreen textDrawn = textDrawn or sectionTextOnscreen - xSplit = displaySettings.screenSize.width() + xSplit = displaySettings.screenSize.width lineWidth = xSplit pixelSplit = true } @@ -356,10 +358,14 @@ class TextLine internal constructor( bits = bothersomeSection.lineText.characters() if (bits.size > 1) { var f = bits.size - 1 - while (f >= 1 && !songLoadCancelEvent.isCancelled) { + while (f >= 1 && songLoadCancelEvent?.isCancelled != true) { val tryThis = Utils.stitchBits(bits, f) val tryThisWidth = - ScreenString.getStringWidth(paint, tryThis, typeface, lineTextSize.toFloat()) + BeatPrompter.platformUtils.fontManager.getStringWidth( + paint, + tryThis, + lineTextSize.toFloat() + ).first if (tryThisWidth >= bothersomeSection.chordTrimWidth) { val possibleSplitPoint = widthWithoutBothersomeSection + tryThisWidth if (possibleSplitPoint <= 0) { @@ -368,10 +374,10 @@ class TextLine internal constructor( pixelSplit = true textDrawn = textDrawn or sectionTextOnscreen chordsDrawn = chordsDrawn or sectionChordOnscreen - xSplit = displaySettings.screenSize.width() + xSplit = displaySettings.screenSize.width lineWidth = xSplit break - } else if (possibleSplitPoint < displaySettings.screenSize.width()) { + } else if (possibleSplitPoint < displaySettings.screenSize.width) { // We have a winner! textDrawn = textDrawn or sectionTextOnscreen chordsDrawn = chordsDrawn or sectionChordOnscreen @@ -386,7 +392,7 @@ class TextLine internal constructor( // so we can't split on that. Just going to have to split to the pixel. chordsDrawn = chordsDrawn or sectionChordOnscreen textDrawn = textDrawn or sectionTextOnscreen - xSplit = displaySettings.screenSize.width() + xSplit = displaySettings.screenSize.width lineWidth = xSplit pixelSplit = true break @@ -397,7 +403,7 @@ class TextLine internal constructor( // There is no text to split. Just going to have to split to the pixel. chordsDrawn = chordsDrawn or sectionChordOnscreen textDrawn = textDrawn or sectionTextOnscreen - xSplit = displaySettings.screenSize.width() + xSplit = displaySettings.screenSize.width lineWidth = xSplit pixelSplit = true } @@ -415,8 +421,8 @@ class TextLine internal constructor( this.textDrawn.add(textDrawn) pixelSplits.add(pixelSplit) this.chordsDrawn.add(chordsDrawn) - } while (!songLoadCancelEvent.isCancelled && bothersomeSection != null) - if (songLoadCancelEvent.isCancelled) + } while (songLoadCancelEvent?.isCancelled != true && bothersomeSection != null) + if (songLoadCancelEvent?.isCancelled == true) throw SongLoadCancelledException() } @@ -453,7 +459,7 @@ class TextLine internal constructor( private val totalXSplits: Int get() = xSplits.sum() - private fun calculateSections(songLoadCancelEvent: SongLoadCancelEvent): List { + private fun calculateSections(songLoadCancelEvent: SongLoadCancelEvent? = null): List { val sections = mutableListOf() var chordPositionStart = 0 var chordTagIndex = -1 @@ -461,7 +467,7 @@ class TextLine internal constructor( val nonChordTags = tags.filter { it !is ChordTag } val chordTags = tags.filterIsInstance() - while (chordTagIndex < chordTags.size && !songLoadCancelEvent.isCancelled) { + while (chordTagIndex < chordTags.size && songLoadCancelEvent?.isCancelled != true) { // If we're at the last chord, capture all tags from here to the end. val isLastChord = chordTagIndex == chordTags.size - 1 val chordPositionEnd = @@ -521,7 +527,7 @@ class TextLine internal constructor( currentX -= xSplits[g] ++g } - paint.typeface = typeface + BeatPrompter.platformUtils.fontManager.setTypeface(paint) canvas.drawColor(backgroundColor, PorterDuff.Mode.SRC) // Fill with transparency. val xSplit = if (xSplits.size > lineNumber) xSplits[lineNumber] else Integer.MAX_VALUE for (i in sections.indices) { @@ -533,7 +539,7 @@ class TextLine internal constructor( .isNotEmpty() ) { paint.color = if (section.isTrueChord) chordColor else annotationColor - paint.textSize = chordTextSize * Utils.FONT_SCALING + BeatPrompter.platformUtils.fontManager.setTextSize(paint, chordTextSize.toFloat()) paint.flags = Paint.ANTI_ALIAS_FLAG canvas.drawText( section.chordText, @@ -547,7 +553,7 @@ class TextLine internal constructor( canvas.clipRect(0, 0, xSplit, thisLineHeight) if (section.lineText.trim().isNotEmpty()) { paint.color = lyricColor - paint.textSize = lineTextSize * Utils.FONT_SCALING + BeatPrompter.platformUtils.fontManager.setTextSize(paint, lineTextSize.toFloat()) paint.flags = Paint.ANTI_ALIAS_FLAG canvas.drawText( section.lineText, diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongChoiceInfo.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongChoiceInfo.kt index c5bb015d..1f0c3073 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongChoiceInfo.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongChoiceInfo.kt @@ -1,6 +1,6 @@ package com.stevenfrew.beatprompter.song.load -import android.graphics.Rect +import com.stevenfrew.beatprompter.graphics.Rect data class SongChoiceInfo( val normalizedTitle: String, diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadInfo.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadInfo.kt index c2180068..03421a83 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadInfo.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadInfo.kt @@ -9,14 +9,14 @@ data class SongLoadInfo( val songFile: SongFile, val variation: String, val songLoadMode: ScrollingMode, - val nextSong: String, - val wasStartedByBandLeader: Boolean, - val wasStartedByMidiTrigger: Boolean, val nativeDisplaySettings: DisplaySettings, val sourceDisplaySettings: DisplaySettings, - val noAudio: Boolean, - val audioLatency: Int, - val transposeShift: Int + val nextSong: String = "", + val wasStartedByBandLeader: Boolean = false, + val wasStartedByMidiTrigger: Boolean = false, + val noAudio: Boolean = false, + val audioLatency: Int = 0, + val transposeShift: Int = 0 ) { val loadId = UUID.randomUUID()!! val initialScrollMode diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadJob.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadJob.kt index 5391ab72..a2ded462 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadJob.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/song/load/SongLoadJob.kt @@ -3,6 +3,7 @@ package com.stevenfrew.beatprompter.song.load import android.os.Handler import android.os.Message import com.stevenfrew.beatprompter.Logger +import com.stevenfrew.beatprompter.cache.Cache import com.stevenfrew.beatprompter.cache.parse.SongParser import com.stevenfrew.beatprompter.events.EventRouter import com.stevenfrew.beatprompter.events.Events @@ -28,7 +29,8 @@ class SongLoadJob(val songLoadInfo: SongLoadInfo) : CoroutineScope { System.gc() try { Logger.logLoader({ "Starting to load '${songLoadInfo.songFile.title}'." }) - val loadedSong = SongParser(songLoadInfo, cancelEvent, handler).parse() + val loadedSong = + SongParser(songLoadInfo, Cache.supportFileResolver, cancelEvent, handler).parse() if (cancelEvent.isCancelled) throw SongLoadCancelledException() Logger.logLoader("Song was loaded successfully.") diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/storage/dropbox/DropboxStorage.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/storage/dropbox/DropboxStorage.kt index 9e5f0632..fac5be2b 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/storage/dropbox/DropboxStorage.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/storage/dropbox/DropboxStorage.kt @@ -13,7 +13,6 @@ import com.dropbox.core.v2.files.GetMetadataErrorException import com.dropbox.core.v2.files.ListFolderResult import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.Logger -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.storage.DownloadResult import com.stevenfrew.beatprompter.storage.FailedDownloadResult @@ -175,16 +174,16 @@ class DropboxStorage(parentFragment: Fragment) : private fun updateDropboxCredentials(cred: DbxCredential): DbxCredential = cred.also { - Preferences.dropboxAccessToken = it.accessToken - Preferences.dropboxRefreshToken = it.refreshToken - Preferences.dropboxExpiryTime = it.expiresAt + BeatPrompter.preferences.dropboxAccessToken = it.accessToken + BeatPrompter.preferences.dropboxRefreshToken = it.refreshToken + BeatPrompter.preferences.dropboxExpiryTime = it.expiresAt } private fun getStoredDropboxCredentials(): DbxCredential? { - val storedAccessToken = Preferences.dropboxAccessToken - val storedRefreshToken = Preferences.dropboxRefreshToken - val storedExpiryTime = Preferences.dropboxExpiryTime - if (storedAccessToken != null && storedRefreshToken != null && storedExpiryTime != 0L) { + val storedAccessToken = BeatPrompter.preferences.dropboxAccessToken + val storedRefreshToken = BeatPrompter.preferences.dropboxRefreshToken + val storedExpiryTime = BeatPrompter.preferences.dropboxExpiryTime + if (storedAccessToken.isNotBlank() && storedRefreshToken.isNotBlank() && storedExpiryTime != 0L) { val cred = DbxCredential( storedAccessToken, storedExpiryTime, diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/IntroActivity.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/IntroActivity.kt index 03212206..ca608a06 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/IntroActivity.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/IntroActivity.kt @@ -1,6 +1,5 @@ package com.stevenfrew.beatprompter.ui -import android.graphics.Color import android.os.Bundle import androidx.fragment.app.Fragment import com.github.paolorotolo.appintro.AppIntro @@ -38,7 +37,7 @@ class IntroActivity // Instead of fragments, you can also use our default slide // Just set a title, description, background and image. AppIntro will do the rest. - val backgroundColor = Color.parseColor("#CCCCCC") + val backgroundColor = BeatPrompter.platformUtils.parseColor("#CCCCCC") pageInfo.forEach { addSlide(AppIntroFragment.newInstance(SliderPage().apply { @@ -51,8 +50,8 @@ class IntroActivity // OPTIONAL METHODS // Override bar/separator color. - setBarColor(Color.parseColor("#AAAAAA")) - setSeparatorColor(Color.parseColor("#888888")) + setBarColor(BeatPrompter.platformUtils.parseColor("#AAAAAA")) + setSeparatorColor(BeatPrompter.platformUtils.parseColor("#888888")) // Hide Skip/Done button. showSkipButton(true) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/MIDIAliasListAdapter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/MIDIAliasListAdapter.kt index aada55cc..02f93258 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/MIDIAliasListAdapter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/MIDIAliasListAdapter.kt @@ -7,14 +7,14 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView -import com.stevenfrew.beatprompter.Preferences +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.cache.MIDIAliasFile class MIDIAliasListAdapter(private val values: List, context: Context) : ArrayAdapter(context, -1, values) { private val layoutId = - if (Preferences.largePrint) + if (BeatPrompter.preferences.largePrint) R.layout.midi_alias_list_item_large else R.layout.midi_alias_list_item diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongDisplayActivity.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongDisplayActivity.kt index 5ccce8df..f32a54af 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongDisplayActivity.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongDisplayActivity.kt @@ -17,7 +17,6 @@ import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.Logger -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.Task import com.stevenfrew.beatprompter.comm.bluetooth.Bluetooth @@ -58,8 +57,8 @@ class SongDisplayActivity songDisplayInstance = this - scrollOnProximity = Preferences.proximityScroll - anyOtherKeyPageDown = Preferences.anyOtherKeyPageDown + scrollOnProximity = BeatPrompter.preferences.proximityScroll + anyOtherKeyPageDown = BeatPrompter.preferences.anyOtherKeyPageDown setContentView(R.layout.activity_song_display) val potentiallyNullSongView: SongView? = findViewById(R.id.song_view) @@ -96,7 +95,7 @@ class SongDisplayActivity return } else { Logger.logLoader({ "Successful load ID match: ${song.loadId}" }) - if (Preferences.bluetoothMode == BluetoothMode.Server) { + if (BeatPrompter.preferences.bluetoothMode == BluetoothMode.Server) { Logger.logLoader({ "Sending ChooseSongMessage for \"${loadedSong.loadJob.songLoadInfo.songFile.normalizedTitle}\"" }) val csm = ChooseSongMessage( SongChoiceInfo( @@ -138,7 +137,7 @@ class SongDisplayActivity songView!!.init(this, song) // If no clock required, set BPM to zero. - if (Preferences.sendMIDIClock || song.sendMidiClock) { + if (BeatPrompter.preferences.sendMIDIClock || song.sendMidiClock) { ClockSignalGeneratorTask.reset() setClockBpmFn = { ClockSignalGeneratorTask.setBPM(it) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListActivity.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListActivity.kt index f6f56383..c17a429c 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListActivity.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListActivity.kt @@ -13,9 +13,10 @@ class SongListActivity : AppCompatActivity() { if (savedInstanceState == null) { val fragmentManager: FragmentManager = supportFragmentManager val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + val songListFragment = SongListFragment() fragmentTransaction.replace( android.R.id.content, - SongListFragment(), + songListFragment, "SongListFragment" + (++mSongListFragmentCounter) ) fragmentTransaction.commit() diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListAdapter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListAdapter.kt index 59487330..e0d8587b 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListAdapter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListAdapter.kt @@ -7,21 +7,21 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView -import com.stevenfrew.beatprompter.Preferences +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.set.PlaylistNode class SongListAdapter(private val values: List, context: Context) : ArrayAdapter(context, -1, values) { private val layoutId = - if (Preferences.largePrint) + if (BeatPrompter.preferences.largePrint) R.layout.song_list_item_large else R.layout.song_list_item - private val showBeatIcons = Preferences.showBeatStyleIcons - private val showKey = Preferences.showKeyInSongList - private val showRating = Preferences.showRatingInSongList - private val showMusicIcon = Preferences.showMusicIcon + private val showBeatIcons = BeatPrompter.preferences.showBeatStyleIcons + private val showKey = BeatPrompter.preferences.showKeyInSongList + private val showRating = BeatPrompter.preferences.showRatingInSongList + private val showMusicIcon = BeatPrompter.preferences.showMusicIcon private val inflater = context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListFragment.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListFragment.kt index 943363a0..01521a6b 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListFragment.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongListFragment.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Point -import android.graphics.Rect import android.net.Uri import android.os.Bundle import android.os.Handler @@ -38,7 +37,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.Cache import com.stevenfrew.beatprompter.cache.CachedCloudCollection @@ -46,20 +44,21 @@ import com.stevenfrew.beatprompter.cache.MIDIAliasFile import com.stevenfrew.beatprompter.cache.ReadCacheTask import com.stevenfrew.beatprompter.cache.SongFile import com.stevenfrew.beatprompter.cache.parse.FileParseError +import com.stevenfrew.beatprompter.chord.ChordMap +import com.stevenfrew.beatprompter.chord.KeySignature +import com.stevenfrew.beatprompter.chord.KeySignatureDefinition import com.stevenfrew.beatprompter.comm.bluetooth.Bluetooth import com.stevenfrew.beatprompter.comm.bluetooth.BluetoothMode import com.stevenfrew.beatprompter.events.EventRouter import com.stevenfrew.beatprompter.events.Events import com.stevenfrew.beatprompter.graphics.DisplaySettings +import com.stevenfrew.beatprompter.graphics.Rect import com.stevenfrew.beatprompter.midi.SongTrigger import com.stevenfrew.beatprompter.midi.TriggerType import com.stevenfrew.beatprompter.set.Playlist import com.stevenfrew.beatprompter.set.PlaylistNode import com.stevenfrew.beatprompter.set.SetListEntry import com.stevenfrew.beatprompter.song.ScrollingMode -import com.stevenfrew.beatprompter.song.chord.ChordMap -import com.stevenfrew.beatprompter.song.chord.KeySignature -import com.stevenfrew.beatprompter.song.chord.KeySignatureDefinition import com.stevenfrew.beatprompter.song.load.SongChoiceInfo import com.stevenfrew.beatprompter.song.load.SongInterruptResult import com.stevenfrew.beatprompter.song.load.SongLoadInfo @@ -76,7 +75,6 @@ import com.stevenfrew.beatprompter.ui.filter.SetListFilter import com.stevenfrew.beatprompter.ui.filter.SongFilter import com.stevenfrew.beatprompter.ui.filter.TagFilter import com.stevenfrew.beatprompter.ui.filter.TemporarySetListFilter -import com.stevenfrew.beatprompter.ui.pref.FontSizePreference import com.stevenfrew.beatprompter.ui.pref.SettingsActivity import com.stevenfrew.beatprompter.ui.pref.SortingPreference import com.stevenfrew.beatprompter.util.Utils @@ -91,1152 +89,1148 @@ import java.util.UUID import kotlin.coroutines.CoroutineContext class SongListFragment - : Fragment(), - AdapterView.OnItemClickListener, - AdapterView.OnItemSelectedListener, - SearchView.OnQueryTextListener, - AdapterView.OnItemLongClickListener, - SharedPreferences.OnSharedPreferenceChangeListener, - CoroutineScope { - private val coroutineJob = Job() - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + coroutineJob - private var songLauncher: ActivityResultLauncher? = null - private var listAdapter: BaseAdapter? = null - private var menu: Menu? = null - - private var playlist = Playlist() - private var nowPlayingNode: PlaylistNode? = null - private var searchText = "" - private var performingCloudSync = false - private var savedListIndex = 0 - private var savedListOffset = 0 - private var filters = listOf() - private val selectedTagFilters = mutableListOf() - private var selectedFilter: Filter = AllSongsFilter(mutableListOf()) - - override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { - if (selectedFilter is MIDIAliasFilesFilter) { - val maf = filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles)[position] - if (maf.errors.isNotEmpty()) - showMIDIAliasErrors(maf.errors) - } else { - val songToLoad = filterPlaylistNodes(playlist)[position] - if (!SongLoadQueueWatcherTask.isAlreadyLoadingSong(songToLoad.songFile)) - playPlaylistNode(songToLoad, false) - } - } - - internal fun startSongActivity(loadID: UUID) { - val intent = Intent(context, SongDisplayActivity::class.java) - intent.putExtra("loadID", ParcelUuid(loadID)) - Logger.logLoader({ "Starting SongDisplayActivity for $loadID!" }) - songLauncher?.launch(intent) - } - - internal fun startSongViaMidiProgramChange( - bankMSB: Byte, - bankLSB: Byte, - program: Byte, - channel: Byte - ) = startSongViaMidiSongTrigger( - SongTrigger( - bankMSB, - bankLSB, - program, - channel, - TriggerType.ProgramChange - ) - ) - - internal fun startSongViaMidiSongSelect(song: Byte) = startSongViaMidiSongTrigger( - SongTrigger( - 0.toByte(), - 0.toByte(), - song, - 0.toByte(), - TriggerType.SongSelect - ) - ) - - internal fun updateBluetoothIcon() { - val bluetoothMode = Preferences.bluetoothMode - val slave = bluetoothMode === BluetoothMode.Client - val connectedToServer = Bluetooth.isConnectedToServer - val master = bluetoothMode === BluetoothMode.Server - val connectedClients = Bluetooth.bluetoothClientCount - val resourceID = - if (slave) - if (connectedToServer) - R.drawable.duncecap - else - R.drawable.duncecap_outline - else if (master) - when (connectedClients) { - 0 -> R.drawable.master0 - 1 -> R.drawable.master1 - 2 -> R.drawable.master2 - 3 -> R.drawable.master3 - 4 -> R.drawable.master4 - 5 -> R.drawable.master5 - 6 -> R.drawable.master6 - 7 -> R.drawable.master7 - 8 -> R.drawable.master8 - else -> R.drawable.master9plus - } - else R.drawable.blank_icon - if (menu != null) { - val btLayout = menu!!.findItem(R.id.btconnectionstatuslayout).actionView as LinearLayout - val btIcon = btLayout.findViewById(R.id.btconnectionstatus) - btIcon?.setImageResource(resourceID) - } - } - - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - if (Preferences.clearTagsOnFolderChange) - selectedTagFilters.clear() - applyFileFilter(filters[position]) - if (performingCloudSync) { - performingCloudSync = false - val listView = requireView().findViewById(R.id.listView) - listView.setSelectionFromTop(savedListIndex, savedListOffset) - } - } - - private fun applyFileFilter(filter: Filter) { - selectedFilter = filter - playlist = if (filter is SongFilter) - Playlist(filter.songs.filter { - if (selectedTagFilters.isNotEmpty()) selectedTagFilters.any { filter -> - filter.songs.contains( - it - ) - } else true - }) - else - Playlist() - sortSongList() - listAdapter = buildListAdapter() - updateListView() - showSetListMissingSongs() - } - - override fun onNothingSelected(parent: AdapterView<*>) { - //applyFileFilter(null) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - val beforeListView = requireView().findViewById(R.id.listView) - val currentPosition = beforeListView.firstVisiblePosition - val v = beforeListView.getChildAt(0) - val top = if (v == null) 0 else v.top - beforeListView.paddingTop - - super.onConfigurationChanged(newConfig) - registerEventHandler() - listAdapter = buildListAdapter() - updateListView().setSelectionFromTop(currentPosition, top) - } - - private fun updateListView(): ListView = - requireView().findViewById(R.id.listView).apply { - onItemClickListener = this@SongListFragment - onItemLongClickListener = this@SongListFragment - adapter = listAdapter - } - - private fun setFilters(initialSelection: Int = 0) { - val spinnerLayout = menu?.findItem(R.id.tagspinnerlayout)?.actionView as? LinearLayout - spinnerLayout?.findViewById(R.id.tagspinner)?.apply { - onItemSelectedListener = this@SongListFragment - adapter = FilterListAdapter(filters, selectedTagFilters, requireActivity()) { - applyFileFilter(selectedFilter) - } - setSelection(initialSelection) - } - } - - @Deprecated("Deprecated in Java") - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - // Inflate the menu; this adds items to the action bar if it is present. - this.menu = menu - inflater.inflate(R.menu.songlistmenu, menu) - setFilters() - (menu.findItem(R.id.search).actionView as SearchView).apply { - setOnQueryTextListener(this@SongListFragment) - isSubmitButtonEnabled = false - } - if (!BuildConfig.DEBUG) { - menu.findItem(R.id.debug_log).apply { - isVisible = false - } - } - updateBluetoothIcon() - super.onCreateOptionsMenu(menu, inflater) - } - - private fun startSongViaMidiSongTrigger(mst: SongTrigger) { - for (node in playlist.nodes) - if (node.songFile.matchesTrigger(mst)) { - Logger.log({ "Found trigger match: '${node.songFile.title}'." }) - playPlaylistNode(node, true) - return - } - // Otherwise, it might be a song that is not currently onscreen. - // Still play it though! - for (sf in Cache.cachedCloudItems.songFiles) - if (sf.matchesTrigger(mst)) { - Logger.log({ "Found trigger match: '${sf.title}'." }) - playSongFile(sf, PlaylistNode(sf), true) - } - } - - private fun playPlaylistNode(node: PlaylistNode, startedByMidiTrigger: Boolean) { - val selectedSong = node.songFile - playSongFile(selectedSong, node, startedByMidiTrigger) - } - - private fun playSongFile( - selectedSong: SongFile, - node: PlaylistNode, - startedByMidiTrigger: Boolean - ) { - val mute = Preferences.mute - val manualMode = Preferences.manualMode - val defaultVariation = selectedSong.variations.first() - val mode = - if (manualMode) - ScrollingMode.Manual - else - selectedSong.bestScrollingMode - val sds = getSongDisplaySettings(mode) - val noAudioWhatsoever = manualMode || mute - playSong(node, defaultVariation, mode, startedByMidiTrigger, sds, sds, noAudioWhatsoever, 0) - } - - private fun shouldPlayNextSong(): Boolean = - when (Preferences.playNextSong) { - getString(R.string.playNextSongAlwaysValue) -> true - getString(R.string.playNextSongSetListsOnlyValue) -> selectedFilter is SetListFilter - else -> false - } - - private fun getSongDisplaySettings(songScrollMode: ScrollingMode): DisplaySettings { - val onlyUseBeatFontSizes = Preferences.onlyUseBeatFontSizes - - val minimumFontSizeBeat = Preferences.minimumBeatFontSize - val maximumFontSizeBeat = Preferences.maximumBeatFontSize - val minimumFontSizeSmooth = - if (onlyUseBeatFontSizes) minimumFontSizeBeat else Preferences.minimumSmoothFontSize - val maximumFontSizeSmooth = - if (onlyUseBeatFontSizes) maximumFontSizeBeat else Preferences.maximumSmoothFontSize - val minimumFontSizeManual = - if (onlyUseBeatFontSizes) minimumFontSizeBeat else Preferences.minimumManualFontSize - val maximumFontSizeManual = - if (onlyUseBeatFontSizes) maximumFontSizeBeat else Preferences.maximumManualFontSize - - val minimumFontSize: Int - val maximumFontSize: Int - when { - songScrollMode === ScrollingMode.Beat -> { - minimumFontSize = minimumFontSizeBeat - maximumFontSize = maximumFontSizeBeat - } - - songScrollMode === ScrollingMode.Smooth -> { - minimumFontSize = minimumFontSizeSmooth - maximumFontSize = maximumFontSizeSmooth - } - - else -> { - minimumFontSize = minimumFontSizeManual - maximumFontSize = maximumFontSizeManual - } - } - - val display = requireActivity().windowManager.defaultDisplay - val size = Point() - display.getSize(size) - return DisplaySettings( - resources.configuration.orientation, - minimumFontSize.toFloat(), - maximumFontSize.toFloat(), - Rect(0, 0, size.x, size.y), - songScrollMode != ScrollingMode.Manual - ) - } - - private fun playSong( - selectedNode: PlaylistNode, - variation: String, - scrollMode: ScrollingMode, - startedByMidiTrigger: Boolean, - nativeSettings: DisplaySettings, - sourceSettings: DisplaySettings, - noAudio: Boolean, - transposeShift: Int - ) { - showLoadingProgressUI(true) - nowPlayingNode = selectedNode - - val nextSongName = - if (selectedNode.nextSong != null && shouldPlayNextSong()) selectedNode.nextSong.songFile.title else "" - val songLoadInfo = SongLoadInfo( - selectedNode.songFile, - variation, - scrollMode, - nextSongName, - false, - startedByMidiTrigger, - nativeSettings, - sourceSettings, - noAudio, - Preferences.audioLatency, - transposeShift - ) - val songLoadJob = SongLoadJob(songLoadInfo) - SongLoadQueueWatcherTask.loadSong(songLoadJob) - } - - private fun addToTemporarySet(song: SongFile) { - filters.asSequence().filterIsInstance().firstOrNull()?.addSong(song) - try { - Cache.initialiseTemporarySetListFile(false, requireContext()) - Utils.appendToTextFile(Cache.temporarySetListFile!!, SetListEntry(song).toString()) - } catch (ioe: IOException) { - Toast.makeText(context, ioe.message, Toast.LENGTH_LONG).show() - } - } - - private fun onSongListLongClick(position: Int) { - val selectedNode = filterPlaylistNodes(playlist)[position] - val selectedSong = selectedNode.songFile - val selectedSet = - if (selectedFilter is SetListFileFilter) (selectedFilter as SetListFileFilter).setListFile else null - val tempSetListFilter = - filters.asSequence().filterIsInstance().firstOrNull() - - val addAllowed = - if (tempSetListFilter != null) - if (selectedFilter !== tempSetListFilter) - !tempSetListFilter.containsSong(selectedSong) - else - false - else - true - val includeRefreshSet = selectedSet != null && selectedFilter !== tempSetListFilter - val includeClearSet = selectedFilter === tempSetListFilter - - val arrayID: Int = if (includeRefreshSet) - if (addAllowed) - R.array.song_options_array_with_refresh_and_add - else - R.array.song_options_array_with_refresh - else if (includeClearSet) - R.array.song_options_array_with_clear - else if (addAllowed) - R.array.song_options_array_with_add - else - R.array.song_options_array - - AlertDialog.Builder(context).apply { - setTitle(R.string.song_options) - setItems(arrayID) { _, which -> - when (which) { - 0 -> showPlayDialog(selectedNode, selectedSong) - 1 -> performingCloudSync = - Cache.performCloudSync(selectedSong, false, this@SongListFragment) - - 2 -> performingCloudSync = - Cache.performCloudSync(selectedSong, true, this@SongListFragment) - - 3 -> when { - includeRefreshSet -> performingCloudSync = - Cache.performCloudSync(selectedSet, false, this@SongListFragment) - - includeClearSet -> Cache.clearTemporarySetList(requireContext()) - else -> addToTemporarySet(selectedSong) - } - - 4 -> addToTemporarySet(selectedSong) - } - } - create().apply { - setCanceledOnTouchOutside(true) - show() - } - } - } - - private fun showPlayDialog( - selectedNode: PlaylistNode, - selectedSong: SongFile - ) { - // Get the layout inflater - val inflater = layoutInflater - - @SuppressLint("InflateParams") - val view = inflater.inflate(R.layout.songlist_long_press_dialog, null) - - val variationSpinner = view - .findViewById(R.id.variationSpinner) - val variationSpinnerAdapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, selectedSong.variations - ) - val transposeOptions = - TransposeOption.getTransposeOptions(selectedSong.key, selectedSong.firstChord) - - val transposeSpinner = view - .findViewById(R.id.transposeSpinner) - val noTranspose = transposeOptions.isEmpty() || !Preferences.showChords - if (noTranspose) { - view.findViewById(R.id.transposeLabel).visibility = View.GONE - transposeSpinner.visibility = View.GONE - } else { - val transposeSpinnerAdapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - transposeOptions - ) - transposeSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - transposeSpinner.adapter = transposeSpinnerAdapter - } - val noAudioCheckbox = view.findViewById(R.id.noAudioCheckbox) - variationSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - variationSpinner.adapter = variationSpinnerAdapter - - val beatScrollable = selectedSong.isBeatScrollable - val smoothScrollable = selectedSong.isSmoothScrollable - val beatButton = view - .findViewById(R.id.toggleButton_beat) - val smoothButton = view - .findViewById(R.id.toggleButton_smooth) - val manualButton = view - .findViewById(R.id.toggleButton_manual) - if (!smoothScrollable) { - val layout = smoothButton.parent as ViewGroup - layout.removeView(smoothButton) - } - if (!beatScrollable) { - val layout = beatButton.parent as ViewGroup - layout.removeView(beatButton) - } - when { - beatScrollable -> { - beatButton.isChecked = true - beatButton.isEnabled = false - } - - smoothScrollable -> { - smoothButton.isChecked = true - smoothButton.isEnabled = false - } - - else -> { - manualButton.isChecked = true - manualButton.isEnabled = false - } - } - - beatButton.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - smoothButton.isChecked = false - manualButton.isChecked = false - smoothButton.isEnabled = true - manualButton.isEnabled = true - beatButton.isEnabled = false - } - } - - smoothButton.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - beatButton.isChecked = false - manualButton.isChecked = false - beatButton.isEnabled = true - manualButton.isEnabled = true - smoothButton.isEnabled = false - } - } - - manualButton.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - beatButton.isChecked = false - smoothButton.isChecked = false - smoothButton.isEnabled = true - beatButton.isEnabled = true - manualButton.isEnabled = false - } - } - - // Inflate and set the layout for the dialog - // Pass null as the parent view because its going in the dialog layout - AlertDialog.Builder(context).apply { - setView(view) - setTitle(R.string.play_options) - transposeSpinner.setSelection(ChordMap.NUMBER_OF_KEYS - 1) - // Add action buttons - setPositiveButton(R.string.play) { _, _ -> - val selectedVariation = variationSpinner.selectedItem as String - val selectedTranspose = - if (noTranspose) null else transposeSpinner.selectedItem as TransposeOption - val noAudio = noAudioCheckbox.isChecked - val mode = - when { - beatButton.isChecked -> ScrollingMode.Beat - smoothButton.isChecked -> ScrollingMode.Smooth - else -> ScrollingMode.Manual - } - val sds = getSongDisplaySettings(mode) - playSong( - selectedNode, - selectedVariation, - mode, - false, - sds, - sds, - noAudio, - selectedTranspose?.offset ?: 0 - ) - } - setNegativeButton(R.string.cancel) { _, _ -> } - create().apply { - setCanceledOnTouchOutside(true) - show() - } - } - } - - private fun onMIDIAliasListLongClick(position: Int) { - val maf = filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles)[position] - val showErrors = maf.errors.isNotEmpty() - val arrayID = - if (showErrors) R.array.midi_alias_options_array_with_show_errors else R.array.midi_alias_options_array - - AlertDialog.Builder(context).apply { - setTitle(R.string.midi_alias_list_options) - .setItems(arrayID) { _, which -> - if (which == 0) - performingCloudSync = Cache.performCloudSync(maf, false, this@SongListFragment) - else if (which == 1) - showMIDIAliasErrors(maf.errors) - } - create().apply { - setCanceledOnTouchOutside(true) - show() - } - } - } - - private fun showMIDIAliasErrors(errors: List) { - @SuppressLint("InflateParams") - val view = layoutInflater.inflate(R.layout.parse_errors_dialog, null) - val tv = view.findViewById(R.id.errors) - val str = StringBuilder() - for (fpe in errors) - str.append(fpe.toString()).append("\n") - tv.text = str.toString().trim() - AlertDialog.Builder(context).apply { - setView(view) - create().apply { - setButton( - AlertDialog.BUTTON_NEUTRAL, "OK" - ) { dialog, _ -> dialog.dismiss() } - setTitle(getString(R.string.midi_alias_file_errors)) - setCanceledOnTouchOutside(true) - show() - } - } - } - - override fun onItemLongClick( - parent: AdapterView<*>, - view: View, - position: Int, - id: Long - ): Boolean { - if (selectedFilter is MIDIAliasFilesFilter) - onMIDIAliasListLongClick(position) - else - onSongListLongClick(position) - return true - } - - private fun registerEventHandler() { - mSongListInstance = this - mSongListEventHandler = SongListEventHandler(this) - // Now ready to receive events. - EventRouter.addSongListEventHandler(tag!!, mSongListEventHandler!!) - } - - override fun onCreate(savedInstanceState: Bundle?) { - songLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) - startNextSong() - } - - super.onCreate(savedInstanceState) - - registerEventHandler() - - Preferences.registerOnSharedPreferenceChangeListener(this) - - setHasOptionsMenu(true) - - Cache.initialiseLocalStorage(requireContext()) - - // Set font stuff first. - val metrics = resources.displayMetrics - Utils.FONT_SCALING = metrics.density - Utils.MAXIMUM_FONT_SIZE = Integer.parseInt(getString(R.string.fontSizeMax)) - Utils.MINIMUM_FONT_SIZE = Integer.parseInt(getString(R.string.fontSizeMin)) - FontSizePreference.FONT_SIZE_MAX = Utils.MAXIMUM_FONT_SIZE - Utils.MINIMUM_FONT_SIZE - FontSizePreference.FONT_SIZE_MIN = 0 - FontSizePreference.FONT_SIZE_OFFSET = Utils.MINIMUM_FONT_SIZE - - val firstRun = Preferences.firstRun - if (firstRun) { - Preferences.firstRun = false - showFirstRunMessages() - } - - ReadCacheTask( - requireContext(), - Cache.CacheEventHandler - ) { onDatabaseReadCompleted(it, firstRun) }.execute(Unit) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = inflater.inflate(R.layout.activity_song_list, container, false) - - private fun onDatabaseReadCompleted(databaseExists: Boolean, firstRun: Boolean) { - if (!databaseExists && firstRun) { - Preferences.storageSystem = StorageType.Demo - Preferences.cloudPath = "/" - Cache.performFullCloudSync(this) - } - } - - private fun initialiseList(cache: CachedCloudCollection) { - playlist = Playlist() - buildFilterList(cache) - } - - override fun onDestroy() { - Preferences.unregisterOnSharedPreferenceChangeListener(this) - EventRouter.removeSongListEventHandler(tag!!) - super.onDestroy() - } - - override fun onResume() { - super.onResume() - - updateBluetoothIcon() - - if (listAdapter != null) - listAdapter!!.notifyDataSetChanged() - - SongLoadQueueWatcherTask.onResume() - } - - private fun showFirstRunMessages() { - // Declare a new thread to do a preference check - val t = Thread { - val i = Intent(context, IntroActivity::class.java) - startActivity(i) - } - - // Start the thread - t.start() - } - - private fun startNextSong(): Boolean { - mSongEndedNaturally = false - if (nowPlayingNode != null && nowPlayingNode!!.nextSong != null && shouldPlayNextSong()) { - playPlaylistNode(nowPlayingNode!!.nextSong!!, false) - return true - } - nowPlayingNode = null - return false - } - - private fun sortSongList() { - if (selectedFilter.canSort) { - val sorting = Preferences.sorting - sorting.forEach { - playlist = when (it) { - SortingPreference.Date -> playlist.sortByDateModified() - SortingPreference.Artist -> playlist.sortByArtist() - SortingPreference.Title -> playlist.sortByTitle() - SortingPreference.Mode -> playlist.sortByMode() - SortingPreference.Rating -> playlist.sortByRating() - SortingPreference.Key -> playlist.sortByKey() - } - } - } - } - - private fun shuffleSongList() { - playlist = playlist.shuffle() - listAdapter = buildListAdapter() - updateListView() - } - - private fun buildListAdapter(): BaseAdapter = - requireActivity().let { - if (selectedFilter is MIDIAliasFilesFilter) - MIDIAliasListAdapter( - filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles), - it - ) - else - SongListAdapter(filterPlaylistNodes(playlist), it) - } - - private fun buildFilterList(cache: CachedCloudCollection) { - Logger.log("Building tag list ...") - val lastSelectedFilter = selectedFilter - val tagAndFolderFilters = mutableListOf() - - // Create filters from song tags and sub-folders. Many songs can share the same - // tag/subfolder, so a bit of clever collection management is required here. - val tagDictionaries = HashMap>() - cache.songFiles.forEach { - it.tags.forEach { tag -> tagDictionaries.getOrPut(tag) { mutableListOf() }.add(it) } - } - - val folderDictionaries = HashMap>() - cache.folders.forEach { - cache.getSongsInFolder(it).let { songList -> - if (songList.isNotEmpty()) - folderDictionaries[it.name] = songList - } - } - - tagDictionaries.forEach { - tagAndFolderFilters.add(TagFilter(it.key, it.value)) - } - folderDictionaries.forEach { - tagAndFolderFilters.add(FolderFilter(it.key, it.value)) - } - tagAndFolderFilters.addAll(cache.setListFiles.mapNotNull { - if (it.file != Cache.temporarySetListFile) - SetListFileFilter(it, cache.songFiles) - else - null - }) - tagAndFolderFilters.sortBy { it.name.lowercase() } - - // Now create the basic "all songs" filter, dead easy ... - val allSongsFilter = createAllSongsFilter(cache) - - // Depending on whether we have a temporary set list file, we can create a temporary - // set list filter ... - val tempSetListFile = - cache.setListFiles.firstOrNull { it.file == Cache.temporarySetListFile } - val tempSetListFilter = - if (tempSetListFile != null) - TemporarySetListFilter(tempSetListFile, cache.songFiles) - else - null - - // Same thing for MIDI alias files ... there's always at least ONE (default aliases), but - // if there aren't any more, don't bother creating a filter. - val midiAliasFilesFilter = - if (cache.midiAliasFiles.size > 1) - MIDIAliasFilesFilter(getString(R.string.midi_alias_files)) - else - null - - // Now bundle them altogether into one list. - filters = listOf( - allSongsFilter, - tempSetListFilter, - tagAndFolderFilters, - midiAliasFilesFilter - ) - .flattenAll() - .filterIsInstance() - .sortedWith(FilterComparator.instance) - - // The default selected filter should be "all songs". - selectedFilter = findFilter(lastSelectedFilter, cache) - applyFileFilter(selectedFilter) - val selectedFilterIndex = filters.indexOf(selectedFilter) - setFilters(if (selectedFilterIndex == -1) 0 else selectedFilterIndex) - requireActivity().invalidateOptionsMenu() - } - - private fun createAllSongsFilter(cache: CachedCloudCollection): Filter = AllSongsFilter(cache - .songFiles - .asSequence() - .filterNot { cache.isFilterOnly(it) } - .toList()) - - private fun findFilter(filter: Filter, cache: CachedCloudCollection): Filter = - filters.find { it == filter } ?: filters.find { it is AllSongsFilter } ?: createAllSongsFilter( - cache - ) - - @Deprecated("Deprecated in Java") - override fun onPrepareOptionsMenu(menu: Menu) = - menu.run { - findItem(R.id.sort_songs)?.isEnabled = selectedFilter.canSort - findItem(R.id.synchronize)?.isEnabled = Cache.canPerformCloudSync() - } - - private fun showSortDialog() { - if (selectedFilter.canSort) { - AlertDialog.Builder(context).apply { - val items = arrayOf( - getString(R.string.byTitle), - getString(R.string.byArtist), - getString(R.string.byDate), - getString(R.string.byKey), - getString(R.string.byMode), - getString(R.string.byRating), - ) - setItems(items) { d, n -> - d.dismiss() - Preferences.sorting = arrayOf( - when (n) { - 1 -> SortingPreference.Artist - 2 -> SortingPreference.Date - 3 -> SortingPreference.Key - 4 -> SortingPreference.Mode - 5 -> SortingPreference.Rating - else -> SortingPreference.Title - } - ) - sortSongList() - listAdapter = buildListAdapter() - updateListView() - } - setTitle(getString(R.string.sortSongs)) - create().apply { - setCanceledOnTouchOutside(true) - show() - } - } - } - } - - private fun openBrowser(uriResource: Int) { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(uriResource))) - startActivity(browserIntent) - } - - private fun openManualURL() = openBrowser(R.string.instructionsUrl) - private fun openPrivacyPolicyURL() = openBrowser(R.string.privacyPolicyUrl) - private fun openBuyMeACoffeeURL() = openBrowser(R.string.buyMeACoffeeUrl) - private fun showDebugLog() = - Utils.showMessageDialog( - BeatPrompter.debugLog, - R.string.debugLogDialogCaption, - this.requireContext() - ) - - @Deprecated("Deprecated in Java") - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.synchronize -> Cache.performFullCloudSync(this) - R.id.shuffle -> shuffleSongList() - R.id.sort_songs -> showSortDialog() - R.id.settings -> startActivity(Intent(context, SettingsActivity::class.java)) - R.id.manual -> openManualURL() - R.id.privacy_policy -> openPrivacyPolicyURL() - R.id.buy_me_a_coffee -> openBuyMeACoffeeURL() - R.id.debug_log -> showDebugLog() - R.id.about -> showAboutDialog() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - private fun showSetListMissingSongs() { - if (selectedFilter is SetListFileFilter) { - val slf = selectedFilter as SetListFileFilter - val missing = slf.mMissingSetListEntries.take(3) - if (missing.isNotEmpty() && !slf.mWarned) { - slf.mWarned = true - val message = StringBuilder(getString(R.string.missing_songs_message, missing.size)) - message.append("\n\n") - missing.forEach { - message.append(it.toDisplayString()) - message.append("\n") - } - AlertDialog.Builder(context).create().apply { - setTitle(R.string.missing_songs_dialog_title) - setMessage(message.toString()) - setButton( - AlertDialog.BUTTON_NEUTRAL, "OK" - ) { dialog, _ -> dialog.dismiss() } - show() - } - } - } - } - - private fun showAboutDialog() { - AlertDialog.Builder(context).apply { - @SuppressLint("InflateParams") - val view = layoutInflater.inflate(R.layout.about_dialog, null) - setView(view) - create().apply { - setCanceledOnTouchOutside(true) - show() - findViewById(R.id.versionInfo)?.text = BeatPrompter.appResources.getString( - R.string.versionInfo, - BuildConfig.VERSION_NAME, - BuildConfig.VERSION_CODE - ) - findViewById(R.id.buyMeACoffeeIcon)?.setOnClickListener { openBuyMeACoffeeURL() } - } - } - } - - fun processBluetoothChooseSongMessage(choiceInfo: SongChoiceInfo) { - val beat = choiceInfo.isBeatScroll - val smooth = choiceInfo.isSmoothScroll - val scrollingMode = - if (beat) ScrollingMode.Beat else if (smooth) ScrollingMode.Smooth else ScrollingMode.Manual - - val mimicDisplay = scrollingMode === ScrollingMode.Manual && Preferences.mimicBandLeaderDisplay - - // Only use the settings from the ChooseSongMessage if the "mimic band leader display" setting is true. - // Also, beat and smooth scrolling should never mimic. - val nativeSettings = getSongDisplaySettings(scrollingMode) - val sourceSettings = if (mimicDisplay) DisplaySettings(choiceInfo) else nativeSettings - - for (sf in Cache.cachedCloudItems.songFiles) - if (sf.normalizedTitle == choiceInfo.normalizedTitle && sf.normalizedArtist == choiceInfo.normalizedArtist) { - val songLoadInfo = SongLoadInfo( - sf, - choiceInfo.variation, - scrollingMode, - "", - wasStartedByBandLeader = true, - wasStartedByMidiTrigger = false, - nativeSettings, - sourceSettings, - choiceInfo.noAudio, - choiceInfo.audioLatency, - choiceInfo.transposeShift - ) - val songLoadJob = SongLoadJob(songLoadInfo) - if (SongDisplayActivity.interruptCurrentSong(songLoadJob) == SongInterruptResult.NoSongToInterrupt) - playSong( - PlaylistNode(sf), - choiceInfo.variation, - scrollingMode, - true, - nativeSettings, - sourceSettings, - choiceInfo.noAudio, - choiceInfo.transposeShift - ) - break - } - } - - internal fun onCacheUpdated(cache: CachedCloudCollection) { - val listView = requireView().findViewById(R.id.listView) - savedListIndex = listView.firstVisiblePosition - val v = listView.getChildAt(0) - savedListOffset = if (v == null) 0 else v.top - listView.paddingTop - initialiseList(cache) - } - - internal fun onCacheCleared(report: Boolean) { - playlist = Playlist() - buildFilterList(Cache.cachedCloudItems) - val context = requireContext() - if (report) { - Toast.makeText( - context, - context.getString(R.string.cache_cleared), - Toast.LENGTH_LONG - ).show() - } - } - - internal fun onTemporarySetListCleared() { - filters.asSequence().filterIsInstance().firstOrNull()?.clear() - buildFilterList(Cache.cachedCloudItems) - } - - private fun showLoadingProgressUI(show: Boolean) { - requireView().findViewById(R.id.songLoadUI).visibility = - if (show) View.VISIBLE else View.GONE - if (!show) - updateLoadingProgress(0, 1) - } - - private fun updateLoadingProgress(currentProgress: Int, maxProgress: Int) = - launch { - requireView().findViewById(R.id.loadingProgress).apply { - max = maxProgress - progress = currentProgress - } - } - - override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { - if (key == getString(R.string.pref_storageLocation_key) || key == getString(R.string.pref_useExternalStorage_key)) - Cache.initialiseLocalStorage(requireContext()) - else if (key == getString(R.string.pref_largePrintList_key) - || key == getString(R.string.pref_showBeatStyleIcons_key) - || key == getString(R.string.pref_showMusicIcon_key) - || key == getString(R.string.pref_showKeyInList_key) - ) { - listAdapter = buildListAdapter() - updateListView() - } - } - - override fun onQueryTextSubmit(searchText: String?): Boolean = true - - override fun onQueryTextChange(searchText: String?): Boolean { - this.searchText = searchText?.lowercase() ?: "" - listAdapter = buildListAdapter() - updateListView() - return true - } - - private fun filterMIDIAliasFiles(fileList: List): List { - return fileList.filter { - it.file != Cache.defaultMidiAliasesFile && - (searchText.isBlank() || it.normalizedName.contains(searchText)) - } - } - - private fun filterPlaylistNodes(playlist: Playlist): List = - playlist.nodes.filter { - searchText.isBlank() || - it.songFile.normalizedArtist.contains(searchText) || - it.songFile.normalizedTitle.contains(searchText) - } - - companion object { - var mSongListEventHandler: SongListEventHandler? = null - var mSongEndedNaturally = false - - lateinit var mSongListInstance: SongListFragment - } - - class TransposeOption(val offset: Int, val key: KeySignature?) { - companion object { - fun getTransposeOptions(key: String?, firstChord: String?): List { - val keySignature = key?.let { KeySignatureDefinition.getKeySignature(key, firstChord) } - val options = mutableListOf() - for (offset in -(ChordMap.NUMBER_OF_KEYS - 1).. 0) "+" else "" - val offsetAmount = - if (offset == 0) BeatPrompter.appResources.getString(R.string.none) else "$offset" - val newKey = - key?.let { " (${it.getDisplayString(Preferences.displayUnicodeAccidentals)})" } ?: "" - return "$offsetSign$offset$newKey" - } - } - - class SongListEventHandler internal constructor(private val songList: SongListFragment) : - Handler() { - override fun handleMessage(msg: Message) { - when (msg.what) { - Events.BLUETOOTH_CHOOSE_SONG -> songList.processBluetoothChooseSongMessage(msg.obj as SongChoiceInfo) - Events.CLOUD_SYNC_ERROR -> { - AlertDialog.Builder(songList.context).apply { - setMessage( - BeatPrompter.appResources.getString( - R.string.cloudSyncErrorMessage, - msg.obj as String - ) - ) - setTitle(BeatPrompter.appResources.getString(R.string.cloudSyncErrorTitle)) - setPositiveButton("OK") { dialog, _ -> dialog.cancel() } - create().apply { - setCanceledOnTouchOutside(true) - show() - } - } - } - - Events.MIDI_PROGRAM_CHANGE -> { - val bytes = msg.obj as ByteArray - songList.startSongViaMidiProgramChange(bytes[0], bytes[1], bytes[2], bytes[3]) - } - - Events.MIDI_SONG_SELECT -> songList.startSongViaMidiSongSelect(msg.arg1.toByte()) - Events.CACHE_UPDATED -> { - BeatPrompter.addDebugMessage("CACHE_UPDATED received") - val cache = msg.obj as CachedCloudCollection - songList.onCacheUpdated(cache) - } - - Events.CONNECTION_ADDED -> { - Toast.makeText( - songList.context, - BeatPrompter.appResources.getString(R.string.connection_added, msg.obj.toString()), - Toast.LENGTH_LONG - ).show() - songList.updateBluetoothIcon() - } - - Events.CONNECTION_LOST -> { - Logger.log("Lost connection to device.") - Toast.makeText( - songList.context, - BeatPrompter.appResources.getString(R.string.connection_lost, msg.obj.toString()), - Toast.LENGTH_LONG - ).show() - songList.updateBluetoothIcon() - } - - Events.SONG_LOAD_CANCELLED -> { - if (!SongLoadQueueWatcherTask.isLoadingASong && !SongLoadQueueWatcherTask.hasASongToLoad) - songList.showLoadingProgressUI(false) - } - - Events.SONG_LOAD_FAILED -> { - songList.showLoadingProgressUI(false) - Toast.makeText(songList.context, msg.obj.toString(), Toast.LENGTH_LONG).show() - } - - Events.SONG_LOAD_COMPLETED -> { - Logger.logLoader({ "Song ${msg.obj} was fully loaded successfully." }) - songList.showLoadingProgressUI(false) - // No point starting up the activity if there are songs in the load queue - if (SongLoadQueueWatcherTask.hasASongToLoad || SongLoadQueueWatcherTask.isLoadingASong) - Logger.logLoader("Abandoning loaded song: there appears to be another song incoming.") - else - songList.startSongActivity(msg.obj as UUID) - } - - Events.SONG_LOAD_LINE_PROCESSED -> songList.updateLoadingProgress(msg.arg1, msg.arg2) - - Events.CACHE_CLEARED -> songList.onCacheCleared(msg.obj as Boolean) - Events.TEMPORARY_SET_LIST_CLEARED -> songList.onTemporarySetListCleared() - - Events.DATABASE_READ_ERROR, Events.DATABASE_WRITE_ERROR -> { - Toast.makeText( - songList.context, - if (msg.what == Events.DATABASE_READ_ERROR) BeatPrompter.appResources.getString(R.string.database_read_error) else BeatPrompter.appResources.getString( - R.string.database_write_error - ), - Toast.LENGTH_LONG - ).show() - } - } - } - } + : Fragment(), + AdapterView.OnItemClickListener, + AdapterView.OnItemSelectedListener, + SearchView.OnQueryTextListener, + AdapterView.OnItemLongClickListener, + SharedPreferences.OnSharedPreferenceChangeListener, + CoroutineScope { + private val coroutineJob = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + coroutineJob + private var songLauncher: ActivityResultLauncher? = null + private var listAdapter: BaseAdapter? = null + private var menu: Menu? = null + + private var playlist = Playlist() + private var nowPlayingNode: PlaylistNode? = null + private var searchText = "" + private var performingCloudSync = false + private var savedListIndex = 0 + private var savedListOffset = 0 + private var filters = listOf() + private val selectedTagFilters = mutableListOf() + private var selectedFilter: Filter = AllSongsFilter(mutableListOf()) + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + if (selectedFilter is MIDIAliasFilesFilter) { + val maf = filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles)[position] + if (maf.errors.isNotEmpty()) + showMIDIAliasErrors(maf.errors) + } else { + val songToLoad = filterPlaylistNodes(playlist)[position] + if (!SongLoadQueueWatcherTask.isAlreadyLoadingSong(songToLoad.songFile)) + playPlaylistNode(songToLoad, false) + } + } + + internal fun startSongActivity(loadID: UUID) { + val intent = Intent(context, SongDisplayActivity::class.java) + intent.putExtra("loadID", ParcelUuid(loadID)) + Logger.logLoader({ "Starting SongDisplayActivity for $loadID!" }) + songLauncher?.launch(intent) + } + + internal fun startSongViaMidiProgramChange( + bankMSB: Byte, + bankLSB: Byte, + program: Byte, + channel: Byte + ) = startSongViaMidiSongTrigger( + SongTrigger( + bankMSB, + bankLSB, + program, + channel, + TriggerType.ProgramChange + ) + ) + + internal fun startSongViaMidiSongSelect(song: Byte) = startSongViaMidiSongTrigger( + SongTrigger( + 0.toByte(), + 0.toByte(), + song, + 0.toByte(), + TriggerType.SongSelect + ) + ) + + internal fun updateBluetoothIcon() { + val bluetoothMode = BeatPrompter.preferences.bluetoothMode + val slave = bluetoothMode === BluetoothMode.Client + val connectedToServer = Bluetooth.isConnectedToServer + val master = bluetoothMode === BluetoothMode.Server + val connectedClients = Bluetooth.bluetoothClientCount + val resourceID = + if (slave) + if (connectedToServer) + R.drawable.duncecap + else + R.drawable.duncecap_outline + else if (master) + when (connectedClients) { + 0 -> R.drawable.master0 + 1 -> R.drawable.master1 + 2 -> R.drawable.master2 + 3 -> R.drawable.master3 + 4 -> R.drawable.master4 + 5 -> R.drawable.master5 + 6 -> R.drawable.master6 + 7 -> R.drawable.master7 + 8 -> R.drawable.master8 + else -> R.drawable.master9plus + } + else R.drawable.blank_icon + if (menu != null) { + val btLayout = menu!!.findItem(R.id.btconnectionstatuslayout).actionView as LinearLayout + val btIcon = btLayout.findViewById(R.id.btconnectionstatus) + btIcon?.setImageResource(resourceID) + } + } + + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + if (BeatPrompter.preferences.clearTagsOnFolderChange) + selectedTagFilters.clear() + applyFileFilter(filters[position]) + if (performingCloudSync) { + performingCloudSync = false + val listView = requireView().findViewById(R.id.listView) + listView.setSelectionFromTop(savedListIndex, savedListOffset) + } + } + + private fun applyFileFilter(filter: Filter) { + selectedFilter = filter + playlist = if (filter is SongFilter) + Playlist(filter.songs.filter { + if (selectedTagFilters.isNotEmpty()) selectedTagFilters.any { filter -> + filter.songs.contains( + it + ) + } else true + }) + else + Playlist() + sortSongList() + listAdapter = buildListAdapter() + updateListView() + showSetListMissingSongs() + } + + override fun onNothingSelected(parent: AdapterView<*>) { + //applyFileFilter(null) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + val beforeListView = requireView().findViewById(R.id.listView) + val currentPosition = beforeListView.firstVisiblePosition + val v = beforeListView.getChildAt(0) + val top = if (v == null) 0 else v.top - beforeListView.paddingTop + + super.onConfigurationChanged(newConfig) + registerEventHandler() + listAdapter = buildListAdapter() + updateListView().setSelectionFromTop(currentPosition, top) + } + + private fun updateListView(): ListView = + requireView().findViewById(R.id.listView).apply { + onItemClickListener = this@SongListFragment + onItemLongClickListener = this@SongListFragment + adapter = listAdapter + } + + private fun setFilters(initialSelection: Int = 0) { + val spinnerLayout = menu?.findItem(R.id.tagspinnerlayout)?.actionView as? LinearLayout + spinnerLayout?.findViewById(R.id.tagspinner)?.apply { + onItemSelectedListener = this@SongListFragment + adapter = FilterListAdapter(filters, selectedTagFilters, requireActivity()) { + applyFileFilter(selectedFilter) + } + setSelection(initialSelection) + } + } + + private fun startSongViaMidiSongTrigger(mst: SongTrigger) { + for (node in playlist.nodes) + if (node.songFile.matchesTrigger(mst)) { + Logger.log({ "Found trigger match: '${node.songFile.title}'." }) + playPlaylistNode(node, true) + return + } + // Otherwise, it might be a song that is not currently onscreen. + // Still play it though! + for (sf in Cache.cachedCloudItems.songFiles) + if (sf.matchesTrigger(mst)) { + Logger.log({ "Found trigger match: '${sf.title}'." }) + playSongFile(sf, PlaylistNode(sf), true) + } + } + + private fun playPlaylistNode(node: PlaylistNode, startedByMidiTrigger: Boolean) { + val selectedSong = node.songFile + playSongFile(selectedSong, node, startedByMidiTrigger) + } + + private fun playSongFile( + selectedSong: SongFile, + node: PlaylistNode, + startedByMidiTrigger: Boolean + ) { + val mute = BeatPrompter.preferences.mute + val manualMode = BeatPrompter.preferences.manualMode + val mode = + if (manualMode) + ScrollingMode.Manual + else + selectedSong.bestScrollingMode + val sds = getSongDisplaySettings(mode) + val noAudioWhatsoever = manualMode || mute + playSong( + node, + mode, + startedByMidiTrigger, + sds, + sds, + noAudioWhatsoever, + 0 + ) + } + + private fun shouldPlayNextSong(): Boolean = + when (BeatPrompter.preferences.playNextSong) { + getString(R.string.playNextSongAlwaysValue) -> true + getString(R.string.playNextSongSetListsOnlyValue) -> selectedFilter is SetListFilter + else -> false + } + + private fun getSongDisplaySettings(songScrollMode: ScrollingMode): DisplaySettings { + val onlyUseBeatFontSizes = BeatPrompter.preferences.onlyUseBeatFontSizes + + val minimumFontSizeBeat = BeatPrompter.preferences.minimumBeatFontSize + val maximumFontSizeBeat = BeatPrompter.preferences.maximumBeatFontSize + val minimumFontSizeSmooth = + if (onlyUseBeatFontSizes) minimumFontSizeBeat else BeatPrompter.preferences.minimumSmoothFontSize + val maximumFontSizeSmooth = + if (onlyUseBeatFontSizes) maximumFontSizeBeat else BeatPrompter.preferences.maximumSmoothFontSize + val minimumFontSizeManual = + if (onlyUseBeatFontSizes) minimumFontSizeBeat else BeatPrompter.preferences.minimumManualFontSize + val maximumFontSizeManual = + if (onlyUseBeatFontSizes) maximumFontSizeBeat else BeatPrompter.preferences.maximumManualFontSize + + val minimumFontSize: Int + val maximumFontSize: Int + when { + songScrollMode === ScrollingMode.Beat -> { + minimumFontSize = minimumFontSizeBeat + maximumFontSize = maximumFontSizeBeat + } + + songScrollMode === ScrollingMode.Smooth -> { + minimumFontSize = minimumFontSizeSmooth + maximumFontSize = maximumFontSizeSmooth + } + + else -> { + minimumFontSize = minimumFontSizeManual + maximumFontSize = maximumFontSizeManual + } + } + + val display = requireActivity().windowManager.defaultDisplay + val size = Point() + display.getSize(size) + return DisplaySettings( + resources.configuration.orientation, + minimumFontSize.toFloat(), + maximumFontSize.toFloat(), + Rect(0, 0, size.x, size.y), + songScrollMode != ScrollingMode.Manual + ) + } + + private fun playSong( + selectedNode: PlaylistNode, + scrollMode: ScrollingMode, + startedByMidiTrigger: Boolean, + nativeSettings: DisplaySettings, + sourceSettings: DisplaySettings, + noAudio: Boolean, + transposeShift: Int + ) { + showLoadingProgressUI(true) + nowPlayingNode = selectedNode + + val nextSongName = + if (selectedNode.nextSong != null && shouldPlayNextSong()) selectedNode.nextSong.songFile.title else "" + val songLoadInfo = SongLoadInfo( + selectedNode.songFile, + if (selectedNode.variation.isNullOrBlank()) selectedNode.songFile.defaultVariation else selectedNode.variation, + scrollMode, + nativeSettings, + sourceSettings, + nextSongName, + false, + startedByMidiTrigger, + noAudio, + BeatPrompter.preferences.audioLatency, + transposeShift + ) + val songLoadJob = SongLoadJob(songLoadInfo) + SongLoadQueueWatcherTask.loadSong(songLoadJob) + } + + private fun addToTemporarySet(song: SongFile) { + filters.asSequence().filterIsInstance().firstOrNull()?.addSong(song) + try { + Cache.initialiseTemporarySetListFile(false, requireContext()) + Utils.appendToTextFile(Cache.temporarySetListFile!!, SetListEntry(song).toString()) + } catch (ioe: IOException) { + Toast.makeText(context, ioe.message, Toast.LENGTH_LONG).show() + } + } + + private fun onSongListLongClick(position: Int) { + val selectedNode = filterPlaylistNodes(playlist)[position] + val selectedSong = selectedNode.songFile + val selectedSet = + if (selectedFilter is SetListFileFilter) (selectedFilter as SetListFileFilter).setListFile else null + val tempSetListFilter = + filters.asSequence().filterIsInstance().firstOrNull() + + val addAllowed = + if (tempSetListFilter != null) + if (selectedFilter !== tempSetListFilter) + !tempSetListFilter.containsSong(selectedSong) + else + false + else + true + val includeRefreshSet = selectedSet != null && selectedFilter !== tempSetListFilter + val includeClearSet = selectedFilter === tempSetListFilter + + val arrayID: Int = if (includeRefreshSet) + if (addAllowed) + R.array.song_options_array_with_refresh_and_add + else + R.array.song_options_array_with_refresh + else if (includeClearSet) + R.array.song_options_array_with_clear + else if (addAllowed) + R.array.song_options_array_with_add + else + R.array.song_options_array + + AlertDialog.Builder(context).apply { + setTitle(R.string.song_options) + setItems(arrayID) { _, which -> + when (which) { + 0 -> showPlayDialog(selectedNode, selectedSong) + 1 -> performingCloudSync = + Cache.performCloudSync(selectedSong, false, this@SongListFragment) + + 2 -> performingCloudSync = + Cache.performCloudSync(selectedSong, true, this@SongListFragment) + + 3 -> when { + includeRefreshSet -> performingCloudSync = + Cache.performCloudSync(selectedSet, false, this@SongListFragment) + + includeClearSet -> Cache.clearTemporarySetList(requireContext()) + else -> addToTemporarySet(selectedSong) + } + + 4 -> addToTemporarySet(selectedSong) + } + } + create().apply { + setCanceledOnTouchOutside(true) + show() + } + } + } + + private fun showPlayDialog( + selectedNode: PlaylistNode, + selectedSong: SongFile + ) { + // Get the layout inflater + val inflater = layoutInflater + + @SuppressLint("InflateParams") + val view = inflater.inflate(R.layout.songlist_long_press_dialog, null) + + val variationSpinner = view + .findViewById(R.id.variationSpinner) + val variationSpinnerAdapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_item, selectedSong.variations + ) + val transposeOptions = + TransposeOption.getTransposeOptions(selectedSong.key, selectedSong.firstChord) + + val transposeSpinner = view + .findViewById(R.id.transposeSpinner) + val noTranspose = transposeOptions.isEmpty() || !BeatPrompter.preferences.showChords + if (noTranspose) { + view.findViewById(R.id.transposeLabel).visibility = View.GONE + transposeSpinner.visibility = View.GONE + } else { + val transposeSpinnerAdapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_item, + transposeOptions + ) + transposeSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + transposeSpinner.adapter = transposeSpinnerAdapter + } + val noAudioCheckbox = view.findViewById(R.id.noAudioCheckbox) + variationSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + variationSpinner.adapter = variationSpinnerAdapter + + val beatScrollable = selectedSong.isBeatScrollable + val smoothScrollable = selectedSong.isSmoothScrollable + val beatButton = view + .findViewById(R.id.toggleButton_beat) + val smoothButton = view + .findViewById(R.id.toggleButton_smooth) + val manualButton = view + .findViewById(R.id.toggleButton_manual) + if (!smoothScrollable) { + val layout = smoothButton.parent as ViewGroup + layout.removeView(smoothButton) + } + if (!beatScrollable) { + val layout = beatButton.parent as ViewGroup + layout.removeView(beatButton) + } + when { + beatScrollable -> { + beatButton.isChecked = true + beatButton.isEnabled = false + } + + smoothScrollable -> { + smoothButton.isChecked = true + smoothButton.isEnabled = false + } + + else -> { + manualButton.isChecked = true + manualButton.isEnabled = false + } + } + + beatButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + smoothButton.isChecked = false + manualButton.isChecked = false + smoothButton.isEnabled = true + manualButton.isEnabled = true + beatButton.isEnabled = false + } + } + + smoothButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + beatButton.isChecked = false + manualButton.isChecked = false + beatButton.isEnabled = true + manualButton.isEnabled = true + smoothButton.isEnabled = false + } + } + + manualButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + beatButton.isChecked = false + smoothButton.isChecked = false + smoothButton.isEnabled = true + beatButton.isEnabled = true + manualButton.isEnabled = false + } + } + + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + AlertDialog.Builder(context).apply { + setView(view) + setTitle(R.string.play_options) + transposeSpinner.setSelection(ChordMap.NUMBER_OF_KEYS - 1) + // Add action buttons + setPositiveButton(R.string.play) { _, _ -> + val selectedVariation = variationSpinner.selectedItem as String + val selectedTranspose = + if (noTranspose) null else transposeSpinner.selectedItem as TransposeOption + val noAudio = noAudioCheckbox.isChecked + val mode = + when { + beatButton.isChecked -> ScrollingMode.Beat + smoothButton.isChecked -> ScrollingMode.Smooth + else -> ScrollingMode.Manual + } + val sds = getSongDisplaySettings(mode) + playSong( + PlaylistNode(selectedNode.songFile, selectedVariation, selectedNode.nextSong), + mode, + false, + sds, + sds, + noAudio, + selectedTranspose?.offset ?: 0 + ) + } + setNegativeButton(R.string.cancel) { _, _ -> } + create().apply { + setCanceledOnTouchOutside(true) + show() + } + } + } + + private fun onMIDIAliasListLongClick(position: Int) { + val maf = filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles)[position] + val showErrors = maf.errors.isNotEmpty() + val arrayID = + if (showErrors) R.array.midi_alias_options_array_with_show_errors else R.array.midi_alias_options_array + + AlertDialog.Builder(context).apply { + setTitle(R.string.midi_alias_list_options) + .setItems(arrayID) { _, which -> + if (which == 0) + performingCloudSync = Cache.performCloudSync(maf, false, this@SongListFragment) + else if (which == 1) + showMIDIAliasErrors(maf.errors) + } + create().apply { + setCanceledOnTouchOutside(true) + show() + } + } + } + + private fun showMIDIAliasErrors(errors: List) { + @SuppressLint("InflateParams") + val view = layoutInflater.inflate(R.layout.parse_errors_dialog, null) + val tv = view.findViewById(R.id.errors) + val str = StringBuilder() + for (fpe in errors) + str.append(fpe.toString()).append("\n") + tv.text = str.toString().trim() + AlertDialog.Builder(context).apply { + setView(view) + create().apply { + setButton( + AlertDialog.BUTTON_NEUTRAL, "OK" + ) { dialog, _ -> dialog.dismiss() } + setTitle(getString(R.string.midi_alias_file_errors)) + setCanceledOnTouchOutside(true) + show() + } + } + } + + override fun onItemLongClick( + parent: AdapterView<*>, + view: View, + position: Int, + id: Long + ): Boolean { + if (selectedFilter is MIDIAliasFilesFilter) + onMIDIAliasListLongClick(position) + else + onSongListLongClick(position) + return true + } + + private fun registerEventHandler() { + mSongListInstance = this + mSongListEventHandler = SongListEventHandler(this) + // Now ready to receive events. + EventRouter.addSongListEventHandler(tag!!, mSongListEventHandler!!) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requireActivity().addMenuProvider(MenuProvider()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + songLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) + startNextSong() + } + + super.onCreate(savedInstanceState) + + registerEventHandler() + + BeatPrompter.preferences.registerOnSharedPreferenceChangeListener(this) + + Cache.initialiseLocalStorage(requireContext()) + + val firstRun = BeatPrompter.preferences.firstRun + if (firstRun) { + BeatPrompter.preferences.firstRun = false + showFirstRunMessages() + } + + ReadCacheTask( + requireContext(), + Cache.CacheEventHandler + ) { onDatabaseReadCompleted(it, firstRun) }.execute(Unit) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.activity_song_list, container, false) + + private fun onDatabaseReadCompleted(databaseExists: Boolean, firstRun: Boolean) { + if (!databaseExists && firstRun) { + BeatPrompter.preferences.storageSystem = StorageType.Demo + BeatPrompter.preferences.cloudPath = "/" + Cache.performFullCloudSync(this) + } + } + + private fun initialiseList(cache: CachedCloudCollection) { + playlist = Playlist() + buildFilterList(cache) + } + + override fun onDestroy() { + BeatPrompter.preferences.unregisterOnSharedPreferenceChangeListener(this) + EventRouter.removeSongListEventHandler(tag!!) + super.onDestroy() + } + + override fun onResume() { + super.onResume() + + updateBluetoothIcon() + + if (listAdapter != null) + listAdapter!!.notifyDataSetChanged() + + SongLoadQueueWatcherTask.onResume() + } + + private fun showFirstRunMessages() { + // Declare a new thread to do a preference check + val t = Thread { + val i = Intent(context, IntroActivity::class.java) + startActivity(i) + } + + // Start the thread + t.start() + } + + private fun startNextSong(): Boolean { + mSongEndedNaturally = false + if (nowPlayingNode != null && nowPlayingNode!!.nextSong != null && shouldPlayNextSong()) { + playPlaylistNode(nowPlayingNode!!.nextSong!!, false) + return true + } + nowPlayingNode = null + return false + } + + private fun sortSongList() { + if (selectedFilter.canSort) { + val sorting = BeatPrompter.preferences.sorting + sorting.forEach { + playlist = when (it) { + SortingPreference.Date -> playlist.sortByDateModified() + SortingPreference.Artist -> playlist.sortByArtist() + SortingPreference.Title -> playlist.sortByTitle() + SortingPreference.Mode -> playlist.sortByMode() + SortingPreference.Rating -> playlist.sortByRating() + SortingPreference.Key -> playlist.sortByKey() + } + } + } + } + + private fun shuffleSongList() { + playlist = playlist.shuffle() + listAdapter = buildListAdapter() + updateListView() + } + + private fun buildListAdapter(): BaseAdapter = + requireActivity().let { + if (selectedFilter is MIDIAliasFilesFilter) + MIDIAliasListAdapter( + filterMIDIAliasFiles(Cache.cachedCloudItems.midiAliasFiles), + it + ) + else + SongListAdapter(filterPlaylistNodes(playlist), it) + } + + private fun buildFilterList(cache: CachedCloudCollection) { + Logger.log("Building tag list ...") + val lastSelectedFilter = selectedFilter + val tagAndFolderFilters = mutableListOf() + + // Create filters from song tags and sub-folders. Many songs can share the same + // tag/subfolder, so a bit of clever collection management is required here. + val tagDictionaries = HashMap>() + cache.songFiles.forEach { + it.tags.forEach { tag -> tagDictionaries.getOrPut(tag) { mutableListOf() }.add(it) } + } + + val folderDictionaries = HashMap>() + cache.folders.forEach { + cache.getSongsInFolder(it).let { songList -> + if (songList.isNotEmpty()) + folderDictionaries[it.name] = songList + } + } + + tagDictionaries.forEach { + tagAndFolderFilters.add(TagFilter(it.key, it.value)) + } + folderDictionaries.forEach { + tagAndFolderFilters.add(FolderFilter(it.key, it.value)) + } + tagAndFolderFilters.addAll(cache.setListFiles.mapNotNull { + if (it.file != Cache.temporarySetListFile) + SetListFileFilter(it, cache.songFiles) + else + null + }) + tagAndFolderFilters.sortBy { it.name.lowercase() } + + // Now create the basic "all songs" filter, dead easy ... + val allSongsFilter = createAllSongsFilter(cache) + + // Depending on whether we have a temporary set list file, we can create a temporary + // set list filter ... + val tempSetListFile = + cache.setListFiles.firstOrNull { it.file == Cache.temporarySetListFile } + val tempSetListFilter = + if (tempSetListFile != null) + TemporarySetListFilter(tempSetListFile, cache.songFiles) + else + null + + // Same thing for MIDI alias files ... there's always at least ONE (default aliases), but + // if there aren't any more, don't bother creating a filter. + val midiAliasFilesFilter = + if (cache.midiAliasFiles.size > 1) + MIDIAliasFilesFilter(getString(R.string.midi_alias_files)) + else + null + + // Now bundle them altogether into one list. + filters = listOf( + allSongsFilter, + tempSetListFilter, + tagAndFolderFilters, + midiAliasFilesFilter + ) + .flattenAll() + .filterIsInstance() + .sortedWith(FilterComparator.instance) + + // The default selected filter should be "all songs". + selectedFilter = findFilter(lastSelectedFilter, cache) + applyFileFilter(selectedFilter) + val selectedFilterIndex = filters.indexOf(selectedFilter) + setFilters(if (selectedFilterIndex == -1) 0 else selectedFilterIndex) + requireActivity().invalidateOptionsMenu() + } + + private fun createAllSongsFilter(cache: CachedCloudCollection): Filter = AllSongsFilter(cache + .songFiles + .asSequence() + .filterNot { cache.isFilterOnly(it) } + .toList()) + + private fun findFilter(filter: Filter, cache: CachedCloudCollection): Filter = + filters.find { it == filter } ?: filters.find { it is AllSongsFilter } ?: createAllSongsFilter( + cache + ) + + private fun showSortDialog() { + if (selectedFilter.canSort) { + AlertDialog.Builder(context).apply { + val items = arrayOf( + getString(R.string.byTitle), + getString(R.string.byArtist), + getString(R.string.byDate), + getString(R.string.byKey), + getString(R.string.byMode), + getString(R.string.byRating), + ) + setItems(items) { d, n -> + d.dismiss() + BeatPrompter.preferences.sorting = arrayOf( + when (n) { + 1 -> SortingPreference.Artist + 2 -> SortingPreference.Date + 3 -> SortingPreference.Key + 4 -> SortingPreference.Mode + 5 -> SortingPreference.Rating + else -> SortingPreference.Title + } + ) + sortSongList() + listAdapter = buildListAdapter() + updateListView() + } + setTitle(getString(R.string.sortSongs)) + create().apply { + setCanceledOnTouchOutside(true) + show() + } + } + } + } + + private fun openBrowser(uriResource: Int) { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(uriResource))) + startActivity(browserIntent) + } + + private fun openManualURL() = openBrowser(R.string.instructionsUrl) + private fun openPrivacyPolicyURL() = openBrowser(R.string.privacyPolicyUrl) + private fun openBuyMeACoffeeURL() = openBrowser(R.string.buyMeACoffeeUrl) + private fun showDebugLog() = + Utils.showMessageDialog( + BeatPrompter.debugLog, + R.string.debugLogDialogCaption, + this.requireContext() + ) + + private fun showSetListMissingSongs() { + if (selectedFilter is SetListFileFilter) { + val slf = selectedFilter as SetListFileFilter + val missing = slf.mMissingSetListEntries.take(3) + if (missing.isNotEmpty() && !slf.mWarned) { + slf.mWarned = true + val message = StringBuilder(getString(R.string.missing_songs_message, missing.size)) + message.append("\n\n") + missing.forEach { + message.append(it.toDisplayString()) + message.append("\n") + } + AlertDialog.Builder(context).create().apply { + setTitle(R.string.missing_songs_dialog_title) + setMessage(message.toString()) + setButton( + AlertDialog.BUTTON_NEUTRAL, "OK" + ) { dialog, _ -> dialog.dismiss() } + show() + } + } + } + } + + private fun showAboutDialog() { + AlertDialog.Builder(context).apply { + @SuppressLint("InflateParams") + val view = layoutInflater.inflate(R.layout.about_dialog, null) + setView(view) + create().apply { + setCanceledOnTouchOutside(true) + show() + findViewById(R.id.versionInfo)?.text = BeatPrompter.appResources.getString( + R.string.versionInfo, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE + ) + findViewById(R.id.buyMeACoffeeIcon)?.setOnClickListener { openBuyMeACoffeeURL() } + } + } + } + + fun processBluetoothChooseSongMessage(choiceInfo: SongChoiceInfo) { + val beat = choiceInfo.isBeatScroll + val smooth = choiceInfo.isSmoothScroll + val scrollingMode = + if (beat) ScrollingMode.Beat else if (smooth) ScrollingMode.Smooth else ScrollingMode.Manual + + val mimicDisplay = + scrollingMode === ScrollingMode.Manual && BeatPrompter.preferences.mimicBandLeaderDisplay + + // Only use the settings from the ChooseSongMessage if the "mimic band leader display" setting is true. + // Also, beat and smooth scrolling should never mimic. + val nativeSettings = getSongDisplaySettings(scrollingMode) + val sourceSettings = if (mimicDisplay) DisplaySettings(choiceInfo) else nativeSettings + + for (sf in Cache.cachedCloudItems.songFiles) + if (sf.normalizedTitle == choiceInfo.normalizedTitle && sf.normalizedArtist == choiceInfo.normalizedArtist) { + val songLoadInfo = SongLoadInfo( + sf, + choiceInfo.variation, + scrollingMode, + nativeSettings, + sourceSettings, + "", + wasStartedByBandLeader = true, + wasStartedByMidiTrigger = false, + choiceInfo.noAudio, + choiceInfo.audioLatency, + choiceInfo.transposeShift + ) + val songLoadJob = SongLoadJob(songLoadInfo) + if (SongDisplayActivity.interruptCurrentSong(songLoadJob) == SongInterruptResult.NoSongToInterrupt) + playSong( + PlaylistNode(sf, choiceInfo.variation), + scrollingMode, + true, + nativeSettings, + sourceSettings, + choiceInfo.noAudio, + choiceInfo.transposeShift + ) + break + } + } + + internal fun onCacheUpdated(cache: CachedCloudCollection) { + val listView = requireView().findViewById(R.id.listView) + savedListIndex = listView.firstVisiblePosition + val v = listView.getChildAt(0) + savedListOffset = if (v == null) 0 else v.top - listView.paddingTop + initialiseList(cache) + } + + internal fun onCacheCleared(report: Boolean) { + playlist = Playlist() + buildFilterList(Cache.cachedCloudItems) + val context = requireContext() + if (report) { + Toast.makeText( + context, + context.getString(R.string.cache_cleared), + Toast.LENGTH_LONG + ).show() + } + } + + internal fun onTemporarySetListCleared() { + filters.asSequence().filterIsInstance().firstOrNull()?.clear() + buildFilterList(Cache.cachedCloudItems) + } + + private fun showLoadingProgressUI(show: Boolean) { + requireView().findViewById(R.id.songLoadUI).visibility = + if (show) View.VISIBLE else View.GONE + if (!show) + updateLoadingProgress(0, 1) + } + + private fun updateLoadingProgress(currentProgress: Int, maxProgress: Int) = + launch { + requireView().findViewById(R.id.loadingProgress).apply { + max = maxProgress + progress = currentProgress + } + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + if (key == getString(R.string.pref_storageLocation_key) || key == getString(R.string.pref_useExternalStorage_key)) + Cache.initialiseLocalStorage(requireContext()) + else if (key == getString(R.string.pref_largePrintList_key) + || key == getString(R.string.pref_showBeatStyleIcons_key) + || key == getString(R.string.pref_showMusicIcon_key) + || key == getString(R.string.pref_showKeyInList_key) + ) { + listAdapter = buildListAdapter() + updateListView() + } + } + + override fun onQueryTextSubmit(searchText: String?): Boolean = true + + override fun onQueryTextChange(searchText: String?): Boolean { + this.searchText = searchText?.lowercase() ?: "" + listAdapter = buildListAdapter() + updateListView() + return true + } + + private fun filterMIDIAliasFiles(fileList: List): List { + return fileList.filter { + it.file != Cache.defaultMidiAliasesFile && + (searchText.isBlank() || it.normalizedName.contains(searchText)) + } + } + + private fun filterPlaylistNodes(playlist: Playlist): List = + playlist.nodes.filter { + searchText.isBlank() || + it.songFile.normalizedArtist.contains(searchText) || + it.songFile.normalizedTitle.contains(searchText) + } + + companion object { + var mSongListEventHandler: SongListEventHandler? = null + var mSongEndedNaturally = false + + lateinit var mSongListInstance: SongListFragment + } + + inner class MenuProvider : + androidx.core.view.MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.songlistmenu, menu) + if (!BuildConfig.DEBUG) { + menu.findItem(R.id.debug_log).apply { + isVisible = false + } + } + (menu.findItem(R.id.search).actionView as SearchView).apply { + setOnQueryTextListener(this@SongListFragment) + isSubmitButtonEnabled = false + } + this@SongListFragment.menu = menu + setFilters() + updateBluetoothIcon() + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.sort_songs)?.isEnabled = selectedFilter.canSort + menu.findItem(R.id.synchronize)?.isEnabled = Cache.canPerformCloudSync() + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.synchronize -> Cache.performFullCloudSync(this@SongListFragment) + R.id.shuffle -> shuffleSongList() + R.id.sort_songs -> showSortDialog() + R.id.settings -> startActivity(Intent(context, SettingsActivity::class.java)) + R.id.manual -> openManualURL() + R.id.privacy_policy -> openPrivacyPolicyURL() + R.id.buy_me_a_coffee -> openBuyMeACoffeeURL() + R.id.debug_log -> showDebugLog() + R.id.about -> showAboutDialog() + } + return true + } + } + + class TransposeOption(val offset: Int, val key: KeySignature?) { + companion object { + fun getTransposeOptions(key: String?, firstChord: String?): List { + val keySignature = key?.let { KeySignatureDefinition.getKeySignature(key, firstChord) } + val options = mutableListOf() + for (offset in -(ChordMap.NUMBER_OF_KEYS - 1).. 0) "+" else "" + if (offset == 0) BeatPrompter.appResources.getString(R.string.none) else "$offset" + val newKey = + key?.let { " (${it.getDisplayString(BeatPrompter.preferences.displayUnicodeAccidentals)})" } + ?: "" + return "$offsetSign$offset$newKey" + } + } + + class SongListEventHandler internal constructor(private val songList: SongListFragment) : + Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + Events.BLUETOOTH_CHOOSE_SONG -> songList.processBluetoothChooseSongMessage(msg.obj as SongChoiceInfo) + Events.CLOUD_SYNC_ERROR -> { + AlertDialog.Builder(songList.context).apply { + setMessage( + BeatPrompter.appResources.getString( + R.string.cloudSyncErrorMessage, + msg.obj as String + ) + ) + setTitle(BeatPrompter.appResources.getString(R.string.cloudSyncErrorTitle)) + setPositiveButton("OK") { dialog, _ -> dialog.cancel() } + create().apply { + setCanceledOnTouchOutside(true) + show() + } + } + } + + Events.MIDI_PROGRAM_CHANGE -> { + val bytes = msg.obj as ByteArray + songList.startSongViaMidiProgramChange(bytes[0], bytes[1], bytes[2], bytes[3]) + } + + Events.MIDI_SONG_SELECT -> songList.startSongViaMidiSongSelect(msg.arg1.toByte()) + Events.CACHE_UPDATED -> { + BeatPrompter.addDebugMessage("CACHE_UPDATED received") + val cache = msg.obj as CachedCloudCollection + songList.onCacheUpdated(cache) + } + + Events.CONNECTION_ADDED -> { + Toast.makeText( + songList.context, + BeatPrompter.appResources.getString(R.string.connection_added, msg.obj.toString()), + Toast.LENGTH_LONG + ).show() + songList.updateBluetoothIcon() + } + + Events.CONNECTION_LOST -> { + Logger.log("Lost connection to device.") + Toast.makeText( + songList.context, + BeatPrompter.appResources.getString(R.string.connection_lost, msg.obj.toString()), + Toast.LENGTH_LONG + ).show() + songList.updateBluetoothIcon() + } + + Events.SONG_LOAD_CANCELLED -> { + if (!SongLoadQueueWatcherTask.isLoadingASong && !SongLoadQueueWatcherTask.hasASongToLoad) + songList.showLoadingProgressUI(false) + } + + Events.SONG_LOAD_FAILED -> { + songList.showLoadingProgressUI(false) + Toast.makeText(songList.context, msg.obj.toString(), Toast.LENGTH_LONG).show() + } + + Events.SONG_LOAD_COMPLETED -> { + Logger.logLoader({ "Song ${msg.obj} was fully loaded successfully." }) + songList.showLoadingProgressUI(false) + // No point starting up the activity if there are songs in the load queue + if (SongLoadQueueWatcherTask.hasASongToLoad || SongLoadQueueWatcherTask.isLoadingASong) + Logger.logLoader("Abandoning loaded song: there appears to be another song incoming.") + else + songList.startSongActivity(msg.obj as UUID) + } + + Events.SONG_LOAD_LINE_PROCESSED -> songList.updateLoadingProgress(msg.arg1, msg.arg2) + + Events.CACHE_CLEARED -> songList.onCacheCleared(msg.obj as Boolean) + Events.TEMPORARY_SET_LIST_CLEARED -> songList.onTemporarySetListCleared() + + Events.DATABASE_READ_ERROR, Events.DATABASE_WRITE_ERROR -> { + Toast.makeText( + songList.context, + if (msg.what == Events.DATABASE_READ_ERROR) BeatPrompter.appResources.getString(R.string.database_read_error) else BeatPrompter.appResources.getString( + R.string.database_write_error + ), + Toast.LENGTH_LONG + ).show() + } + } + } + } } diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongView.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongView.kt index c7e735f5..98d75987 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongView.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/SongView.kt @@ -7,6 +7,7 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.Rect +import android.graphics.RectF import android.media.AudioAttributes import android.media.SoundPool import android.util.AttributeSet @@ -15,9 +16,8 @@ import android.view.MotionEvent import android.widget.OverScroller import android.widget.Toast import androidx.appcompat.widget.AppCompatImageView -import androidx.core.view.GestureDetectorCompat +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.Logger -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.Task import com.stevenfrew.beatprompter.audio.AudioPlayer @@ -31,6 +31,7 @@ import com.stevenfrew.beatprompter.comm.bluetooth.message.ToggleStartStopMessage import com.stevenfrew.beatprompter.comm.midi.Midi import com.stevenfrew.beatprompter.events.EventRouter import com.stevenfrew.beatprompter.events.Events +import com.stevenfrew.beatprompter.graphics.bitmaps.AndroidBitmap import com.stevenfrew.beatprompter.song.PlayState import com.stevenfrew.beatprompter.song.ScrollingMode import com.stevenfrew.beatprompter.song.Song @@ -41,11 +42,12 @@ 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.line.Line import com.stevenfrew.beatprompter.ui.pref.MetronomeContext import com.stevenfrew.beatprompter.util.Utils +import com.stevenfrew.beatprompter.util.inflate import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor @@ -57,13 +59,14 @@ class SongView : AppCompatImageView, GestureDetector.OnGestureListener { - private val destinationGraphicRect = Rect(0, 0, 0, 0) + private val destinationGraphicRect = Rect() private var currentBeatCountRect = Rect() + private var originalBeatCountRect = Rect() private var endSongByPedalCounter = 0 private var metronomeOn: Boolean = false private var initialized = false private var skipping = false - private var currentVolume = Preferences.defaultTrackVolume + private var currentVolume = BeatPrompter.preferences.defaultTrackVolume private var lastCommentEvent: CommentEvent? = null private var lastCommentTime: Long = 0 private var lastTempMessageTime: Long = 0 @@ -84,7 +87,6 @@ class SongView private var nanosecondsPerBeat = Utils.nanosecondsPerBeat(120.0) private val backgroundColorLookup = IntArray(101) - private val commentTextColor: Int private val pageDownMarkerColor: Int private val beatCounterColor: Int private val defaultCurrentLineHighlightColor: Int @@ -107,7 +109,7 @@ class SongView private var songTitleContrastBackground: Int = 0 private var songTitleContrastBeatCounter: Int = 0 private val scrollIndicatorRect = Rect() - private var gestureDetector: GestureDetectorCompat? = null + private var gestureDetector: GestureDetector? = null private var screenAction = ScreenAction.Scroll private val scrollMarkerColor: Int private var songDisplayActivity: SongDisplayActivity? = null @@ -127,40 +129,41 @@ class SongView constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { scroller = OverScroller(context) - gestureDetector = GestureDetectorCompat(context, this) + gestureDetector = GestureDetector(context, this) songPixelPosition = 0 - audioPlayerFactory = AudioPlayerFactory(Preferences.audioPlayer, context) + audioPlayerFactory = AudioPlayerFactory(BeatPrompter.preferences.audioPlayer, context) silenceAudioPlayer = audioPlayerFactory.createSilencePlayer() - screenAction = Preferences.screenAction - showScrollIndicator = Preferences.showScrollIndicator - showSongTitle = Preferences.showSongTitle - val commentDisplayTimeSeconds = Preferences.commentDisplayTime + screenAction = BeatPrompter.preferences.screenAction + showScrollIndicator = BeatPrompter.preferences.showScrollIndicator + showSongTitle = BeatPrompter.preferences.showSongTitle + val commentDisplayTimeSeconds = BeatPrompter.preferences.commentDisplayTime commentDisplayTimeNanoseconds = Utils.milliToNano(commentDisplayTimeSeconds * 1000) - externalTriggerSafetyCatch = Preferences.midiTriggerSafetyCatch - highlightCurrentLine = Preferences.highlightCurrentLine - showPageDownMarker = Preferences.showPageDownMarker - highlightBeatSectionStart = Preferences.highlightBeatSectionStart - beatCounterColor = Preferences.beatCounterColor - commentTextColor = Preferences.commentColor - pageDownMarkerColor = Preferences.pageDownMarkerColor - scrollMarkerColor = Preferences.scrollIndicatorColor - val mHighlightBeatSectionStartColor = Preferences.beatSectionStartHighlightColor + externalTriggerSafetyCatch = BeatPrompter.preferences.midiTriggerSafetyCatch + highlightCurrentLine = BeatPrompter.preferences.highlightCurrentLine + showPageDownMarker = BeatPrompter.preferences.showPageDownMarker + highlightBeatSectionStart = BeatPrompter.preferences.highlightBeatSectionStart + beatCounterColor = BeatPrompter.preferences.beatCounterColor + pageDownMarkerColor = BeatPrompter.preferences.pageDownMarkerColor + scrollMarkerColor = BeatPrompter.preferences.scrollIndicatorColor + val mHighlightBeatSectionStartColor = BeatPrompter.preferences.beatSectionStartHighlightColor beatSectionStartHighlightColors = createStrobingHighlightColourArray(mHighlightBeatSectionStartColor) defaultCurrentLineHighlightColor = - Utils.makeHighlightColour(Preferences.currentLineHighlightColor) - defaultPageDownLineHighlightColor = Utils.makeHighlightColour(Preferences.pageDownMarkerColor) - pulse = Preferences.pulseDisplay - metronomePref = if (Preferences.mute) MetronomeContext.Off else Preferences.metronomeContext + Utils.makeHighlightColour(BeatPrompter.preferences.currentLineHighlightColor) + defaultPageDownLineHighlightColor = + Utils.makeHighlightColour(BeatPrompter.preferences.pageDownMarkerColor) + pulse = BeatPrompter.preferences.pulseDisplay + metronomePref = + if (BeatPrompter.preferences.mute) MetronomeContext.Off else BeatPrompter.preferences.metronomeContext songTitleContrastBeatCounter = Utils.makeContrastingColour(beatCounterColor) - val backgroundColor = Preferences.backgroundColor + val backgroundColor = BeatPrompter.preferences.backgroundColor val pulseColor = if (pulse) - Preferences.pulseColor + BeatPrompter.preferences.pulseColor else backgroundColor val bgR = Color.red(backgroundColor) @@ -206,7 +209,7 @@ class SongView { try { audioPlayerFactory.create(it.audioFile.file, it.volume) - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -230,7 +233,14 @@ class SongView it.seekTo(0) } - currentBeatCountRect = song.beatCounterRect + originalBeatCountRect = Rect( + song.beatCounterRect.left, + song.beatCounterRect.top, + song.beatCounterRect.right, + song.beatCounterRect.bottom + ) + currentBeatCountRect = Rect(originalBeatCountRect) + this.song = song calculateManualScrollPositions() @@ -278,7 +288,7 @@ class SongView if (time - lastCommentTime < commentDisplayTimeNanoseconds) showComment = true } - var currentY = beatCounterRect.height() + displayOffset + var currentY = beatCounterRect.height + displayOffset var currentLine = currentLine var yScrollOffset = 0 if (currentLine.scrollMode !== ScrollingMode.Beat) @@ -304,7 +314,7 @@ class SongView // In smooth mode, if we're on the last line, prevent it scrolling up more than necessary ... i.e. keep as much song onscreen as possible. if (currentLine.scrollMode === ScrollingMode.Smooth) { val remainingSongHeight = height - currentLine.songPixelPosition - val remainingScreenHeight = displaySettings.screenSize.height() - currentY + val remainingScreenHeight = displaySettings.screenSize.height - currentY yScrollOffset = min( (currentLine.measurements.lineHeight * scrollPercentage).toInt(), remainingSongHeight - remainingScreenHeight @@ -322,8 +332,8 @@ class SongView val startY = currentY var firstLineOnscreen: Line? = null - while (currentY < displaySettings.screenSize.height()) { - if (currentY > beatCounterRect.height() - currentLine.measurements.lineHeight) { + while (currentY < displaySettings.screenSize.height) { + if (currentY > beatCounterRect.height - currentLine.measurements.lineHeight) { if (firstLineOnscreen == null) firstLineOnscreen = currentLine val graphics = currentLine.getGraphics(paint) @@ -333,7 +343,12 @@ class SongView val sourceRect = currentLine.measurements.graphicRectangles[f] destinationGraphicRect.set(sourceRect) destinationGraphicRect.offset(0, currentY) - canvas.drawBitmap(graphic.bitmap, sourceRect, destinationGraphicRect, paint) + canvas.drawBitmap( + (graphic.bitmap as AndroidBitmap).androidBitmap, + sourceRect, + destinationGraphicRect, + paint + ) currentY += currentLine.measurements.graphicHeights[f] } val highlightColor = getLineHighlightColor(currentLine, time) @@ -342,7 +357,7 @@ class SongView canvas.drawRect( 0f, lineTop.toFloat(), - displaySettings.screenSize.width().toFloat(), + displaySettings.screenSize.width.toFloat(), (lineTop + currentLine.measurements.lineHeight).toFloat(), paint ) @@ -357,14 +372,19 @@ class SongView } if (smoothMode) { - val prevLine = currentLine.previousLine + val prevLine = this.currentLine.previousLine if (prevLine != null && startY > 0) { paint.alpha = (255.0 - 255.0 * scrollPercentage).toInt() currentY = startY - prevLine.measurements.lineHeight val graphics = prevLine.getGraphics(paint) for (f in graphics.indices) { val graphic = graphics[f] - canvas.drawBitmap(graphic.bitmap, 0f, currentY.toFloat(), paint) + canvas.drawBitmap( + (graphic.bitmap as AndroidBitmap).androidBitmap, + 0f, + currentY.toFloat(), + paint + ) currentY += prevLine.measurements.graphicHeights[f] } paint.alpha = 255 @@ -372,7 +392,7 @@ class SongView } } paint.color = backgroundColorLookup[100] - canvas.drawRect(beatCounterRect, paint) + canvas.drawRect(originalBeatCountRect, paint) paint.color = scrollMarkerColor if (currentLine.scrollMode === ScrollingMode.Beat && showScrollIndicator) canvas.drawRect(scrollIndicatorRect, paint) @@ -381,9 +401,9 @@ class SongView canvas.drawRect(currentBeatCountRect, paint) canvas.drawLine( 0f, - beatCounterRect.height().toFloat(), - displaySettings.screenSize.width().toFloat(), - beatCounterRect.height().toFloat(), + beatCounterRect.height.toFloat(), + displaySettings.screenSize.width.toFloat(), + beatCounterRect.height.toFloat(), paint ) if (showPageDownMarker) @@ -392,11 +412,11 @@ class SongView showSongTitle(canvas) if (showTempMessage) { if (endSongByPedalCounter == 0) - showTempMessage("$currentVolume%", 80, Color.BLACK, canvas) + showTempMessage("$currentVolume%", 80, VOLUME_TEXT_COLOR, canvas) else { val message = "Press pedal " + (SONG_END_PEDAL_PRESSES - endSongByPedalCounter) + " more times to end song." - showTempMessage(message, 20, Color.BLUE, canvas) + showTempMessage(message, 20, END_SONG_WARNING_TEXT_COLOR, canvas) } } else endSongByPedalCounter = 0 @@ -418,7 +438,7 @@ class SongView is CommentEvent -> processCommentEvent(innerEvent, time) is BeatEvent -> currentBeatCountRect = processBeatEvent(innerEvent/*, true*/) is ClickEvent -> processClickEvent() - is MIDIEvent -> processMIDIEvent(innerEvent) + is MidiEvent -> processMIDIEvent(innerEvent) is PauseEvent -> processPauseEvent(innerEvent) is LineEvent -> processLineEvent(innerEvent) is AudioEvent -> processAudioEvent(innerEvent) @@ -474,29 +494,29 @@ class SongView } private fun drawTitleScreen(canvas: Canvas) { - canvas.drawColor(Color.BLACK) - val midX = song!!.displaySettings.screenSize.width() shr 1 - val fifteenPercent = song!!.displaySettings.screenSize.height() * 0.15f + canvas.drawColor(TITLE_SCREEN_BACKGROUND_COLOR) + val midX = song!!.displaySettings.screenSize.width shr 1 + val fifteenPercent = song!!.displaySettings.screenSize.height * 0.15f var startY = - floor(((song!!.displaySettings.screenSize.height() - song!!.totalStartScreenTextHeight) / 2).toDouble()).toInt() + floor(((song!!.displaySettings.screenSize.height - song!!.totalStartScreenTextHeight) / 2).toDouble()).toInt() val nextSongSS = song!!.nextSongString if (nextSongSS != null) { - paint.color = if (skipping) Color.RED else Color.WHITE + paint.color = if (skipping) NEXT_SONG_TITLE_WHEN_SKIPPING_COLOR else NEXT_SONG_TITLE_COLOR val halfDiff = (fifteenPercent - nextSongSS.height) / 2.0f canvas.drawRect( 0f, - song!!.displaySettings.screenSize.height() - fifteenPercent, - song!!.displaySettings.screenSize.width().toFloat(), - song!!.displaySettings.screenSize.height().toFloat(), + song!!.displaySettings.screenSize.height - fifteenPercent, + song!!.displaySettings.screenSize.width.toFloat(), + song!!.displaySettings.screenSize.height.toFloat(), paint ) val nextSongY = - song!!.displaySettings.screenSize.height() - (nextSongSS.descenderOffset + halfDiff).toInt() + song!!.displaySettings.screenSize.height - (nextSongSS.descenderOffset + halfDiff).toInt() startY -= (fifteenPercent / 2.0f).toInt() paint.apply { color = nextSongSS.color - textSize = nextSongSS.fontSize * Utils.FONT_SCALING - typeface = nextSongSS.typeface + BeatPrompter.platformUtils.fontManager.setTextSize(this, nextSongSS.fontSize) + BeatPrompter.platformUtils.fontManager.setTypeface(this, nextSongSS.bold) flags = Paint.ANTI_ALIAS_FLAG } canvas.drawText( @@ -510,8 +530,8 @@ class SongView startY += ss.height paint.apply { color = ss.color - textSize = ss.fontSize * Utils.FONT_SCALING - typeface = ss.typeface + BeatPrompter.platformUtils.fontManager.setTextSize(this, ss.fontSize) + BeatPrompter.platformUtils.fontManager.setTypeface(this, ss.bold) flags = Paint.ANTI_ALIAS_FLAG } canvas.drawText( @@ -524,39 +544,39 @@ class SongView } private fun showTempMessage(message: String, textSize: Int, textColor: Int, canvas: Canvas) { - val popupMargin = 25 paint.strokeWidth = 2.0f - paint.textSize = textSize * Utils.FONT_SCALING + val textSizeFloat = textSize.toFloat() + BeatPrompter.platformUtils.fontManager.setTextSize(paint, textSizeFloat) + val textMeasurement = + BeatPrompter.platformUtils.fontManager.measure(message, paint, textSizeFloat) paint.flags = Paint.ANTI_ALIAS_FLAG - val outRect = Rect() - paint.getTextBounds(message, 0, message.length, outRect) - val textWidth = paint.measureText(message) - val textHeight = outRect.height() - val volumeControlWidth = textWidth + popupMargin * 2.0f - val volumeControlHeight = textHeight + popupMargin * 2 - val x = (song!!.displaySettings.screenSize.width() - volumeControlWidth) / 2.0f - val y = (song!!.displaySettings.screenSize.height() - volumeControlHeight) / 2 - paint.color = Color.BLACK - canvas.drawRect( + val screenMargin = song!!.displaySettings.screenSize.height * 0.05f + val boxRect = + Rect(0, 0, textMeasurement.width, textMeasurement.height).inflate(TEMP_MESSAGE_MARGIN) + val x = (song!!.displaySettings.screenSize.width - boxRect.width()) / 2.0f + val y = + song!!.displaySettings.screenSize.height - (boxRect.height() + screenMargin) + paint.color = TEMP_MESSAGE_BOX_OUTLINE_COLOR + val rect = RectF( x, - y.toFloat(), - x + volumeControlWidth, - (y + volumeControlHeight).toFloat(), + y, + x + boxRect.width(), + y + boxRect.height(), + ) + canvas.drawRect( + rect, paint ) - paint.color = Color.rgb(255, 255, 200) + paint.color = TEMP_MESSAGE_BACKGROUND_COLOR canvas.drawRect( - x + 1, - (y + 1).toFloat(), - x + (volumeControlWidth - 2), - (y + (volumeControlHeight - 2)).toFloat(), + rect.inflate(-1), paint ) paint.color = textColor canvas.drawText( message, - (song!!.displaySettings.screenSize.width() - textWidth) / 2, - ((song!!.displaySettings.screenSize.height() - textHeight) / 2 + textHeight).toFloat(), + x + TEMP_MESSAGE_MARGIN, + y + TEMP_MESSAGE_MARGIN + (textMeasurement.height - textMeasurement.descenderOffset), paint ) } @@ -564,9 +584,9 @@ class SongView private fun showPageDownMarkers(canvas: Canvas) { if (song!!.currentLine.scrollMode == ScrollingMode.Manual && songPixelPosition < song!!.scrollEndPixel) { val scrollPosition = - ((manualScrollPositions.mPageDownPosition - songPixelPosition) + song!!.displaySettings.beatCounterRect.height()).toFloat() - val screenHeight = song!!.displaySettings.screenSize.height().toFloat() - val screenWidth = song!!.displaySettings.screenSize.width().toFloat() + ((manualScrollPositions.mPageDownPosition - songPixelPosition) + song!!.displaySettings.beatCounterRect.height).toFloat() + val screenHeight = song!!.displaySettings.screenSize.height.toFloat() + val screenWidth = song!!.displaySettings.screenSize.width.toFloat() val lineSize = screenWidth / 10.0f paint.strokeWidth = (screenWidth + screenHeight) / 200.0f @@ -580,8 +600,8 @@ class SongView private fun showSongTitle(canvas: Canvas) = song?.apply { paint.apply { - textSize = songTitleHeader.fontSize * Utils.FONT_SCALING - typeface = songTitleHeader.typeface + BeatPrompter.platformUtils.fontManager.setTextSize(this, songTitleHeader.fontSize) + BeatPrompter.platformUtils.fontManager.setTypeface(this, songTitleHeader.bold) flags = Paint.ANTI_ALIAS_FLAG color = songTitleContrastBackground } @@ -619,7 +639,7 @@ class SongView private fun showComment(canvas: Canvas) { if (lastCommentEvent != null) - lastCommentEvent!!.comment.draw(canvas, paint, commentTextColor) + lastCommentEvent!!.comment.draw(canvas, paint) } private fun startToggle(playState: PlayState) { @@ -642,7 +662,7 @@ class SongView if (startState !== PlayState.Playing) { if (startState === PlayState.AtTitleScreen) if (e != null) - if (e.y > displaySettings.screenSize.height() * 0.85f) + if (e.y > displaySettings.screenSize.height * 0.85f) if (nextSong.isNotBlank()) { endSong(true) return true @@ -710,9 +730,9 @@ class SongView } else { if (screenAction == ScreenAction.Volume) { if (e != null) { - if (e.y < displaySettings.screenSize.height() * 0.5) + if (e.y < displaySettings.screenSize.height * 0.5) changeVolume(+5) - else if (e.y > displaySettings.screenSize.height() * 0.5) + else if (e.y > displaySettings.screenSize.height * 0.5) changeVolume(-5) } } else if (currentLine.scrollMode !== ScrollingMode.Manual) { @@ -793,11 +813,11 @@ class SongView private fun processBeatEvent(event: BeatEvent): Rect { nanosecondsPerBeat = Utils.nanosecondsPerBeat(event.bpm) - val beatWidth = song!!.displaySettings.screenSize.width().toDouble() / event.bpb.toDouble() + val beatWidth = song!!.displaySettings.screenSize.width.toDouble() / event.bpb.toDouble() val currentBeatCounterWidth = (beatWidth * (event.beat + 1).toDouble()).toInt() if (event.willScrollOnBeat != -1) { val thirdWidth = beatWidth / 3 - val thirdHeight = song!!.beatCounterRect.height() / 3.0 + val thirdHeight = song!!.beatCounterRect.height / 3.0 val scrollIndicatorStart = (beatWidth * event.willScrollOnBeat + thirdWidth).toInt() val scrollIndicatorEnd = (beatWidth * (event.willScrollOnBeat + 1) - thirdWidth).toInt() scrollIndicatorRect.apply { @@ -815,10 +835,10 @@ class SongView left = (currentBeatCounterWidth - beatWidth).toInt() top = 0 right = currentBeatCounterWidth - bottom = song!!.beatCounterRect.height() + bottom = song!!.beatCounterRect.height } else - song!!.beatCounterRect + Rect(originalBeatCountRect) } private fun isTrackPlaying(): Boolean = audioPlayers.values.any { it.isPlaying } @@ -828,35 +848,34 @@ class SongView private fun processPauseEvent(event: PauseEvent) { lastBeatTime = -1 - val currentBeatCounterWidth = (song!!.displaySettings.screenSize.width() + val currentBeatCounterWidth = (song!!.displaySettings.screenSize.width .toDouble() / (event.beats - 1).toDouble() * event.beat.toDouble()).toInt() currentBeatCountRect.apply { left = 0 top = 0 right = currentBeatCounterWidth - bottom = song!!.beatCounterRect.height() + bottom = song!!.beatCounterRect.height } clearScrollIndicatorRect() } - private fun clearScrollIndicatorRect() { + private fun clearScrollIndicatorRect() = scrollIndicatorRect.apply { left = -1 top = -1 right = -1 bottom = -1 } - } - private fun processMIDIEvent(event: MIDIEvent) = + private fun processMIDIEvent(event: MidiEvent) = event.messages.forEach { Midi.putMessage(it) } private fun processLineEvent(event: LineEvent) = song?.apply { currentLine = event.line - if (currentLine.scrollMode == ScrollingMode.Manual) { - currentBeatCountRect = beatCounterRect + if (currentLine.scrollMode === ScrollingMode.Manual) { + currentBeatCountRect = Rect(originalBeatCountRect) calculateManualScrollPositions() } } @@ -947,7 +966,7 @@ class SongView if (currentLine.scrollMode !== ScrollingMode.Manual) currentEvent.previousBeatEvent else null currentBeatCountRect = //val nextBeatEvent = mCurrentEvent.mNextBeatEvent - if (prevBeatEvent == null) beatCounterRect else processBeatEvent(prevBeatEvent/*, nextBeatEvent != null && !musicPlaying*/) + if (prevBeatEvent == null) Rect(originalBeatCountRect) else processBeatEvent(prevBeatEvent/*, nextBeatEvent != null && !musicPlaying*/) songStartTime = System.nanoTime() - nano if (redraw) invalidate() @@ -1368,6 +1387,16 @@ class SongView } companion object { + private const val TEMP_MESSAGE_MARGIN = 25 + + private const val NEXT_SONG_TITLE_COLOR = Color.WHITE + private const val NEXT_SONG_TITLE_WHEN_SKIPPING_COLOR = Color.RED + private const val END_SONG_WARNING_TEXT_COLOR = Color.BLUE + private const val VOLUME_TEXT_COLOR = Color.BLACK + private val TEMP_MESSAGE_BACKGROUND_COLOR = Color.rgb(255, 255, 200) + private const val TITLE_SCREEN_BACKGROUND_COLOR = Color.BLACK + private const val TEMP_MESSAGE_BOX_OUTLINE_COLOR = Color.BLACK + private val SongViewAudioAttributes = AudioAttributes .Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/AllSongsFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/AllSongsFilter.kt index 5881ad7e..2bfdd3b5 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/AllSongsFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/AllSongsFilter.kt @@ -6,7 +6,7 @@ import com.stevenfrew.beatprompter.cache.SongFile class AllSongsFilter(songs: List) : SongFilter( BeatPrompter.appResources.getString(R.string.no_tag_selected), - songs, + songs.map { it to null }, true ) { override fun equals(other: Any?): Boolean = other == null || other is AllSongsFilter diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/FolderFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/FolderFilter.kt index 33cd9408..a9611496 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/FolderFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/FolderFilter.kt @@ -5,7 +5,7 @@ import com.stevenfrew.beatprompter.cache.SongFile class FolderFilter( folderName: String, songs: List -) : SongFilter(folderName, songs, true) { +) : SongFilter(folderName, songs.map { it to null }, true) { override fun equals(other: Any?): Boolean = other is FolderFilter && name == other.name override fun hashCode(): Int = javaClass.hashCode() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFileFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFileFilter.kt index bdc390de..94cff1f5 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFileFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFileFilter.kt @@ -10,7 +10,7 @@ open class SetListFileFilter( ) : SetListFilter(setListFile.setTitle, getSongList(setListFile.setListEntries, setSongs)) { var mMissingSetListEntries = getMissingSetListEntries( setListFile.setListEntries, - songs + songs.map { it.first } ) var mWarned = mMissingSetListEntries.isEmpty() @@ -18,7 +18,13 @@ open class SetListFileFilter( private fun getSongList( setListEntries: List, songFiles: List - ): List = getMatches(setListEntries, songFiles).mapNotNull { it.second ?: it.third } + ): List> = + getMatches(setListEntries, songFiles) + .filter { it.second != null || it.third != null } + .map { + (it.second + ?: it.third)!! to it.first.variation.ifBlank { null } + } private fun getMissingSetListEntries( setListEntries: List, @@ -35,10 +41,21 @@ open class SetListFileFilter( ): List> = setListEntries.map { entry -> val exactMatch = - songFiles.firstOrNull { song: SongFile -> entry.matches(song) == SetListMatch.TitleAndArtistMatch } + songFiles.firstOrNull { song -> + songHasVariation(song, entry.variation) && entry.matches( + song + ) == SetListMatch.TitleAndArtistMatch + } val inexactMatch = - songFiles.firstOrNull { song: SongFile -> entry.matches(song) == SetListMatch.TitleMatch } + songFiles.firstOrNull { song -> + songHasVariation(song, entry.variation) && entry.matches( + song + ) == SetListMatch.TitleMatch + } Triple(entry, exactMatch, inexactMatch) } + + private fun songHasVariation(song: SongFile, variation: String): Boolean = + variation.isBlank() || song.variations.contains(variation) } } diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFilter.kt index 5265d19b..ac333380 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SetListFilter.kt @@ -4,9 +4,9 @@ import com.stevenfrew.beatprompter.cache.SongFile open class SetListFilter internal constructor( name: String, - songs: List + songs: List> ) : SongFilter(name, songs, false) { - fun containsSong(sf: SongFile): Boolean = songs.contains(sf) + fun containsSong(sf: SongFile): Boolean = songs.any { it.first == sf } override fun equals(other: Any?): Boolean = other is SetListFilter && name == other.name override fun hashCode(): Int = javaClass.hashCode() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SongFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SongFilter.kt index 052682df..5d631f94 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SongFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/SongFilter.kt @@ -4,7 +4,7 @@ import com.stevenfrew.beatprompter.cache.SongFile abstract class SongFilter internal constructor( name: String, - songs: List, + songs: List>, canSort: Boolean ) : Filter(name, canSort) { val songs = songs.toMutableList() diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TagFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TagFilter.kt index 8d6b71a5..e931b3cf 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TagFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TagFilter.kt @@ -2,9 +2,8 @@ package com.stevenfrew.beatprompter.ui.filter import com.stevenfrew.beatprompter.cache.SongFile -class TagFilter(tag: String, songs: MutableList) : SongFilter(tag, songs, true) { - +class TagFilter(tag: String, songs: MutableList) : + SongFilter(tag, songs.map { it to null }, true) { override fun equals(other: Any?): Boolean = other is TagFilter && name == other.name - override fun hashCode(): Int = javaClass.hashCode() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TemporarySetListFilter.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TemporarySetListFilter.kt index 4fcd46fa..878401d9 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TemporarySetListFilter.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/filter/TemporarySetListFilter.kt @@ -7,7 +7,7 @@ class TemporarySetListFilter( setListFile: SetListFile, songs: List ) : SetListFileFilter(setListFile, songs) { - fun addSong(sf: SongFile) = songs.add(sf) + fun addSong(sf: SongFile) = songs.add(sf to null) override fun equals(other: Any?): Boolean = other != null && other is TemporarySetListFilter override fun hashCode(): Int = javaClass.hashCode() diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/CloudPathPreference.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/CloudPathPreference.kt index 360b752a..7dbdc4a8 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/CloudPathPreference.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/CloudPathPreference.kt @@ -6,17 +6,16 @@ import android.widget.TextView import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.stevenfrew.beatprompter.BeatPrompter -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R class CloudPathPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs) { override fun onBindViewHolder(view: PreferenceViewHolder) { super.onBindViewHolder(view) val textView = view.findViewById(android.R.id.summary) as TextView - val path = Preferences.cloudPath + val path = BeatPrompter.preferences.cloudPath textView.text = if (path.isBlank()) BeatPrompter.appResources.getString(R.string.no_cloud_folder_currently_set) else - Preferences.cloudDisplayPath + BeatPrompter.preferences.cloudDisplayPath } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FileSettingsFragment.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FileSettingsFragment.kt index a5034057..9ba8f361 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FileSettingsFragment.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FileSettingsFragment.kt @@ -11,7 +11,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.stevenfrew.beatprompter.Preferences +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.events.EventRouter import com.stevenfrew.beatprompter.events.Events @@ -45,7 +45,7 @@ class FileSettingsFragment : PreferenceFragmentCompat(), // Load the preferences from an XML resource addPreferencesFromResource(R.xml.filepreferences) - Preferences.registerOnSharedPreferenceChangeListener(this) + BeatPrompter.preferences.registerOnSharedPreferenceChangeListener(this) val clearCachePrefName = getString(R.string.pref_clearCache_key) val clearCachePref = findPreference(clearCachePrefName) @@ -65,22 +65,22 @@ class FileSettingsFragment : PreferenceFragmentCompat(), val cloudPref = findPreference(cloudPrefName) cloudPref?.setOnPreferenceChangeListener { _, value -> EventRouter.sendEventToCache(Events.CLEAR_CACHE, true) - Preferences.storageSystem = StorageType.valueOf(value.toString()) - Preferences.cloudPath = "" - Preferences.cloudDisplayPath = "" + BeatPrompter.preferences.storageSystem = StorageType.valueOf(value.toString()) + BeatPrompter.preferences.cloudPath = "" + BeatPrompter.preferences.cloudDisplayPath = "" cloudPref.forceUpdate() true } } override fun onDestroy() { - Preferences.unregisterOnSharedPreferenceChangeListener(this) + BeatPrompter.preferences.unregisterOnSharedPreferenceChangeListener(this) EventRouter.setSettingsEventHandler(null) super.onDestroy() } private fun onCloudPathChanged(newValue: Any?) { - val displayPath = Preferences.cloudDisplayPath + val displayPath = BeatPrompter.preferences.cloudDisplayPath val cloudPathPrefName = getString(R.string.pref_cloudPath_key) val cloudPathPref = findPreference(cloudPathPrefName) @@ -91,7 +91,7 @@ class FileSettingsFragment : PreferenceFragmentCompat(), } private fun setCloudPath() { - val cloudType = Preferences.storageSystem + val cloudType = BeatPrompter.preferences.storageSystem if (cloudType !== StorageType.Demo) { val cs = Storage.getInstance(cloudType, this) val progressDialog = @@ -104,8 +104,8 @@ class FileSettingsFragment : PreferenceFragmentCompat(), } cs.selectFolder(requireActivity(), object : FolderSelectionListener { override fun onFolderSelected(folderInfo: FolderInfo) { - Preferences.cloudDisplayPath = folderInfo.displayPath - Preferences.cloudPath = folderInfo.id + BeatPrompter.preferences.cloudDisplayPath = folderInfo.displayPath + BeatPrompter.preferences.cloudPath = folderInfo.id } override fun onFolderSelectedError(t: Throwable, context: Context) = diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreference.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreference.kt index 3eb27ea9..a4afd89f 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreference.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreference.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import androidx.preference.DialogPreference +import com.stevenfrew.beatprompter.BeatPrompter class FontSizePreference( context: Context?, @@ -34,9 +35,8 @@ class FontSizePreference( } companion object { - // Set by onCreate() in SongListActivity.java - var FONT_SIZE_OFFSET: Int = 0 - var FONT_SIZE_MAX: Int = 0 - var FONT_SIZE_MIN: Int = 0 + val FONT_SIZE_OFFSET: Int = BeatPrompter.platformUtils.fontManager.minimumFontSize.toInt() + val FONT_SIZE_MAX: Int = + (BeatPrompter.platformUtils.fontManager.maximumFontSize - BeatPrompter.platformUtils.fontManager.minimumFontSize).toInt() } } diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreferenceDialog.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreferenceDialog.kt index ec78d184..0a8ec155 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreferenceDialog.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/FontSizePreferenceDialog.kt @@ -6,8 +6,8 @@ import android.view.View import android.widget.SeekBar import android.widget.TextView import androidx.preference.PreferenceDialogFragmentCompat +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.R -import com.stevenfrew.beatprompter.util.Utils import java.util.Locale @@ -40,7 +40,10 @@ class FontSizePreferenceDialog : PreferenceDialogFragmentCompat(), SeekBar.OnSee currentValue = progress val size = currentValue + FontSizePreference.FONT_SIZE_OFFSET textView!!.text = String.format(Locale.getDefault(), "%d", size) - textView!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, size * Utils.FONT_SCALING) + textView!!.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + size * BeatPrompter.platformUtils.fontManager.fontScaling + ) } override fun onStartTrackingTouch(seekBar: SeekBar) {} diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/ImageListPreference.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/ImageListPreference.kt index e76bd297..0b75366f 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/ImageListPreference.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/ImageListPreference.kt @@ -8,7 +8,6 @@ import androidx.preference.PreferenceViewHolder import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.stevenfrew.beatprompter.BeatPrompter -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R class ImageListPreference(private val context: Context, attrs: AttributeSet) : @@ -96,7 +95,7 @@ class ImageListPreference(private val context: Context, attrs: AttributeSet) : override fun onBindViewHolder(view: PreferenceViewHolder) { super.onBindViewHolder(view) val imageView = view.findViewById(R.id.iconImageView) as ImageView - val iconResource = when (Preferences.getStringPreference(key, "")) { + val iconResource = when (BeatPrompter.preferences.getStringPreference(key, "")) { BeatPrompter.appResources.getString(R.string.googleDriveValue) -> R.drawable.ic_google_drive BeatPrompter.appResources.getString(R.string.dropboxValue) -> R.drawable.ic_dropbox BeatPrompter.appResources.getString(R.string.oneDriveValue) -> R.drawable.ic_onedrive diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/MidiConnectionsPreference.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/MidiConnectionsPreference.kt index 145b41ea..302855c7 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/MidiConnectionsPreference.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/MidiConnectionsPreference.kt @@ -7,7 +7,6 @@ import android.widget.ImageView import androidx.preference.MultiSelectListPreference import androidx.preference.PreferenceViewHolder import com.stevenfrew.beatprompter.BeatPrompter -import com.stevenfrew.beatprompter.Preferences import com.stevenfrew.beatprompter.R class MidiConnectionsPreference(context: Context, attrs: AttributeSet) : @@ -18,7 +17,7 @@ class MidiConnectionsPreference(context: Context, attrs: AttributeSet) : val usbImageView = view.findViewById(R.id.midiUsbOnTheGoIconImageView) as ImageView val nativeImageView = view.findViewById(R.id.midiNativeIconImageView) as ImageView val bluetoothImageView = view.findViewById(R.id.midiBluetoothIconImageView) as ImageView - val currentPrefValue = Preferences.getStringSetPreference( + val currentPrefValue = BeatPrompter.preferences.getStringSetPreference( key, BeatPrompter.appResources.getStringSet(R.array.pref_midiConnectionTypes_defaultValues) ) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/SettingsFragment.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/SettingsFragment.kt index 8a77ed8e..3ce618bc 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/SettingsFragment.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/ui/pref/SettingsFragment.kt @@ -8,7 +8,7 @@ import android.os.Handler import android.os.Message import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.stevenfrew.beatprompter.Preferences +import com.stevenfrew.beatprompter.BeatPrompter import com.stevenfrew.beatprompter.R import com.stevenfrew.beatprompter.events.EventRouter @@ -39,7 +39,7 @@ class SettingsFragment : PreferenceFragmentCompat(), val darkModePrefName = getString(R.string.pref_darkMode_key) val darkModePref = findPreference(darkModePrefName) darkModePref?.setOnPreferenceClickListener { - Preferences.darkMode = !Preferences.darkMode + BeatPrompter.preferences.darkMode = !BeatPrompter.preferences.darkMode true } } @@ -56,13 +56,13 @@ class SettingsFragment : PreferenceFragmentCompat(), } override fun onDestroy() { - Preferences.unregisterOnSharedPreferenceChangeListener(this) + BeatPrompter.preferences.unregisterOnSharedPreferenceChangeListener(this) EventRouter.setSettingsEventHandler(null) super.onDestroy() } private fun onCloudPathChanged(newValue: Any?) { - val displayPath = Preferences.cloudDisplayPath + val displayPath = BeatPrompter.preferences.cloudDisplayPath val cloudPathPrefName = getString(R.string.pref_cloudPath_key) val cloudPathPref = findPreference(cloudPathPrefName) diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/AndroidUtils.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/AndroidUtils.kt new file mode 100644 index 00000000..c4ae71ec --- /dev/null +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/AndroidUtils.kt @@ -0,0 +1,22 @@ +package com.stevenfrew.beatprompter.util + +import android.content.res.Resources +import android.graphics.Color +import com.stevenfrew.beatprompter.R +import com.stevenfrew.beatprompter.graphics.bitmaps.AndroidBitmapFactory +import com.stevenfrew.beatprompter.graphics.fonts.AndroidFontManager +import com.stevenfrew.beatprompter.graphics.fonts.FontManager + +class AndroidUtils(resources: Resources) : PlatformUtils { + override val fontManager: FontManager + override val bitmapFactory = AndroidBitmapFactory + + override fun parseColor(colorString: String): Int = Color.parseColor(colorString) + + init { + val minimumFontSize = resources.getString(R.string.fontSizeMin).toFloat() + val maximumFontSize = resources.getString(R.string.fontSizeMax).toFloat() + fontManager = + AndroidFontManager(minimumFontSize, maximumFontSize, resources.displayMetrics.density) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/ApplicationContextResources.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/ApplicationContextResources.kt new file mode 100644 index 00000000..60043cb9 --- /dev/null +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/ApplicationContextResources.kt @@ -0,0 +1,13 @@ +package com.stevenfrew.beatprompter.util + +import android.content.res.AssetManager +import android.content.res.Resources + +class ApplicationContextResources(private val resources: Resources) : GlobalAppResources { + override fun getString(resID: Int): String = resources.getString(resID) + override fun getString(resID: Int, vararg args: Any): String = + resources.getString(resID, *args) + + override fun getStringSet(resID: Int): Set = resources.getStringArray(resID).toSet() + override val assetManager: AssetManager = resources.assets +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Extensions.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Extensions.kt index 5bb8ba1a..eab84a7b 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Extensions.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Extensions.kt @@ -1,5 +1,7 @@ package com.stevenfrew.beatprompter.util +import android.graphics.Rect +import android.graphics.RectF import android.hardware.usb.UsbConstants.USB_ENDPOINT_XFER_BULK import android.hardware.usb.UsbDevice import android.hardware.usb.UsbInterface @@ -19,6 +21,12 @@ fun String.splitAndTrim(separator: String): List = */ fun String.removeControlCharacters(): String = replace("\uFEFF", "") +fun RectF.inflate(amount: Int): RectF = + RectF(this.left - amount, this.top - amount, this.right + amount, this.bottom + amount) + +fun Rect.inflate(amount: Int): Rect = + Rect(this.left - amount, this.top - amount, this.right + amount, this.bottom + amount) + /** * Replaces weird apostrophe with usual apostrophe ... prevents failed matches based on apostrophe difference. */ diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/GlobalAppResources.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/GlobalAppResources.kt index de6c5be5..886be1e6 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/GlobalAppResources.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/GlobalAppResources.kt @@ -1,14 +1,11 @@ package com.stevenfrew.beatprompter.util -import android.content.SharedPreferences import android.content.res.AssetManager interface GlobalAppResources { fun getString(resID: Int): String fun getString(resID: Int, vararg args: Any): String fun getStringSet(resID: Int): Set - val preferences: SharedPreferences - val privatePreferences: SharedPreferences val assetManager: AssetManager } diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/PlatformUtils.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/PlatformUtils.kt new file mode 100644 index 00000000..3d6e5f60 --- /dev/null +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/PlatformUtils.kt @@ -0,0 +1,10 @@ +package com.stevenfrew.beatprompter.util + +import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapFactory +import com.stevenfrew.beatprompter.graphics.fonts.FontManager + +interface PlatformUtils { + val fontManager: FontManager + val bitmapFactory: BitmapFactory + fun parseColor(colorString: String): Int +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Utils.kt b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Utils.kt index 60d72e53..cd8f8349 100644 --- a/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Utils.kt +++ b/app/src/main/kotlin/com/stevenfrew/beatprompter/util/Utils.kt @@ -21,11 +21,6 @@ object Utils { // Size of a "long", in bytes. internal const val LONG_BUFFER_SIZE = 8 - // Set by onCreate() in SongListActivity.java - internal var MAXIMUM_FONT_SIZE: Int = 0 - internal var MINIMUM_FONT_SIZE: Int = 0 - var FONT_SCALING: Float = 0.toFloat() - // Special token. const val TRACK_AUDIO_LENGTH_VALUE = -93781472L private val splitters = arrayOf(" ", "-") diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a9561326..19e0cfc6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,251 +1,251 @@  - BeatPrompter - Von ... - Playback - Audio Track-Lautstärke ist nicht eine Zahl zwischen 0 und 100. - Schlagen Scrolling - Durch Künstler - Nach Datum - Mit dem Titel - Stornieren - Kann nicht Audiodatei finden: %1$s - Zentrum - Überprüfung %1$s - Wählen Sie Sync-Ordner - Cache leeren - Konnte nicht "%1$s" als Farbe analysieren. - Farben - Löschen %1$s - Dokumentation finden Sie unter - Der Höchstwert ist %1$d, fand Wert war %2$.2f. - Der Mindestwert ist %1$d, fand Wert war %2$.2f. - Konnte nicht "%1$s" als Numerisch-Wert analysieren. - Synchronisieren %1$s - Synchronisieren Dateien - Konnte nicht "%1$s" als Wert für die Dauer analysieren (Verwendung mm:ss oder einfach nur Sekunden). - Die Datei "%1$s" nicht gefunden! - Schriftgrößen - Die Mindestschriftgröße bevorzugt ist größer als die maximale Schriftgröße . Verwendung mindestens für alle. - Force-refresh (Song-Dateien) - Der Höchstwert ist %1$d, fand Wert war %2$d. - Der Mindestwert ist %1$d, fand Wert war %2$d. - Konnte nicht "%1$s" als Integer-Wert analysieren. - Springen - Links - Zeile %1$d länger als %2$d Zeichen. Kürzen … - das - Während Count-in - Auf - Aus - Konnte nicht Audiodatei finden. - Nein {title} in Song-Datei "%1$s" gefunden - Kein Playback - Alle Lieder - Ein Wert wurde bereits definiert für {%1$s} - … Und %1$d andere Fehler - Insgesamt {pause} Zeit überschreitet bezeichnet Song Zeit. Deaktivieren des glatten Bildlaufmodus. - Spielen - Spielen Optionen - Spielen … - Hintergrundfarbe - Die Farbe des Beatcounter Leiste am oberen Rand des Bildschirms. - Beatcounter Farbe - Wenn Schlägen-pro-takt nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. - SPT - Standard Schläge-pro-Takt - Wenn Bars pro Zeile nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. - TPZ - Standard Takt-pro-Zeile - Wenn ein Tempo nicht durch eine Datei definiert ist, wird diese Geschwindigkeit verwendet werden. - SPM - Standardtempo - Akkorde Farbe - Wenn die Anzahl der "Zähl-in" Takt nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. Gilt nur für Songs zu schlagen-Scrolling. - takt - Standardanzahl in - Wenn ein Song im "Smooth Scrolling-Modus" angezeigt wird, dann erzeugt diese Einstellung eine automatische Pause zu Beginn des Liedes, in der gesamten Songlänge berücksichtigt. - Sekunden - Standard anfängliche Pause - Macht alle Linien die gleiche Höhe - Feste Zeilenhöhe - Markieren Sie die Farbe anzuwenden lyrischen Text hervorgehoben. - Markierungsfarbe - Immer verwendet Standardfarben. - Ignorieren Farben in Dateien - Linie Begründung - Linie begründung - Songtexte Farbe - Der maximale Prozentsatz der maximalen Zeilenhöhe, die die Texte beschäftigen wird. - Maximale Text Höhe - Die größte Schriftgröße, die verwendet wird in Beat-Scrolling-Modus . - Maximale Schriftgröße (Beat-Scrolling) - Metronom-Klick - Erzeugt ein Klickgeräusch in der Zeit mit dem Beat. Gilt nur für Songs zu schlagen-Scrolling. - Metronom-klick - Die kleinste Schriftgröße, die verwendet wird - Mindestschriftgröße - Die minimale Anzahl von Zeilen, sollte zu jeder Zeit sichtbar sein. - Zeilen - Mindest sichtbare Zeilen - Der Hintergrund wird diese Farbe mit der Zeit im Rhythmus pulsieren. - Hintergrund Puls Farbe - Der Hintergrund wird mit der Zeit im Rhythmus pulsieren. - Hintergrund Puls - Scrolling Stil - Scrolling stil - Zeigt das Scrolling Stil des Songs als Symbol in der Hauptliste . - Anzeigen Scroll-Stil-Ikonen - In der Hauptsongliste, zeigen ein Symbol, das anzeigt, dass ein Song ein Playback hat. - Anzeigen Musik-Ikone - Im Beatcounter Abschnitt, markieren Sie den Beat, der die Anzeige scrollen. - Zeigen Sie Scroll-Anzeige. - Der Abstand vom Beginn des Liedes, in Millisekunden, an dem der Träger Audiospur beginnen. - ms - Audiospur Start Offset - Rechte - Scrolling-Modus - Scrollbar Versatz größer ist als oder gleich der Anzahl der Schläge in der Bar. Beim Zurücksetzen auf Null. - Einstellungen … - Glatt - Sanftes Rollen - Song-Optionen - Sortieren Lieder … - Sortieren … - TAP ZWEIMAL ZU BEGINNEN - Einstellungen - Erstellt von - Die größte Schriftgröße , die in glattem Scrolling-Modus verwendet werden soll. - Maximale Schriftgröße (smooth scrolling) - Verwendet die Beat-Scrolling die Schriftgröße für alle Songs. - Verwenden Sie immer die Beat-Scrolling die Schriftgröße. - Die kleinste Schriftgröße , die in glattem Scrolling-Modus verwendet werden soll. - Mindestschriftgröße (smooth scrolling) - Vollversion kaufen - Verbunden mit Bandleader - angeschlossen - Bitte kaufen BeatPrompter - Jeder Akkord Text, der nicht als gültiger Akkord erkannt wird in dieser Farbe angezeigt. - Farbe der nicht- Akkord Anmerkungen - Um nur die Kommentare zu sehen , die für Sie auf den Song Einführung Bildschirm gemeint sind , geben Sie Ihren Namen(n) hier. - Benutzerdefinierte Kommentare User - Es sei denn, in der Datei angegeben , dies ist die Standardvolumen , das Playbacks zu spielen. - Standard-Track-Lautstärke - Kontrollen während der Durchführung - Welche Kontrollen sollten zur Verfügung stehen , während ein Song abgespielt wird? - Kontrollen während der Durchführung - Nichts - Scrollen, Anhalten, Neustart - Volumen - Vielen Dank! Sie sind offiziell eine nette Person! :) - Speichert Daten auf externen Speicher (SD-Karte, etc.). Dies zu ändern, wird den Cache löschen. - Verwenden Externe Speicher - Verwenden Sie Benutzerdefinierte Lagerort - Wenn diese Option ausgewählt , werden Song-Daten in den benutzerdefinierten Ort gespeichert werden. Dies zu ändern, wird den Cache löschen. - Wo soll Lied und Audio-Dateien BeatPrompter Store heruntergeladen ? Dies zu ändern, wird den Cache löschen. - Lagerraum - Verwenden Interner Speicher - Manuelle Scrolling - Wenn ein Song über Beat-Timings verfügt, werden hierdurch MIDI-Taktsignale gesendet. - Senden Sie MIDI-Clock-Signale - In der Standardeinstellung Songs spielen immer im manuellen Scrollen Modus ohne Playback. - Manueller Modus - Google-API nicht angeschlossen. - Die Zeit, die ein in-Song Kommentar Bildschirm bleiben. - Kommentar Anzeigezeit - Kommentar Textfarbe - Markieren Sie Farbe auf die aktuelle Zeile anzuwenden. - Aktuelle Zeile markieren Farbe - Markiert die aktuelle Zeile mit dem gewählten Highlight-Farbe. - Markieren Sie aktuelle Zeile - Zeigt die Song-Akkorde über den Text an der richtigen Stelle. - Zeige Akkorde - Zeigt den ersten Schlag im Takt Indikatorabschnitt. - Zeigen ersten Schlag. - Zeigt den aktuellen Songtitel über den oberen Teil der Beatcounter Abschnitt. - Zeigen Songtitel. - Song-Anzeigeoptionen - Songliste Optionen - Force-Refresh-Set-Liste - Unbekannt Songs In Set-Liste - Diese Set-Liste enthält %1$d unbekannte Songs, darunter: - Kein {set} in Set-Datei \"%1$s\" gefunden. - Immer - Nie - Set Nur Listen - Wiedergabe automatisch Next Song - Wiedergabe automatisch Next Song - Sollte der nächste Song automatisch gestartet werden, wenn der aktuelle Song endet? - Zeigt die Songliste in größeren Text. - Großer Text - Sonstiges - Zugriff %1$s - Zur temporären Setliste hinzufügen - Schlecht geformtes Tag. - Der MIDI-Kanal muss der letzte Parameter sein - Der Kanalspezifiziererwert darf keine Unterstriche enthalten. - Temporäre Setliste löschen - BeatPrompter liest alle Ihre Songdateien, Audiodateien, Setlisten und mehr aus der Cloud oder dem lokalen Speicher. Derzeit werden Google Drive, Dropbox oder Microsoft OneDrive unterstützt. - Verbindung zum Führer herstellen - Voreinstellung MIDI Alias Set - Tag ist leer. - Force refresh MIDI-Alias-Datei - Die MIDI-Kanalwerte müssen zwischen 1 und 16 liegen. - Ungültiger MIDI-Alias-Argumentindex. - Mit ein wenig Änderung an Ihren Song-Dateien, wird BeatPrompter halten die Anzeige perfekt in der Zeit mit dem Beat. Nie zu schnell, nie zu langsam, und immer schön und groß und lesbar! - Geladen: - Nur das untere Halbbyte eines Wertes kann mit dem Kanalspezifizierer zusammengeführt werden. - Immer - Nie - Im Pausenmodus oder im Titelbildschirm - Wenn pausiert, am Titelbildschirm oder auf der letzten Zeile des Songs - Wenn auf Titelbildschirm - MIDI-Alias-Dateifehler - MIDI Aliases - MIDI Aliases - MIDI-Alias-Nachricht mit mehr als zwei Teilen gefunden. - Ein \"midi_alias\" -Tag enthält mehr als zwei Teile. - MIDI-Alias mit keinem Namen gefunden. - Diese MIDI-Nachricht kann nicht geparst werden. - MIDI Event Offset setzt es vor dem Beginn des Songs. - MIDI-Tags können maximal ein Semikolon-Zeichen enthalten. - Ein Aliaswert darf nicht mehrere Unterstriche enthalten. - MIDI-Offset sollte ein numerischer Wert oder ein Beat-Offset sein. - Die Datei \"%1$s\" ist keine gültige MIDI-Aliasing-Datei. - Nicht genügend Parameter für den MIDI-Befehl. - BeatPrompter zeigt nicht nur Texte und Akkorde. Es kann Playbacks abspielen und externe MIDI-Geräte steuern (und darauf reagieren). - Powerwash komplett. - Was ist der Standard-MIDI-Kanal zu schreiben, wenn nicht anders angegeben? - Standardausgang MIDI-Kanal - Aktive eingehende MIDI-Kanäle - Welche MIDI-Kanäle sollte TunePrompter hören? - MIDI-Trigger während der Song-Anzeige - Können MIDI-Trigger-Nachrichten den aktuellen Song unterbrechen? - Auslösesicherung - Löscht alle heruntergeladenen Dateien und alle gespeicherten Anmeldeinformationen trüben. - Powerwash - Die Farbe des Markers zeigt que Wenn die Anzeige scrollen. - Scroll Marker Farbe - Output MIDI Triggernachricht - Sollte der Trigger MIDI-Meldung ausgegeben werden, wenn der Song beginnt? - Output MIDI-Trigger - Die Verarbeitung Song: %1$s - Immer - Wenn manuell gestartet - Nie - MIDI-Alias-Datei-Fehler anzeigen - Programmwechsel-Trigger-Tags müssen zwischen einem und vier Werten enthalten. - Songauswahl-Trigger-Tags müssen nur einen Wert enthalten. - Vorübergehend - Werte, die Unterstriche enthalten, müssen hexadezimal sein. - Tag "%1$s" stimmt nicht mit bekannten MIDI-Befehlen und Aliasen überein. - MIDI-Parameter müssen zwischen 0 und 127 liegen. - Willkommen bei BeatPrompter - BeatPrompter ist eine Lyrics und Akkorde prompter App, die den Schwerpunkt auf Timing und Lesbarkeit setzt. - Obwohl Sie es im Hochformat verwenden können, funktioniert BeatPrompter am besten im Querformat. - " Die maximale MIDI-Event-Offset ist 16 Schläge oder 10 Sekunden." - Nur MIDI-Tags, die am oder nach dem ersten Song Linie sind, können Verschiebungen haben. - {t:BeatPrompter Demo-Song}\n + BeatPrompter + Von ... + Playback + Audio Track-Lautstärke ist nicht eine Zahl zwischen 0 und 100. + Schlagen Scrolling + Durch Künstler + Nach Datum + Mit dem Titel + Stornieren + Kann nicht Audiodatei finden: %1$s + Zentrum + Überprüfung %1$s + Wählen Sie Sync-Ordner + Cache leeren + Konnte nicht "%1$s" als Farbe analysieren. + Farben + Löschen %1$s + Dokumentation finden Sie unter + Der Höchstwert ist %1$d, fand Wert war %2$.2f. + Der Mindestwert ist %1$d, fand Wert war %2$.2f. + Konnte nicht "%1$s" als Numerisch-Wert analysieren. + Synchronisieren %1$s + Synchronisieren Dateien + Konnte nicht "%1$s" als Wert für die Dauer analysieren (Verwendung mm:ss oder einfach nur Sekunden). + Die Datei "%1$s" nicht gefunden! + Schriftgrößen + Die Mindestschriftgröße bevorzugt ist größer als die maximale Schriftgröße . Verwendung mindestens für alle. + Force-refresh (Song-Dateien) + Der Höchstwert ist %1$d, fand Wert war %2$d. + Der Mindestwert ist %1$d, fand Wert war %2$d. + Konnte nicht "%1$s" als Integer-Wert analysieren. + Springen + Links + Zeile %1$d länger als %2$d Zeichen. Kürzen … + das + Während Count-in + Auf + Aus + Konnte nicht Audiodatei finden. + Nein {title} in Song-Datei "%1$s" gefunden + Kein Playback + Alle Lieder + Ein Wert wurde bereits definiert für {%1$s} + … Und %1$d andere Fehler + Insgesamt {pause} Zeit überschreitet bezeichnet Song Zeit. Deaktivieren des glatten Bildlaufmodus. + Spielen + Spielen Optionen + Spielen … + Hintergrundfarbe + Die Farbe des Beatcounter Leiste am oberen Rand des Bildschirms. + Beatcounter Farbe + Wenn Schlägen-pro-takt nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. + SPT + Standard Schläge-pro-Takt + Wenn Bars pro Zeile nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. + TPZ + Standard Takt-pro-Zeile + Wenn ein Tempo nicht durch eine Datei definiert ist, wird diese Geschwindigkeit verwendet werden. + SPM + Standardtempo + Akkorde Farbe + Wenn die Anzahl der "Zähl-in" Takt nicht durch eine Datei definiert ist, wird dieser Wert verwendet werden. Gilt nur für Songs zu schlagen-Scrolling. + takt + Standardanzahl in + Wenn ein Song im "Smooth Scrolling-Modus" angezeigt wird, dann erzeugt diese Einstellung eine automatische Pause zu Beginn des Liedes, in der gesamten Songlänge berücksichtigt. + Sekunden + Standard anfängliche Pause + Macht alle Linien die gleiche Höhe + Feste Zeilenhöhe + Markieren Sie die Farbe anzuwenden lyrischen Text hervorgehoben. + Markierungsfarbe + Immer verwendet Standardfarben. + Ignorieren Farben in Dateien + Linie Begründung + Linie begründung + Songtexte Farbe + Der maximale Prozentsatz der maximalen Zeilenhöhe, die die Texte beschäftigen wird. + Maximale Text Höhe + Die größte Schriftgröße, die verwendet wird in Beat-Scrolling-Modus . + Maximale Schriftgröße (Beat-Scrolling) + Metronom-Klick + Erzeugt ein Klickgeräusch in der Zeit mit dem Beat. Gilt nur für Songs zu schlagen-Scrolling. + Metronom-klick + Die kleinste Schriftgröße, die verwendet wird + Mindestschriftgröße + Die minimale Anzahl von Zeilen, sollte zu jeder Zeit sichtbar sein. + Zeilen + Mindest sichtbare Zeilen + Der Hintergrund wird diese Farbe mit der Zeit im Rhythmus pulsieren. + Hintergrund Puls Farbe + Der Hintergrund wird mit der Zeit im Rhythmus pulsieren. + Hintergrund Puls + Scrolling Stil + Scrolling stil + Zeigt das Scrolling Stil des Songs als Symbol in der Hauptliste . + Anzeigen Scroll-Stil-Ikonen + In der Hauptsongliste, zeigen ein Symbol, das anzeigt, dass ein Song ein Playback hat. + Anzeigen Musik-Ikone + Im Beatcounter Abschnitt, markieren Sie den Beat, der die Anzeige scrollen. + Zeigen Sie Scroll-Anzeige. + Der Abstand vom Beginn des Liedes, in Millisekunden, an dem der Träger Audiospur beginnen. + ms + Audiospur Start Offset + Rechte + Scrolling-Modus + Scrollbar Versatz größer ist als oder gleich der Anzahl der Schläge in der Bar. Beim Zurücksetzen auf Null. + Einstellungen … + Glatt + Sanftes Rollen + Song-Optionen + Sortieren Lieder … + Sortieren … + TAP ZWEIMAL ZU BEGINNEN + Einstellungen + Erstellt von + Die größte Schriftgröße , die in glattem Scrolling-Modus verwendet werden soll. + Maximale Schriftgröße (smooth scrolling) + Verwendet die Beat-Scrolling die Schriftgröße für alle Songs. + Verwenden Sie immer die Beat-Scrolling die Schriftgröße. + Die kleinste Schriftgröße , die in glattem Scrolling-Modus verwendet werden soll. + Mindestschriftgröße (smooth scrolling) + Vollversion kaufen + Verbunden mit Bandleader + angeschlossen + Bitte kaufen BeatPrompter + Jeder Akkord Text, der nicht als gültiger Akkord erkannt wird in dieser Farbe angezeigt. + Farbe der nicht- Akkord Anmerkungen + Um nur die Kommentare zu sehen , die für Sie auf den Song Einführung Bildschirm gemeint sind , geben Sie Ihren Namen(n) hier. + Benutzerdefinierte Kommentare User + Es sei denn, in der Datei angegeben , dies ist die Standardvolumen , das Playbacks zu spielen. + Standard-Track-Lautstärke + Kontrollen während der Durchführung + Welche Kontrollen sollten zur Verfügung stehen , während ein Song abgespielt wird? + Kontrollen während der Durchführung + Nichts + Scrollen, Anhalten, Neustart + Volumen + Vielen Dank! Sie sind offiziell eine nette Person! :) + Speichert Daten auf externen Speicher (SD-Karte, etc.). Dies zu ändern, wird den Cache löschen. + Verwenden Externe Speicher + Verwenden Sie Benutzerdefinierte Lagerort + Wenn diese Option ausgewählt , werden Song-Daten in den benutzerdefinierten Ort gespeichert werden. Dies zu ändern, wird den Cache löschen. + Wo soll Lied und Audio-Dateien BeatPrompter Store heruntergeladen ? Dies zu ändern, wird den Cache löschen. + Lagerraum + Verwenden Interner Speicher + Manuelle Scrolling + Wenn ein Song über Beat-Timings verfügt, werden hierdurch MIDI-Taktsignale gesendet. + Senden Sie MIDI-Clock-Signale + In der Standardeinstellung Songs spielen immer im manuellen Scrollen Modus ohne Playback. + Manueller Modus + Google-API nicht angeschlossen. + Die Zeit, die ein in-Song Kommentar Bildschirm bleiben. + Kommentar Anzeigezeit + Kommentar Textfarbe + Markieren Sie Farbe auf die aktuelle Zeile anzuwenden. + Aktuelle Zeile markieren Farbe + Markiert die aktuelle Zeile mit dem gewählten Highlight-Farbe. + Markieren Sie aktuelle Zeile + Zeigt die Song-Akkorde über den Text an der richtigen Stelle. + Zeige Akkorde + Zeigt den ersten Schlag im Takt Indikatorabschnitt. + Zeigen ersten Schlag. + Zeigt den aktuellen Songtitel über den oberen Teil der Beatcounter Abschnitt. + Zeigen Songtitel. + Song-Anzeigeoptionen + Songliste Optionen + Force-Refresh-Set-Liste + Unbekannt Songs In Set-Liste + Diese Set-Liste enthält %1$d unbekannte Songs, darunter: + Kein {set} in Set-Datei \"%1$s\" gefunden. + Immer + Nie + Set Nur Listen + Wiedergabe automatisch Next Song + Wiedergabe automatisch Next Song + Sollte der nächste Song automatisch gestartet werden, wenn der aktuelle Song endet? + Zeigt die Songliste in größeren Text. + Großer Text + Sonstiges + Zugriff %1$s + Zur temporären Setliste hinzufügen + Schlecht geformtes Tag. + Der MIDI-Kanal muss der letzte Parameter sein + Der Kanalspezifiziererwert darf keine Unterstriche enthalten. + Temporäre Setliste löschen + BeatPrompter liest alle Ihre Songdateien, Audiodateien, Setlisten und mehr aus der Cloud oder dem lokalen Speicher. Derzeit werden Google Drive, Dropbox oder Microsoft OneDrive unterstützt. + Verbindung zum Führer herstellen + Voreinstellung MIDI Alias Set + Tag ist leer. + Force refresh MIDI-Alias-Datei + Die MIDI-Kanalwerte müssen zwischen 1 und 16 liegen. + Ungültiger MIDI-Alias-Argumentindex. + Mit ein wenig Änderung an Ihren Song-Dateien, wird BeatPrompter halten die Anzeige perfekt in der Zeit mit dem Beat. Nie zu schnell, nie zu langsam, und immer schön und groß und lesbar! + Geladen: + Nur das untere Halbbyte eines Wertes kann mit dem Kanalspezifizierer zusammengeführt werden. + Immer + Nie + Im Pausenmodus oder im Titelbildschirm + Wenn pausiert, am Titelbildschirm oder auf der letzten Zeile des Songs + Wenn auf Titelbildschirm + MIDI-Alias-Dateifehler + MIDI Aliases + MIDI Aliases + MIDI-Alias-Nachricht mit mehr als zwei Teilen gefunden. + Ein \"midi_alias\" -Tag enthält mehr als zwei Teile. + MIDI-Alias mit keinem Namen gefunden. + Diese MIDI-Nachricht kann nicht geparst werden. + MIDI Event Offset setzt es vor dem Beginn des Songs. + MIDI-Tags können maximal ein Semikolon-Zeichen enthalten. + Ein Aliaswert darf nicht mehrere Unterstriche enthalten. + MIDI-Offset sollte ein numerischer Wert oder ein Beat-Offset sein. + Die Datei \"%1$s\" ist keine gültige MIDI-Aliasing-Datei. + Nicht genügend Parameter für den MIDI-Befehl. + BeatPrompter zeigt nicht nur Texte und Akkorde. Es kann Playbacks abspielen und externe MIDI-Geräte steuern (und darauf reagieren). + Powerwash komplett. + Was ist der Standard-MIDI-Kanal zu schreiben, wenn nicht anders angegeben? + Standardausgang MIDI-Kanal + Aktive eingehende MIDI-Kanäle + Welche MIDI-Kanäle sollte TunePrompter hören? + MIDI-Trigger während der Song-Anzeige + Können MIDI-Trigger-Nachrichten den aktuellen Song unterbrechen? + Auslösesicherung + Löscht alle heruntergeladenen Dateien und alle gespeicherten Anmeldeinformationen trüben. + Powerwash + Die Farbe des Markers zeigt que Wenn die Anzeige scrollen. + Scroll Marker Farbe + Output MIDI Triggernachricht + Sollte der Trigger MIDI-Meldung ausgegeben werden, wenn der Song beginnt? + Output MIDI-Trigger + Die Verarbeitung Song: %1$s + Immer + Wenn manuell gestartet + Nie + MIDI-Alias-Datei-Fehler anzeigen + Programmwechsel-Trigger-Tags müssen zwischen einem und vier Werten enthalten. + Songauswahl-Trigger-Tags müssen nur einen Wert enthalten. + Vorübergehend + Werte, die Unterstriche enthalten, müssen hexadezimal sein. + Tag "%1$s" stimmt nicht mit bekannten MIDI-Befehlen und Aliasen überein. + MIDI-Parameter müssen zwischen 0 und 127 liegen. + Willkommen bei BeatPrompter + BeatPrompter ist eine Lyrics und Akkorde prompter App, die den Schwerpunkt auf Timing und Lesbarkeit setzt. + Obwohl Sie es im Hochformat verwenden können, funktioniert BeatPrompter am besten im Querformat. + " Die maximale MIDI-Event-Offset ist 16 Schläge oder 10 Sekunden." + Nur MIDI-Tags, die am oder nach dem ersten Song Linie sind, können Verschiebungen haben. + {t:BeatPrompter Demo-Song}\n {st:Demo-Song}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -271,163 +271,167 @@ Em]zeigt den Beat Progression.\n [D]Überprüfen Sie die Dokumentation für weitere Informationen.\n [G]www.beatprompter.co.uk\n [Em]Ich hoffe, dass Sie es mögen! - OneDrive-API nicht verbunden. - Cache gelöscht. - Dateien - Es wurde kein Speichersystem ausgewählt. - Es wurde kein Speichersystem ausgewählt. - Löscht alle heruntergeladenen Dateien. - Cache leeren - Speichersystem - Wo sind Ihre Song-Dateien gespeichert? - Speichersystem - Dateien synchronisieren … - Tags löschen, wenn sich der Filter ändert? - Wenn true, werden alle ausgewählten Tag-Filter gelöscht, wenn Sie Ordner/Setlisten/etc. wechseln. - Speicherordner - Dies ist der Ordner, in dem die BeatPrompter-Song-Dateien gespeichert sind. - %1$d gegenstände gefunden. - %1$d/%2$d gegenstände gefunden. - Laden … - Band Leader (server) - Bandmitglied (Auftraggeber) - Keiner - Verlorene Verbindung zum Bandführer - hat sich getrennt - Bluetooth-Modus - Konfigurieren Sie, wie Ihr Gerät mit Bandmitgliedern kommuniziert. - Bluetooth-Modus - Abrufen von Dateien aus Unterordnern auch? - Inklusive Unterordner - Force Refresh (einschließlich Abhängigkeiten) - Mit Schlüssel - Kann keine Bilddatei finden: %1$s - Audiodatei konnte nicht geladen werden. Fehlt oder korrupt? - Bilddatei konnte nicht gelesen werden: %1$s - Schlüssel - On, wenn es keine Backing Track gibt - Mehrere Bilder in einer Zeile gefunden. Nur mit dem ersten. - In der Haupt-Song-Liste, zeigen Sie die Taste des Songs. - Song-Key anzeigen - Song BPM anzeigen - Soll die anfängliche BPM des Songs auf dem Liedtitelbildschirm angezeigt werden? - Lied BPM auf Titelbildschirm anzeigen - Zeigt die Taste des Songs auf dem Liedtitelbildschirm an. - Liedtaste auf Titelbildschirm anzeigen. - Nein - Ja - Ja, abgerundet auf ganzzahlig - Text wurde auf einer Zeile gefunden, in der ein Bild angegeben wurde. Text ignorieren - Unbekannter Bildskalierungsmodus, Standardeinstellung zum Streckmodus. - Bei der Kommunikation mit dem Speicherdienst ist ein Fehler aufgetreten (\"%1$s\").\\n\\nNicht alle Dateien wurden abgerufen oder erfolgreich geprüft.\\n\\nÜberprüfen Sie Ihre Verbindung und versuchen Sie es erneut. - Synchronisierungsfehler - Mimische Bandleaderanzeige im manuellen Modus - Wenn Sie einen Song mit einem Bandleader im manuellen Modus ausführen, wird Ihre Anzeige automatisch auf die gleiche Ausrichtung und Schriftgröße eingestellt. - Mindestschriftgröße (manuelles Scrollen) - Maximale Schriftgröße (manuelles Scrollen) - Die kleinste Schriftgröße, die im manuellen Bildlaufmodus verwendet wird. - Die größte Schriftgröße, die im manuellen Bildlaufmodus verwendet wird. - Näherungssensor scrollt die Seite. - Wenn Ihr Gerät über einen Näherungssensor verfügt, funktioniert das Auslösen wie beim Drücken eines Seiten-Pedals. - Lokaler Speicher - Verbunden mit %1$s - Verbindung zu %1$s verloren - Der Name des MIDI-Alias-Sets wurde bereits definiert. - Der Setname wurde bereits definiert. - Das Bild nach der Skalierung überschreitet die maximale Größe von 8192 x 8192. Ignorieren der Linie. - Ein MIDI-Alias wurde nur mit einem Namen definiert. - Ein MIDI-Alias kann nur einen Kanalwert haben. - Anweisungen gefunden, aber noch kein MIDI-Aliasname definiert. - Eine MIDI-Aliasdatei muss eine {midi_aliases} Setnamendefinition enthalten. - Der Cache enthält mehrere Dateien, die dem Dateinamen \'{%1$s}\' entsprechen. - Unerwartetes Tag in Datei gefunden: {%1$s} - Ein {%1$s} -Tag wurde in derselben Zeile wie ein {%2$s} -Tag gefunden. Das ist nicht erlaubt. - Beide <bars> Tag und Kommas wurden in derselben Zeile gefunden. Tag-Wert verwenden - Das Single-Use-Tag {%1$s} wurde in dieser Datei mehrfach verwendet. - Das Tag {%1$s} wurde in dieser Zeile mehrfach verwendet. - Das Tag {%1$s} wurde mehrmals hintereinander ohne ein zwischenzeitliches End-Tag verwendet. - Das Tag {%1$s} wurde vor einem entsprechenden Starttag gefunden. - Das Tag {%1$s} wurde ohne angegebenen Wert gefunden. - Kein Setname in Setlistendatei definiert. - Mehrere Beatstart- oder Beatstop-Tags wurden in derselben Zeile gefunden. Alle außer dem ersten ignorieren. - Der Stammordner des Speichersystems konnte nicht gefunden werden. - Ein Beatstart-Tag kann nur in einem Song mit einer definierten BPM-Geschwindigkeit verwendet werden. - Unerwartetes Tag: %1$s. - %1$s ist keine gültige Audiodatei. - %1$s wird gescannt - Fehler beim Lesen der Datei - Farbe der Anzeige nach unten - Farbe für die Anzeige der Bildlaufposition. - Schlagabschnitts-Starthervorhebungsfarbe - Markieren Sie die Farbe für die Linie, die den Beginn eines Beat-Abschnitts darstellt. - Heben Sie den Beginn der Beat-Abschnitte hervor - Hebt den Beginn der Beats mit der von Ihnen gewählten Hervorhebungsfarbe hervor - Seitenmarkierungsmarkierungen anzeigen - Zeigt die Position an, zu der die Anzeige gescrollt wird, wenn Sie die Abwärtspfeiltaste drücken. - Band Leader Gerät - Geben Sie an, mit welchem Gerät Sie sich im Bandmitgliedsmodus verbinden möchten. - Band Leader Gerät - * Bilddatei konnte nicht gefunden werden * - Leerer Wert in MIDI-Direktive gefunden. - Ungültiger Argumentindex in MIDI-Direktive gefunden. - Ungültiger Byte-Wert in MIDI-Direktive gefunden. - Die ausgewählte Songdatei enthält keine Zeilen. - Anweisungen … - Jede andere Taste ist eine Seite nach unten. - Jede andere Taste, die auf einer angeschlossenen Tastatur gedrückt wird, wirkt wie ein Abblättern. - Bluetooth - Native - Farbe des Chorus-Abschnitts - Hervorhebungsfarbe für Refrains. - Verbindungstyp - Verbindungstyp - Welche Art von MIDI-Verbindung verwenden Sie? - Stumm - Spielen Sie niemals Audio ab - Mischen - Datenschutzrichtlinie … - Erlauben Sie BeatPrompter, über Bluetooth mit anderen Geräten zu kommunizieren. - "Aktueller Stand: " - Gewährt - Verweigert - Erlaubnisse - Audio - Erlauben Sie BeatPrompter, Dateien aus lokalen Ordnern zu lesen. - Audio-Player - Welchen Player sollte die App für die Audiowiedergabe verwenden? - Fehler bei der Authentifizierung für den Zugriff auf Google Drive. - Nach Modus - Kaufen Sie mir einen Kaffee … - Fehler - Abrufen des Stammordners … - Songbewertung anzeigen - Zeigen Sie in der Hauptsongliste die Bewertung des Songs an. - Nach Bewertung - Variation - Kein Ton - Variantennamen wurden bereits definiert. - Es gibt mehr Audio-Tags als Variationen. - Dunkelmodus ein-/ausschalten - Datenbank wird gelesen … - Das Lesen des Datenbankelements ist fehlgeschlagen - Zu kompensierende Latenzmenge bei der Audiowiedergabe. - Audio-Latenz - " Synchronisieren %1$s (wiederholen %2$d/%3$d)" - Bluetooth-MIDI-Geräte - Welche Bluetooth-Geräte sollten für MIDI in Betracht gezogen werden? - Bluetooth-MIDI-Geräte - Beim Lesen der Datenbank ist ein Fehler aufgetreten. - Beim Schreiben der Datenbank ist ein Fehler aufgetreten. - Datenbank wird neu erstellt ... - with_midi_*-Direktiven können nicht in Aliasnamen verwendet werden, die Parameter enthalten - Immer Kreuzakkorde anzeigen - Alle B-Akkorde werden in entsprechende Kreuz-Akkorde umgewandelt. - Unicode-Vorzeichen anzeigen - Akkorde, die die Zeichen b/# enthalten, werden in ♭/♯ umgewandelt. - \'%1$s\' konnte nicht als gültiger Schlüssel analysiert werden - Die Berechnung der neuen Tonart ist fehlgeschlagen (Verschiebung von %1$s um %2$d Halbtöne) - Akkord \'%1$s\' konnte nicht analysiert werden. - Das Tag {transpose} darf keinen numerischen Wert größer als 11 oder kleiner als -11 enthalten. - Der Tag {chord_map} muss zwei durch = getrennte Werte enthalten - Transponieren + OneDrive-API nicht verbunden. + Cache gelöscht. + Dateien + Es wurde kein Speichersystem ausgewählt. + Es wurde kein Speichersystem ausgewählt. + Löscht alle heruntergeladenen Dateien. + Cache leeren + Speichersystem + Wo sind Ihre Song-Dateien gespeichert? + Speichersystem + Dateien synchronisieren … + Tags löschen, wenn sich der Filter ändert? + Wenn true, werden alle ausgewählten Tag-Filter gelöscht, wenn Sie Ordner/Setlisten/etc. wechseln. + Speicherordner + Dies ist der Ordner, in dem die BeatPrompter-Song-Dateien gespeichert sind. + %1$d gegenstände gefunden. + %1$d/%2$d gegenstände gefunden. + Laden … + Band Leader (server) + Bandmitglied (Auftraggeber) + Keiner + Verlorene Verbindung zum Bandführer + hat sich getrennt + Bluetooth-Modus + Konfigurieren Sie, wie Ihr Gerät mit Bandmitgliedern kommuniziert. + Bluetooth-Modus + Abrufen von Dateien aus Unterordnern auch? + Inklusive Unterordner + Force Refresh (einschließlich Abhängigkeiten) + Mit Schlüssel + Kann keine Bilddatei finden: %1$s + Audiodatei konnte nicht geladen werden. Fehlt oder korrupt? + Bilddatei konnte nicht gelesen werden: %1$s + Schlüssel + On, wenn es keine Backing Track gibt + Mehrere Bilder in einer Zeile gefunden. Nur mit dem ersten. + In der Haupt-Song-Liste, zeigen Sie die Taste des Songs. + Song-Key anzeigen + Song BPM anzeigen + Soll die anfängliche BPM des Songs auf dem Liedtitelbildschirm angezeigt werden? + Lied BPM auf Titelbildschirm anzeigen + Zeigt die Taste des Songs auf dem Liedtitelbildschirm an. + Liedtaste auf Titelbildschirm anzeigen. + Nein + Ja + Ja, abgerundet auf ganzzahlig + Text wurde auf einer Zeile gefunden, in der ein Bild angegeben wurde. Text ignorieren + Unbekannter Bildskalierungsmodus, Standardeinstellung zum Streckmodus. + Bei der Kommunikation mit dem Speicherdienst ist ein Fehler aufgetreten (\"%1$s\").\\n\\nNicht alle Dateien wurden abgerufen oder erfolgreich geprüft.\\n\\nÜberprüfen Sie Ihre Verbindung und versuchen Sie es erneut. + Synchronisierungsfehler + Mimische Bandleaderanzeige im manuellen Modus + Wenn Sie einen Song mit einem Bandleader im manuellen Modus ausführen, wird Ihre Anzeige automatisch auf die gleiche Ausrichtung und Schriftgröße eingestellt. + Mindestschriftgröße (manuelles Scrollen) + Maximale Schriftgröße (manuelles Scrollen) + Die kleinste Schriftgröße, die im manuellen Bildlaufmodus verwendet wird. + Die größte Schriftgröße, die im manuellen Bildlaufmodus verwendet wird. + Näherungssensor scrollt die Seite. + Wenn Ihr Gerät über einen Näherungssensor verfügt, funktioniert das Auslösen wie beim Drücken eines Seiten-Pedals. + Lokaler Speicher + Verbunden mit %1$s + Verbindung zu %1$s verloren + Der Name des MIDI-Alias-Sets wurde bereits definiert. + Der Setname wurde bereits definiert. + Das Bild nach der Skalierung überschreitet die maximale Größe von 8192 x 8192. Ignorieren der Linie. + Ein MIDI-Alias wurde nur mit einem Namen definiert. + Ein MIDI-Alias kann nur einen Kanalwert haben. + Anweisungen gefunden, aber noch kein MIDI-Aliasname definiert. + Eine MIDI-Aliasdatei muss eine {midi_aliases} Setnamendefinition enthalten. + Der Cache enthält mehrere Dateien, die dem Dateinamen \'{%1$s}\' entsprechen. + Unerwartetes Tag in Datei gefunden: {%1$s} + Ein {%1$s} -Tag wurde in derselben Zeile wie ein {%2$s} -Tag gefunden. Das ist nicht erlaubt. + Beide <bars> Tag und Kommas wurden in derselben Zeile gefunden. Tag-Wert verwenden + Das Single-Use-Tag {%1$s} wurde in dieser Datei mehrfach verwendet. + Das Tag {%1$s} wurde in dieser Zeile mehrfach verwendet. + Das Tag {%1$s} wurde mehrmals hintereinander ohne ein zwischenzeitliches End-Tag verwendet. + Das Tag {%1$s} wurde vor einem entsprechenden Starttag gefunden. + Das Tag {%1$s} wurde ohne angegebenen Wert gefunden. + Kein Setname in Setlistendatei definiert. + Mehrere Beatstart- oder Beatstop-Tags wurden in derselben Zeile gefunden. Alle außer dem ersten ignorieren. + Der Stammordner des Speichersystems konnte nicht gefunden werden. + Ein Beatstart-Tag kann nur in einem Song mit einer definierten BPM-Geschwindigkeit verwendet werden. + Unerwartetes Tag: %1$s. + %1$s ist keine gültige Audiodatei. + %1$s wird gescannt + Fehler beim Lesen der Datei + Farbe der Anzeige nach unten + Farbe für die Anzeige der Bildlaufposition. + Schlagabschnitts-Starthervorhebungsfarbe + Markieren Sie die Farbe für die Linie, die den Beginn eines Beat-Abschnitts darstellt. + Heben Sie den Beginn der Beat-Abschnitte hervor + Hebt den Beginn der Beats mit der von Ihnen gewählten Hervorhebungsfarbe hervor + Seitenmarkierungsmarkierungen anzeigen + Zeigt die Position an, zu der die Anzeige gescrollt wird, wenn Sie die Abwärtspfeiltaste drücken. + Band Leader Gerät + Geben Sie an, mit welchem Gerät Sie sich im Bandmitgliedsmodus verbinden möchten. + Band Leader Gerät + * Bilddatei konnte nicht gefunden werden * + Leerer Wert in MIDI-Direktive gefunden. + Ungültiger Argumentindex in MIDI-Direktive gefunden. + Ungültiger Byte-Wert in MIDI-Direktive gefunden. + Die ausgewählte Songdatei enthält keine Zeilen. + Anweisungen … + Jede andere Taste ist eine Seite nach unten. + Jede andere Taste, die auf einer angeschlossenen Tastatur gedrückt wird, wirkt wie ein Abblättern. + Bluetooth + Native + Farbe des Chorus-Abschnitts + Hervorhebungsfarbe für Refrains. + Verbindungstyp + Verbindungstyp + Welche Art von MIDI-Verbindung verwenden Sie? + Stumm + Spielen Sie niemals Audio ab + Mischen + Datenschutzrichtlinie … + Erlauben Sie BeatPrompter, über Bluetooth mit anderen Geräten zu kommunizieren. + "Aktueller Stand: " + Gewährt + Verweigert + Erlaubnisse + Audio + Erlauben Sie BeatPrompter, Dateien aus lokalen Ordnern zu lesen. + Audio-Player + Welchen Player sollte die App für die Audiowiedergabe verwenden? + Fehler bei der Authentifizierung für den Zugriff auf Google Drive. + Nach Modus + Kaufen Sie mir einen Kaffee … + Fehler + Abrufen des Stammordners … + Songbewertung anzeigen + Zeigen Sie in der Hauptsongliste die Bewertung des Songs an. + Nach Bewertung + Variation + Kein Ton + Variantennamen wurden bereits definiert. + Es gibt mehr Audio-Tags als Variationen. + Dunkelmodus ein-/ausschalten + Datenbank wird gelesen … + Das Lesen des Datenbankelements ist fehlgeschlagen + Zu kompensierende Latenzmenge bei der Audiowiedergabe. + Audio-Latenz + " Synchronisieren %1$s (wiederholen %2$d/%3$d)" + Bluetooth-MIDI-Geräte + Welche Bluetooth-Geräte sollten für MIDI in Betracht gezogen werden? + Bluetooth-MIDI-Geräte + Beim Lesen der Datenbank ist ein Fehler aufgetreten. + Beim Schreiben der Datenbank ist ein Fehler aufgetreten. + Datenbank wird neu erstellt ... + with_midi_*-Direktiven können nicht in Aliasnamen verwendet werden, die Parameter enthalten + Immer Kreuzakkorde anzeigen + Alle B-Akkorde werden in entsprechende Kreuz-Akkorde umgewandelt. + Unicode-Vorzeichen anzeigen + Akkorde, die die Zeichen b/# enthalten, werden in ♭/♯ umgewandelt. + \'%1$s\' konnte nicht als gültiger Schlüssel analysiert werden + Die Berechnung der neuen Tonart ist fehlgeschlagen (Verschiebung von %1$s um %2$d Halbtöne) + Akkord \'%1$s\' konnte nicht analysiert werden. + Notiz „%1$s“ konnte nicht analysiert werden + Das Tag {transpose} darf keinen numerischen Wert größer als 11 oder kleiner als -11 enthalten. + Der Tag {chord_map} muss zwei durch = getrennte Werte enthalten + Transponieren + varstart/varxstart-Tag enthält unbekannte Variationen: %1$s + Bevorzugte Variante + Wenn ein Lied diese Variation enthält, wird sie standardmäßig geladen. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d5c82d9c..fc54fd87 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,252 +1,252 @@  - BeatPrompter - Creado por - Acerca de - Pista de acompañamiento - Volumen de pista de audio no es un número entre 0 y 100. - Batir Desplazamiento - Por el artista - Por fecha - Por titulo - Cancelar - No se puede encontrar el archivo de audio: %1$s - … y %1$d otro error(s). - Todas las canciones - Siempre utiliza los colores predeterminados. - Inicio de la pista de audio compensada - Un valor ya se ha definido para {%1$s}. - Color de fondo - De impulsos de fondo - El color de impulsos de fondo - barras - El color contador de pulsaciones - LPB - BPL - LPM - Centrar - Comprobación %1$s - Elegir carpeta de sincronización - Colores acordes - Limpiar cache - Colores - No se pudo encontrar el archivo de audio. - No se pudo cargar el archivo de audio. ¿Se encuentra o está dañado? - No se pudo analizar "%1$s" como un color. - No se pudo analizar "%1$s" como un valor de duración (uso MM: SS) o tan sólo unos segundos. - No se pudo analizar "%1$s" como un valor entero. - No se pudo analizar "%1$s" como un valor numérico. - Crea un sonido de clic en el tiempo con el ritmo. Sólo se aplica a batir de desplazamiento canciones. - Bares predeterminados por línea - Defecto pulsaciones por barra - Predeterminado claqueta - Pausa inicial por defecto - Tempo predeterminado - La eliminación de %1$s - Documentación se puede encontrar en - Sincronizando %1$s - Sincronizando archivos - Durante claqueta - El archivo "%1$s" no encontrado! - Altura de la línea fija - Los tamaños de fuente - Fuerza de actualización (archivo de canción solamente) - El color de realce - El color de realce para aplicar al texto resaltado lírica. - Si se está mostrando una canción en "modo de desplazamiento suave", entonces esta configuración crea una pausa automática al inicio de la canción, cuenta en la longitud total canción. - Si un tempo no está definida por un archivo, se utilizará esta velocidad. - Si las barras por línea no está definido por un archivo, se utilizará este valor. - Si el número de "claqueta" barras no está definida por un archivo, se utilizará este valor. - Si pulsaciones por barra no está definido por un archivo, se utilizará este valor. - No haga caso de los colores en los archivos - En la sección de contador de pulsos, resalte el ritmo que la pantalla se desplazará sucesivamente. - En la lista principal de la canción, mostrar un icono que indica que una canción tiene una pista de fondo. - Saltar - Izquierda - Línea %1$d fue más largo de %2$d caracteres. - Justificación de la línea - Justificación de la Línea - líneas - Las letras de color - Hace todas las líneas de la misma altura - El tamaño máximo de fuente ( beat- desplazamiento) - La altura máxima de la letra - El valor máximo es %1$d, valor encontrado fue %2$d. - El valor máximo es %1$d, valor que se encuentra fue %2$.2f. - Metrónomo - Haga clic del metrónomo - El tamaño mínimo de fuente - El valor mínimo es %1$d, valor que se encuentra fue %2$d. - El valor mínimo es %1$d, valor que se encuentra fue %2$.2f. - Líneas visibles mínimos - ms - No {title} encontrado en el archivo de la canción "%1$s" - Sin pista de acompañamiento - Apagado - En - Jugar - Jugar … - Opciones de Juego - Derecha - Scrollbeat desplazamiento es mayor que o igual que el número de latidos en el bar. Puesta a cero. - Modo de desplazamiento - Estilo de desplazamiento - Estilo de Desplazamiento - segundos - Ajustes - Ajustes … - Mostrar icono de la música - Mostrar indicador de desplazamiento. - Mostrar iconos de estilo de desplazamiento - Muestra el estilo de desplazamiento de la canción como un icono en la lista principal. - Suave - Desplazamiento suave - Canciones Options - Ordenar … - Ordenar canciones … - TAP DOS VECES PARA INICIAR - el - El fondo pulsará en el tiempo con el ritmo. - El fondo pulsará este color en el tiempo con el ritmo. - El color de la barra de contador de pulsos en la parte superior de la pantalla. - El tamaño de fuente más grande que será utilizado en el modo de ritmo de desplazamiento. - El porcentaje máximo de la altura máxima de la línea que las letras ocuparán. - La preferencia mínimo tamaño de fuente es mayor que el tamaño máximo de fuente. Usando mínimo para todos. - El número mínimo de líneas que debería ser visible en un momento dado. - El desplazamiento desde el inicio de la canción, en milisegundos, a la que comenzará la pista de audio de soporte. - El tamaño de fuente más pequeño que se utilizará. - Total {pause} de tiempo supera el tiempo designado canción. Deshabilitando el modo de desplazamiento suave. - El tamaño de fuente más grande que será utilizado en el modo de liso-desplazamiento. - Tamaño de la fuente máxima (desplazamiento suave) - Utiliza las preferencias de tamaño de fuente del golpe de desplazamiento para todas las canciones. - Siempre utilice las preferencias de tamaño de fuente del golpe de desplazamiento. - El tamaño de fuente más pequeño que se utiliza en el modo suave-desplazamiento. - Tamaño de fuente mínimo (desplazamiento suave) - Compra la version completa - Conectado al líder de la banda - ha conectado - Por favor compra BeatPrompter - Cualquier texto de acorde que no se reconoce como un acorde válida será mostrado en este color. - Color de las anotaciones no acordes - Sólo para ver los comentarios que están destinados para que en la pantalla de introducción de la canción, introduzca su nombre(s) aquí. - Comentarios del usuario personalizados - A menos que se especifique en el archivo , esto es el volumen predeterminado que pistas de acompañamiento tocarán en. - Volumen de la pista por defecto - Controles durante la ejecución - ¿Qué controles deben estar disponibles mientras se está reproduciendo una canción? - Controles durante la ejecución - Nada - Desplazamiento, haciendo una pausa, reiniciar - Volumen - ¡Gracias! Usted está oficialmente una buena persona! :) - Almacena los datos en una memoria externa (tarjeta SD , etc). El cambio de este borrará la caché. - Uso de almacenamiento externo - Utilice la ubicación de almacenamiento personalizada - Si se selecciona, los datos de canciones se almacenan en la ubicación personalizada . El cambio de este borrará la caché. - ¿Dónde se debe almacenar BeatPrompter descargado canciones y archivos de audio ? El cambio de este borrará la caché. - Ubicación de almacenamiento - Uso de almacenamiento interno - Manual de Desplazamiento - " Si una canción tiene tiempos de ritmo, esto enviará señales de reloj MIDI." - Enviar señales de reloj MIDI - Por defecto, las canciones siempre juegan en el modo de desplazamiento manual con ninguna pista de fondo. - Modo manual - Google API no está conectado. - La cantidad de tiempo que un comentario en la canción permanecerá en pantalla. - Tiempo de visualización comentario - Color del comentario de texto - El color de realce para aplicar a la línea actual. - Corriente de línea El color de realce - Destaca la línea actual con su color de realce elegido. - Resalte la línea actual - Muestra los acordes de la canción por encima de las letras en el punto correcto. - Mostrar Acordes - Muestra el primer tiempo en la sección indicador de latido. - Mostrar primer tiempo. - Muestra el título actual canción sobre la parte superior de la sección de contador de pulsos. - Mostrar título de la canción. - Opciones de visualización canción - Opciones de la lista de canciones - Lista de actualización ajustado vigor - Desconocido Songs In Set List - Esta lista juego contiene %1$d canciones desconocidas, incluyendo: - No {set} conjunto de archivos que se encuentra en conjunto \"%1$s\". - Siempre - Nunca - Sólo Set Lists - Juega automáticamente en la siguiente canción - Juega automáticamente en la siguiente canción - En caso de que la siguiente canción se inicie automáticamente cuando termina la canción actual? - Muestra la lista de canciones en un texto más grande. - Texto más grande - Diverso - Añadir a la lista de conjunto temporal - Acceso %1$s - Etiqueta mal formados. - El canal MIDI debe ser el parámetro de carga - El valor del canal especificador no puede contener guiones. - Conectar con el líder - Setlist temporal clara - BeatPrompter lee todos sus archivos de canciones, archivos de audio, listas de conjuntos y más desde la nube o el almacenamiento local. Actualmente es compatible con Google Drive, Dropbox o Microsoft OneDrive. - Por defecto MIDI Alias Set - La etiqueta está vacía. - Forzar la actualización del archivo de alias MIDI - Los valores del canal MIDI deben estar entre 1 y 16. - Índice de argumento de alias MIDI no válido. - Con un poco de modificación a sus archivos de canciones, BeatPrompter mantendrá la pantalla perfectamente en el tiempo con el ritmo. Nunca demasiado rápido, nunca demasiado lento, y siempre agradable y grande y legible! - Cargando: - Sólo el nibble inferior de un valor se puede combinar con el especificador de canal. - Siempre - Nunca - Cuando está en pausa, o en la pantalla de título - Cuando está en pausa, en la pantalla de título o en la última línea de la canción - Cuando en la pantalla de título - Errores de archivo de alias MIDI - Alias de MIDI - Alias de MIDI - MIDI alias mensaje encontrado con más de dos partes. - Una etiqueta de \"midi_alias\" contiene más de dos partes. - Alias MIDI encontrado sin nombre. - No se puede analizar este mensaje MIDI. - El desplazamiento de eventos MIDI lo coloca antes del inicio de la canción. - Las etiquetas MIDI pueden contener un máximo de un carácter de punto y coma. - Un valor de alias no puede contener varios subrayados. - El desplazamiento MIDI debe ser un valor numérico o caracteres de desplazamiento de tiempo. - El archivo \"%1$s\" no es un archivo de aliasing MIDI válido. - No hay suficientes parámetros suministrados al comando MIDI. - BeatPrompter no solo muestra letras y acordes. Puede reproducir pistas de acompañamiento y controlar (y responder a) dispositivos MIDI externos también. - Powerwash completo. - ¿Cuál es el canal MIDI por defecto a escribir, a menos que se especifique lo contrario? - Salida predeterminada Canal MIDI - ¿Qué canales MIDI debería escuchar TunePrompter? - Canales MIDI entrantes activos - MIDI dispara durante la visualización de la canción - ¿Pueden los mensajes de disparo MIDI interrumpir la canción actual? - Gatillo de seguridad - Borra todos los archivos descargados y todas las credenciales de nube almacenadas. - Lavado a presión - El color del marcador que se muestra cuando se desplaza la pantalla. - Color del marcador de desplazamiento - Mensaje MIDI de disparo de salida - ¿Debe aparecer el mensaje de disparador MIDI cuando se inicia la canción? - Salida de disparo MIDI - Canción de procesamiento: %1$s - Siempre - Cuando se inicia manualmente - Nunca - Mostrar errores de archivo de alias de MIDI - Las etiquetas de activación de cambio de programa deben contener entre uno y cuatro valores. - Las etiquetas de disparo de selección de canción deben contener sólo un valor. - Temporal - Los valores que contienen subrayados deben estar en hexadecimal. - La etiqueta "%1$s" no coincide con ningún comando MIDI y alias conocidos. - Los parámetros MIDI deben ser un valor entre 0 y 127. - Bienvenido a BeatPrompter - BeatPrompter es una aplicación de prontuario de letras y acordes que pone el énfasis en el tiempo y la legibilidad. - Aunque puede utilizarlo en modo vertical, Best Prompter funciona mejor en modo horizontal. - El máximo evento MIDI desplazamiento es de 16 latidos o 10 segundos. - Sólo etiquetas MIDI que se encuentran en o después de la primera línea de la canción pueden tener compensaciones. - {t:BeatPrompter Canción De Demo}\n + BeatPrompter + Creado por + Acerca de + Pista de acompañamiento + Volumen de pista de audio no es un número entre 0 y 100. + Batir Desplazamiento + Por el artista + Por fecha + Por titulo + Cancelar + No se puede encontrar el archivo de audio: %1$s + … y %1$d otro error(s). + Todas las canciones + Siempre utiliza los colores predeterminados. + Inicio de la pista de audio compensada + Un valor ya se ha definido para {%1$s}. + Color de fondo + De impulsos de fondo + El color de impulsos de fondo + barras + El color contador de pulsaciones + LPB + BPL + LPM + Centrar + Comprobación %1$s + Elegir carpeta de sincronización + Colores acordes + Limpiar cache + Colores + No se pudo encontrar el archivo de audio. + No se pudo cargar el archivo de audio. ¿Se encuentra o está dañado? + No se pudo analizar "%1$s" como un color. + No se pudo analizar "%1$s" como un valor de duración (uso MM: SS) o tan sólo unos segundos. + No se pudo analizar "%1$s" como un valor entero. + No se pudo analizar "%1$s" como un valor numérico. + Crea un sonido de clic en el tiempo con el ritmo. Sólo se aplica a batir de desplazamiento canciones. + Bares predeterminados por línea + Defecto pulsaciones por barra + Predeterminado claqueta + Pausa inicial por defecto + Tempo predeterminado + La eliminación de %1$s + Documentación se puede encontrar en + Sincronizando %1$s + Sincronizando archivos + Durante claqueta + El archivo "%1$s" no encontrado! + Altura de la línea fija + Los tamaños de fuente + Fuerza de actualización (archivo de canción solamente) + El color de realce + El color de realce para aplicar al texto resaltado lírica. + Si se está mostrando una canción en "modo de desplazamiento suave", entonces esta configuración crea una pausa automática al inicio de la canción, cuenta en la longitud total canción. + Si un tempo no está definida por un archivo, se utilizará esta velocidad. + Si las barras por línea no está definido por un archivo, se utilizará este valor. + Si el número de "claqueta" barras no está definida por un archivo, se utilizará este valor. + Si pulsaciones por barra no está definido por un archivo, se utilizará este valor. + No haga caso de los colores en los archivos + En la sección de contador de pulsos, resalte el ritmo que la pantalla se desplazará sucesivamente. + En la lista principal de la canción, mostrar un icono que indica que una canción tiene una pista de fondo. + Saltar + Izquierda + Línea %1$d fue más largo de %2$d caracteres. + Justificación de la línea + Justificación de la Línea + líneas + Las letras de color + Hace todas las líneas de la misma altura + El tamaño máximo de fuente ( beat- desplazamiento) + La altura máxima de la letra + El valor máximo es %1$d, valor encontrado fue %2$d. + El valor máximo es %1$d, valor que se encuentra fue %2$.2f. + Metrónomo + Haga clic del metrónomo + El tamaño mínimo de fuente + El valor mínimo es %1$d, valor que se encuentra fue %2$d. + El valor mínimo es %1$d, valor que se encuentra fue %2$.2f. + Líneas visibles mínimos + ms + No {title} encontrado en el archivo de la canción "%1$s" + Sin pista de acompañamiento + Apagado + En + Jugar + Jugar … + Opciones de Juego + Derecha + Scrollbeat desplazamiento es mayor que o igual que el número de latidos en el bar. Puesta a cero. + Modo de desplazamiento + Estilo de desplazamiento + Estilo de Desplazamiento + segundos + Ajustes + Ajustes … + Mostrar icono de la música + Mostrar indicador de desplazamiento. + Mostrar iconos de estilo de desplazamiento + Muestra el estilo de desplazamiento de la canción como un icono en la lista principal. + Suave + Desplazamiento suave + Canciones Options + Ordenar … + Ordenar canciones … + TAP DOS VECES PARA INICIAR + el + El fondo pulsará en el tiempo con el ritmo. + El fondo pulsará este color en el tiempo con el ritmo. + El color de la barra de contador de pulsos en la parte superior de la pantalla. + El tamaño de fuente más grande que será utilizado en el modo de ritmo de desplazamiento. + El porcentaje máximo de la altura máxima de la línea que las letras ocuparán. + La preferencia mínimo tamaño de fuente es mayor que el tamaño máximo de fuente. Usando mínimo para todos. + El número mínimo de líneas que debería ser visible en un momento dado. + El desplazamiento desde el inicio de la canción, en milisegundos, a la que comenzará la pista de audio de soporte. + El tamaño de fuente más pequeño que se utilizará. + Total {pause} de tiempo supera el tiempo designado canción. Deshabilitando el modo de desplazamiento suave. + El tamaño de fuente más grande que será utilizado en el modo de liso-desplazamiento. + Tamaño de la fuente máxima (desplazamiento suave) + Utiliza las preferencias de tamaño de fuente del golpe de desplazamiento para todas las canciones. + Siempre utilice las preferencias de tamaño de fuente del golpe de desplazamiento. + El tamaño de fuente más pequeño que se utiliza en el modo suave-desplazamiento. + Tamaño de fuente mínimo (desplazamiento suave) + Compra la version completa + Conectado al líder de la banda + ha conectado + Por favor compra BeatPrompter + Cualquier texto de acorde que no se reconoce como un acorde válida será mostrado en este color. + Color de las anotaciones no acordes + Sólo para ver los comentarios que están destinados para que en la pantalla de introducción de la canción, introduzca su nombre(s) aquí. + Comentarios del usuario personalizados + A menos que se especifique en el archivo , esto es el volumen predeterminado que pistas de acompañamiento tocarán en. + Volumen de la pista por defecto + Controles durante la ejecución + ¿Qué controles deben estar disponibles mientras se está reproduciendo una canción? + Controles durante la ejecución + Nada + Desplazamiento, haciendo una pausa, reiniciar + Volumen + ¡Gracias! Usted está oficialmente una buena persona! :) + Almacena los datos en una memoria externa (tarjeta SD , etc). El cambio de este borrará la caché. + Uso de almacenamiento externo + Utilice la ubicación de almacenamiento personalizada + Si se selecciona, los datos de canciones se almacenan en la ubicación personalizada . El cambio de este borrará la caché. + ¿Dónde se debe almacenar BeatPrompter descargado canciones y archivos de audio ? El cambio de este borrará la caché. + Ubicación de almacenamiento + Uso de almacenamiento interno + Manual de Desplazamiento + " Si una canción tiene tiempos de ritmo, esto enviará señales de reloj MIDI." + Enviar señales de reloj MIDI + Por defecto, las canciones siempre juegan en el modo de desplazamiento manual con ninguna pista de fondo. + Modo manual + Google API no está conectado. + La cantidad de tiempo que un comentario en la canción permanecerá en pantalla. + Tiempo de visualización comentario + Color del comentario de texto + El color de realce para aplicar a la línea actual. + Corriente de línea El color de realce + Destaca la línea actual con su color de realce elegido. + Resalte la línea actual + Muestra los acordes de la canción por encima de las letras en el punto correcto. + Mostrar Acordes + Muestra el primer tiempo en la sección indicador de latido. + Mostrar primer tiempo. + Muestra el título actual canción sobre la parte superior de la sección de contador de pulsos. + Mostrar título de la canción. + Opciones de visualización canción + Opciones de la lista de canciones + Lista de actualización ajustado vigor + Desconocido Songs In Set List + Esta lista juego contiene %1$d canciones desconocidas, incluyendo: + No {set} conjunto de archivos que se encuentra en conjunto \"%1$s\". + Siempre + Nunca + Sólo Set Lists + Juega automáticamente en la siguiente canción + Juega automáticamente en la siguiente canción + En caso de que la siguiente canción se inicie automáticamente cuando termina la canción actual? + Muestra la lista de canciones en un texto más grande. + Texto más grande + Diverso + Añadir a la lista de conjunto temporal + Acceso %1$s + Etiqueta mal formados. + El canal MIDI debe ser el parámetro de carga + El valor del canal especificador no puede contener guiones. + Conectar con el líder + Setlist temporal clara + BeatPrompter lee todos sus archivos de canciones, archivos de audio, listas de conjuntos y más desde la nube o el almacenamiento local. Actualmente es compatible con Google Drive, Dropbox o Microsoft OneDrive. + Por defecto MIDI Alias Set + La etiqueta está vacía. + Forzar la actualización del archivo de alias MIDI + Los valores del canal MIDI deben estar entre 1 y 16. + Índice de argumento de alias MIDI no válido. + Con un poco de modificación a sus archivos de canciones, BeatPrompter mantendrá la pantalla perfectamente en el tiempo con el ritmo. Nunca demasiado rápido, nunca demasiado lento, y siempre agradable y grande y legible! + Cargando: + Sólo el nibble inferior de un valor se puede combinar con el especificador de canal. + Siempre + Nunca + Cuando está en pausa, o en la pantalla de título + Cuando está en pausa, en la pantalla de título o en la última línea de la canción + Cuando en la pantalla de título + Errores de archivo de alias MIDI + Alias de MIDI + Alias de MIDI + MIDI alias mensaje encontrado con más de dos partes. + Una etiqueta de \"midi_alias\" contiene más de dos partes. + Alias MIDI encontrado sin nombre. + No se puede analizar este mensaje MIDI. + El desplazamiento de eventos MIDI lo coloca antes del inicio de la canción. + Las etiquetas MIDI pueden contener un máximo de un carácter de punto y coma. + Un valor de alias no puede contener varios subrayados. + El desplazamiento MIDI debe ser un valor numérico o caracteres de desplazamiento de tiempo. + El archivo \"%1$s\" no es un archivo de aliasing MIDI válido. + No hay suficientes parámetros suministrados al comando MIDI. + BeatPrompter no solo muestra letras y acordes. Puede reproducir pistas de acompañamiento y controlar (y responder a) dispositivos MIDI externos también. + Powerwash completo. + ¿Cuál es el canal MIDI por defecto a escribir, a menos que se especifique lo contrario? + Salida predeterminada Canal MIDI + ¿Qué canales MIDI debería escuchar TunePrompter? + Canales MIDI entrantes activos + MIDI dispara durante la visualización de la canción + ¿Pueden los mensajes de disparo MIDI interrumpir la canción actual? + Gatillo de seguridad + Borra todos los archivos descargados y todas las credenciales de nube almacenadas. + Lavado a presión + El color del marcador que se muestra cuando se desplaza la pantalla. + Color del marcador de desplazamiento + Mensaje MIDI de disparo de salida + ¿Debe aparecer el mensaje de disparador MIDI cuando se inicia la canción? + Salida de disparo MIDI + Canción de procesamiento: %1$s + Siempre + Cuando se inicia manualmente + Nunca + Mostrar errores de archivo de alias de MIDI + Las etiquetas de activación de cambio de programa deben contener entre uno y cuatro valores. + Las etiquetas de disparo de selección de canción deben contener sólo un valor. + Temporal + Los valores que contienen subrayados deben estar en hexadecimal. + La etiqueta "%1$s" no coincide con ningún comando MIDI y alias conocidos. + Los parámetros MIDI deben ser un valor entre 0 y 127. + Bienvenido a BeatPrompter + BeatPrompter es una aplicación de prontuario de letras y acordes que pone el énfasis en el tiempo y la legibilidad. + Aunque puede utilizarlo en modo vertical, Best Prompter funciona mejor en modo horizontal. + El máximo evento MIDI desplazamiento es de 16 latidos o 10 segundos. + Sólo etiquetas MIDI que se encuentran en o después de la primera línea de la canción pueden tener compensaciones. + {t:BeatPrompter Canción De Demo}\n {st:Canción De Demo}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -272,162 +272,166 @@ letras y acordes [Em]en el tiempo con el ritmo.\n [D]Consulte la documentación para más detalles.\n [G]www.beatprompter.co.uk\n [Em]espero que les guste! - API de OneDrive no está conectada. - Se borró el caché. - Archivos - No se ha seleccionado ningún sistema de almacenamiento. - No se ha seleccionado ningún sistema de almacenamiento. - Borra todos los archivos descargados. - Limpiar cache - Sistema de almacenamiento - ¿Dónde se almacenan los archivos de canciones? - Sistema de almacenamiento - Sincronizar archivos … - ¿Borrar las etiquetas cuando cambia el filtro? - Si es true, los filtros de etiquetas seleccionados se borrarán cada vez que cambie de carpeta/setlists/etc. - Carpeta de almacenamiento - Esta es la carpeta en la que se almacenan los archivos de canciones BeatPrompter. - %1$d artículos encontrados. - %1$d/%2$d artículos encontrados. - Cargando … - Líder de banda (servidor) - Miembro de la banda (cliente) - Ninguna - Conexión perdida con el líder de la banda - se ha desconectado - Modo Bluetooth - Configure cómo se comunica su dispositivo con los miembros de la banda. - Modo Bluetooth - Obtener archivos de subcarpetas también? - Incluir subcarpétas - Actualización de fuerzas (incluidas las dependencias) - Por clave - No se puede encontrar el archivo de imagen: %1$s - No se pudo leer el archivo de imagen: %1$s - Llave - Encendido cuando no hay pista de acompañamiento - Múltiples imágenes encontradas en una línea. Sólo con el primero. - En la lista de canciones principal, muestre la clave de la canción. - Mostrar la clave de la canción - Mostrar BPM de canciones - ¿Debe aparecer el BPM inicial de la canción en la pantalla de título de la canción? - Mostrar la canción BPM en la pantalla de título - Muestra la clave de la canción en la pantalla de título de la canción - Liedtaste auf Titelbildschirm anzeigen. - No - - Sí, redondeado a entero - Se encontró texto en una línea en la que se especificó una imagen. Ignorar texto. - Modo de escala de imagen desconocido, predeterminado para el modo de estiramiento. - Se produjo un error al comunicarse con el servicio de almacenamiento (\"%1$s\").\\n\\nNo todos los archivos fueron recuperados o escaneados con éxito.\\n\\nVerifique su conexión e intente nuevamente. - Error de Sincronización - Indicador de líder de banda mímica en modo manual - Si realiza una canción con un líder de banda en modo manual, esto configurará automáticamente su pantalla con la misma orientación y tamaños de fuente. - Tamaño de fuente mínimo (desplazamiento manual) - Tamaño máximo de la fuente (desplazamiento manual) - El tamaño de letra más pequeño que se usará en el modo de desplazamiento manual. - El tamaño de fuente más grande que se usará en el modo de desplazamiento manual. - El sensor de proximidad se desplaza por la página. - Si su dispositivo tiene un sensor de proximidad, activarlo actuará como presionar un pedal de avance de página. - Almacenamiento Local - Conectado a %1$s - Se perdió la conexión a %1$s - El nombre del conjunto de alias MIDI ya ha sido definido. - Establecer nombre ya ha sido definido. - La imagen después de la escala excede el tamaño máximo de 8192 x 8192. Ignorando la línea. - Se ha definido un alias MIDI con solo un nombre. - Un alias MIDI solo puede tener un valor de canal. - Se encontraron instrucciones, pero aún no se ha definido ningún nombre de alias MIDI. - Un archivo de alias MIDI debe contener una definición de nombre de conjunto {midi_aliases}. - Hay varios archivos en el caché que coinciden con el nombre de archivo \'{%1$s}\'. - Etiqueta inesperada encontrada en el archivo: {%1$s} - Se encontró una etiqueta {%1$s} en la misma línea que una etiqueta {%2$s}. Esto no esta permitido. - Ambos a <bars> etiqueta y comas se encontraron en la misma línea. Usando el valor de la etiqueta. - La etiqueta de un solo uso {%1$s} se ha usado varias veces en este archivo. - La etiqueta {%1$s} se ha usado varias veces en esta línea. - La etiqueta {%1$s} se usó varias veces consecutivas sin una etiqueta final intermedia. - La etiqueta {%1$s} se ha encontrado antes de la etiqueta inicial correspondiente. - La etiqueta {%1$s} se ha encontrado sin un valor especificado. - Ningún nombre de conjunto definido en el archivo de lista de conjuntos. - Se encontraron varias etiquetas beatstart o beatstop en la misma línea. Ignorando todo menos el primero. - No se pudo encontrar la carpeta raíz del sistema de almacenamiento. - Una etiqueta beatstart solo se puede utilizar en una canción con una velocidad de BPM definida. - Etiqueta inesperada: %1$s. - %1$s no es un archivo de audio válido. - Escaneando %1$s - Error al leer el archivo. - Color indicador de posición página abajo - Color para los indicadores de posición de desplazamiento de página abajo. - Color de resaltado de inicio de sección de golpe - Resalte el color para la línea que es el inicio de una sección de tiempo. - Resaltar inicio de las secciones de tiempo - Resalta el inicio de las secciones de tiempo con su color de resaltado elegido - Mostrar página hacia abajo los marcadores de posición de desplazamiento - Muestra la posición a la que se desplazará la pantalla cuando presione la página hacia abajo. - Dispositivo de líder de banda - Especifique a qué dispositivo conectarse cuando está en modo miembro de banda. - Dispositivo de líder de banda - * No se pudo encontrar el archivo de imagen * - Valor en blanco encontrado en la directiva MIDI. - Se ha encontrado un índice de argumento no válido en la directiva MIDI. - Se ha encontrado un valor de byte no válido en la directiva MIDI. - El archivo de la canción seleccionada no tiene líneas. - Instrucciones … - Cualquier otra clave es page-down. - Cualquier otra tecla presionada en un teclado conectado actuará como página abajo. - Bluetooth - Nativo - Color de resaltado de sección de coro - Resaltar el color para los coros. - Tipo de Conección - Tipo de Conección - ¿Qué tipo de conexión MIDI usas? - Mudo - Nunca reproducir audio - Barajar - Política de privacidad … - Permite que BeatPrompter se comunique con otros dispositivos a través de Bluetooth. - "Estado actual: " - Concedido - Denegada - Permisos - Audio - Permite que BeatPrompter lea archivos de carpetas locales. - Reproductor de audio - ¿Qué reproductora debe usar la aplicación para la reproducción de audio? - Error al autenticarse para acceder a Google Drive. - Por modo - Cómprame un café … - Error - Obteniendo carpeta raíz … - Mostrar clasificación de canciones - En la lista principal de canciones, muestra la calificación de la canción. - Por valoración - Variación - Sin Audio - Los nombres de las variaciones ya se han definido. - Las etiquetas de audio superan en número a las variaciones. - Alternar modo oscuro - Leyendo base de datos … - No se pudo leer el elemento de la base de datos - Cantidad de latencia a compensar en la reproducción de audio. - Latencia de audio - Sincronizando %1$s (rever %2$d/%3$d) - Dispositivos MIDI Bluetooth - ¿Qué dispositivos Bluetooth se deben considerar para MIDI? - Dispositivos MIDI Bluetooth - Se produjo un error al leer la base de datos. - Se produjo un error al escribir la base de datos. - Reconstruyendo base de datos... - Las directivas with_midi_* no se pueden utilizar en alias que contengan parámetros - Mostrar siempre acordes agudos - Cualquier acorde bemol se convertirá en un acorde sostenido apropiado. - Mostrar alteraciones accidentales de Unicode - Los acordes que contienen caracteres b/# se convertirán para utilizar ♭/♯. - \'%1$s\' no se pudo analizar como una clave válida - Impossibile calcolare la nuova tonalità (spostamento di %1$s di %2$d semitoni) - No se pudo analizar el acorde \'%1$s\' - La etiqueta {transpose} no debe contener un valor numérico mayor que 11 ni menor que -11 - La etiqueta {chord_map} debe contener dos valores separados por = - Transponer + API de OneDrive no está conectada. + Se borró el caché. + Archivos + No se ha seleccionado ningún sistema de almacenamiento. + No se ha seleccionado ningún sistema de almacenamiento. + Borra todos los archivos descargados. + Limpiar cache + Sistema de almacenamiento + ¿Dónde se almacenan los archivos de canciones? + Sistema de almacenamiento + Sincronizar archivos … + ¿Borrar las etiquetas cuando cambia el filtro? + Si es true, los filtros de etiquetas seleccionados se borrarán cada vez que cambie de carpeta/setlists/etc. + Carpeta de almacenamiento + Esta es la carpeta en la que se almacenan los archivos de canciones BeatPrompter. + %1$d artículos encontrados. + %1$d/%2$d artículos encontrados. + Cargando … + Líder de banda (servidor) + Miembro de la banda (cliente) + Ninguna + Conexión perdida con el líder de la banda + se ha desconectado + Modo Bluetooth + Configure cómo se comunica su dispositivo con los miembros de la banda. + Modo Bluetooth + Obtener archivos de subcarpetas también? + Incluir subcarpétas + Actualización de fuerzas (incluidas las dependencias) + Por clave + No se puede encontrar el archivo de imagen: %1$s + No se pudo leer el archivo de imagen: %1$s + Llave + Encendido cuando no hay pista de acompañamiento + Múltiples imágenes encontradas en una línea. Sólo con el primero. + En la lista de canciones principal, muestre la clave de la canción. + Mostrar la clave de la canción + Mostrar BPM de canciones + ¿Debe aparecer el BPM inicial de la canción en la pantalla de título de la canción? + Mostrar la canción BPM en la pantalla de título + Muestra la clave de la canción en la pantalla de título de la canción + Liedtaste auf Titelbildschirm anzeigen. + No + + Sí, redondeado a entero + Se encontró texto en una línea en la que se especificó una imagen. Ignorar texto. + Modo de escala de imagen desconocido, predeterminado para el modo de estiramiento. + Se produjo un error al comunicarse con el servicio de almacenamiento (\"%1$s\").\\n\\nNo todos los archivos fueron recuperados o escaneados con éxito.\\n\\nVerifique su conexión e intente nuevamente. + Error de Sincronización + Indicador de líder de banda mímica en modo manual + Si realiza una canción con un líder de banda en modo manual, esto configurará automáticamente su pantalla con la misma orientación y tamaños de fuente. + Tamaño de fuente mínimo (desplazamiento manual) + Tamaño máximo de la fuente (desplazamiento manual) + El tamaño de letra más pequeño que se usará en el modo de desplazamiento manual. + El tamaño de fuente más grande que se usará en el modo de desplazamiento manual. + El sensor de proximidad se desplaza por la página. + Si su dispositivo tiene un sensor de proximidad, activarlo actuará como presionar un pedal de avance de página. + Almacenamiento Local + Conectado a %1$s + Se perdió la conexión a %1$s + El nombre del conjunto de alias MIDI ya ha sido definido. + Establecer nombre ya ha sido definido. + La imagen después de la escala excede el tamaño máximo de 8192 x 8192. Ignorando la línea. + Se ha definido un alias MIDI con solo un nombre. + Un alias MIDI solo puede tener un valor de canal. + Se encontraron instrucciones, pero aún no se ha definido ningún nombre de alias MIDI. + Un archivo de alias MIDI debe contener una definición de nombre de conjunto {midi_aliases}. + Hay varios archivos en el caché que coinciden con el nombre de archivo \'{%1$s}\'. + Etiqueta inesperada encontrada en el archivo: {%1$s} + Se encontró una etiqueta {%1$s} en la misma línea que una etiqueta {%2$s}. Esto no esta permitido. + Ambos a <bars> etiqueta y comas se encontraron en la misma línea. Usando el valor de la etiqueta. + La etiqueta de un solo uso {%1$s} se ha usado varias veces en este archivo. + La etiqueta {%1$s} se ha usado varias veces en esta línea. + La etiqueta {%1$s} se usó varias veces consecutivas sin una etiqueta final intermedia. + La etiqueta {%1$s} se ha encontrado antes de la etiqueta inicial correspondiente. + La etiqueta {%1$s} se ha encontrado sin un valor especificado. + Ningún nombre de conjunto definido en el archivo de lista de conjuntos. + Se encontraron varias etiquetas beatstart o beatstop en la misma línea. Ignorando todo menos el primero. + No se pudo encontrar la carpeta raíz del sistema de almacenamiento. + Una etiqueta beatstart solo se puede utilizar en una canción con una velocidad de BPM definida. + Etiqueta inesperada: %1$s. + %1$s no es un archivo de audio válido. + Escaneando %1$s + Error al leer el archivo. + Color indicador de posición página abajo + Color para los indicadores de posición de desplazamiento de página abajo. + Color de resaltado de inicio de sección de golpe + Resalte el color para la línea que es el inicio de una sección de tiempo. + Resaltar inicio de las secciones de tiempo + Resalta el inicio de las secciones de tiempo con su color de resaltado elegido + Mostrar página hacia abajo los marcadores de posición de desplazamiento + Muestra la posición a la que se desplazará la pantalla cuando presione la página hacia abajo. + Dispositivo de líder de banda + Especifique a qué dispositivo conectarse cuando está en modo miembro de banda. + Dispositivo de líder de banda + * No se pudo encontrar el archivo de imagen * + Valor en blanco encontrado en la directiva MIDI. + Se ha encontrado un índice de argumento no válido en la directiva MIDI. + Se ha encontrado un valor de byte no válido en la directiva MIDI. + El archivo de la canción seleccionada no tiene líneas. + Instrucciones … + Cualquier otra clave es page-down. + Cualquier otra tecla presionada en un teclado conectado actuará como página abajo. + Bluetooth + Nativo + Color de resaltado de sección de coro + Resaltar el color para los coros. + Tipo de Conección + Tipo de Conección + ¿Qué tipo de conexión MIDI usas? + Mudo + Nunca reproducir audio + Barajar + Política de privacidad … + Permite que BeatPrompter se comunique con otros dispositivos a través de Bluetooth. + "Estado actual: " + Concedido + Denegada + Permisos + Audio + Permite que BeatPrompter lea archivos de carpetas locales. + Reproductor de audio + ¿Qué reproductora debe usar la aplicación para la reproducción de audio? + Error al autenticarse para acceder a Google Drive. + Por modo + Cómprame un café … + Error + Obteniendo carpeta raíz … + Mostrar clasificación de canciones + En la lista principal de canciones, muestra la calificación de la canción. + Por valoración + Variación + Sin Audio + Los nombres de las variaciones ya se han definido. + Las etiquetas de audio superan en número a las variaciones. + Alternar modo oscuro + Leyendo base de datos … + No se pudo leer el elemento de la base de datos + Cantidad de latencia a compensar en la reproducción de audio. + Latencia de audio + Sincronizando %1$s (rever %2$d/%3$d) + Dispositivos MIDI Bluetooth + ¿Qué dispositivos Bluetooth se deben considerar para MIDI? + Dispositivos MIDI Bluetooth + Se produjo un error al leer la base de datos. + Se produjo un error al escribir la base de datos. + Reconstruyendo base de datos... + Las directivas with_midi_* no se pueden utilizar en alias que contengan parámetros + Mostrar siempre acordes agudos + Cualquier acorde bemol se convertirá en un acorde sostenido apropiado. + Mostrar alteraciones accidentales de Unicode + Los acordes que contienen caracteres b/# se convertirán para utilizar ♭/♯. + \'%1$s\' no se pudo analizar como una clave válida + Impossibile calcolare la nuova tonalità (spostamento di %1$s di %2$d semitoni) + No se pudo analizar el acorde \'%1$s\' + No se pudo analizar la nota \'%1$s\' + La etiqueta {transpose} no debe contener un valor numérico mayor que 11 ni menor que -11 + La etiqueta {chord_map} debe contener dos valores separados por = + Transponer + La etiqueta varstart/varxstart contiene variaciones desconocidas: %1$s + Variación preferida + Si una canción contiene esta variación, se cargará de forma predeterminada. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3d8b5da9..9d2c95bc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,252 +1,252 @@  - BeatPrompter - Scrollbeat décalage est supérieur ou égal au nombre de battements dans le bar. Remise à zéro. - … Et %1$d autre erreur(s). - Sur … - Toutes les chansons - Utilise toujours les couleurs par défaut. - Début de piste audio compensé - Volume de la piste audio est pas un nombre entre 0 et 100. - Une valeur a déjà été définie pour {%1$s}. - La couleur d\'arrière-plan - Impulsion de fond - Couleur d\'impulsion de fond - Playback - barres - Battre compteur couleur - Battre Scrolling - BPB - BPL - BPM - Par artiste - Par date - Par titre - Annuler - Vous ne pouvez pas trouver le fichier audio: %1$s - Centre - Vérification %1$s - Choisissez Sync Folder - Couleur Accords - Vider le cache - Couleurs - Impossible de trouver le fichier audio. - Impossible de charger le fichier audio. Est-il manquant ou endommagé? - Impossible d\'analyser "%1$s" comme une couleur. - Impossible d\'analyser "%1$s" comme une valeur de durée (utilisation mm:ss ou quelques secondes). - Impossible d\'analyser "%1$s" comme une valeur entière. - Impossible d\'analyser "%1$s" comme une valeur numerique. - Créé par - Crée un clic dans le temps avec le rythme. Applicable uniquement pour battre défilement des chansons. - Par défaut barres par ligne - Par défaut battements par bar - Par défaut décompte - Pause initiale par défaut - Par défaut tempo - Suppression de %1$s - La documentation peut être trouvé à - Synchronisation %1$s - Synchronisation fichiers - Pendant décompte - Le fichier "%1$s" non trouvé! - Hauteur de la ligne fixe - Les tailles de police - Refresh Force (fichier chanson uniquement) - Couleur de surbrillance - Couleur de surbrillance à appliquer à mis en évidence le texte lyrique. - Si une chanson est affiché en mode "défilement régulier", ce paramètre crée une pause automatique au début de la chanson, pris en compte dans la durée de la chanson totale. - Si un tempo est pas défini par un fichier, cette vitesse sera utilisée. - Si les barres par ligne ne sont pas définis par un fichier, cette valeur sera utilisée. - Si battements par bar ne sont pas définis par un fichier, cette valeur sera utilisée. - Si le nombre de "décompte" barres est pas définie par un fichier, cette valeur sera utilisée. Applicable uniquement pour battre défilement des chansons. - Ignorer les couleurs dans les fichiers - Dans la section de compteur de temps, mettre en évidence le rythme que l\'écran défile sur. - Dans la liste des chansons principale, montrer une icône indiquant qu\'une chanson a une piste d\'accompagnement. - Saut - À gauche - Ligne %1$d était plus long que %2$d caractères. Troncature … - Justification de la ligne - Ligne Justification - lignes - Paroles couleur - Rend toutes les lignes de la même hauteur - Taille maximale de police ( beat- défilement) - Hauteur paroles maximum - Valeur maximale est %1$d, valeur trouvée était %2$d. - La valeur maximale est de %1$d, valeur trouvée était %2$.2f. - Cliquez metronome - Metronome Cliquez - Taille de police minimale - La valeur minimale est %1$d, valeur trouvée était %2$d. - La valeur minimale est %1$d, valeur trouvée était %2$.2f. - Lignes visibles minimum - ms - Non {title} trouvée dans le fichier de la chanson "%1$s" - Aucune piste d\'accompagnement - De - Sur - Jouer - Jouer … - Options de jeu - Droite - Mode de défilement - Le style scrolling - Défilement style - secondes - Paramètres - Paramètres … - Afficher icône de la musique - Afficher l\'indicateur de défilement. - Afficher les icônes de style de défilement - Affiche le type de défilement de la chanson comme une icône dans la liste principale. - Lisse - Défilement lisse - Chanson options - Trier … - Trier les chansons … - TAP DEUX FOIS POUR COMMENCER - la - L\'arrière-plan d\'impulsion dans le temps au rythme. - L\'arrière-plan d\'impulsion de cette couleur dans le temps au rythme. - La couleur de la contre barre de battement en haut de l\'écran. - La plus grande taille de la police qui sera utilisée en mode beat-défilement. - Le pourcentage maximum de la hauteur maximale de la ligne que les paroles vont occuper. - La préférence minimum de taille de police est plus grande que la taille maximale de la police. Utilisation minimum pour tous. - Le nombre minimal de lignes qui doit être visible à un moment donné. - Le décalage entre le début de la chanson, en millisecondes, à laquelle la piste audio de support va commencer. - La plus petite taille de la police qui sera utilisée. - Total {pause} temps dépasse le temps de la chanson désignée. Désactiver le mode de défilement régulier. - La plus grande taille de la police qui sera utilisée en mode lisse défilement. - Taille de police maximale (défilement régulier) - Utilise les préférences la taille battement défilement pour toutes les chansons . - Toujours utiliser les polices préférences de taille de battement à défilement horizontal. - La plus petite taille de la police qui sera utilisée en mode lisse défilement. - Taille de police minimale (défilement lisse) - Acheter la version complète - Connecté au chef d\'orchestre - a connecté - S\'il vous plaît acheter BeatPrompter - Tout texte d\'accord qui ne soit pas reconnu comme un accord valide sera affiché dans cette couleur. - Couleur des annotations non - accord - Pour seulement voir les commentaires qui sont destinés à vous sur l\'écran d\'introduction de la chanson , entrez votre nom(s) ici. - Personnalisées Commentaires utilisateur - Sauf indication contraire dans le fichier , cela est le volume par défaut playbacks vont jouer à. - Volume de la piste par défaut - Contrôles pendant l\'exécution - Quels contrôles devraient être disponibles alors qu\'une chanson est jouée? - Contrôles pendant l\'exécution - Rien - Défilement, pause, redémarrage - Volume - Merci! Vous êtes officiellement une bonne personne! :) - Stocke les données sur le stockage externe (carte SD , etc.). La modification de cette efface le cache. - Utilisation du stockage externe - Utilisez Stockage Emplacement personnalisé - Si elle est sélectionnée , les données de morceau sont stockées dans l\' emplacement personnalisé . La modification de cette efface le cache. - Où devrait magasin BeatPrompter téléchargé la chanson et des fichiers audio ? La modification de cette efface le cache. - Emplacement de stockage - Utilisation de stockage interne - Manuel Scrolling - Si une chanson a des timings de battement, cela enverra des signaux d\'horloge MIDI. - Envoyer des signaux d\'horloge MIDI - Par défaut, les chansons jouent toujours en mode de défilement manuel sans piste d\'accompagnement. - Mode manuel - API Google non connecté. - La quantité de temps qu\'un commentaire en morceau reste à l\'écran. - Temps d\'affichage du commentaire - Commentaire couleur du texte - Couleur de surbrillance à appliquer à la ligne actuelle. - Courant de ligne Couleur de surbrillance - Faits saillants de la ligne actuelle avec votre couleur de surbrillance choisi. - Mettez en surbrillance la ligne actuelle - Affiche les accords de la chanson ci-dessus les paroles au point correcte. - Afficher les Accords - Affiche le premier temps dans la section de l\'indicateur de battement - Voir premier temps. - Affiche le titre de la chanson en cours sur la partie supérieure de la section rythme du compteur. - Afficher le titre de la chanson. - Chanson Options d\'affichage - Options de la liste de chansons - Force de rafraîchissement set list - Inconnu Songs In Set List - Cette liste de jeu contient %1$d chansons inconnues, y compris: - Non {set} trouvée dans le fichier de jeu \"%1$s\". - Toujours - Jamais - Set Lists Seulement - Lecture automatique Chanson suivante - Lecture automatique Chanson suivante - Si le morceau suivant démarre automatiquement lorsque le morceau en cours se termine? - Affiche la liste des chansons dans un texte plus grand. - Bigger text - Divers - Accès %1$s - Ajouter à la liste d\'ensemble temporaire - Tag mal formé. - Le canal MIDI doit être le paramètre de charge - La valeur chaîne spécificateur ne peut pas contenir de soulignement. - Setlist temporaire effacer - BeatPrompter lit tous vos fichiers de chanson, vos fichiers audio, vos listes de sets et plus encore à partir du stockage en nuage ou local. Il supporte actuellement Google Drive, Dropbox ou Microsoft OneDrive. - Se connecter au chef - Par défaut MIDI Alias Set - Le tag est vide. - Forcer l\'actualisation du fichier d\'alias MIDI - Les valeurs des canaux MIDI doivent être comprises entre 1 et 16. - Index d\'argument d\'alias MIDI non valide. - Avec un peu de modification à vos fichiers de chanson, BeatPrompter gardera l\'affichage parfaitement dans le temps avec le battement. Jamais trop vite, jamais trop lent, et toujours agréable et grand et lisible! - Chargement: - Seul le nibble inférieur d\'une valeur peut être fusionné avec le spécificateur de canal. - Toujours - Jamais - En mode pause ou à l\'écran titre - Lorsqu\'il est en pause, à l\'écran titre ou à la dernière ligne de la chanson - Quand à l\'écran titre - Erreurs de fichier d\'alias MIDI - Alias MIDI - Alias MIDI - MIDI alias message trouvé avec plus de deux parties. - Une balise \"midi_alias\" contient plus de deux parties. - Alias MIDI trouvé sans nom. - Impossible d\'analyser ce message MIDI. - Le décalage d\'événement MIDI le place avant le début du morceau. - Les balises MIDI peuvent contenir au maximum un caractère de point-virgule. - Une valeur d\'alias ne peut contenir plusieurs soulignements. - Le décalage MIDI doit être une valeur numérique ou des caractères de décalage de battement. - Le fichier \"%1$s\" n\'est pas un fichier d\'aliasing MIDI valide. - Pas assez de paramètres fournis à la commande MIDI. - BeatPrompter n\'a pas seulement des paroles et des accords. Il peut lire des morceaux d\'accompagnement, et contrôler (et réagir) à des dispositifs externes de MIDI aussi. - Powerwash complet. - Quel est le canal MIDI par défaut à écrire, sauf indication contraire? - Sortie par défaut Canal MIDI - Quels canaux MIDI TunePrompter devrait-il écouter? - Canaux MIDI entrants actifs - MIDI déclenche l\'affichage des morceaux - Les messages de déclenchement MIDI peuvent-ils interrompre le morceau en cours? - Déclencheur de sécurité - Efface tous les fichiers téléchargés et toutes les informations d\'identification du nuage stockées. - Lavage à haute pression - La couleur du marqueur qui s\'affiche lorsque l\'affichage défile. - Couleur du marqueur de défilement - Sortie message de déclenchement MIDI - Le message de déclenchement MIDI devrait-il être émis lorsque le morceau commence? - Sortie Gâchette MIDI - Traitement de la chanson: %1$s - Toujours - Lorsque démarré manuellement - Jamais - Afficher les erreurs de fichier d\'alias MIDI - Les variables de déclenchement de changement de programme doivent contenir entre une et quatre valeurs. - Les variables de déclenchement de sélection de morceau ne doivent contenir qu\'une seule valeur. - Temporaire - Les valeurs qui contiennent des caractères de soulignement doivent être en hexadécimal. - Tag "%1$s" ne correspond à aucune commande MIDI et alias connus. - Les paramètres MIDI doivent être compris entre 0 et 127. - Bienvenue sur BeatPrompter - BeatPrompter est un paroles et des accords application prompter qui met l\'accent sur le calendrier et la lisibilité. - Bien que vous puissiez l\'utiliser en mode portrait, BeatPrompter fonctionne mieux en mode paysage. - L\'événement MIDI décalage maximum est de 16 battements ou 10 secondes. - Seules les balises MIDI qui sont sur ou après la première ligne de la chanson peuvent avoir des décalages. - {t:BeatPrompter Morceau De Démo}\n + BeatPrompter + Scrollbeat décalage est supérieur ou égal au nombre de battements dans le bar. Remise à zéro. + … Et %1$d autre erreur(s). + Sur … + Toutes les chansons + Utilise toujours les couleurs par défaut. + Début de piste audio compensé + Volume de la piste audio est pas un nombre entre 0 et 100. + Une valeur a déjà été définie pour {%1$s}. + La couleur d\'arrière-plan + Impulsion de fond + Couleur d\'impulsion de fond + Playback + barres + Battre compteur couleur + Battre Scrolling + BPB + BPL + BPM + Par artiste + Par date + Par titre + Annuler + Vous ne pouvez pas trouver le fichier audio: %1$s + Centre + Vérification %1$s + Choisissez Sync Folder + Couleur Accords + Vider le cache + Couleurs + Impossible de trouver le fichier audio. + Impossible de charger le fichier audio. Est-il manquant ou endommagé? + Impossible d\'analyser "%1$s" comme une couleur. + Impossible d\'analyser "%1$s" comme une valeur de durée (utilisation mm:ss ou quelques secondes). + Impossible d\'analyser "%1$s" comme une valeur entière. + Impossible d\'analyser "%1$s" comme une valeur numerique. + Créé par + Crée un clic dans le temps avec le rythme. Applicable uniquement pour battre défilement des chansons. + Par défaut barres par ligne + Par défaut battements par bar + Par défaut décompte + Pause initiale par défaut + Par défaut tempo + Suppression de %1$s + La documentation peut être trouvé à + Synchronisation %1$s + Synchronisation fichiers + Pendant décompte + Le fichier "%1$s" non trouvé! + Hauteur de la ligne fixe + Les tailles de police + Refresh Force (fichier chanson uniquement) + Couleur de surbrillance + Couleur de surbrillance à appliquer à mis en évidence le texte lyrique. + Si une chanson est affiché en mode "défilement régulier", ce paramètre crée une pause automatique au début de la chanson, pris en compte dans la durée de la chanson totale. + Si un tempo est pas défini par un fichier, cette vitesse sera utilisée. + Si les barres par ligne ne sont pas définis par un fichier, cette valeur sera utilisée. + Si battements par bar ne sont pas définis par un fichier, cette valeur sera utilisée. + Si le nombre de "décompte" barres est pas définie par un fichier, cette valeur sera utilisée. Applicable uniquement pour battre défilement des chansons. + Ignorer les couleurs dans les fichiers + Dans la section de compteur de temps, mettre en évidence le rythme que l\'écran défile sur. + Dans la liste des chansons principale, montrer une icône indiquant qu\'une chanson a une piste d\'accompagnement. + Saut + À gauche + Ligne %1$d était plus long que %2$d caractères. Troncature … + Justification de la ligne + Ligne Justification + lignes + Paroles couleur + Rend toutes les lignes de la même hauteur + Taille maximale de police ( beat- défilement) + Hauteur paroles maximum + Valeur maximale est %1$d, valeur trouvée était %2$d. + La valeur maximale est de %1$d, valeur trouvée était %2$.2f. + Cliquez metronome + Metronome Cliquez + Taille de police minimale + La valeur minimale est %1$d, valeur trouvée était %2$d. + La valeur minimale est %1$d, valeur trouvée était %2$.2f. + Lignes visibles minimum + ms + Non {title} trouvée dans le fichier de la chanson "%1$s" + Aucune piste d\'accompagnement + De + Sur + Jouer + Jouer … + Options de jeu + Droite + Mode de défilement + Le style scrolling + Défilement style + secondes + Paramètres + Paramètres … + Afficher icône de la musique + Afficher l\'indicateur de défilement. + Afficher les icônes de style de défilement + Affiche le type de défilement de la chanson comme une icône dans la liste principale. + Lisse + Défilement lisse + Chanson options + Trier … + Trier les chansons … + TAP DEUX FOIS POUR COMMENCER + la + L\'arrière-plan d\'impulsion dans le temps au rythme. + L\'arrière-plan d\'impulsion de cette couleur dans le temps au rythme. + La couleur de la contre barre de battement en haut de l\'écran. + La plus grande taille de la police qui sera utilisée en mode beat-défilement. + Le pourcentage maximum de la hauteur maximale de la ligne que les paroles vont occuper. + La préférence minimum de taille de police est plus grande que la taille maximale de la police. Utilisation minimum pour tous. + Le nombre minimal de lignes qui doit être visible à un moment donné. + Le décalage entre le début de la chanson, en millisecondes, à laquelle la piste audio de support va commencer. + La plus petite taille de la police qui sera utilisée. + Total {pause} temps dépasse le temps de la chanson désignée. Désactiver le mode de défilement régulier. + La plus grande taille de la police qui sera utilisée en mode lisse défilement. + Taille de police maximale (défilement régulier) + Utilise les préférences la taille battement défilement pour toutes les chansons . + Toujours utiliser les polices préférences de taille de battement à défilement horizontal. + La plus petite taille de la police qui sera utilisée en mode lisse défilement. + Taille de police minimale (défilement lisse) + Acheter la version complète + Connecté au chef d\'orchestre + a connecté + S\'il vous plaît acheter BeatPrompter + Tout texte d\'accord qui ne soit pas reconnu comme un accord valide sera affiché dans cette couleur. + Couleur des annotations non - accord + Pour seulement voir les commentaires qui sont destinés à vous sur l\'écran d\'introduction de la chanson , entrez votre nom(s) ici. + Personnalisées Commentaires utilisateur + Sauf indication contraire dans le fichier , cela est le volume par défaut playbacks vont jouer à. + Volume de la piste par défaut + Contrôles pendant l\'exécution + Quels contrôles devraient être disponibles alors qu\'une chanson est jouée? + Contrôles pendant l\'exécution + Rien + Défilement, pause, redémarrage + Volume + Merci! Vous êtes officiellement une bonne personne! :) + Stocke les données sur le stockage externe (carte SD , etc.). La modification de cette efface le cache. + Utilisation du stockage externe + Utilisez Stockage Emplacement personnalisé + Si elle est sélectionnée , les données de morceau sont stockées dans l\' emplacement personnalisé . La modification de cette efface le cache. + Où devrait magasin BeatPrompter téléchargé la chanson et des fichiers audio ? La modification de cette efface le cache. + Emplacement de stockage + Utilisation de stockage interne + Manuel Scrolling + Si une chanson a des timings de battement, cela enverra des signaux d\'horloge MIDI. + Envoyer des signaux d\'horloge MIDI + Par défaut, les chansons jouent toujours en mode de défilement manuel sans piste d\'accompagnement. + Mode manuel + API Google non connecté. + La quantité de temps qu\'un commentaire en morceau reste à l\'écran. + Temps d\'affichage du commentaire + Commentaire couleur du texte + Couleur de surbrillance à appliquer à la ligne actuelle. + Courant de ligne Couleur de surbrillance + Faits saillants de la ligne actuelle avec votre couleur de surbrillance choisi. + Mettez en surbrillance la ligne actuelle + Affiche les accords de la chanson ci-dessus les paroles au point correcte. + Afficher les Accords + Affiche le premier temps dans la section de l\'indicateur de battement + Voir premier temps. + Affiche le titre de la chanson en cours sur la partie supérieure de la section rythme du compteur. + Afficher le titre de la chanson. + Chanson Options d\'affichage + Options de la liste de chansons + Force de rafraîchissement set list + Inconnu Songs In Set List + Cette liste de jeu contient %1$d chansons inconnues, y compris: + Non {set} trouvée dans le fichier de jeu \"%1$s\". + Toujours + Jamais + Set Lists Seulement + Lecture automatique Chanson suivante + Lecture automatique Chanson suivante + Si le morceau suivant démarre automatiquement lorsque le morceau en cours se termine? + Affiche la liste des chansons dans un texte plus grand. + Bigger text + Divers + Accès %1$s + Ajouter à la liste d\'ensemble temporaire + Tag mal formé. + Le canal MIDI doit être le paramètre de charge + La valeur chaîne spécificateur ne peut pas contenir de soulignement. + Setlist temporaire effacer + BeatPrompter lit tous vos fichiers de chanson, vos fichiers audio, vos listes de sets et plus encore à partir du stockage en nuage ou local. Il supporte actuellement Google Drive, Dropbox ou Microsoft OneDrive. + Se connecter au chef + Par défaut MIDI Alias Set + Le tag est vide. + Forcer l\'actualisation du fichier d\'alias MIDI + Les valeurs des canaux MIDI doivent être comprises entre 1 et 16. + Index d\'argument d\'alias MIDI non valide. + Avec un peu de modification à vos fichiers de chanson, BeatPrompter gardera l\'affichage parfaitement dans le temps avec le battement. Jamais trop vite, jamais trop lent, et toujours agréable et grand et lisible! + Chargement: + Seul le nibble inférieur d\'une valeur peut être fusionné avec le spécificateur de canal. + Toujours + Jamais + En mode pause ou à l\'écran titre + Lorsqu\'il est en pause, à l\'écran titre ou à la dernière ligne de la chanson + Quand à l\'écran titre + Erreurs de fichier d\'alias MIDI + Alias MIDI + Alias MIDI + MIDI alias message trouvé avec plus de deux parties. + Une balise \"midi_alias\" contient plus de deux parties. + Alias MIDI trouvé sans nom. + Impossible d\'analyser ce message MIDI. + Le décalage d\'événement MIDI le place avant le début du morceau. + Les balises MIDI peuvent contenir au maximum un caractère de point-virgule. + Une valeur d\'alias ne peut contenir plusieurs soulignements. + Le décalage MIDI doit être une valeur numérique ou des caractères de décalage de battement. + Le fichier \"%1$s\" n\'est pas un fichier d\'aliasing MIDI valide. + Pas assez de paramètres fournis à la commande MIDI. + BeatPrompter n\'a pas seulement des paroles et des accords. Il peut lire des morceaux d\'accompagnement, et contrôler (et réagir) à des dispositifs externes de MIDI aussi. + Powerwash complet. + Quel est le canal MIDI par défaut à écrire, sauf indication contraire? + Sortie par défaut Canal MIDI + Quels canaux MIDI TunePrompter devrait-il écouter? + Canaux MIDI entrants actifs + MIDI déclenche l\'affichage des morceaux + Les messages de déclenchement MIDI peuvent-ils interrompre le morceau en cours? + Déclencheur de sécurité + Efface tous les fichiers téléchargés et toutes les informations d\'identification du nuage stockées. + Lavage à haute pression + La couleur du marqueur qui s\'affiche lorsque l\'affichage défile. + Couleur du marqueur de défilement + Sortie message de déclenchement MIDI + Le message de déclenchement MIDI devrait-il être émis lorsque le morceau commence? + Sortie Gâchette MIDI + Traitement de la chanson: %1$s + Toujours + Lorsque démarré manuellement + Jamais + Afficher les erreurs de fichier d\'alias MIDI + Les variables de déclenchement de changement de programme doivent contenir entre une et quatre valeurs. + Les variables de déclenchement de sélection de morceau ne doivent contenir qu\'une seule valeur. + Temporaire + Les valeurs qui contiennent des caractères de soulignement doivent être en hexadécimal. + Tag "%1$s" ne correspond à aucune commande MIDI et alias connus. + Les paramètres MIDI doivent être compris entre 0 et 127. + Bienvenue sur BeatPrompter + BeatPrompter est un paroles et des accords application prompter qui met l\'accent sur le calendrier et la lisibilité. + Bien que vous puissiez l\'utiliser en mode portrait, BeatPrompter fonctionne mieux en mode paysage. + L\'événement MIDI décalage maximum est de 16 battements ou 10 secondes. + Seules les balises MIDI qui sont sur ou après la première ligne de la chanson peuvent avoir des décalages. + {t:BeatPrompter Morceau De Démo}\n {st:Morceau De Démo}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -272,162 +272,166 @@ Paroles et [Em]accords dans le temps avec un battement.\n [D]Consultez la documentation pour plus de détails.\n [G]www.beatprompter.co.uk\n [Em]Je vous espère comme ça! - API OneDrive non connectée. - Cache effacé. - Dossiers - Aucun système de stockage n\'a été sélectionné. - Aucun système de stockage n\'a été sélectionné. - Vider le cache - Supprime tous les fichiers téléchargés. - Système de stockage - Où sont stockés vos fichiers de morceaux? - Système de stockage - Synchroniser des fichiers … - Effacer les balises lorsque le filtre change? - Si la valeur est true, tous les filtres de tags sélectionnés seront effacés chaque fois que vous changerez de dossier/setlist/etc. - Dossier de stockage - C\'est le dossier dans lequel les fichiers de chansons BeatPrompter sont stockés. - %1$d objets trouvés. - %1$d/%2$d objets trouvés. - Chargement … - Band Leader (serveur) - Membre du groupe (client) - Aucun - Connexion perdue au chef de bande - s\'est déconnecté - Mode Bluetooth - Configurez la façon dont votre appareil communique avec les membres de la bande. - Mode Bluetooth - Rechercher des fichiers à partir de sous-dossiers aussi? - Inclure les sous-dossiers - Force le rafraîchissement (y compris les dépendances) - Par clé - Impossible de trouver le fichier image: %1$s - Impossible de lire le fichier image: %1$s - Clé - Sur quand il n\'y a pas de piste de soutien - Plusieurs images trouvées en une seule ligne. Utiliser uniquement le premier. - Dans la liste des chansons principales, affiche la clé de la chanson. - Afficher la touche de musique - Afficher le BPM de la chanson - Le BPM initial de la chanson doit-il être affiché sur l\'écran de titre de la chanson? - Afficher le BPM de la chanson sur l\'écran de titre - Affiche la touche de la chanson sur l\'écran de titre de la chanson. - Afficher la touche de musique sur l\'écran de titre. - Non - Oui - Oui, arrondis en entier - Le texte a été trouvé sur une ligne où une image a été spécifiée. Ignorer le texte. - Mode d\'échelle d\'image inconnue, défaillant au mode étirement. - Une erreur s\'est produite lors de la communication avec le service de stockage (\"%1$s\").\\n\\nTous les fichiers n\'ont pas été récupérés ou analysés avec succès.\\n\\nVérifiez votre connexion et réessayez. - Erreur de Synchronisation - Affichage du leader du groupe Mimic en mode manuel - Si vous jouez un morceau avec un chef de groupe en mode manuel, l\'affichage sera automatiquement réglé sur la même orientation et la même taille de police. - Taille de police minimale (défilement manuel) - Taille maximale de la police (défilement manuel) - La plus petite taille de police qui sera utilisée en mode de défilement manuel. - La plus grande taille de police qui sera utilisée en mode de défilement manuel. - Le capteur de proximité fait défiler la page. - Si votre appareil est doté d\'un capteur de proximité, le déclenchera comme appuyer sur une pédale de descente de la page. - Stockage Local - Connecté à %1$s - Connexion perdue avec %1$s - Le nom du jeu d\'alias MIDI a déjà été défini. - Le nom de l\'ensemble a déjà été défini. - L\'image après la mise à l\'échelle dépasse la taille maximale de 8192 x 8192. Ligne ignorée. - Un alias MIDI a été défini avec seulement un nom. - Un alias MIDI ne peut avoir qu\'une seule valeur de canal. - Instructions trouvées, mais aucun nom d\'alias MIDI n\'a encore été défini. - Un fichier d\'alias MIDI doit contenir une définition de nom de jeu {midi_aliases}. - Il y a plusieurs fichiers dans le cache qui correspondent au nom de fichier \'{%1$s}\'. - Balise inattendue dans le fichier: {%1$s} - Une balise {%1$s} a été trouvée sur la même ligne qu\'une balise {%2$s}. Ceci n\'est pas autorisé - Les deux <bars> balises et virgules ont été trouvées sur la même ligne. Utilisation de la valeur de la balise. - La balise à usage unique {%1$s} a été utilisée plusieurs fois dans ce fichier. - La balise {%1$s} a été utilisée plusieurs fois dans cette ligne. - La balise {%1$s} a été utilisée plusieurs fois consécutives sans balise de fin intermédiaire. - La balise {%1$s} a été trouvée avant une balise de départ correspondante. - La balise {%1$s} a été trouvée sans valeur spécifiée. - Aucun nom de jeu défini dans le fichier de liste de jeux. - Plusieurs tags Beatstart ou Beatstop ont été trouvés sur la même ligne. Ignorer tout sauf le premier. - Impossible de trouver le dossier racine du système de stockage. - Une balise Beatstart ne peut être utilisée que dans une chanson avec une vitesse de BPM définie. - Balise inattendue: %1$s. - %1$s n\'est pas un fichier audio valide. - Numérisation %1$s - Erreur de lecture du fichier. - Couleur de l\'indicateur de position - Couleur pour les indicateurs de position de défilement de page. - Couleur d\'accompagnement du début de la section rythmique - Mettez en surbrillance la couleur de la ligne qui est le début d’une section de temps. - Mettre en évidence le début des sections de temps - Met en évidence le début des sections de temps avec la couleur de surbrillance choisie. - Afficher les marqueurs de position de défilement de la page - Indique la position sur laquelle l\'écran fera défiler lorsque vous appuierez sur la page vers le bas. - Dispositif de chef de bande - Spécifiez le périphérique auquel se connecter en mode membre de la bande. - Dispositif de chef de bande - * Impossible de trouver le fichier image * - Valeur vide trouvée dans la directive MIDI. - Index d\'argument non valide trouvé dans la directive MIDI. - Valeur d\'octet invalide trouvée dans la directive MIDI. - Le fichier de chanson sélectionné n\'a pas de lignes. - Instructions … - Toute autre touche est en bas de page. - Toute autre touche enfoncée sur un clavier connecté agira comme une page vers le bas. - Bluetooth - Originaire - Section de reflet couleur de surbrillance - Mettez en surbrillance la couleur pour les refrains. - Type de Connexion - Type de Cconnexion - Quel type de connexion MIDI utilisez-vous? - Muet - Ne jamais jouer de l\'audio - Mélanger - Politique de confidentialité … - Autorisez BeatPrompter à communiquer avec d’autres appareils via Bluetooth. - "Situation actuelle: " - Accordée - Nié - Autorisations - Audio - Autorisez BeatPrompter à lire les fichiers des dossiers locaux. - Lecteur audio - Quel lecteur l’application doit-elle utiliser pour la lecture audio? - Impossible de s’authentifier pour accéder à Google Drive. - Par mode - Achetez-moi un café … - Erreur - Obtention du dossier racine … - Afficher la classification de la chanson - Zeigen Sie in der Hauptsongliste die Bewertung des Songs an. - Par évaluation - Variation - Pas de son - Les noms de variantes ont déjà été définis. - Les balises audio sont plus nombreuses que les variations. - Activer le mode sombre - Base de données de lecture … - Échec de la lecture de l\'élément de base de données - Quantité de latence à compenser lors de la lecture audio. - Latence audio - Synchronisation %1$s (recommencez %2$d/%3$d) - Appareils MIDI Bluetooth - Quels appareils Bluetooth faut-il prendre en compte pour le MIDI ? - Appareils MIDI Bluetooth - Une erreur s\'est produite lors de la lecture de la base de données. - Une erreur s\'est produite lors de l\'écriture de la base de données. - Reconstruction de la base de données... - Les directives with_midi_* ne peuvent pas être utilisées dans des alias contenant des paramètres - Affichez toujours les accords dièses - Tous les accords plats seront convertis en accords dièses appropriés. - Afficher les altérations Unicode - Les accords contenant des caractères b/# seront convertis pour utiliser ♭/♯. - \'%1$s\' n\'a pas pu être analysé comme une clé valide - Échec du calcul de la nouvelle tonalité (décalage de %1$s de %2$d demi-tons) - Échec de l\'analyse de l\'accord \'%1$s\' - La balise {transpose} ne doit pas contenir une valeur numérique supérieure à 11 ou inférieure à -11 - La balise {chord_map} doit contenir deux valeurs séparées par = - Transposer + API OneDrive non connectée. + Cache effacé. + Dossiers + Aucun système de stockage n\'a été sélectionné. + Aucun système de stockage n\'a été sélectionné. + Vider le cache + Supprime tous les fichiers téléchargés. + Système de stockage + Où sont stockés vos fichiers de morceaux? + Système de stockage + Synchroniser des fichiers … + Effacer les balises lorsque le filtre change? + Si la valeur est true, tous les filtres de tags sélectionnés seront effacés chaque fois que vous changerez de dossier/setlist/etc. + Dossier de stockage + C\'est le dossier dans lequel les fichiers de chansons BeatPrompter sont stockés. + %1$d objets trouvés. + %1$d/%2$d objets trouvés. + Chargement … + Band Leader (serveur) + Membre du groupe (client) + Aucun + Connexion perdue au chef de bande + s\'est déconnecté + Mode Bluetooth + Configurez la façon dont votre appareil communique avec les membres de la bande. + Mode Bluetooth + Rechercher des fichiers à partir de sous-dossiers aussi? + Inclure les sous-dossiers + Force le rafraîchissement (y compris les dépendances) + Par clé + Impossible de trouver le fichier image: %1$s + Impossible de lire le fichier image: %1$s + Clé + Sur quand il n\'y a pas de piste de soutien + Plusieurs images trouvées en une seule ligne. Utiliser uniquement le premier. + Dans la liste des chansons principales, affiche la clé de la chanson. + Afficher la touche de musique + Afficher le BPM de la chanson + Le BPM initial de la chanson doit-il être affiché sur l\'écran de titre de la chanson? + Afficher le BPM de la chanson sur l\'écran de titre + Affiche la touche de la chanson sur l\'écran de titre de la chanson. + Afficher la touche de musique sur l\'écran de titre. + Non + Oui + Oui, arrondis en entier + Le texte a été trouvé sur une ligne où une image a été spécifiée. Ignorer le texte. + Mode d\'échelle d\'image inconnue, défaillant au mode étirement. + Une erreur s\'est produite lors de la communication avec le service de stockage (\"%1$s\").\\n\\nTous les fichiers n\'ont pas été récupérés ou analysés avec succès.\\n\\nVérifiez votre connexion et réessayez. + Erreur de Synchronisation + Affichage du leader du groupe Mimic en mode manuel + Si vous jouez un morceau avec un chef de groupe en mode manuel, l\'affichage sera automatiquement réglé sur la même orientation et la même taille de police. + Taille de police minimale (défilement manuel) + Taille maximale de la police (défilement manuel) + La plus petite taille de police qui sera utilisée en mode de défilement manuel. + La plus grande taille de police qui sera utilisée en mode de défilement manuel. + Le capteur de proximité fait défiler la page. + Si votre appareil est doté d\'un capteur de proximité, le déclenchera comme appuyer sur une pédale de descente de la page. + Stockage Local + Connecté à %1$s + Connexion perdue avec %1$s + Le nom du jeu d\'alias MIDI a déjà été défini. + Le nom de l\'ensemble a déjà été défini. + L\'image après la mise à l\'échelle dépasse la taille maximale de 8192 x 8192. Ligne ignorée. + Un alias MIDI a été défini avec seulement un nom. + Un alias MIDI ne peut avoir qu\'une seule valeur de canal. + Instructions trouvées, mais aucun nom d\'alias MIDI n\'a encore été défini. + Un fichier d\'alias MIDI doit contenir une définition de nom de jeu {midi_aliases}. + Il y a plusieurs fichiers dans le cache qui correspondent au nom de fichier \'{%1$s}\'. + Balise inattendue dans le fichier: {%1$s} + Une balise {%1$s} a été trouvée sur la même ligne qu\'une balise {%2$s}. Ceci n\'est pas autorisé + Les deux <bars> balises et virgules ont été trouvées sur la même ligne. Utilisation de la valeur de la balise. + La balise à usage unique {%1$s} a été utilisée plusieurs fois dans ce fichier. + La balise {%1$s} a été utilisée plusieurs fois dans cette ligne. + La balise {%1$s} a été utilisée plusieurs fois consécutives sans balise de fin intermédiaire. + La balise {%1$s} a été trouvée avant une balise de départ correspondante. + La balise {%1$s} a été trouvée sans valeur spécifiée. + Aucun nom de jeu défini dans le fichier de liste de jeux. + Plusieurs tags Beatstart ou Beatstop ont été trouvés sur la même ligne. Ignorer tout sauf le premier. + Impossible de trouver le dossier racine du système de stockage. + Une balise Beatstart ne peut être utilisée que dans une chanson avec une vitesse de BPM définie. + Balise inattendue: %1$s. + %1$s n\'est pas un fichier audio valide. + Numérisation %1$s + Erreur de lecture du fichier. + Couleur de l\'indicateur de position + Couleur pour les indicateurs de position de défilement de page. + Couleur d\'accompagnement du début de la section rythmique + Mettez en surbrillance la couleur de la ligne qui est le début d’une section de temps. + Mettre en évidence le début des sections de temps + Met en évidence le début des sections de temps avec la couleur de surbrillance choisie. + Afficher les marqueurs de position de défilement de la page + Indique la position sur laquelle l\'écran fera défiler lorsque vous appuierez sur la page vers le bas. + Dispositif de chef de bande + Spécifiez le périphérique auquel se connecter en mode membre de la bande. + Dispositif de chef de bande + * Impossible de trouver le fichier image * + Valeur vide trouvée dans la directive MIDI. + Index d\'argument non valide trouvé dans la directive MIDI. + Valeur d\'octet invalide trouvée dans la directive MIDI. + Le fichier de chanson sélectionné n\'a pas de lignes. + Instructions … + Toute autre touche est en bas de page. + Toute autre touche enfoncée sur un clavier connecté agira comme une page vers le bas. + Bluetooth + Originaire + Section de reflet couleur de surbrillance + Mettez en surbrillance la couleur pour les refrains. + Type de Connexion + Type de Cconnexion + Quel type de connexion MIDI utilisez-vous? + Muet + Ne jamais jouer de l\'audio + Mélanger + Politique de confidentialité … + Autorisez BeatPrompter à communiquer avec d’autres appareils via Bluetooth. + "Situation actuelle: " + Accordée + Nié + Autorisations + Audio + Autorisez BeatPrompter à lire les fichiers des dossiers locaux. + Lecteur audio + Quel lecteur l’application doit-elle utiliser pour la lecture audio? + Impossible de s’authentifier pour accéder à Google Drive. + Par mode + Achetez-moi un café … + Erreur + Obtention du dossier racine … + Afficher la classification de la chanson + Zeigen Sie in der Hauptsongliste die Bewertung des Songs an. + Par évaluation + Variation + Pas de son + Les noms de variantes ont déjà été définis. + Les balises audio sont plus nombreuses que les variations. + Activer le mode sombre + Base de données de lecture … + Échec de la lecture de l\'élément de base de données + Quantité de latence à compenser lors de la lecture audio. + Latence audio + Synchronisation %1$s (recommencez %2$d/%3$d) + Appareils MIDI Bluetooth + Quels appareils Bluetooth faut-il prendre en compte pour le MIDI ? + Appareils MIDI Bluetooth + Une erreur s\'est produite lors de la lecture de la base de données. + Une erreur s\'est produite lors de l\'écriture de la base de données. + Reconstruction de la base de données... + Les directives with_midi_* ne peuvent pas être utilisées dans des alias contenant des paramètres + Affichez toujours les accords dièses + Tous les accords plats seront convertis en accords dièses appropriés. + Afficher les altérations Unicode + Les accords contenant des caractères b/# seront convertis pour utiliser ♭/♯. + \'%1$s\' n\'a pas pu être analysé comme une clé valide + Échec du calcul de la nouvelle tonalité (décalage de %1$s de %2$d demi-tons) + Échec de l\'analyse de l\'accord \'%1$s\' + Échec de l\'analyse de la note \'%1$s\' + La balise {transpose} ne doit pas contenir une valeur numérique supérieure à 11 ou inférieure à -11 + La balise {chord_map} doit contenir deux valeurs séparées par = + Transposer + La balise varstart/varxstart contient des variantes inconnues : %1$s + Variation préférée + Si une chanson contient cette variation, elle sera chargée par défaut. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 11162fab..263b1714 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,252 +1,252 @@  - BeatPrompter - … E %1$d altro errore(s). - Di … - Tutte le canzoni - Utilizza sempre colori predefiniti. - Offset inizio traccia audio - Volume della traccia audio non è un numero compreso tra 0 e 100. - Un valore è già stato definito per {%1$s}. - Colore di sfondo - Impulso sfondo - Il colore di sfondo impulso - Base musicale - barre - Colore beatcounter - Battere scorrimento - BPB - BPL - BPM - Con l\'artista - Per data - Per titolo - Annulla - Impossibile trovare il file audio: %1$s - Centro - Controllo %1$s - Scegli cartella Sync - Colore Chords - Cancella cache - Colori - Impossibile trovare il file audio. - Impossibile caricare il file audio. E \'mancante o danneggiato? - Impossibile analizzare "%1$s" come colore. - Impossibile analizzare "%1$s" come un valore di durata (uso mm:ss o pochi secondi). - Impossibile analizzare "%1$s" come un valore intero. - Impossibile analizzare "%1$s" come un valore numerico. - Creato da - Crea uno scatto a tempo con il ritmo. Applicabile solo a battere a scorrimento canzoni. - Predefinite bar-per-line - Predefinite beat-per-bar - Count-in di default - Pausa iniziale di default - Tempo di default - Eliminazione %1$s - La documentazione è disponibile all\'indirizzo - Sincronizzazione %1$s - Sincronizzazione di file - Durante count-in - Il file "%1$s" non trovato! - Altezza della linea fissa - Dimensioni carattere - Aggiornamento Force (file di song only) - Colore di evidenziazione - Evidenziare colore da applicare al testo evidenziato lirica. - Se un brano viene mostrato in "modalità di scorrimento uniforme", questa impostazione crea una pausa automatica all\'inizio del brano, in considerazione in la lunghezza totale canzone. - Se un tempo non è definito da un file, verrà utilizzato questa velocità. - Se bar-per-line non è definito da un file, verrà utilizzato questo valore. - Se battiti al bar non è definito da un file, verrà utilizzato questo valore. - Se il numero di "count-in" barre non è definito da un file, verrà utilizzato questo valore. Applicabile solo a battere a scorrimento canzoni. - Ignora i colori nei file - Nella sezione beatcounter, evidenziare il ritmo che il display scorrerà su. - Nell\'elenco canzone principale, mostrerà un\'icona che indica che una canzone ha una base musicale. - Saltare - Sinistra - Linea %1$d era più lungo di %2$d caratteri. Troncando … - Giustificazione linea - Linea Giustificazione - linee - Colore della canzone - Rende tutte le righe della stessa altezza - La dimensione massima dei caratteri ( beat- scrolling) - Altezza testi Massimo - Il valore massimo è %1$d, valore trovato era %2$d. - Il valore massimo è %1$d, valore trovato era %2$.2f. - Metronomo - Metronomo - Dimensione minima del carattere - Il valore minimo è %1$d, valore trovato era %2$d. - Il valore minimo è %1$d, valore trovato era %2$.2f. - Linee minimi visibili - ms - No {title} trovati nel file del brano "%1$s" - No base musicale - Via - Sopra - Giocare - Giocare … - Opzioni di Giocare - Destra - Scrollbeat offset è maggiore o uguale al numero di battute al bar. Ripristino a zero. - Modalità di scorrimento - Stile di scorrimento - Stile di Scorrimento - secondi - Impostazioni - Impostazioni … - Icona di spettacolo musicale - Mostra indicatore di scorrimento. - Icone di mostrare lo stile di scorrimento - Mostra lo stile di scorrimento della canzone come icona nella lista principale. - Liscio - Liscio Scorrimento - Opzioni Canzone - Ordina … - Ordina canzone … - TAP DUE VOLTE PER AVVIARE - il - Lo sfondo sarà impulsi in tempo per il ritmo. - Lo sfondo sarà pulsare questo colore nel tempo al ritmo. - Il colore della barra beatcounter nella parte superiore dello schermo. - Il più grande dimensione del font che verrà utilizzato in modalità battito -scrolling. - La percentuale massima dell\'altezza massima della linea che i testi occuperanno. - La preferenza minima dimensione del carattere è più grande della dimensione massima dei caratteri. Utilizzando minima per tutti. - Il numero minimo di righe che deve essere visibile in qualsiasi momento. - L\'offset dall\'inizio del brano, in millisecondi, in cui inizierà la traccia audio di supporto. - La dimensione più piccola di carattere che verrà utilizzato. - {pause} tempo totale supera ora canzone designato. Disabilitare la modalità di scorrimento uniforme. - Il più grande dimensione del font che verrà utilizzato in modalità liscia-scrolling. - Font size massima (scorrimento continuo) - Utilizza le preferenze relative alle dimensioni dei caratteri battere a scorrimento per tutte le canzoni. - Usare sempre le preferenze relative alle dimensioni dei caratteri Beat-scrolling. - La più piccola dimensione del font che verrà utilizzato in modalità liscia-scrolling. - Font size minima (scorrimento continuo) - Compra la versione completa - Collegato al leader della band - è collegato - Si prega di acquistare BeatPrompter - Qualsiasi testo accordo che non viene riconosciuta come un accordo valido verrà mostrato in questo colore. - Colore di annotazioni non corda - Per vedere solo i commenti che sono pensati per voi sullo schermo introduzione canzone , inserisci il tuo nome(s) qui. - Commenti personalizzate per l\'utente - Se non diversamente specificato nel file , questo è il volume di default che basi giocheranno a. - Volume della traccia di default - Controlli durante la performance - Quali controlli dovrebbe essere disponibile , mentre un brano è in riproduzione? - Controlli durante la performance - Niente - Scorrimento, pausa, riavvio - Volume - Grazie! Sei ufficialmente una brava persona! :) - Memorizza i dati su storage esterno (SD card, ecc ). La modifica di questo sarà cancellare la cache. - Usa External Storage - Utilizzare personalizzato posizione di archiviazione - Wenn diese Option ausgewählt , werden Song-Daten in den benutzerdefinierten Ort gespeichert werden. Dies zu ändern, wird den Cache löschen. - Dove dovrebbe BeatPrompter negozio scaricato file audio canzone e ? La modifica di questo sarà cancellare la cache. - Posizione di archiviazione - Usa Storage interno - Manuale di scorrimento - Se una canzone ha dei tempi di battuta, questo invierà segnali di clock MIDI. - Invia segnali di clock MIDI - Per impostazione predefinita, le canzoni giocano sempre in modalità di scorrimento manuale senza base musicale. - Modalità manuale - API di Google non è collegato. - La quantità di tempo che un commento in-canzone rimarrà sullo schermo. - Tempo di visualizzazione commento - Colore del testo commento - Evidenziare colore da applicare alla linea di corrente. - Linea corrente Evidenziare colore - Mette in evidenza la linea corrente con il colore di evidenziazione prescelto. - Evidenziare la linea corrente - Consente di visualizzare gli accordi delle canzoni i testi nel punto giusto sopra. - Mostra Chords - Indica la prima battuta nella sezione dell\'indicatore battito. - Mostra prima battuta. - Consente di visualizzare il titolo del brano corrente sopra la parte superiore della sezione beatcounter. - Mostra il titolo del brano. - Opzioni di visualizzazione canzone - Canzone Opzioni elenco - Forza set list di aggiornamento - Desconocido Songs In Set List - Questo elenco set contiene %1$d brani inediti, tra cui: - No {set} trovato nel file set \"%1$s\". - Sempre - Mai - Solo Set liste - Gioca automatico alla prossima canzone - Gioca automatico alla prossima canzone - Se il brano successivo avvia automaticamente quando il brano corrente? - Consente di visualizzare l\'elenco dei brani in testo più ampio. - Testo più grande - Miscellaneo - Accesso %1$s - Aggiungi alla lista insieme temporanea - Tag mal-formata. - Il canale MIDI deve essere l\'ultimo parametro - Il valore del canale identificatore non può contenere sottolineature. - Setlist temporanea chiaro - BeatPrompter legge tutti i tuoi file di canzoni, file audio, set di elenchi e altro da cloud o archiviazione locale. Attualmente supporta Google Drive, Dropbox o Microsoft OneDrive. - Collegare al leader - Predefinito MIDI Alias Set - Tag è vuoto. - Forza aggiornare file alias MIDI - Valori dei canali MIDI devono essere compresi tra 1 e 16. - MIDI indice argomento alias non valido. - Con un po \'di modifica ai file di canzoni, BeatPrompter manterrà il display perfettamente a tempo con il ritmo. Mai troppo veloce, non troppo lento, e sempre bella e grande e leggibile! - Caricamento: - Solo il nibble inferiore di un valore può essere fusa con l\'identificatore di canale. - Sempre - Mai - Durante la pausa, o lo schermo del titolo - Durante la pausa, lo schermo del titolo, o in ultima linea di canto - Quando alla schermata dei titoli - MIDI Alias errori file - Alias MIDI - Alias MIDI - MIDI messaggio alias trovato con più di due parti. - Un tag \"midi_alias\" contiene più di due parti. - Alias MIDI trovato con nessun nome. - Impossibile analizzare questo messaggio MIDI. - Compensati evento MIDI mette prima dell\'inizio del brano. - Tag MIDI possono contenere un massimo di un punto e virgola. - Un valore alias non può contenere più di sottolineatura. - MIDI compensato dovrebbe essere un valore numerico o battere caratteri offset. - Il file \"%1$s\" non è un file MIDI aliasing valido. - Non abbastanza parametri forniti al comando MIDI. - BeatPrompter non si limita a visualizzare testi e accordi. Può riprodurre basi, e il controllo (e rispondere a) dispositivi MIDI esterni troppo. - Powerwash completa. - Qual è il canale MIDI di default di scrivere, se non diversamente specificato? - Canale di uscita MIDI di default - Quali sono i canali MIDI dovrebbe TunePrompter ascoltare? - Attivi canali MIDI in arrivo - MIDI attiva durante la visualizzazione canzone - Possono messaggi trigger MIDI interrompere il brano corrente? - Chiusura di sicurezza trigger - Cancella tutti i file scaricati, e tutte le credenziali memorizzate nube. - Powerwash - Il colore del marcatore ha fatto spettacoli in cui il display scorre. - Colore dell\'indicatore di scorrimento - Uscita MIDI messaggio trigger - Se il messaggio MIDI grilletto essere uscita quando la canzone inizia? - Uscita trigger MIDI - Canzone elaborazione: %1$s - Sempre - Quando viene avviato manualmente - Mai - Errori di file alias visualizza MIDI - Program change tag di trigger devono contenere da uno a quattro valori. - Canzone tag selezionate di trigger devono contenere un solo valore. - Temporaneo - I valori che contengono trattini devono essere in formato esadecimale. - Tag "%1$s" non corrisponde a tutti i comandi MIDI noti e alias. - Parametri MIDI deve essere un valore compreso tra 0 e 127. - Benvenuti a BeatPrompter - BeatPrompter è un testo e gli accordi suggeritore app che pone l\'accento sulla tempistica e la leggibilità. - Anche se è possibile utilizzarlo in modalità verticale, BeatPrompter funziona meglio in modalità orizzontale. - L\'offset evento MIDI massima è di 16 battiti o 10 secondi. - Solo i tag MIDI che sono a partire dalla prima riga canzone può avere compensazioni. - {t:BeatPrompter Demo Song}\n + BeatPrompter + … E %1$d altro errore(s). + Di … + Tutte le canzoni + Utilizza sempre colori predefiniti. + Offset inizio traccia audio + Volume della traccia audio non è un numero compreso tra 0 e 100. + Un valore è già stato definito per {%1$s}. + Colore di sfondo + Impulso sfondo + Il colore di sfondo impulso + Base musicale + barre + Colore beatcounter + Battere scorrimento + BPB + BPL + BPM + Con l\'artista + Per data + Per titolo + Annulla + Impossibile trovare il file audio: %1$s + Centro + Controllo %1$s + Scegli cartella Sync + Colore Chords + Cancella cache + Colori + Impossibile trovare il file audio. + Impossibile caricare il file audio. E \'mancante o danneggiato? + Impossibile analizzare "%1$s" come colore. + Impossibile analizzare "%1$s" come un valore di durata (uso mm:ss o pochi secondi). + Impossibile analizzare "%1$s" come un valore intero. + Impossibile analizzare "%1$s" come un valore numerico. + Creato da + Crea uno scatto a tempo con il ritmo. Applicabile solo a battere a scorrimento canzoni. + Predefinite bar-per-line + Predefinite beat-per-bar + Count-in di default + Pausa iniziale di default + Tempo di default + Eliminazione %1$s + La documentazione è disponibile all\'indirizzo + Sincronizzazione %1$s + Sincronizzazione di file + Durante count-in + Il file "%1$s" non trovato! + Altezza della linea fissa + Dimensioni carattere + Aggiornamento Force (file di song only) + Colore di evidenziazione + Evidenziare colore da applicare al testo evidenziato lirica. + Se un brano viene mostrato in "modalità di scorrimento uniforme", questa impostazione crea una pausa automatica all\'inizio del brano, in considerazione in la lunghezza totale canzone. + Se un tempo non è definito da un file, verrà utilizzato questa velocità. + Se bar-per-line non è definito da un file, verrà utilizzato questo valore. + Se battiti al bar non è definito da un file, verrà utilizzato questo valore. + Se il numero di "count-in" barre non è definito da un file, verrà utilizzato questo valore. Applicabile solo a battere a scorrimento canzoni. + Ignora i colori nei file + Nella sezione beatcounter, evidenziare il ritmo che il display scorrerà su. + Nell\'elenco canzone principale, mostrerà un\'icona che indica che una canzone ha una base musicale. + Saltare + Sinistra + Linea %1$d era più lungo di %2$d caratteri. Troncando … + Giustificazione linea + Linea Giustificazione + linee + Colore della canzone + Rende tutte le righe della stessa altezza + La dimensione massima dei caratteri ( beat- scrolling) + Altezza testi Massimo + Il valore massimo è %1$d, valore trovato era %2$d. + Il valore massimo è %1$d, valore trovato era %2$.2f. + Metronomo + Metronomo + Dimensione minima del carattere + Il valore minimo è %1$d, valore trovato era %2$d. + Il valore minimo è %1$d, valore trovato era %2$.2f. + Linee minimi visibili + ms + No {title} trovati nel file del brano "%1$s" + No base musicale + Via + Sopra + Giocare + Giocare … + Opzioni di Giocare + Destra + Scrollbeat offset è maggiore o uguale al numero di battute al bar. Ripristino a zero. + Modalità di scorrimento + Stile di scorrimento + Stile di Scorrimento + secondi + Impostazioni + Impostazioni … + Icona di spettacolo musicale + Mostra indicatore di scorrimento. + Icone di mostrare lo stile di scorrimento + Mostra lo stile di scorrimento della canzone come icona nella lista principale. + Liscio + Liscio Scorrimento + Opzioni Canzone + Ordina … + Ordina canzone … + TAP DUE VOLTE PER AVVIARE + il + Lo sfondo sarà impulsi in tempo per il ritmo. + Lo sfondo sarà pulsare questo colore nel tempo al ritmo. + Il colore della barra beatcounter nella parte superiore dello schermo. + Il più grande dimensione del font che verrà utilizzato in modalità battito -scrolling. + La percentuale massima dell\'altezza massima della linea che i testi occuperanno. + La preferenza minima dimensione del carattere è più grande della dimensione massima dei caratteri. Utilizzando minima per tutti. + Il numero minimo di righe che deve essere visibile in qualsiasi momento. + L\'offset dall\'inizio del brano, in millisecondi, in cui inizierà la traccia audio di supporto. + La dimensione più piccola di carattere che verrà utilizzato. + {pause} tempo totale supera ora canzone designato. Disabilitare la modalità di scorrimento uniforme. + Il più grande dimensione del font che verrà utilizzato in modalità liscia-scrolling. + Font size massima (scorrimento continuo) + Utilizza le preferenze relative alle dimensioni dei caratteri battere a scorrimento per tutte le canzoni. + Usare sempre le preferenze relative alle dimensioni dei caratteri Beat-scrolling. + La più piccola dimensione del font che verrà utilizzato in modalità liscia-scrolling. + Font size minima (scorrimento continuo) + Compra la versione completa + Collegato al leader della band + è collegato + Si prega di acquistare BeatPrompter + Qualsiasi testo accordo che non viene riconosciuta come un accordo valido verrà mostrato in questo colore. + Colore di annotazioni non corda + Per vedere solo i commenti che sono pensati per voi sullo schermo introduzione canzone , inserisci il tuo nome(s) qui. + Commenti personalizzate per l\'utente + Se non diversamente specificato nel file , questo è il volume di default che basi giocheranno a. + Volume della traccia di default + Controlli durante la performance + Quali controlli dovrebbe essere disponibile , mentre un brano è in riproduzione? + Controlli durante la performance + Niente + Scorrimento, pausa, riavvio + Volume + Grazie! Sei ufficialmente una brava persona! :) + Memorizza i dati su storage esterno (SD card, ecc ). La modifica di questo sarà cancellare la cache. + Usa External Storage + Utilizzare personalizzato posizione di archiviazione + Wenn diese Option ausgewählt , werden Song-Daten in den benutzerdefinierten Ort gespeichert werden. Dies zu ändern, wird den Cache löschen. + Dove dovrebbe BeatPrompter negozio scaricato file audio canzone e ? La modifica di questo sarà cancellare la cache. + Posizione di archiviazione + Usa Storage interno + Manuale di scorrimento + Se una canzone ha dei tempi di battuta, questo invierà segnali di clock MIDI. + Invia segnali di clock MIDI + Per impostazione predefinita, le canzoni giocano sempre in modalità di scorrimento manuale senza base musicale. + Modalità manuale + API di Google non è collegato. + La quantità di tempo che un commento in-canzone rimarrà sullo schermo. + Tempo di visualizzazione commento + Colore del testo commento + Evidenziare colore da applicare alla linea di corrente. + Linea corrente Evidenziare colore + Mette in evidenza la linea corrente con il colore di evidenziazione prescelto. + Evidenziare la linea corrente + Consente di visualizzare gli accordi delle canzoni i testi nel punto giusto sopra. + Mostra Chords + Indica la prima battuta nella sezione dell\'indicatore battito. + Mostra prima battuta. + Consente di visualizzare il titolo del brano corrente sopra la parte superiore della sezione beatcounter. + Mostra il titolo del brano. + Opzioni di visualizzazione canzone + Canzone Opzioni elenco + Forza set list di aggiornamento + Desconocido Songs In Set List + Questo elenco set contiene %1$d brani inediti, tra cui: + No {set} trovato nel file set \"%1$s\". + Sempre + Mai + Solo Set liste + Gioca automatico alla prossima canzone + Gioca automatico alla prossima canzone + Se il brano successivo avvia automaticamente quando il brano corrente? + Consente di visualizzare l\'elenco dei brani in testo più ampio. + Testo più grande + Miscellaneo + Accesso %1$s + Aggiungi alla lista insieme temporanea + Tag mal-formata. + Il canale MIDI deve essere l\'ultimo parametro + Il valore del canale identificatore non può contenere sottolineature. + Setlist temporanea chiaro + BeatPrompter legge tutti i tuoi file di canzoni, file audio, set di elenchi e altro da cloud o archiviazione locale. Attualmente supporta Google Drive, Dropbox o Microsoft OneDrive. + Collegare al leader + Predefinito MIDI Alias Set + Tag è vuoto. + Forza aggiornare file alias MIDI + Valori dei canali MIDI devono essere compresi tra 1 e 16. + MIDI indice argomento alias non valido. + Con un po \'di modifica ai file di canzoni, BeatPrompter manterrà il display perfettamente a tempo con il ritmo. Mai troppo veloce, non troppo lento, e sempre bella e grande e leggibile! + Caricamento: + Solo il nibble inferiore di un valore può essere fusa con l\'identificatore di canale. + Sempre + Mai + Durante la pausa, o lo schermo del titolo + Durante la pausa, lo schermo del titolo, o in ultima linea di canto + Quando alla schermata dei titoli + MIDI Alias errori file + Alias MIDI + Alias MIDI + MIDI messaggio alias trovato con più di due parti. + Un tag \"midi_alias\" contiene più di due parti. + Alias MIDI trovato con nessun nome. + Impossibile analizzare questo messaggio MIDI. + Compensati evento MIDI mette prima dell\'inizio del brano. + Tag MIDI possono contenere un massimo di un punto e virgola. + Un valore alias non può contenere più di sottolineatura. + MIDI compensato dovrebbe essere un valore numerico o battere caratteri offset. + Il file \"%1$s\" non è un file MIDI aliasing valido. + Non abbastanza parametri forniti al comando MIDI. + BeatPrompter non si limita a visualizzare testi e accordi. Può riprodurre basi, e il controllo (e rispondere a) dispositivi MIDI esterni troppo. + Powerwash completa. + Qual è il canale MIDI di default di scrivere, se non diversamente specificato? + Canale di uscita MIDI di default + Quali sono i canali MIDI dovrebbe TunePrompter ascoltare? + Attivi canali MIDI in arrivo + MIDI attiva durante la visualizzazione canzone + Possono messaggi trigger MIDI interrompere il brano corrente? + Chiusura di sicurezza trigger + Cancella tutti i file scaricati, e tutte le credenziali memorizzate nube. + Powerwash + Il colore del marcatore ha fatto spettacoli in cui il display scorre. + Colore dell\'indicatore di scorrimento + Uscita MIDI messaggio trigger + Se il messaggio MIDI grilletto essere uscita quando la canzone inizia? + Uscita trigger MIDI + Canzone elaborazione: %1$s + Sempre + Quando viene avviato manualmente + Mai + Errori di file alias visualizza MIDI + Program change tag di trigger devono contenere da uno a quattro valori. + Canzone tag selezionate di trigger devono contenere un solo valore. + Temporaneo + I valori che contengono trattini devono essere in formato esadecimale. + Tag "%1$s" non corrisponde a tutti i comandi MIDI noti e alias. + Parametri MIDI deve essere un valore compreso tra 0 e 127. + Benvenuti a BeatPrompter + BeatPrompter è un testo e gli accordi suggeritore app che pone l\'accento sulla tempistica e la leggibilità. + Anche se è possibile utilizzarlo in modalità verticale, BeatPrompter funziona meglio in modalità orizzontale. + L\'offset evento MIDI massima è di 16 battiti o 10 secondi. + Solo i tag MIDI che sono a partire dalla prima riga canzone può avere compensazioni. + {t:BeatPrompter Demo Song}\n {st:Demo Song}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -272,162 +272,166 @@ testi e accordi [Em]nel tempo con una battuta.\n [D]Consultare la documentazione per i dettagli.\n [G]www.beatprompter.co.uk\n [Em]Io vi auguro di simile! - API Microsoft Onedrive non connesso. - Cache cancellata. - File - Nessun sistema di archiviazione è stato selezionato. - Nessun sistema di archiviazione è stato selezionato. - Elimina tutti i file scaricati. - Cancella cache - Sistema di archiviazione - Dove vengono memorizzati i file musicali? - Sistema di archiviazione - Sincronizza i file … - Cancella i tag quando cambia il filtro? - Se true, tutti i filtri tag selezionati verranno cancellati ogni volta che si cambia cartella/setlist/ecc. - Cartella di archiviazione - Questa è la cartella che i file dei brani BeatPrompter sono memorizzati in. - %1$d oggetti trovati. - %1$d/%2$d oggetti trovati. - Caricamento in corso … - Band Leader (server) - Membro Band (client) - Nessuna - Connessione persa al leader della band - ha disconnesso - Modalità Bluetooth - Configurare il modo in cui il tuo dispositivo comunica con i membri della band. - Modalità Bluetooth - Prendi anche i file dalle sottocartelle? - Includi le sottocartelle - Aggiorna forza (incluse le dipendenze) - Per chiave - Impossibile trovare il file immagine: %1$s - Impossibile leggere il file di immagine: %1$s - Chiave - Quando non c\'è traccia di supporto - Immagini multiple trovate in una sola riga. Utilizza solo la prima. - Nell\'elenco principale delle canzoni, mostri la chiave del brano. - Mostra la chiave del brano - Mostra Song BPM - Dovrebbe essere visualizzato il BPM iniziale della canzone sulla schermata del titolo della song? - Mostra la canzone BPM sulla schermata del titolo - Exibe a tecla da música na tela do título da música. - Mostra la chiave del brano nella schermata del titolo. - No - - Sì, arrotondato all\'intero - Il testo è stato trovato in una riga in cui è stata specificata un\'immagine. Ignorare il testo. - Modalità di scala immagine sconosciuta, in base alla modalità di stretching. - Si è verificato un errore durante la comunicazione con il servizio di archiviazione (\"%1$s\").\\n\\nNon tutti i file sono stati recuperati o analizzati correttamente.\\n\\nControllare la connessione e riprovare. - Errore di Sincronizzazione - Display mimic leader della band in modalità manuale - Se si sta eseguendo una canzone con una band leader in modalità manuale, questo imposterà automaticamente lo schermo con lo stesso orientamento e le stesse dimensioni dei caratteri. - Dimensione minima del carattere (scorrimento manuale) - Dimensione massima del carattere (scorrimento manuale) - La dimensione del carattere più piccola che verrà utilizzata nella modalità di scorrimento manuale. - La dimensione del carattere più grande che verrà utilizzata nella modalità di scorrimento manuale. - Il sensore di prossimità scorre la pagina. - Se il tuo dispositivo ha un sensore di prossimità, attivandolo agirà come premendo un pedale di scorrimento verso il basso. - Memoria Locale - Connesso a %1$s - Connessione persa a %1$s - Il nome del set di alias MIDI è già stato definito. - Il nome del set è già stato definito. - L\'immagine dopo il ridimensionamento supera la dimensione massima di 8192 x 8192. Linea di ignoranza. - Un alias MIDI è stato definito con un solo nome. - Un alias MIDI può avere solo un valore di canale. - Istruzioni trovate, ma nessun nome alias MIDI è stato ancora definito. - Un file alias MIDI deve contenere una definizione del nome del set {midi_aliases}. - Ci sono più file nella cache che corrispondono al nome del file \'{%1$s}\'. - Tag imprevisto trovato nel file: {%1$s} - Un tag {%1$s} è stato trovato sulla stessa riga di un tag {%2$s}. Questo non è permesso. - Entrambi a <bars> tag e virgole sono stati trovati sulla stessa riga. Usando il valore del tag. - Il tag monouso {%1$s} è stato utilizzato più volte in questo file. - Il tag {%1$s} è stato utilizzato più volte in questa riga. - Il tag {%1$s} è stato utilizzato più volte consecutive senza un tag di fine intermedio. - Il tag {%1$s} è stato trovato prima di un tag iniziale corrispondente. - Il tag {%1$s} è stato trovato senza alcun valore specificato. - Nessun nome del set definito nel file dell\'elenco di set. - Sulla stessa riga sono stati trovati più tag beatstart o beatstop. Ignorando tutto tranne il primo. - Impossibile trovare la cartella principale del sistema di archiviazione. - Un tag beatstart può essere utilizzato solo in una song con una velocità BPM definita. - Tag imprevisto: %1$s. - %1$s non è un file audio valido. - Scansione %1$s - Errore durante la lettura del file. - Colore indicatore di posizione pagina-giù - Colore per gli indicatori di posizione di scorrimento pagina giù. - Colore dell\'evidenziazione iniziale della sezione beat - Evidenzia il colore per la linea che è l\'inizio di una sezione beat. - Evidenzia l\'inizio delle sezioni di battuta - Evidenzia l\'inizio delle sezioni di battuta con il colore di evidenziazione selezionato. - Mostra la pagina verso il basso per scorrere i marcatori di posizione - Mostra la posizione in cui si scorre il display quando si preme page-down. - Band Leader Device - Specificare a quale dispositivo connettersi quando si è in modalità membro della band. - Band Leader Device - * Impossibile trovare il file immagine * - Valore vuoto trovato nella direttiva MIDI. - Indice argomento non valido trovato nella direttiva MIDI. - Valore byte non valido trovato nella direttiva MIDI. - Il file di brano selezionato non ha linee. - Istruzioni … - Qualsiasi altro tasto è page-down. - Qualsiasi altro tasto premuto su una tastiera collegata si comporterà come pagina-giù. - Bluetooth - Nativo - Colore di evidenziazione della sezione del coro - Evidenzia il colore per i cori. - Tipo di Connessione - Tipo di Connessione - Che tipo di connessione MIDI usi? - Muto - Non riprodurre mai l\'audio - Rimescolare - Informativa sulla privacy … - Consenti a BeatPrompter di comunicare con altri dispositivi tramite Bluetooth. - "Stato attuale: " - Accordata - Negata - Autorizzazioni - Audio - Consentire a BeatPrompter di leggere i file dalle cartelle locali. - Lettore audio - Quale lettore deve utilizzare l\'app per la riproduzione audio? - Impossibile eseguire l\'autenticazione per l\'accesso a Google Drive. - Per modalità - Comprami un caffè … - Errore - Ottenere la cartella principale … - Mostra valutazione canzone - Nell\'elenco dei brani principali, mostra la valutazione del brano. - Per valutazione - Variazione - Nessun Audio - I nomi delle varianti sono già stati definiti. - I tag audio sono più numerosi delle variazioni. - Attiva/disattiva la modalità oscura - Lettura del database … - Impossibile leggere l\'elemento del database - Quantità di latenza da compensare nella riproduzione audio. - Latenza audio - Sincronizzazione %1$s (riprovare %2$d/%3$d) - Dispositivi MIDI Bluetooth - Quali dispositivi Bluetooth dovrebbero essere presi in considerazione per il MIDI? - Dispositivi MIDI Bluetooth - Si è verificato un errore durante la lettura del database. - Si è verificato un errore durante la scrittura del database. - Ricostruzione del database... - As directivas with_midi_* não podem ser utilizadas em alias que contenham parâmetros - Mostra sempre gli accordi acuti - Tutti gli accordi bemolle verranno convertiti negli accordi diesis appropriati. - Visualizza alterazioni Unicode - Gli accordi che contengono i caratteri b/# verranno convertiti in ♭/♯. - \'%1$s\' non può essere analizzato come chiave valida - Impossibile calcolare la nuova tonalità (spostamento di %1$s di %2$d semitoni) - Impossibile analizzare l\'accordo \'%1$s\' - Il tag {transpose} non deve contenere un valore numerico maggiore di 11 o minore di -11 - Il tag {chord_map} deve contenere due valori separati da = - Trasporre + API Microsoft Onedrive non connesso. + Cache cancellata. + File + Nessun sistema di archiviazione è stato selezionato. + Nessun sistema di archiviazione è stato selezionato. + Elimina tutti i file scaricati. + Cancella cache + Sistema di archiviazione + Dove vengono memorizzati i file musicali? + Sistema di archiviazione + Sincronizza i file … + Cancella i tag quando cambia il filtro? + Se true, tutti i filtri tag selezionati verranno cancellati ogni volta che si cambia cartella/setlist/ecc. + Cartella di archiviazione + Questa è la cartella che i file dei brani BeatPrompter sono memorizzati in. + %1$d oggetti trovati. + %1$d/%2$d oggetti trovati. + Caricamento in corso … + Band Leader (server) + Membro Band (client) + Nessuna + Connessione persa al leader della band + ha disconnesso + Modalità Bluetooth + Configurare il modo in cui il tuo dispositivo comunica con i membri della band. + Modalità Bluetooth + Prendi anche i file dalle sottocartelle? + Includi le sottocartelle + Aggiorna forza (incluse le dipendenze) + Per chiave + Impossibile trovare il file immagine: %1$s + Impossibile leggere il file di immagine: %1$s + Chiave + Quando non c\'è traccia di supporto + Immagini multiple trovate in una sola riga. Utilizza solo la prima. + Nell\'elenco principale delle canzoni, mostri la chiave del brano. + Mostra la chiave del brano + Mostra Song BPM + Dovrebbe essere visualizzato il BPM iniziale della canzone sulla schermata del titolo della song? + Mostra la canzone BPM sulla schermata del titolo + Exibe a tecla da música na tela do título da música. + Mostra la chiave del brano nella schermata del titolo. + No + + Sì, arrotondato all\'intero + Il testo è stato trovato in una riga in cui è stata specificata un\'immagine. Ignorare il testo. + Modalità di scala immagine sconosciuta, in base alla modalità di stretching. + Si è verificato un errore durante la comunicazione con il servizio di archiviazione (\"%1$s\").\\n\\nNon tutti i file sono stati recuperati o analizzati correttamente.\\n\\nControllare la connessione e riprovare. + Errore di Sincronizzazione + Display mimic leader della band in modalità manuale + Se si sta eseguendo una canzone con una band leader in modalità manuale, questo imposterà automaticamente lo schermo con lo stesso orientamento e le stesse dimensioni dei caratteri. + Dimensione minima del carattere (scorrimento manuale) + Dimensione massima del carattere (scorrimento manuale) + La dimensione del carattere più piccola che verrà utilizzata nella modalità di scorrimento manuale. + La dimensione del carattere più grande che verrà utilizzata nella modalità di scorrimento manuale. + Il sensore di prossimità scorre la pagina. + Se il tuo dispositivo ha un sensore di prossimità, attivandolo agirà come premendo un pedale di scorrimento verso il basso. + Memoria Locale + Connesso a %1$s + Connessione persa a %1$s + Il nome del set di alias MIDI è già stato definito. + Il nome del set è già stato definito. + L\'immagine dopo il ridimensionamento supera la dimensione massima di 8192 x 8192. Linea di ignoranza. + Un alias MIDI è stato definito con un solo nome. + Un alias MIDI può avere solo un valore di canale. + Istruzioni trovate, ma nessun nome alias MIDI è stato ancora definito. + Un file alias MIDI deve contenere una definizione del nome del set {midi_aliases}. + Ci sono più file nella cache che corrispondono al nome del file \'{%1$s}\'. + Tag imprevisto trovato nel file: {%1$s} + Un tag {%1$s} è stato trovato sulla stessa riga di un tag {%2$s}. Questo non è permesso. + Entrambi a <bars> tag e virgole sono stati trovati sulla stessa riga. Usando il valore del tag. + Il tag monouso {%1$s} è stato utilizzato più volte in questo file. + Il tag {%1$s} è stato utilizzato più volte in questa riga. + Il tag {%1$s} è stato utilizzato più volte consecutive senza un tag di fine intermedio. + Il tag {%1$s} è stato trovato prima di un tag iniziale corrispondente. + Il tag {%1$s} è stato trovato senza alcun valore specificato. + Nessun nome del set definito nel file dell\'elenco di set. + Sulla stessa riga sono stati trovati più tag beatstart o beatstop. Ignorando tutto tranne il primo. + Impossibile trovare la cartella principale del sistema di archiviazione. + Un tag beatstart può essere utilizzato solo in una song con una velocità BPM definita. + Tag imprevisto: %1$s. + %1$s non è un file audio valido. + Scansione %1$s + Errore durante la lettura del file. + Colore indicatore di posizione pagina-giù + Colore per gli indicatori di posizione di scorrimento pagina giù. + Colore dell\'evidenziazione iniziale della sezione beat + Evidenzia il colore per la linea che è l\'inizio di una sezione beat. + Evidenzia l\'inizio delle sezioni di battuta + Evidenzia l\'inizio delle sezioni di battuta con il colore di evidenziazione selezionato. + Mostra la pagina verso il basso per scorrere i marcatori di posizione + Mostra la posizione in cui si scorre il display quando si preme page-down. + Band Leader Device + Specificare a quale dispositivo connettersi quando si è in modalità membro della band. + Band Leader Device + * Impossibile trovare il file immagine * + Valore vuoto trovato nella direttiva MIDI. + Indice argomento non valido trovato nella direttiva MIDI. + Valore byte non valido trovato nella direttiva MIDI. + Il file di brano selezionato non ha linee. + Istruzioni … + Qualsiasi altro tasto è page-down. + Qualsiasi altro tasto premuto su una tastiera collegata si comporterà come pagina-giù. + Bluetooth + Nativo + Colore di evidenziazione della sezione del coro + Evidenzia il colore per i cori. + Tipo di Connessione + Tipo di Connessione + Che tipo di connessione MIDI usi? + Muto + Non riprodurre mai l\'audio + Rimescolare + Informativa sulla privacy … + Consenti a BeatPrompter di comunicare con altri dispositivi tramite Bluetooth. + "Stato attuale: " + Accordata + Negata + Autorizzazioni + Audio + Consentire a BeatPrompter di leggere i file dalle cartelle locali. + Lettore audio + Quale lettore deve utilizzare l\'app per la riproduzione audio? + Impossibile eseguire l\'autenticazione per l\'accesso a Google Drive. + Per modalità + Comprami un caffè … + Errore + Ottenere la cartella principale … + Mostra valutazione canzone + Nell\'elenco dei brani principali, mostra la valutazione del brano. + Per valutazione + Variazione + Nessun Audio + I nomi delle varianti sono già stati definiti. + I tag audio sono più numerosi delle variazioni. + Attiva/disattiva la modalità oscura + Lettura del database … + Impossibile leggere l\'elemento del database + Quantità di latenza da compensare nella riproduzione audio. + Latenza audio + Sincronizzazione %1$s (riprovare %2$d/%3$d) + Dispositivi MIDI Bluetooth + Quali dispositivi Bluetooth dovrebbero essere presi in considerazione per il MIDI? + Dispositivi MIDI Bluetooth + Si è verificato un errore durante la lettura del database. + Si è verificato un errore durante la scrittura del database. + Ricostruzione del database... + As directivas with_midi_* não podem ser utilizadas em alias que contenham parâmetros + Mostra sempre gli accordi acuti + Tutti gli accordi bemolle verranno convertiti negli accordi diesis appropriati. + Visualizza alterazioni Unicode + Gli accordi che contengono i caratteri b/# verranno convertiti in ♭/♯. + \'%1$s\' non può essere analizzato come chiave valida + Impossibile calcolare la nuova tonalità (spostamento di %1$s di %2$d semitoni) + Impossibile analizzare l\'accordo \'%1$s\' + Impossibile analizzare la nota \'%1$s\' + Il tag {transpose} non deve contenere un valore numerico maggiore di 11 o minore di -11 + Il tag {chord_map} deve contenere due valori separati da = + Trasporre + Il tag varstart/varxstart contiene varianti sconosciute: %1$s + Variante preferita + Se una canzone contiene questa variazione, questa verrà caricata per impostazione predefinita. \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index bc43c103..25d208d1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,252 +1,252 @@  - BeatPrompter - … E %1$d outro erro(s). - Sobre … - Sempre usa cores padrão. - Todas as musicas - Volume da faixa de áudio não é um número entre 0 e 100. - Faixa de apoio - Início faixa de áudio compensar - Um valor já foi definido para {%1$s}. - Cor de fundo - Pulso de fundo - Cor de pulso fundo - bares - Batida contador cor - Bata Scrolling - BPB - BPL - BPM - Pelo artista - Por data - Por título - Cancelar - Não é possível localizar arquivo de áudio: %1$s - Centro - Verificando %1$s - Escolha sincronização de pastas - Cor Chords - Limpar cache - Colours - Não foi possível encontrar o arquivo de áudio. - Não foi possível carregar arquivo de áudio. É falta ou danificado? - Não foi possível analisar "%1$s" como uma cor. - Não foi possível analisar "%1$s" como um valor de duração (uso mm:ss ou apenas alguns segundos). - Não foi possível analisar "%1$s" como um valor inteiro. - Não foi possível analisar "%1$s" como um valor numerico. - Criado por - Padrão bares-per-line - Cria um som de clique no tempo com a batida. Apenas aplicável a bater-scrolling canções. - Padrão batidas por bar - Contam-in padrão - Pausa inicial padrão - Tempo padrão - Excluindo %1$s - A documentação pode ser encontrada em - Sincronizando de arquivos - Sincronizando %1$s - Durante a contagem-in - O arquivo "%1$s" não encontrado! - Altura de linha fixa - Tamanhos de fonte - Refresh Force (arquivo de música apenas) - Cor de destaque - Cor de destaque para aplicar destaque texto lírico. - Se uma música está sendo mostrado em "modo de rolagem suave", então essa configuração cria uma pausa automática no início da canção, tido em conta o comprimento total canção. - Se um ritmo não é definido por um arquivo, será usado este velocidade. - Se as barras-por-line não é definido por um arquivo, será usado este valor. - Se batidas por bar não é definido por um arquivo, será usado este valor. - Se o número de "contagem de" barras não é definida por um ficheiro, será utilizado este valor. Apenas aplicável a bater-scrolling canções. - Ignorar cores em arquivos - Na seção contador de batidas, realce a batida que o visor irá rolar por diante. - Na lista de músicas principal, mostrar um ícone indicando que uma canção tem uma faixa de apoio. - Saltar - Esquerda - Linha %1$d era mais do que %2$d caracteres. Truncando … - Justificação de linha - Linha Justificação - linhas - Cor Lyrics - Faz todas as linhas da mesma altura - Tamanho máximo da fonte ( bater-scrolling) - Altura letras máximas - O valor máximo é %1$d, valor encontrado foi %2$d. - O valor máximo é %1$d, valor encontrado foi %2$.2f. - Clique Metronome - Metronome Clique - Tamanho mínimo da fonte - O valor mínimo é %1$d, valor encontrado foi %2$d. - O valor mínimo é %1$d, valor encontrado foi %2$.2f. - Linhas visíveis mínimos - ms - Não {title} encontrada no arquivo de música "%1$s" - Nenhuma faixa de apoio - Fora - Em - Toque - Toque … - Formas de Jogar - Certo - Scrollbeat deslocamento é maior do que ou igual ao número de batidas na barra. Repor a zero. - Modo de Rolagem - Estilo de rolagem - Estilo de Rolagem - segundos - Configurações - Configurações … - Mostrar ícone da música - Mostrar indicador de rolagem. - Mostrar ícones de estilo de rolagem - Mostra o estilo de rolagem da música como um ícone na lista principal. - Suave - Rolagem Suave - Opções de Música - Organize … - Classificar músicas … - TAP DUAS VEZES PARA INICIAR - a - O fundo vai pulsar no tempo com o ritmo. - O fundo irá pulsar esta cor no tempo com o ritmo. - O maior tamanho de fonte que vai ser usado no modo de rolagem batida. - A cor da barra de contador de batidas na parte superior da tela. - A percentagem máxima da altura máxima da linha que a letra vai ocupar. - A preferência mínimo o tamanho da fonte é maior do que o tamanho máximo da fonte. Usando mínima para todos. - O número mínimo de linhas que devem ser visíveis a qualquer momento. - O deslocamento do início da canção, em milissegundos, em que a faixa de áudio de apoio começará. - O menor tamanho de fonte que vai ser usado. - Total {pause} tempo excede o tempo canção designado. Desativando o modo de rolagem suave. - O maior tamanho de fonte que será usado no modo suave-scrolling . - Tamanho da fonte máxima (rolagem suave) - Usa as preferências de tamanho font- rolagem bater para todas as músicas. - Sempre use as preferências de tamanho de fonte-scrolling batida. - O menor tamanho de fonte que vai ser utilizado no modo de liso-rolagem. - Tamanho de fonte mínimo (rolagem suave) - Comprar versão completa - Conectado a líder da banda - tem ligado - Por favor, compre BeatPrompter - Qualquer texto acorde que não é reconhecido como um acorde válido será mostrado nesta cor. - Cor de anotações não-corda - Para ver apenas os comentários que são destinados para você na tela introdução da canção , digite seu nome(s) aqui. - Personalizado Comentários do usuário - A menos que especificado no arquivo , este é o volume padrão que faixas de apoio vai jogar. - Volume da faixa padrão - Controles durante a execução - O que controla deve estar disponível enquanto uma música está tocando? - Controles durante a execução - Nada - Scrolling, parando, reiniciando - Volume - Obrigado! Você está oficialmente uma boa pessoa! :) - Armazena dados em memória externa (cartão SD, etc) . Alterar este irá limpar o cache. - Use External Storage - Use Armazenamento Local personalizado - Se selecionado, os dados de música serão armazenados no local personalizado. Alterar este irá limpar o cache. - Onde deve BeatPrompter loja baixado música e arquivos de áudio ? Alterar este irá limpar o cache. - Local de armazenamento - Use Armazenamento interno - Manual Scrolling - " Se uma música tiver tempos de batida, isto enviará sinais de relógio MIDI." - Enviar sinais de relógio MIDI - Por padrão, as canções sempre jogar no modo de rolagem manual com nenhuma faixa de apoio. - Modo manual - Google API não conectado. - A quantidade de tempo que um comentário em música permanecerá na tela. - Tempo de exibição comentário - Cor do texto comentário - Cor de destaque para aplicar para a linha atual. - A cor de destaque Linha atual - Destaca a linha atual com a sua cor de destaque escolhido. - Realce linha atual - Exibe os acordes da canção acima das letras no ponto correto. - Mostrar Chords - Mostra a primeira batida na seção indicador de batida. - Mostrar primeiro tempo. - Mostra o título da música atual sobre a parte superior da seção batida contador. - Mostrar o título da canção. - Opções de música de Exibição - Canção Opções da Lista - Set list refresh vigor - Canções desconhecidas em Set List - Esta lista conjunto contém%1$d desconhecidos canções, incluindo: - Não {set} encontrado no arquivo de conjunto \"%1$s\". - Sempre - Nunca - Apenas Set Lists - Automaticamente jogar Próximo Canção - Automaticamente jogar Próximo Canção - Caso a próxima música iniciar automaticamente quando a música actual termina? - Exibe a lista de músicas no texto maior. - Texto maior - Variado - Acessando %1$s - Adicionar à lista de conjuntos temporários - Etiqueta mal formada. - O canal MIDI deve ser o último parâmetro - O valor do especificador de canal não pode conter sublinhados. - Limpar setlist temporário - BeatPrompter lê todos os arquivos de músicas, arquivos de áudio, listas de conjuntos e muito mais do armazenamento em nuvem ou local. Atualmente, suporta o Google Drive, o Dropbox ou o Microsoft OneDrive. - Conecte-se ao líder - Conjunto de alias MIDI padrão - A etiqueta está vazia. - Forçar atualização do arquivo de alias MIDI - Os valores dos canais MIDI devem estar entre 1 e 16. - Índice de argumento de alias MIDI inválido. - Com um pouco de modificação em seus arquivos de música, o BeatPrompter manterá a tela perfeitamente ao ritmo da batida. Nunca muito rápido, nunca muito lento, e sempre agradável e grande e legível! - Carregamento: - Somente o nibble inferior de um valor pode ser mesclado com o especificador de canal. - Sempre - Nunca - Quando pausado, ou na tela de título - Quando pausado, na tela de título ou na última linha da música - Quando na tela de título - Erros de Arquivo de Alias MIDI - Alias de MIDI - Alias de MIDI - MIDI alias mensagem encontrada com mais de duas partes. - Uma tag \"midi_alias\" contém mais de duas partes. - Alias MIDI encontrado sem nome. - Não é possível analisar esta mensagem MIDI. - O deslocamento de evento MIDI o coloca antes do início da música. - As tags MIDI podem conter no máximo um caractere de ponto-e-vírgula. - Um valor de alias não pode conter vários sublinhados. - O offset MIDI deve ser um valor numérico ou caracteres de offset de batida. - O arquivo \"%1$s\" não é um arquivo de aliasing MIDI válido. - Não há parâmetros suficientes fornecidos para o comando MIDI. - BeatPrompter não apenas exibir letras e acordes. Ele pode reproduzir faixas de apoio, e controlar (e responder a) dispositivos MIDI externos também. - Powerwash completo. - Qual é o canal MIDI padrão a ser gravado, a menos que seja especificado o contrário? - Canal MIDI de saída padrão - Quais os canais MIDI que o TunePrompter deve ouvir? - Canais MIDI de Entrada Ativos - MIDI dispara durante a exibição da música - As mensagens de trigger MIDI podem interromper a música atual? - Gatilho de segurança - Limpa todos os arquivos baixados e todas as credenciais de nuvem armazenadas. - Powerwash - A cor do marcador fez mostra quando o visor irá rolar. - Cor do marcador de rolagem - Saída MIDI mensagem de disparo - Caso o gatilho ser saída de mensagem MIDI Quando a música começa? - Disparadores MIDI de saída - Canção de processamento: %1$s - Sempre - Quando iniciado manualmente - Nunca - Mostrar erros de ficheiro de alias MIDI - As tags de trigger de mudança de programa devem conter entre um e quatro valores. - As tags de trigger de seleção de música devem conter apenas um valor. - Temporário - Os valores que contêm sublinhados devem estar em hexadecimal. - A tag "%1$s" não corresponde a quaisquer comandos e aliases MIDI conhecidos. - Os parâmetros MIDI devem ser um valor entre 0 e 127. - Bem-vindo ao BeatPrompter - BeatPrompter é um aplicativo prompter letras e acordes que coloca a ênfase no timing e legibilidade. - Embora você possa usá-lo no modo retrato, o BeatPrompter funciona melhor no modo paisagem. - O deslocamento máximo evento MIDI é de 16 batimentos ou 10 segundos. - Somente as tags MIDI que estão em ou após a primeira linha música pode ter compensações. - {t:BeatPrompter Música De Demo}\n + BeatPrompter + … E %1$d outro erro(s). + Sobre … + Sempre usa cores padrão. + Todas as musicas + Volume da faixa de áudio não é um número entre 0 e 100. + Faixa de apoio + Início faixa de áudio compensar + Um valor já foi definido para {%1$s}. + Cor de fundo + Pulso de fundo + Cor de pulso fundo + bares + Batida contador cor + Bata Scrolling + BPB + BPL + BPM + Pelo artista + Por data + Por título + Cancelar + Não é possível localizar arquivo de áudio: %1$s + Centro + Verificando %1$s + Escolha sincronização de pastas + Cor Chords + Limpar cache + Colours + Não foi possível encontrar o arquivo de áudio. + Não foi possível carregar arquivo de áudio. É falta ou danificado? + Não foi possível analisar "%1$s" como uma cor. + Não foi possível analisar "%1$s" como um valor de duração (uso mm:ss ou apenas alguns segundos). + Não foi possível analisar "%1$s" como um valor inteiro. + Não foi possível analisar "%1$s" como um valor numerico. + Criado por + Padrão bares-per-line + Cria um som de clique no tempo com a batida. Apenas aplicável a bater-scrolling canções. + Padrão batidas por bar + Contam-in padrão + Pausa inicial padrão + Tempo padrão + Excluindo %1$s + A documentação pode ser encontrada em + Sincronizando de arquivos + Sincronizando %1$s + Durante a contagem-in + O arquivo "%1$s" não encontrado! + Altura de linha fixa + Tamanhos de fonte + Refresh Force (arquivo de música apenas) + Cor de destaque + Cor de destaque para aplicar destaque texto lírico. + Se uma música está sendo mostrado em "modo de rolagem suave", então essa configuração cria uma pausa automática no início da canção, tido em conta o comprimento total canção. + Se um ritmo não é definido por um arquivo, será usado este velocidade. + Se as barras-por-line não é definido por um arquivo, será usado este valor. + Se batidas por bar não é definido por um arquivo, será usado este valor. + Se o número de "contagem de" barras não é definida por um ficheiro, será utilizado este valor. Apenas aplicável a bater-scrolling canções. + Ignorar cores em arquivos + Na seção contador de batidas, realce a batida que o visor irá rolar por diante. + Na lista de músicas principal, mostrar um ícone indicando que uma canção tem uma faixa de apoio. + Saltar + Esquerda + Linha %1$d era mais do que %2$d caracteres. Truncando … + Justificação de linha + Linha Justificação + linhas + Cor Lyrics + Faz todas as linhas da mesma altura + Tamanho máximo da fonte ( bater-scrolling) + Altura letras máximas + O valor máximo é %1$d, valor encontrado foi %2$d. + O valor máximo é %1$d, valor encontrado foi %2$.2f. + Clique Metronome + Metronome Clique + Tamanho mínimo da fonte + O valor mínimo é %1$d, valor encontrado foi %2$d. + O valor mínimo é %1$d, valor encontrado foi %2$.2f. + Linhas visíveis mínimos + ms + Não {title} encontrada no arquivo de música "%1$s" + Nenhuma faixa de apoio + Fora + Em + Toque + Toque … + Formas de Jogar + Certo + Scrollbeat deslocamento é maior do que ou igual ao número de batidas na barra. Repor a zero. + Modo de Rolagem + Estilo de rolagem + Estilo de Rolagem + segundos + Configurações + Configurações … + Mostrar ícone da música + Mostrar indicador de rolagem. + Mostrar ícones de estilo de rolagem + Mostra o estilo de rolagem da música como um ícone na lista principal. + Suave + Rolagem Suave + Opções de Música + Organize … + Classificar músicas … + TAP DUAS VEZES PARA INICIAR + a + O fundo vai pulsar no tempo com o ritmo. + O fundo irá pulsar esta cor no tempo com o ritmo. + O maior tamanho de fonte que vai ser usado no modo de rolagem batida. + A cor da barra de contador de batidas na parte superior da tela. + A percentagem máxima da altura máxima da linha que a letra vai ocupar. + A preferência mínimo o tamanho da fonte é maior do que o tamanho máximo da fonte. Usando mínima para todos. + O número mínimo de linhas que devem ser visíveis a qualquer momento. + O deslocamento do início da canção, em milissegundos, em que a faixa de áudio de apoio começará. + O menor tamanho de fonte que vai ser usado. + Total {pause} tempo excede o tempo canção designado. Desativando o modo de rolagem suave. + O maior tamanho de fonte que será usado no modo suave-scrolling . + Tamanho da fonte máxima (rolagem suave) + Usa as preferências de tamanho font- rolagem bater para todas as músicas. + Sempre use as preferências de tamanho de fonte-scrolling batida. + O menor tamanho de fonte que vai ser utilizado no modo de liso-rolagem. + Tamanho de fonte mínimo (rolagem suave) + Comprar versão completa + Conectado a líder da banda + tem ligado + Por favor, compre BeatPrompter + Qualquer texto acorde que não é reconhecido como um acorde válido será mostrado nesta cor. + Cor de anotações não-corda + Para ver apenas os comentários que são destinados para você na tela introdução da canção , digite seu nome(s) aqui. + Personalizado Comentários do usuário + A menos que especificado no arquivo , este é o volume padrão que faixas de apoio vai jogar. + Volume da faixa padrão + Controles durante a execução + O que controla deve estar disponível enquanto uma música está tocando? + Controles durante a execução + Nada + Scrolling, parando, reiniciando + Volume + Obrigado! Você está oficialmente uma boa pessoa! :) + Armazena dados em memória externa (cartão SD, etc) . Alterar este irá limpar o cache. + Use External Storage + Use Armazenamento Local personalizado + Se selecionado, os dados de música serão armazenados no local personalizado. Alterar este irá limpar o cache. + Onde deve BeatPrompter loja baixado música e arquivos de áudio ? Alterar este irá limpar o cache. + Local de armazenamento + Use Armazenamento interno + Manual Scrolling + " Se uma música tiver tempos de batida, isto enviará sinais de relógio MIDI." + Enviar sinais de relógio MIDI + Por padrão, as canções sempre jogar no modo de rolagem manual com nenhuma faixa de apoio. + Modo manual + Google API não conectado. + A quantidade de tempo que um comentário em música permanecerá na tela. + Tempo de exibição comentário + Cor do texto comentário + Cor de destaque para aplicar para a linha atual. + A cor de destaque Linha atual + Destaca a linha atual com a sua cor de destaque escolhido. + Realce linha atual + Exibe os acordes da canção acima das letras no ponto correto. + Mostrar Chords + Mostra a primeira batida na seção indicador de batida. + Mostrar primeiro tempo. + Mostra o título da música atual sobre a parte superior da seção batida contador. + Mostrar o título da canção. + Opções de música de Exibição + Canção Opções da Lista + Set list refresh vigor + Canções desconhecidas em Set List + Esta lista conjunto contém%1$d desconhecidos canções, incluindo: + Não {set} encontrado no arquivo de conjunto \"%1$s\". + Sempre + Nunca + Apenas Set Lists + Automaticamente jogar Próximo Canção + Automaticamente jogar Próximo Canção + Caso a próxima música iniciar automaticamente quando a música actual termina? + Exibe a lista de músicas no texto maior. + Texto maior + Variado + Acessando %1$s + Adicionar à lista de conjuntos temporários + Etiqueta mal formada. + O canal MIDI deve ser o último parâmetro + O valor do especificador de canal não pode conter sublinhados. + Limpar setlist temporário + BeatPrompter lê todos os arquivos de músicas, arquivos de áudio, listas de conjuntos e muito mais do armazenamento em nuvem ou local. Atualmente, suporta o Google Drive, o Dropbox ou o Microsoft OneDrive. + Conecte-se ao líder + Conjunto de alias MIDI padrão + A etiqueta está vazia. + Forçar atualização do arquivo de alias MIDI + Os valores dos canais MIDI devem estar entre 1 e 16. + Índice de argumento de alias MIDI inválido. + Com um pouco de modificação em seus arquivos de música, o BeatPrompter manterá a tela perfeitamente ao ritmo da batida. Nunca muito rápido, nunca muito lento, e sempre agradável e grande e legível! + Carregamento: + Somente o nibble inferior de um valor pode ser mesclado com o especificador de canal. + Sempre + Nunca + Quando pausado, ou na tela de título + Quando pausado, na tela de título ou na última linha da música + Quando na tela de título + Erros de Arquivo de Alias MIDI + Alias de MIDI + Alias de MIDI + MIDI alias mensagem encontrada com mais de duas partes. + Uma tag \"midi_alias\" contém mais de duas partes. + Alias MIDI encontrado sem nome. + Não é possível analisar esta mensagem MIDI. + O deslocamento de evento MIDI o coloca antes do início da música. + As tags MIDI podem conter no máximo um caractere de ponto-e-vírgula. + Um valor de alias não pode conter vários sublinhados. + O offset MIDI deve ser um valor numérico ou caracteres de offset de batida. + O arquivo \"%1$s\" não é um arquivo de aliasing MIDI válido. + Não há parâmetros suficientes fornecidos para o comando MIDI. + BeatPrompter não apenas exibir letras e acordes. Ele pode reproduzir faixas de apoio, e controlar (e responder a) dispositivos MIDI externos também. + Powerwash completo. + Qual é o canal MIDI padrão a ser gravado, a menos que seja especificado o contrário? + Canal MIDI de saída padrão + Quais os canais MIDI que o TunePrompter deve ouvir? + Canais MIDI de Entrada Ativos + MIDI dispara durante a exibição da música + As mensagens de trigger MIDI podem interromper a música atual? + Gatilho de segurança + Limpa todos os arquivos baixados e todas as credenciais de nuvem armazenadas. + Powerwash + A cor do marcador fez mostra quando o visor irá rolar. + Cor do marcador de rolagem + Saída MIDI mensagem de disparo + Caso o gatilho ser saída de mensagem MIDI Quando a música começa? + Disparadores MIDI de saída + Canção de processamento: %1$s + Sempre + Quando iniciado manualmente + Nunca + Mostrar erros de ficheiro de alias MIDI + As tags de trigger de mudança de programa devem conter entre um e quatro valores. + As tags de trigger de seleção de música devem conter apenas um valor. + Temporário + Os valores que contêm sublinhados devem estar em hexadecimal. + A tag "%1$s" não corresponde a quaisquer comandos e aliases MIDI conhecidos. + Os parâmetros MIDI devem ser um valor entre 0 e 127. + Bem-vindo ao BeatPrompter + BeatPrompter é um aplicativo prompter letras e acordes que coloca a ênfase no timing e legibilidade. + Embora você possa usá-lo no modo retrato, o BeatPrompter funciona melhor no modo paisagem. + O deslocamento máximo evento MIDI é de 16 batimentos ou 10 segundos. + Somente as tags MIDI que estão em ou após a primeira linha música pode ter compensações. + {t:BeatPrompter Música De Demo}\n {st:Música De Demo}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -272,162 +272,166 @@ letras e [em]acordes no tempo, com uma batida.\n [D]Verifique a documentação para obter mais detalhes.\n [G]www.beatprompter.co.uk\n [Em]Espero que gostem! - API OneDrive não está conectada. - Cache desmarcado. - Arquivos - Nenhum sistema de armazenamento foi selecionado. - Nenhum sistema de armazenamento foi selecionado. - Exclui todos os arquivos baixados. - Limpar cache - Sistema de armazenamento - Onde estão os arquivos de músicas armazenados? - Sistema de armazenamento - Sincronize arquivos … - Limpar tags quando o filtro é alterado? - Se verdadeiro, todos os filtros de tag selecionados serão limpos sempre que você alternar pastas/setlists/etc. - Pasta de armazenamento - Esta é a pasta que os arquivos de música BeatPrompter são armazenados. - %1$d itens encontrados. - %1$d/%2$d itens encontrados. - Carregando … - Band Leader (servidor) - Membro da banda (cliente) - Nenhum - Perdeu conexão com o líder da banda - desconectou - Modo Bluetooth - Configure como seu dispositivo se comunica com os membros da banda. - Modo Bluetooth - Obter arquivos de subpastas também? - Incluir subpastas - Forçar atualização (incluindo dependências) - Por chave - Não foi possível encontrar o arquivo de imagem: %1$s - Não foi possível ler o arquivo de imagem: %1$s - Chave - Quando não há trilha de apoio - Múltiplas imagens encontradas em uma linha. Utilizando apenas o primeiro. - Na lista principal de músicas, mostre a chave da música. - Mostrar chave da música - Show Song BPM - O BPM inicial da música deve ser mostrado na tela do título da música? - Mostrar o BPM da música na tela de título - Exibe a tecla da música na tela do título da música. - Mostre a tecla de música na tela de título. - Sim - Não - Sim, arredondado para o inteiro - O texto foi encontrado em uma linha onde uma imagem foi especificada. Ignorando o texto. - Modo de escala de imagem desconhecida, padrão para o modo de estiramento. - Houve um erro na comunicação com o serviço de armazenamento (\"%1$s\").\\n\\nNem todos os arquivos foram buscados ou verificados com êxito.\\n\\nVerifique sua conexão e tente novamente. - Erro de Sincronização - Exibição do líder da banda Mimic no modo manual - Se estiver executando uma música com um líder de banda no modo manual, isso automaticamente ajustará sua exibição para a mesma orientação e tamanhos de fonte. - Tamanho mínimo da fonte (rolagem manual) - Tamanho máximo da fonte (rolagem manual) - O tamanho de fonte mais pequeno que será usado no modo de rolagem manual. - O maior tamanho de fonte que será usado no modo de rolagem manual. - O sensor de proximidade desliza a página. - Se o seu dispositivo tiver um sensor de proximidade, desencadear-se-á agirá como pressionando um pedal de deslocamento da página. - Armazenamento Local - Conectado a %1$s - Conexão perdida com %1$s - O nome do conjunto de alias MIDI já foi definido. - O nome do conjunto já foi definido. - A imagem após o dimensionamento excede o tamanho máximo de 8192 x 8192. Ignorando a linha. - Um alias MIDI foi definido apenas com um nome. - Um alias MIDI só pode ter um valor de canal. - Instruções encontradas, mas nenhum nome de alias MIDI foi definido ainda. - Um arquivo de alias MIDI deve conter uma definição de nome de conjunto {midi_aliases}. - Existem vários arquivos no cache que correspondem ao nome do arquivo \'{%1$s}\'. - Tag inesperada encontrada no arquivo: {%1$s} - Uma tag de {%1$s} foi encontrada na mesma linha que uma tag de {%2$s}. Isso não é permitido. - Ambos os <bars> tags e vírgulas foram encontrados na mesma linha. Usando o valor da tag. - A tag de uso único {%1$s} foi usada várias vezes nesse arquivo. - A tag {%1$s} foi usada várias vezes nesta linha. - A tag {%1$s} foi usada várias vezes consecutivas sem uma tag final intermediária. - A tag {%1$s} foi encontrada antes de uma tag inicial correspondente. - A tag {%1$s} foi encontrada sem nenhum valor especificado. - Nenhum nome de conjunto definido no arquivo de lista de conjuntos. - Várias tags beatstart ou beatstop foram encontradas na mesma linha. Ignorando tudo menos o primeiro. - Não foi possível encontrar a pasta raiz do sistema de armazenamento. - Uma tag beatstart só pode ser usada em uma música com uma velocidade de BPM definida. - Tag inesperada: %1$s. - %1$s não é um arquivo de áudio válido. - Digitalização %1$s - Erro ao ler o arquivo. - Cor do indicador da posição de baixo para baixo - Cor para os indicadores de posição de rolagem de página para baixo. - Começo da seção de batida realçar cor - Realce a cor para a linha que é o início de uma seção de batida. - Destaque início das seções de batida - Destaca o início das seções de batida com a cor de destaque escolhida. - Mostrar marcadores de posição de rolagem da página para baixo - Mostra a posição em que a tela será rolada para quando você pressiona a página para baixo. - Dispositivo de Líder de Banda - Especifique o dispositivo ao qual se conectar quando estiver no modo de membro de banda. - Dispositivo de Líder de Banda - * Não foi possível encontrar o arquivo de imagem * - Valor em branco encontrado na diretiva MIDI. - Índice de argumento inválido encontrado na diretiva MIDI. - Valor de byte inválido encontrado na diretiva MIDI. - O arquivo de música selecionado não tem linhas. - Instruções … - Qualquer outra chave é page-down. - Qualquer outra tecla pressionada em um teclado conectado atuará como page-down. - Bluetooth - Nativo - Cor de destaque de seção de coro - Destaque cor para refrões. - Tipo de Conexão - Tipo de Conexão - Que tipo de conexão MIDI você usa? - Mudo - Nunca reproduza áudio - Aleatória - Política de privacidade … - Permita que o BeatPrompter comunique com outros dispositivos via Bluetooth. - "Situação atual: " - Concedido - Negado - Permissões - Áudio - Permita que o BeatPrompter leia arquivos de pastas locais. - Leitor de áudio - Qual player o aplicativo deve usar para reprodução de áudio? - Falha ao autenticar para acesso ao Google Drive. - Por modo - Compre-me um café … - Erro - Obtendo pasta raiz … - Mostrar classificação de música - Na lista de músicas principais, mostre a classificação da música. - Por classificação - Variação - Sem Audio - Os nomes das variações já estão definidos. - As etiquetas de áudio superam as variações. - Alternar modo escuro - Lendo base de dados … - Falha ao ler o item da base de dados - Quantidade de latência a ser compensada na reprodução de áudio. - Latência de áudio - Sincronizando %1$s (tente novamente %2$d/%3$d) - Dispositivos MIDI Bluetooth - Que dispositivos Bluetooth devem ser considerados para MIDI? - Dispositivos MIDI Bluetooth - Ocorreu um erro ao ler a base de dados. - Ocorreu um erro ao escrever a base de dados. - Reconstruir base de dados... - As directivas with_midi_* não podem ser utilizadas em alias que contenham parâmetros - Exibir sempre acordes sustenidos - Quaisquer acordes bemol serão convertidos em acordes sustenidos apropriados. - Exibir acidentes Unicode - Os acordes que contenham caracteres b/# serão convertidos para utilizar ♭/♯. - \'%1$s\' não pôde ser analisado como uma chave válida - Falha no cálculo da nova tonalidade (deslocamento de %1$s em %2$d semitons) - Falha ao analisar o acorde \'%1$s\' - A etiqueta {transpose} não deve conter um valor numérico superior a 11 ou inferior a -11 - A tag {chord_map} deve conter dois valores separados por = - Transpor + API OneDrive não está conectada. + Cache desmarcado. + Arquivos + Nenhum sistema de armazenamento foi selecionado. + Nenhum sistema de armazenamento foi selecionado. + Exclui todos os arquivos baixados. + Limpar cache + Sistema de armazenamento + Onde estão os arquivos de músicas armazenados? + Sistema de armazenamento + Sincronize arquivos … + Limpar tags quando o filtro é alterado? + Se verdadeiro, todos os filtros de tag selecionados serão limpos sempre que você alternar pastas/setlists/etc. + Pasta de armazenamento + Esta é a pasta que os arquivos de música BeatPrompter são armazenados. + %1$d itens encontrados. + %1$d/%2$d itens encontrados. + Carregando … + Band Leader (servidor) + Membro da banda (cliente) + Nenhum + Perdeu conexão com o líder da banda + desconectou + Modo Bluetooth + Configure como seu dispositivo se comunica com os membros da banda. + Modo Bluetooth + Obter arquivos de subpastas também? + Incluir subpastas + Forçar atualização (incluindo dependências) + Por chave + Não foi possível encontrar o arquivo de imagem: %1$s + Não foi possível ler o arquivo de imagem: %1$s + Chave + Quando não há trilha de apoio + Múltiplas imagens encontradas em uma linha. Utilizando apenas o primeiro. + Na lista principal de músicas, mostre a chave da música. + Mostrar chave da música + Show Song BPM + O BPM inicial da música deve ser mostrado na tela do título da música? + Mostrar o BPM da música na tela de título + Exibe a tecla da música na tela do título da música. + Mostre a tecla de música na tela de título. + Sim + Não + Sim, arredondado para o inteiro + O texto foi encontrado em uma linha onde uma imagem foi especificada. Ignorando o texto. + Modo de escala de imagem desconhecida, padrão para o modo de estiramento. + Houve um erro na comunicação com o serviço de armazenamento (\"%1$s\").\\n\\nNem todos os arquivos foram buscados ou verificados com êxito.\\n\\nVerifique sua conexão e tente novamente. + Erro de Sincronização + Exibição do líder da banda Mimic no modo manual + Se estiver executando uma música com um líder de banda no modo manual, isso automaticamente ajustará sua exibição para a mesma orientação e tamanhos de fonte. + Tamanho mínimo da fonte (rolagem manual) + Tamanho máximo da fonte (rolagem manual) + O tamanho de fonte mais pequeno que será usado no modo de rolagem manual. + O maior tamanho de fonte que será usado no modo de rolagem manual. + O sensor de proximidade desliza a página. + Se o seu dispositivo tiver um sensor de proximidade, desencadear-se-á agirá como pressionando um pedal de deslocamento da página. + Armazenamento Local + Conectado a %1$s + Conexão perdida com %1$s + O nome do conjunto de alias MIDI já foi definido. + O nome do conjunto já foi definido. + A imagem após o dimensionamento excede o tamanho máximo de 8192 x 8192. Ignorando a linha. + Um alias MIDI foi definido apenas com um nome. + Um alias MIDI só pode ter um valor de canal. + Instruções encontradas, mas nenhum nome de alias MIDI foi definido ainda. + Um arquivo de alias MIDI deve conter uma definição de nome de conjunto {midi_aliases}. + Existem vários arquivos no cache que correspondem ao nome do arquivo \'{%1$s}\'. + Tag inesperada encontrada no arquivo: {%1$s} + Uma tag de {%1$s} foi encontrada na mesma linha que uma tag de {%2$s}. Isso não é permitido. + Ambos os <bars> tags e vírgulas foram encontrados na mesma linha. Usando o valor da tag. + A tag de uso único {%1$s} foi usada várias vezes nesse arquivo. + A tag {%1$s} foi usada várias vezes nesta linha. + A tag {%1$s} foi usada várias vezes consecutivas sem uma tag final intermediária. + A tag {%1$s} foi encontrada antes de uma tag inicial correspondente. + A tag {%1$s} foi encontrada sem nenhum valor especificado. + Nenhum nome de conjunto definido no arquivo de lista de conjuntos. + Várias tags beatstart ou beatstop foram encontradas na mesma linha. Ignorando tudo menos o primeiro. + Não foi possível encontrar a pasta raiz do sistema de armazenamento. + Uma tag beatstart só pode ser usada em uma música com uma velocidade de BPM definida. + Tag inesperada: %1$s. + %1$s não é um arquivo de áudio válido. + Digitalização %1$s + Erro ao ler o arquivo. + Cor do indicador da posição de baixo para baixo + Cor para os indicadores de posição de rolagem de página para baixo. + Começo da seção de batida realçar cor + Realce a cor para a linha que é o início de uma seção de batida. + Destaque início das seções de batida + Destaca o início das seções de batida com a cor de destaque escolhida. + Mostrar marcadores de posição de rolagem da página para baixo + Mostra a posição em que a tela será rolada para quando você pressiona a página para baixo. + Dispositivo de Líder de Banda + Especifique o dispositivo ao qual se conectar quando estiver no modo de membro de banda. + Dispositivo de Líder de Banda + * Não foi possível encontrar o arquivo de imagem * + Valor em branco encontrado na diretiva MIDI. + Índice de argumento inválido encontrado na diretiva MIDI. + Valor de byte inválido encontrado na diretiva MIDI. + O arquivo de música selecionado não tem linhas. + Instruções … + Qualquer outra chave é page-down. + Qualquer outra tecla pressionada em um teclado conectado atuará como page-down. + Bluetooth + Nativo + Cor de destaque de seção de coro + Destaque cor para refrões. + Tipo de Conexão + Tipo de Conexão + Que tipo de conexão MIDI você usa? + Mudo + Nunca reproduza áudio + Aleatória + Política de privacidade … + Permita que o BeatPrompter comunique com outros dispositivos via Bluetooth. + "Situação atual: " + Concedido + Negado + Permissões + Áudio + Permita que o BeatPrompter leia arquivos de pastas locais. + Leitor de áudio + Qual player o aplicativo deve usar para reprodução de áudio? + Falha ao autenticar para acesso ao Google Drive. + Por modo + Compre-me um café … + Erro + Obtendo pasta raiz … + Mostrar classificação de música + Na lista de músicas principais, mostre a classificação da música. + Por classificação + Variação + Sem Audio + Os nomes das variações já estão definidos. + As etiquetas de áudio superam as variações. + Alternar modo escuro + Lendo base de dados … + Falha ao ler o item da base de dados + Quantidade de latência a ser compensada na reprodução de áudio. + Latência de áudio + Sincronizando %1$s (tente novamente %2$d/%3$d) + Dispositivos MIDI Bluetooth + Que dispositivos Bluetooth devem ser considerados para MIDI? + Dispositivos MIDI Bluetooth + Ocorreu um erro ao ler a base de dados. + Ocorreu um erro ao escrever a base de dados. + Reconstruir base de dados... + As directivas with_midi_* não podem ser utilizadas em alias que contenham parâmetros + Exibir sempre acordes sustenidos + Quaisquer acordes bemol serão convertidos em acordes sustenidos apropriados. + Exibir acidentes Unicode + Os acordes que contenham caracteres b/# serão convertidos para utilizar ♭/♯. + \'%1$s\' não pôde ser analisado como uma chave válida + Falha no cálculo da nova tonalidade (deslocamento de %1$s em %2$d semitons) + Falha ao analisar o acorde \'%1$s\' + Falha ao analisar a nota \'%1$s\' + A etiqueta {transpose} não deve conter um valor numérico superior a 11 ou inferior a -11 + A tag {chord_map} deve conter dois valores separados por = + Transpor + A etiqueta varstart/varxstart contém variações desconhecidas: %1$s + Variação preferida + Se uma música contiver esta variação, esta será carregada por defeito. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b707eb9b..e7a36a5d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,714 +1,718 @@ - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - abc - BeatPrompter - Steven Frew - Email: steven.fullhouse@gmail.com - tinyurl.com/beatprompterdocs - Google Drive - Local Storage - Shuffle - Dropbox - OneDrive - Connected to %1$s - Lost connection to %1$s - MIDI alias set name has already been defined. - Set name has already been defined. - Image after scaling exceeds maximum size of 8192 x 8192. Ignoring line. - A MIDI alias has been defined with only a name. - A MIDI alias can only have one channel value. - Instructions found, but no MIDI alias name has been defined yet. - A MIDI alias file must contain a {midi_aliases} set name definition. - There are multiple files in the cache that match the filename \'{%1$s}\'. - Unexpected tag found in file: {%1$s} - A {%1$s} tag was found on the same line as a {%2$s} tag. This is not allowed. - Connect to leader - Both a <bars> tag and commas were found on the same line. Using tag value. - Clear cache - The single-use tag {%1$s} has been used multiple times in this file. - The tag {%1$s} has been used multiple times in this line. - The tag {%1$s} has been used multiple times in succession without an intervening ending tag. - The tag {%1$s} has been found before a corresponding starting tag. - The tag {%1$s} has been found with no specified value. - No set name defined in set list file. - Sort … - Settings … - Multiple beatstart or beatstop tags were found on the same line. Ignoring all but the first. - Could not find the root folder of the storage system. - A beatstart tag can only be used in a song with a defined BPM speed. - All Songs - Colours - Font Sizes - Google API not connected. - OneDrive API not connected. - No {set} found in set file "%1$s". - Unexpected tag: %1$s. - This set list contains %1$d unknown songs, including: - Unknown Songs In Set List - Program change trigger tags must contain between one and four values. - Song select trigger tags must contain only one value. - Could not read image file: %1$s - MIDI parameters must be a value between 0 and 127. - MIDI channel values must be between 1 and 16. - An alias value cannot contain multiple underscores. - The channel specifier value cannot contain underscores. - Values containing underscores must be in hexadecimal. - Only the lower nibble of a value can be merged with the channel specifier. - File "%1$s" is not a valid MIDI aliasing file. - MIDI alias found with no name. - Invalid MIDI alias argument index. - MIDI alias message found with more than two parts. - A "midi_alias" tag contains more than two parts. - Can\'t parse this MIDI message. - Tag "%1$s" does not match any known MIDI commands or aliases. - Unknown image scaling mode, defaulting to stretch mode. - Multiple images found in one line. Only using the first one. - Text was found on a line where an image was specified. Ignoring text. - MIDI Aliases - MIDI Alias File Errors - Badly-formed tag. - Tag is empty. - The MIDI channel must be the last parameter. - Not enough parameters supplied to MIDI command. - Default MIDI Alias Set - - About … - Temporary - Song Options - Song List - MIDI Aliases - Miscellaneous - Song Display - Created by - Documentation can be found at - Force refresh set list - Force refresh MIDI alias file - Show MIDI alias file errors - Clear temporary set list - Force refresh (song file only) - Add to temporary set list - Force refresh (including dependencies) - Play … - - Play - Cancel - No backing track - Play Options - Backing Track - Scrolling Mode - Beat Scrolling - Smooth Scrolling - Manual Scrolling - - Settings - SongDisplayActivity - Dummy Button - DUMMY\nCONTENT - MIDI - - TestFullscreenActivity - - 10 - 0 - 230 - 110 - If a tempo is not defined by a file, this speed will be used. - BPM - Default tempo - - 1 - 0 - 31 - 3 - If beats-per-bar is not defined by a file, this value will be used. - BPB - Default beats-per-bar - - 1 - 0 - 31 - 0 - If bars-per-line is not defined by a file, this value will be used. - BPL - Default bars-per-line - - pref_countIn - 0 - 0 - 4 - 0 - If the number of "count-in" bars is not defined by a file, this value will be used. Only applicable to beat-scrolling songs. - bars - Default count-in - - pref_defaultPause - 0 - 0 - 120 - 0 - If a song is being shown in "smooth scrolling mode", then this setting creates an automatic pause at the start of the song, factored into the total song length. - seconds - Default initial pause - - Please buy BeatPrompter - Buy full version - Thanks! You are officially a nice person! :) - TAP TWICE TO START - has connected - has disconnected - Connected to band leader - Lost connection to band leader - … and %1$d other error(s). - File "%1$s" not found! - No {title} found in song file "%1$s" - "%1$s" is not a valid audio file. - Checking %1$s - Synchronizing %1$s - Scanning %1$s - Deleting %1$s - Synchronizing Files - Accessing %1$s - Choose Sync Folder - the - By title - By artist - By date - By key - Sort songs … - Error reading file. - Minimum value is %1$d, value found was %2$d. - Maximum value is %1$d, value found was %2$d. - Could not parse "%1$s" as an integer value. - Could not parse "%1$s" as a duration value (use mm:ss or just seconds). - Minimum value is %1$d, value found was %2$.2f. - Maximum value is %1$d, value found was %2$.2f. - Could not parse "%1$s" as a numeric value. - Could not parse "%1$s" as a color. - The minimum font size preference is larger than the maximum font size. Using minimum for all. - Cannot find audio file: %1$s - Cannot find image file: %1$s - Audio track volume is not a number between 0 and 100. - A value has already been defined for {%1$s}. - Line %1$d was longer than %2$d characters. Truncating … - Total {pause} time exceeds designated song time. Disabling smooth scrolling mode. - Scrollbeat offset is greater than or equal to the number of beats in the bar. Resetting to zero. - Loading:\u0020 - Processing song: %1$s - - pref_lyricsHeight - 10 - 0 - 80 - 40 - The maximum percentage of the maximum line height that the lyrics will occupy. - % - Maximum lyrics height - - pref_commentDisplayTime - 1 - 0 - 19 - 4 - The amount of time that an in-song comment will remain onscreen. - s - Comment display time - - pref_firstRun - - pref_oneDriveAccessToken - pref_dropboxV2AccessToken - pref_dropboxV2RefreshToken - pref_dropboxV2ExpiryTime - pref_songSource - - pref_sorting - - pref_minLines - 1 - 1 - 7 - 1 - The minimum number of lines that should be visible at any one time. - lines - Minimum visible lines - - -600000 - 0 - 1200000 - 600000 - The offset from the start of the song, in milliseconds, at which the backing audio track will begin. - ms - Audio track start offset - - pref_beatCounterColor - Beat counter colour - The colour of the beat counter bar at the top of the screen. - #ff008800 - - pref_pageDownScrollHighlightColor - Page-down position indicator colour - Colour for the page-down scroll position indicators. - #ff990099 - - pref_beatSectionStartHighlightColor - Beat-section start highlight colour - Highlight color for the line that is the start of a beat section. - #ffaaaa00 - - pref_chorusSectionHighlightColor - Chorus section highlight colour - Highlight color for choruses. - #ff888888 - - pref_clearCache - Clear cache - Deletes all cached files. - - pref_mimicBandLeaderDisplay - Mimic band leader display in manual mode - If performing a song with a band leader in manual mode, this will automatically set your display to the same orientation and font sizes. - - pref_proximityScroll - Proximity sensor scrolls page. - If your device has a proximity sensor, triggering it will act like pressing a "page down" pedal. - - pref_anyOtherKeyPageDown - Any other key is page-down. - Any other key pressed on a connected keyboard will act like page-down. - - pref_includeSubfolders - Include Subfolders - Fetch files from subfolders also? - - pref_powerwash - Powerwash - Deletes all cached files, and clears all stored cloud credentials. - - pref_scrollMarker - Scroll marker colour - The colour of the marker that shows when the display will scroll. - #ff000000 - - pref_midiIncomingChannels - Active Incoming MIDI Channels - What MIDI channels should BeatPrompter listen to? - 65535 - - pref_defaultMIDIOutputChannel - Default output MIDI channel - What is the default MIDI channel to write to, unless otherwise specified? - 1 - - pref_chordColor - Chords colour - #ffff0000 - - pref_commentTextColor - Comment text colour - #ffff00ff - - pref_lyricColor - Lyrics colour - #ff000000 - - pref_backgroundColor - Background colour - #ffffffff - - pref_manualMode - Manual Mode - By default, songs always play in manual scrolling mode with no backing track. - - pref_mute - Mute - Never play audio. - - pref_sendMidi - Send MIDI clock signals - If a song has beat timings, this will send MIDI clock signals. - - pref_highlightColor - Highlight colour - Highlight colour to apply to highlighted lyric text. - #ff00ffff - - pref_alwaysDisplaySharpChords - Always display sharp chords - Any flat chords will be converted to appropriate sharp chords. - false - - pref_displayUnicodeAccidentals - Display Unicode accidentals - Chords that contain b/# characters will be converted to use ♭/♯. - false - - pref_currentLineHighlightColor - Current Line Highlight colour - Highlight colour to apply to the current line. - #ffffaa00 - - pref_pulseColor - Background pulse colour - The background will pulse this colour in time to the rhythm. - #ffdddddd - - pref_annotationColor - Colour of non-chord annotations - Any chord text that isn\'t recognised as a valid chord will be shown in this color. - #ff00aa00 - - pref_ignoreColorInfo - Ignore colours in files - Always uses default colours. - false - - pref_showBeatStyleIcons - Show scroll style icons - Shows the scrolling style of the song as an icon in the main list. - true - - pref_showChords - Show Chords - Displays the song chords above the lyrics at the correct point. - true - - pref_highlightCurrentLine - Highlight Current Line - Highlights the current line with your chosen highlight colour. - true - - pref_highlightBeatSectionStart - Highlight start of beat sections - Highlights the start of beat sections with your chosen highlight colour. - true - - pref_highlightPageDownLine - Show page down scroll position markers - Shows the position that the display will be scrolled to when you press page-down. - true - - pref_showMusicIcon - Show music icon - In the main song list, show an icon indicating that a song has a backing track. - true - - pref_showKeyInList - Show song key - In the main song list, show the key of the song. - false - - pref_showRatingInList - Show song rating - In the main song list, show the rating of the song. - false - - pref_largePrintList - Bigger text - Displays the song list in larger text. - false - - pref_pulse - Background pulse - The background will pulse in time to the rhythm. - true - - pref_clearTagFilterOnFolderChange - Clear tags when filter changes? - If true, any selected tag filters will be cleared whenever you switch folders/setlists/etc. - - pref_cloudPath - Storage Folder - This is the folder that the BeatPrompter song files are stored in. - - - pref_cloudDisplayPath - - pref_customComments - Custom Comments User - To only see the comments that are meant for you on the song introduction screen, enter your name(s) here. - - - 8 - 150 - - pref_minFontSize - Minimum font size (beat-scrolling) - The smallest font size that will be used in beat-scrolling mode. - 8 - - pref_maxFontSize - Maximum font size (beat-scrolling) - The largest font size that will be used in beat-scrolling mode. - 52 - - pref_minFontSizeSmooth - Minimum font size (smooth scrolling) - The smallest font size that will be used in smooth-scrolling mode. - 8 - - pref_maxFontSizeSmooth - Maximum font size (smooth scrolling) - The largest font size that will be used in smooth-scrolling mode. - 52 - - pref_minFontSizeManual - Minimum font size (manual scrolling) - The smallest font size that will be used in manual-scrolling mode. - 8 - - pref_maxFontSizeManual - Maximum font size (manual scrolling) - The largest font size that will be used in manual-scrolling mode. - 52 - - pref_alwaysUseBeatFontPrefs - Always use the beat-scrolling font size preferences. - Uses the beat-scrolling font size preferences for all songs. - false - - pref_showScrollIndicator - Show scroll indicator. - In the beat counter section, highlight the beat that the display will scroll on. - true - - pref_showSongTitle - Show song title. - Displays the current song title over the top of the beat counter section. - false - - pref_showSongKey - Show song key on title screen. - Displays the key of the song on the song title screen. - false - - pref_showFirstBeat - Show first beat. - Shows the first beat in the beat indicator section. - true - - pref_scrollingStyle - Scrolling style - jump - Scrolling Style - Jump - Smooth - jump - smooth - - pref_lineJustification - Line justification - left - Line Justification - Left - Centre - Right - left - centre - right - - pref_metronome - Metronome click - DuringCountIn - Creates a clicking sound in time with the beat. Only applicable to beat-scrolling songs. - Metronome Click - On - On when there is no backing track - Off - During count-in - On - OnWhenNoTrack - Off - DuringCountIn - - pref_showSongBPM - Show song BPM on title screen - No - Should the initial BPM of the song be shown on the song title screen? - Show Song BPM - Yes - Yes, rounded to integer - No - Yes - Rounded - No - - BPM - Key - - pref_useExternalStorage - Use External Storage - Stores data on external storage (SD card, etc). Changing this will clear the cache. - - pref_useCustomStorageLocation - Use Custom Storage Location - If selected, song data will be stored in the custom location. Changing this will clear the cache. - - pref_storageLocation - Storage Location - Where should BeatPrompter store cached song and audio files? Changing this will clear the cache. - - pref_screenaction - Controls during performance - Scroll - What controls should be available while a song is playing? - Controls During Performance - Scrolling, pausing, restarting - Volume - Nothing - Scroll - Volume - None - - Local - GoogleDrive - Dropbox - OneDrive - - pref_cloudStorageSystem - Storage System - Demo - Where are your song files stored? - Storage System - - pref_midiConnectionTypes - Connection Type - USBOnTheGo,Native,Bluetooth - What type of MIDI connection do you use? - Connection Type - - Files - - pref_automaticallyPlayNextSong - Automatically play next song - sets - Should the next song start automatically when the current song ends? - Automatically Play Next Song - Never - Always - Set Lists Only - never - always - sets - - pref_sendMidiTriggerOnStart - Output MIDI trigger - ManualStartOnly - Should the MIDI trigger message be output when the song starts? - Output MIDI trigger message - Never - Always - When started manually - Never - Always - ManualStartOnly - - pref_bluetoothMode - Bluetooth mode - None - Configure how your device communicates with band members. - Bluetooth Mode - - pref_bandLeaderDevice - Band Leader Device - _ - Specify which device to connect to when in band-member mode. - Band Leader Device - - pref_midiTriggerSafetyCatch - Trigger safety catch - WhenAtTitleScreenOrPausedOrLastLine - Can MIDI trigger messages interrupt the current song? - MIDI triggers during song display - - None - Band Leader (server) - Band Member (client) - - None - Server - Client - - Never - When at title screen - When paused, or at title screen - When paused, at title screen, or on last line of song - Always - Never - WhenAtTitleScreen - WhenAtTitleScreenOrPaused - WhenAtTitleScreenOrPausedOrLastLine - Always - - pref_defaultTrackVolume - 1 - 0 - 99 - 99 - Unless specified in the file, this is the default volume that backing tracks will play at. - % - Default track volume - - pref_audioLatency - 0 - 0 - 1000 - 0 - Amount of latency to compensate for in audio playback. - ms - Audio latency - - pref_fixedLineHeight - false - Fixed line height - Makes all lines the same height - - Use Internal Storage - Could not load audio file. Is it missing or corrupt? - Could not find audio file. - *Could not find image file* - - IntroActivity - - Welcome to BeatPrompter - Although you can use it in portrait mode, BeatPrompter works best in landscape mode. - Mr Blue Sky - BeatPrompter reads all your song files, audio files, set lists and more from cloud or local storage. It currently supports Google Drive, Dropbox or Microsoft OneDrive. - We Got The Beat - With a bit of modification to your song files, BeatPrompter will keep the display perfectly in time with the beat. Never too fast, never too slow, and always nice and big and readable! - More Than Words - BeatPrompter doesn\'t just display lyrics and chords. It can play backing tracks, and control (and respond to) external MIDI devices too. - Turn, Turn, Turn - BeatPrompter is a lyrics and chords prompter app that places the emphasis on timing and readability. - - MIDI offset should be a numerical value or beat offset characters. - MIDI tags can contain a maximum of one semi-colon character. - MIDI event offset puts it before the start of the song. - Blank value found in MIDI directive. - Invalid argument index found in MIDI directive. - Invalid byte value found in MIDI directive. - - pref_wasPowerwashed - Cache cleared. - Powerwash complete. - - Only MIDI tags that are on or after the first song line can have offsets. - The maximum MIDI event offset is 16 beats or 10 seconds. - {t:BeatPrompter Demo Song}\n + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + abc + BeatPrompter + Steven Frew + Email: steven.fullhouse@gmail.com + tinyurl.com/beatprompterdocs + Google Drive + Local Storage + Shuffle + Dropbox + OneDrive + Connected to %1$s + Lost connection to %1$s + MIDI alias set name has already been defined. + Set name has already been defined. + Image after scaling exceeds maximum size of 8192 x 8192. Ignoring line. + A MIDI alias has been defined with only a name. + A MIDI alias can only have one channel value. + Instructions found, but no MIDI alias name has been defined yet. + A MIDI alias file must contain a {midi_aliases} set name definition. + There are multiple files in the cache that match the filename \'{%1$s}\'. + Unexpected tag found in file: {%1$s} + A {%1$s} tag was found on the same line as a {%2$s} tag. This is not allowed. + Connect to leader + Both a <bars> tag and commas were found on the same line. Using tag value. + Clear cache + The single-use tag {%1$s} has been used multiple times in this file. + The tag {%1$s} has been used multiple times in this line. + The tag {%1$s} has been used multiple times in succession without an intervening ending tag. + The tag {%1$s} has been found before a corresponding starting tag. + The tag {%1$s} has been found with no specified value. + No set name defined in set list file. + Sort … + Settings … + Multiple beatstart or beatstop tags were found on the same line. Ignoring all but the first. + Could not find the root folder of the storage system. + A beatstart tag can only be used in a song with a defined BPM speed. + All Songs + Colours + Font Sizes + Google API not connected. + OneDrive API not connected. + No {set} found in set file "%1$s". + Unexpected tag: %1$s. + This set list contains %1$d unknown songs, including: + Unknown Songs In Set List + Program change trigger tags must contain between one and four values. + Song select trigger tags must contain only one value. + Could not read image file: %1$s + MIDI parameters must be a value between 0 and 127. + MIDI channel values must be between 1 and 16. + An alias value cannot contain multiple underscores. + The channel specifier value cannot contain underscores. + Values containing underscores must be in hexadecimal. + Only the lower nibble of a value can be merged with the channel specifier. + File "%1$s" is not a valid MIDI aliasing file. + MIDI alias found with no name. + Invalid MIDI alias argument index. + MIDI alias message found with more than two parts. + A "midi_alias" tag contains more than two parts. + Can\'t parse this MIDI message. + Tag "%1$s" does not match any known MIDI commands or aliases. + Unknown image scaling mode, defaulting to stretch mode. + Multiple images found in one line. Only using the first one. + Text was found on a line where an image was specified. Ignoring text. + MIDI Aliases + MIDI Alias File Errors + Badly-formed tag. + Tag is empty. + The MIDI channel must be the last parameter. + Not enough parameters supplied to MIDI command. + Default MIDI Alias Set + + About … + Temporary + Song Options + Song List + MIDI Aliases + Miscellaneous + Song Display + Created by + Documentation can be found at + Force refresh set list + Force refresh MIDI alias file + Show MIDI alias file errors + Clear temporary set list + Force refresh (song file only) + Add to temporary set list + Force refresh (including dependencies) + Play … + + Play + Cancel + No backing track + Play Options + Backing Track + Scrolling Mode + Beat Scrolling + Smooth Scrolling + Manual Scrolling + + Settings + SongDisplayActivity + Dummy Button + DUMMY\nCONTENT + MIDI + + TestFullscreenActivity + + 10 + 0 + 230 + 110 + If a tempo is not defined by a file, this speed will be used. + BPM + Default tempo + + 1 + 0 + 31 + 3 + If beats-per-bar is not defined by a file, this value will be used. + BPB + Default beats-per-bar + + 1 + 0 + 31 + 0 + If bars-per-line is not defined by a file, this value will be used. + BPL + Default bars-per-line + + pref_countIn + 0 + 0 + 4 + 0 + If the number of "count-in" bars is not defined by a file, this value will be used. Only applicable to beat-scrolling songs. + bars + Default count-in + + pref_defaultPause + 0 + 0 + 120 + 0 + If a song is being shown in "smooth scrolling mode", then this setting creates an automatic pause at the start of the song, factored into the total song length. + seconds + Default initial pause + + Please buy BeatPrompter + Buy full version + Thanks! You are officially a nice person! :) + TAP TWICE TO START + has connected + has disconnected + Connected to band leader + Lost connection to band leader + … and %1$d other error(s). + File "%1$s" not found! + No {title} found in song file "%1$s" + "%1$s" is not a valid audio file. + Checking %1$s + Synchronizing %1$s + Scanning %1$s + Deleting %1$s + Synchronizing Files + Accessing %1$s + Choose Sync Folder + the + By title + By artist + By date + By key + Sort songs … + Error reading file. + Minimum value is %1$d, value found was %2$d. + Maximum value is %1$d, value found was %2$d. + Could not parse "%1$s" as an integer value. + Could not parse "%1$s" as a duration value (use mm:ss or just seconds). + Minimum value is %1$d, value found was %2$.2f. + Maximum value is %1$d, value found was %2$.2f. + Could not parse "%1$s" as a numeric value. + Could not parse "%1$s" as a color. + The minimum font size preference is larger than the maximum font size. Using minimum for all. + Cannot find audio file: %1$s + Cannot find image file: %1$s + Audio track volume is not a number between 0 and 100. + A value has already been defined for {%1$s}. + Line %1$d was longer than %2$d characters. Truncating … + Total {pause} time exceeds designated song time. Disabling smooth scrolling mode. + Scrollbeat offset is greater than or equal to the number of beats in the bar. Resetting to zero. + Loading:\u0020 + Processing song: %1$s + + pref_lyricsHeight + 10 + 0 + 80 + 40 + The maximum percentage of the maximum line height that the lyrics will occupy. + % + Maximum lyrics height + + pref_commentDisplayTime + 1 + 0 + 19 + 4 + The amount of time that an in-song comment will remain onscreen. + s + Comment display time + + pref_firstRun + + pref_oneDriveAccessToken + pref_dropboxV2AccessToken + pref_dropboxV2RefreshToken + pref_dropboxV2ExpiryTime + pref_songSource + + pref_sorting + + pref_minLines + 1 + 1 + 7 + 1 + The minimum number of lines that should be visible at any one time. + lines + Minimum visible lines + + -600000 + 0 + 1200000 + 600000 + The offset from the start of the song, in milliseconds, at which the backing audio track will begin. + ms + Audio track start offset + + pref_beatCounterColor + Beat counter colour + The colour of the beat counter bar at the top of the screen. + #ff008800 + + pref_pageDownScrollHighlightColor + Page-down position indicator colour + Colour for the page-down scroll position indicators. + #ff990099 + + pref_beatSectionStartHighlightColor + Beat-section start highlight colour + Highlight color for the line that is the start of a beat section. + #ffaaaa00 + + pref_chorusSectionHighlightColor + Chorus section highlight colour + Highlight color for choruses. + #ff888888 + + pref_clearCache + Clear cache + Deletes all cached files. + + pref_mimicBandLeaderDisplay + Mimic band leader display in manual mode + If performing a song with a band leader in manual mode, this will automatically set your display to the same orientation and font sizes. + + pref_proximityScroll + Proximity sensor scrolls page. + If your device has a proximity sensor, triggering it will act like pressing a "page down" pedal. + + pref_anyOtherKeyPageDown + Any other key is page-down. + Any other key pressed on a connected keyboard will act like page-down. + + pref_includeSubfolders + Include Subfolders + Fetch files from subfolders also? + + pref_powerwash + Powerwash + Deletes all cached files, and clears all stored cloud credentials. + + pref_scrollMarker + Scroll marker colour + The colour of the marker that shows when the display will scroll. + #ff000000 + + pref_midiIncomingChannels + Active Incoming MIDI Channels + What MIDI channels should BeatPrompter listen to? + 65535 + + pref_defaultMIDIOutputChannel + Default output MIDI channel + What is the default MIDI channel to write to, unless otherwise specified? + 1 + + pref_chordColor + Chords colour + #ffff0000 + + pref_commentTextColor + Comment text colour + #ffff00ff + + pref_lyricColor + Lyrics colour + #ff000000 + + pref_backgroundColor + Background colour + #ffffffff + + pref_manualMode + Manual Mode + By default, songs always play in manual scrolling mode with no backing track. + + pref_mute + Mute + Never play audio. + + pref_sendMidi + Send MIDI clock signals + If a song has beat timings, this will send MIDI clock signals. + + pref_highlightColor + Highlight colour + Highlight colour to apply to highlighted lyric text. + #ff00ffff + + pref_preferredVariation + Preferred variation + If a song contains this variation, it will be loaded by default. + + pref_alwaysDisplaySharpChords + Always display sharp chords + Any flat chords will be converted to appropriate sharp chords. + false + + pref_displayUnicodeAccidentals + Display Unicode accidentals + Chords that contain b/# characters will be converted to use ♭/♯. + false + + pref_currentLineHighlightColor + Current Line Highlight colour + Highlight colour to apply to the current line. + #ffffaa00 + + pref_pulseColor + Background pulse colour + The background will pulse this colour in time to the rhythm. + #ffdddddd + + pref_annotationColor + Colour of non-chord annotations + Any chord text that isn\'t recognised as a valid chord will be shown in this color. + #ff00aa00 + + pref_ignoreColorInfo + Ignore colours in files + Always uses default colours. + false + + pref_showBeatStyleIcons + Show scroll style icons + Shows the scrolling style of the song as an icon in the main list. + true + + pref_showChords + Show Chords + Displays the song chords above the lyrics at the correct point. + true + + pref_highlightCurrentLine + Highlight Current Line + Highlights the current line with your chosen highlight colour. + true + + pref_highlightBeatSectionStart + Highlight start of beat sections + Highlights the start of beat sections with your chosen highlight colour. + true + + pref_highlightPageDownLine + Show page down scroll position markers + Shows the position that the display will be scrolled to when you press page-down. + true + + pref_showMusicIcon + Show music icon + In the main song list, show an icon indicating that a song has a backing track. + true + + pref_showKeyInList + Show song key + In the main song list, show the key of the song. + false + + pref_showRatingInList + Show song rating + In the main song list, show the rating of the song. + false + + pref_largePrintList + Bigger text + Displays the song list in larger text. + false + + pref_pulse + Background pulse + The background will pulse in time to the rhythm. + true + + pref_clearTagFilterOnFolderChange + Clear tags when filter changes? + If true, any selected tag filters will be cleared whenever you switch folders/setlists/etc. + + pref_cloudPath + Storage Folder + This is the folder that the BeatPrompter song files are stored in. + + + pref_cloudDisplayPath + + pref_customComments + Custom Comments User + To only see the comments that are meant for you on the song introduction screen, enter your name(s) here. + + + 8 + 150 + + pref_minFontSize + Minimum font size (beat-scrolling) + The smallest font size that will be used in beat-scrolling mode. + 8 + + pref_maxFontSize + Maximum font size (beat-scrolling) + The largest font size that will be used in beat-scrolling mode. + 52 + + pref_minFontSizeSmooth + Minimum font size (smooth scrolling) + The smallest font size that will be used in smooth-scrolling mode. + 8 + + pref_maxFontSizeSmooth + Maximum font size (smooth scrolling) + The largest font size that will be used in smooth-scrolling mode. + 52 + + pref_minFontSizeManual + Minimum font size (manual scrolling) + The smallest font size that will be used in manual-scrolling mode. + 8 + + pref_maxFontSizeManual + Maximum font size (manual scrolling) + The largest font size that will be used in manual-scrolling mode. + 52 + + pref_alwaysUseBeatFontPrefs + Always use the beat-scrolling font size preferences. + Uses the beat-scrolling font size preferences for all songs. + false + + pref_showScrollIndicator + Show scroll indicator. + In the beat counter section, highlight the beat that the display will scroll on. + true + + pref_showSongTitle + Show song title. + Displays the current song title over the top of the beat counter section. + false + + pref_showSongKey + Show song key on title screen. + Displays the key of the song on the song title screen. + false + + pref_showFirstBeat + Show first beat. + Shows the first beat in the beat indicator section. + true + + pref_scrollingStyle + Scrolling style + jump + Scrolling Style + Jump + Smooth + jump + smooth + + pref_lineJustification + Line justification + left + Line Justification + Left + Centre + Right + left + centre + right + + pref_metronome + Metronome click + DuringCountIn + Creates a clicking sound in time with the beat. Only applicable to beat-scrolling songs. + Metronome Click + On + On when there is no backing track + Off + During count-in + On + OnWhenNoTrack + Off + DuringCountIn + + pref_showSongBPM + Show song BPM on title screen + No + Should the initial BPM of the song be shown on the song title screen? + Show Song BPM + Yes + Yes, rounded to integer + No + Yes + Rounded + No + + BPM + Key + + pref_useExternalStorage + Use External Storage + Stores data on external storage (SD card, etc). Changing this will clear the cache. + + pref_useCustomStorageLocation + Use Custom Storage Location + If selected, song data will be stored in the custom location. Changing this will clear the cache. + + pref_storageLocation + Storage Location + Where should BeatPrompter store cached song and audio files? Changing this will clear the cache. + + pref_screenaction + Controls during performance + Scroll + What controls should be available while a song is playing? + Controls During Performance + Scrolling, pausing, restarting + Volume + Nothing + Scroll + Volume + None + + Local + GoogleDrive + Dropbox + OneDrive + + pref_cloudStorageSystem + Storage System + Demo + Where are your song files stored? + Storage System + + pref_midiConnectionTypes + Connection Type + USBOnTheGo,Native,Bluetooth + What type of MIDI connection do you use? + Connection Type + + Files + + pref_automaticallyPlayNextSong + Automatically play next song + sets + Should the next song start automatically when the current song ends? + Automatically Play Next Song + Never + Always + Set Lists Only + never + always + sets + + pref_sendMidiTriggerOnStart + Output MIDI trigger + ManualStartOnly + Should the MIDI trigger message be output when the song starts? + Output MIDI trigger message + Never + Always + When started manually + Never + Always + ManualStartOnly + + pref_bluetoothMode + Bluetooth mode + None + Configure how your device communicates with band members. + Bluetooth Mode + + pref_bandLeaderDevice + Band Leader Device + _ + Specify which device to connect to when in band-member mode. + Band Leader Device + + pref_midiTriggerSafetyCatch + Trigger safety catch + WhenAtTitleScreenOrPausedOrLastLine + Can MIDI trigger messages interrupt the current song? + MIDI triggers during song display + + None + Band Leader (server) + Band Member (client) + + None + Server + Client + + Never + When at title screen + When paused, or at title screen + When paused, at title screen, or on last line of song + Always + Never + WhenAtTitleScreen + WhenAtTitleScreenOrPaused + WhenAtTitleScreenOrPausedOrLastLine + Always + + pref_defaultTrackVolume + 1 + 0 + 99 + 99 + Unless specified in the file, this is the default volume that backing tracks will play at. + % + Default track volume + + pref_audioLatency + 0 + 0 + 1000 + 0 + Amount of latency to compensate for in audio playback. + ms + Audio latency + + pref_fixedLineHeight + false + Fixed line height + Makes all lines the same height + + Use Internal Storage + Could not load audio file. Is it missing or corrupt? + Could not find audio file. + *Could not find image file* + + IntroActivity + + Welcome to BeatPrompter + Although you can use it in portrait mode, BeatPrompter works best in landscape mode. + Mr Blue Sky + BeatPrompter reads all your song files, audio files, set lists and more from cloud or local storage. It currently supports Google Drive, Dropbox or Microsoft OneDrive. + We Got The Beat + With a bit of modification to your song files, BeatPrompter will keep the display perfectly in time with the beat. Never too fast, never too slow, and always nice and big and readable! + More Than Words + BeatPrompter doesn\'t just display lyrics and chords. It can play backing tracks, and control (and respond to) external MIDI devices too. + Turn, Turn, Turn + BeatPrompter is a lyrics and chords prompter app that places the emphasis on timing and readability. + + MIDI offset should be a numerical value or beat offset characters. + MIDI tags can contain a maximum of one semi-colon character. + MIDI event offset puts it before the start of the song. + Blank value found in MIDI directive. + Invalid argument index found in MIDI directive. + Invalid byte value found in MIDI directive. + + pref_wasPowerwashed + Cache cleared. + Powerwash complete. + + Only MIDI tags that are on or after the first song line can have offsets. + The maximum MIDI event offset is 16 beats or 10 seconds. + {t:BeatPrompter Demo Song}\n {st:Demo Song}\n {audio:demo_song.mp3}\n {bpm:110}{bpb:4}{bpl:2}{count:0}\n @@ -736,92 +740,95 @@ lyrics and [Em]chords in time with a beat.\n [Em]I hope you like it! - No storage system has been selected. - No storage folder has been selected. - Synchronize files … - Loading … - %1$d items found. - %1$d/%2$d items found. - The selected song file has no lines. - - There was an error communicating with the storage service ("%1$s").\n\nNot all of the files were fetched or scanned successfully.\n\nCheck your connection and try again. - Sync Error - - Instructions … - Privacy policy … - - USB On-The-Go - Native - Bluetooth - - USBOnTheGo - Native - Bluetooth - - pref_bluetoothPermission - Bluetooth - Allow BeatPrompter to communicate with other devices via Bluetooth. - - Current status: - Granted - Denied - Permissions - - Audio - - pref_localStoragePermission - Allow BeatPrompter to read files from local folders. - - MediaPlayer - ExoPlayer - MediaPlayer - ExoPlayer - Audio Player - pref_audioplayer - What player should the app use for audio playback? - MediaPlayer - - Failed to authenticate for Google Drive access. - By mode - - pref_buyMeACoffee - Buy me a coffee … - https://www.buymeacoffee.com/peeveen - Error - https://drive.google.com/open?id=19Unw7FkSWNWGAncC_5D3DC0IANxvLMKG1pj6vfamnOI - https://github.com/peeveen/app-policies/blob/4bb1d77d9cb750f236dde266723f06caeb6cf708/beatprompter/privacy-policy.md - Getting root folder … - - By rating - Variation - No Audio - Variation names have already been defined. - Audio tags outnumber variations. - - pref_darkMode - Toggle Dark Mode - - Reading database … - Failed to read database item - - Synchronizing %1$s (retry %2$d/%3$d) - - pref_bluetoothMidiDevices - Bluetooth MIDI Devices - What Bluetooth devices should be considered for MIDI? - Bluetooth MIDI Devices - An error occurred while reading the database - An error occurred while writing the database. - Show debug log ... - Debug Log - %1$s.%2$d - Rebuilding database ... - with_midi_* directives cannot be used in aliases that contain parameters - \'%1$s\' could not be parsed as a valid key - Failed to calculate new key (shifting %1$s by %2$d semitones) - Failed to parse chord \'%1$s\' - {transpose} tag should not contain a numeric value greater than 11, or less than -11 - {chord_map} tag must contain two values separated by = - Transpose + No storage system has been selected. + No storage folder has been selected. + Synchronize files … + Loading … + %1$d items found. + %1$d/%2$d items found. + The selected song file has no lines. + + There was an error communicating with the storage service ("%1$s").\n\nNot all of the files were fetched or scanned successfully.\n\nCheck your connection and try again. + Sync Error + + Instructions … + Privacy policy … + + USB On-The-Go + Native + Bluetooth + + USBOnTheGo + Native + Bluetooth + + pref_bluetoothPermission + Bluetooth + Allow BeatPrompter to communicate with other devices via Bluetooth. + + Current status: + Granted + Denied + Permissions + + Audio + + pref_localStoragePermission + Allow BeatPrompter to read files from local folders. + + MediaPlayer + ExoPlayer + MediaPlayer + ExoPlayer + Audio Player + pref_audioplayer + What player should the app use for audio playback? + MediaPlayer + + Failed to authenticate for Google Drive access. + By mode + + pref_buyMeACoffee + Buy me a coffee … + https://www.buymeacoffee.com/peeveen + Error + https://drive.google.com/open?id=19Unw7FkSWNWGAncC_5D3DC0IANxvLMKG1pj6vfamnOI + https://github.com/peeveen/app-policies/blob/4bb1d77d9cb750f236dde266723f06caeb6cf708/beatprompter/privacy-policy.md + Getting root folder … + + By rating + Variation + No Audio + Variation names have already been defined. + Audio tags outnumber variations. + + pref_darkMode + Toggle Dark Mode + + Reading database … + Failed to read database item + + Synchronizing %1$s (retry %2$d/%3$d) + + pref_bluetoothMidiDevices + Bluetooth MIDI Devices + What Bluetooth devices should be considered for MIDI? + Bluetooth MIDI Devices + An error occurred while reading the database + An error occurred while writing the database. + Show debug log ... + Debug Log + %1$s.%2$d + Rebuilding database … + with_midi_* directives cannot be used in aliases that contain parameters + \'%1$s\' could not be parsed as a valid key + Failed to calculate new key (shifting %1$s by %2$d semitones) + Failed to parse chord \'%1$s\' + Failed to parse note \'%1$s\' + {transpose} tag should not contain a numeric value greater than 11, or less than -11 + {chord_map} tag must contain two values separated by = + Transpose + + varstart/varxstart tag contains unknown variations: %1$s. diff --git a/app/src/main/res/xml/songdisplaypreferences.xml b/app/src/main/res/xml/songdisplaypreferences.xml index 73141796..2b3bac72 100644 --- a/app/src/main/res/xml/songdisplaypreferences.xml +++ b/app/src/main/res/xml/songdisplaypreferences.xml @@ -12,6 +12,12 @@ app:fragment="com.stevenfrew.beatprompter.ui.pref.FontSizeSettingsFragment" app:key="font-size_screen_preference" app:title="Font Sizes" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/002-BarsAndCommasSameLine.txt b/app/src/test/data/songs/002-BarsAndCommasSameLine.txt new file mode 100644 index 00000000..e2e3a1a3 --- /dev/null +++ b/app/src/test/data/songs/002-BarsAndCommasSameLine.txt @@ -0,0 +1,11 @@ +{t:002-BarsAndCommasSameLine} +{c:Commas should take precedence} +{bpl:4}{bpm:120} + +This is four bars per line, +,This has one comma, so is one line +{bpl:2},This is two bars per line, with a one-bar comma after the bpl +This, with no commas, should now be two bars +,,{bpl:4}This is four bars per line, with two commas BEFORE the bpl ... should still be two bars +This, with no commas, should now be four bars +And we’re done (four bars here). diff --git a/app/src/test/data/songs/003-TimingTrickery.expectedEvents.xml b/app/src/test/data/songs/003-TimingTrickery.expectedEvents.xml new file mode 100644 index 00000000..a277ce9d --- /dev/null +++ b/app/src/test/data/songs/003-TimingTrickery.expectedEvents.xml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/003-TimingTrickery.txt b/app/src/test/data/songs/003-TimingTrickery.txt new file mode 100644 index 00000000..b9cc0288 --- /dev/null +++ b/app/src/test/data/songs/003-TimingTrickery.txt @@ -0,0 +1,41 @@ +{t:003-TimingTrickery} +{st:The Trickster} +{bpm:100.1}{bpb:5}{bpl:2}{count:1}{scrollbeat:3} + +BPB=5, scrollbeat=3, BPL=2 / scroll on beat 3 of bar 2 +(<) scroll on beat 2 of bar 2< +(>) scroll on beat 4 of bar 2> +,1 bar / scroll on beat 3 +,,,{scrollbeat:2}Three bars, scrollbeat=2 / scroll on beat 2 of third bar +{bpb:3}2 bars, BPB=3 (>) / scrollbeat should now be 2, because it can’t be zero, so > will cause scroll on 3rd beat of 2nd bar> +,1 bar, (>>) / scroll on first beat of next bar>> +2 bars / scroll on 2nd beat of 2nd bar +{bpb:4}2 bars, BPB=4, scrollbeat will now be 3 / scroll on 3rd beat of 2nd bar +2 bars / scroll on 3rd beat of 2nd bar +,,,,4 bars, (>>>) / scroll on 2nd beat of next (5th) bar>>> +{bpb:6}2 bars, BPB=6, scrollbeat will now be 5 / scroll on 5th beat of 2nd bar +2 bars / scroll on 5th beat of 2nd bar +{bpb:3}2 bars, BPB=3, scrollbeat will now be 2 / scroll on 2nd beat of 2nd bar +,,,3 bars / scroll on 2nd beat of 3rd bar +2 bars, (<<) / scroll on 3rd beat of 1st bar<< +{bpb:5}BPB=5, scrollbeat will now be 4 / scroll on 4th beat of 3rd bar (extra bar because of early finish of previous line) +2 bars / scroll on 4th beat of 2nd bar +2 bars, (>) / scroll on 5th beat of 2nd bar> +{bpb:4}BPB=4, scrollbeat now 3 / scroll on 3rd beat of 2nd bar +{scrollbeat:4}scrollbeat=4 / scroll on 4th beat of 2nd bar +,1 bar / scroll on 4th beat of this bar +2 bars, (>) / scroll on 1st beat of 3rd (next line) bar> +,,{bpb:2}2 bars, BPB=2, scrollbeat will be 2 (<) / scroll on 1st beat of 2nd bar< +2 bars, (<<) / scroll on 2nd beat of FIRST bar<< +2 bars / should last THREE bars because of early finish of previous +{bpb:7}2 bars, BPB=7, scrollbeat will be 7 / scroll on 7th beat of 2nd bar +2 bars / scroll on 7th beat of 2nd bar +,,,,,,{bpb:3}6 bars, BPB=3, scrollbeat will be 3, (<) / scroll on 2nd beat of 6th bar< +2 bars / scroll on 3rd beat of 2nd bar +,,,,,5 bars / scroll on 3rd beat of 5th bar +{bpb:4}BPB=4, scrollbeat will now be 4, (>>) / scroll on 2nd beat of NEXT (3rd) bar>> +2 bars / scroll on 4th beat of 2nd bar +,1 bar / scroll on 4th beat of this bar +,,2 bars, (<) / scroll on 3rd beat of 2nd bar< +2 bars / scroll on 4th beat of 2nd bar +2 bars, (<) / scroll on 3th beat of 2nd bar< \ No newline at end of file diff --git a/app/src/test/data/songs/004-Scrollbeat&BPBChange.expectedEvents.xml b/app/src/test/data/songs/004-Scrollbeat&BPBChange.expectedEvents.xml new file mode 100644 index 00000000..59075c51 --- /dev/null +++ b/app/src/test/data/songs/004-Scrollbeat&BPBChange.expectedEvents.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/004-Scrollbeat&BPBChange.txt b/app/src/test/data/songs/004-Scrollbeat&BPBChange.txt new file mode 100644 index 00000000..2710d60b --- /dev/null +++ b/app/src/test/data/songs/004-Scrollbeat&BPBChange.txt @@ -0,0 +1,9 @@ +{t:004-Scrollbeat&BPBChange} +{bpm:100}{bpb:3}{bpl:2}{count:1}{scrollbeat:2} + +,One-bar line, scrollbeat is pushed to first beat of NEXT line>> +Basic two-bar line 123123, scrollbeat back to 2 +{bpb:4}Now four beats per bar, scrollbeat should be 3 (1 fewer than bar length) +Still four beats per bar, scrollbeat still 3 + + diff --git a/app/src/test/data/songs/005-BPBChange1.expectedEvents.xml b/app/src/test/data/songs/005-BPBChange1.expectedEvents.xml new file mode 100644 index 00000000..d2c538ba --- /dev/null +++ b/app/src/test/data/songs/005-BPBChange1.expectedEvents.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/005-BPBChange1.txt b/app/src/test/data/songs/005-BPBChange1.txt new file mode 100644 index 00000000..53ee0c13 --- /dev/null +++ b/app/src/test/data/songs/005-BPBChange1.txt @@ -0,0 +1,10 @@ +{t:005-BPBChange1} +{count:0}{bpl:1}{bpm:120} + +[F]I know nothing stays the [Dm]same>> +But if you're willing to play the [A#]game>> +,,It's coming around again[F] +So [D#6]don't [D#]mind if I [D+]fall a[D]part +{bpb:6}There's [F]more room in a [Gadd4]broken [G]heart[Gadd4][G] +{bpb:4}[C]You pay the grocer, +[Am]fix the toaster, diff --git a/app/src/test/data/songs/006-BPBChange2.expectedEvents.xml b/app/src/test/data/songs/006-BPBChange2.expectedEvents.xml new file mode 100644 index 00000000..07208a46 --- /dev/null +++ b/app/src/test/data/songs/006-BPBChange2.expectedEvents.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/006-BPBChange2.txt b/app/src/test/data/songs/006-BPBChange2.txt new file mode 100644 index 00000000..d3f29d9f --- /dev/null +++ b/app/src/test/data/songs/006-BPBChange2.txt @@ -0,0 +1,8 @@ +{t:006-BPBChange2} +{bpm:113}{bpl:2}{count:0}{scrollbeat:3} + +I ain't [G]saying I loved you [F]first, +I ain't [G]saying I loved you [F]first, +{scrollbeat:5}{bpb:6},But I [C]loved you [A#]best. + +{bpb:4}I [Am]know we must abide \ No newline at end of file diff --git a/app/src/test/data/songs/007-Scrollbeat.expectedEvents.xml b/app/src/test/data/songs/007-Scrollbeat.expectedEvents.xml new file mode 100644 index 00000000..33336359 --- /dev/null +++ b/app/src/test/data/songs/007-Scrollbeat.expectedEvents.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/007-Scrollbeat.txt b/app/src/test/data/songs/007-Scrollbeat.txt new file mode 100644 index 00000000..cff31845 --- /dev/null +++ b/app/src/test/data/songs/007-Scrollbeat.txt @@ -0,0 +1,23 @@ +{t:007-Scrollbeat} +{st:Scrolly Joe} +{bpm:120}{bpb:4}{bpl:1}{count:0}{scrollbeat:4} + +BPL=1, BPB=4, Scrollbeat=4 +{scrollbeat:3}Scrollbeat now 3 +{scrollbeat:2}Scrollbeat now 2 +{scrollbeat:1}Scrollbeat now 1 +{scrollbeat:2}Scrollbeat now 2 +{scrollbeat:3}Scrollbeat now 3 +{scrollbeat:4}Scrollbeat now 4 +{scrollbeat:1}Scrollbeat now 1 +{scrollbeat:4}Scrollbeat now 4 +(<<<)<<< +(<<)<< +(<)< +Simple line +(<)< +(<<)<< +(<<<)<<< +Simple line + + diff --git a/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.expectedEvents.xml b/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.expectedEvents.xml new file mode 100644 index 00000000..aa6c00d4 --- /dev/null +++ b/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.expectedEvents.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.txt b/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.txt new file mode 100644 index 00000000..77e2a850 --- /dev/null +++ b/app/src/test/data/songs/008-BPBReductionWithScrollbeatAdjustments.txt @@ -0,0 +1,6 @@ +{t:008-BPBReductionWithScrollbeatAdjustments} +{bpm:84.35}{bpl:1} + +{bpb:6}[G]And [Gsus4]I'll be, I'll be< +{bpb:4}Following [C]you< + diff --git a/app/src/test/data/songs/009-NoTitle.txt b/app/src/test/data/songs/009-NoTitle.txt new file mode 100644 index 00000000..3325cfd9 --- /dev/null +++ b/app/src/test/data/songs/009-NoTitle.txt @@ -0,0 +1,2 @@ +[A]wfwef[B]wefwefwe +[C][wefwefw[D] \ No newline at end of file diff --git a/app/src/test/data/songs/010-SmoothScrolling.expectedEvents.xml b/app/src/test/data/songs/010-SmoothScrolling.expectedEvents.xml new file mode 100644 index 00000000..08eb2319 --- /dev/null +++ b/app/src/test/data/songs/010-SmoothScrolling.expectedEvents.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/data/songs/010-SmoothScrolling.txt b/app/src/test/data/songs/010-SmoothScrolling.txt new file mode 100644 index 00000000..59404b90 --- /dev/null +++ b/app/src/test/data/songs/010-SmoothScrolling.txt @@ -0,0 +1,80 @@ +{t:Billie Jean} +{a:(Michael Jackson)} +{count:0} +{time:3:40} +{tag:INTERNACIONAIS}{tag:TIME}{tag:80s} +- +- +- +- +- +- +- +RIFF... +[F#m]- [E]- [F#m]- [E]- +[F#m]- [E]- [F#m]- [E]- +- +{soh:#FFFF00}[F#m] She was [E]more like a [F#m]beauty queen +from a [E]movie scene.{eoh} +[F#m] I said, "Don't [E]mind, but what [F#m]do you mean?" +"[E]I am the [Bm]one... +who will [Bm7]dance on the floor in the [F#m]round." [E] +[F#m] She said, am the [Bm]one +who will [Bm7]dance on the floor in the [F#m]round." [E] +[F#m]- [E]- +- +{soh:#FFFF00}[F#m] She told me [E]her name was [F#m]Billie Jean, +as she [E]caused a scene,{eoh} +[F#m] then every [E]head turned, with [F#m]eyes that [E]dreamed of +being the [Bm]one... +who will [Bm7]dance on the floor in the [F#m]round." [E] [F#m] [E] +- +{soh:#FFFF00}[D]People always told me, "Be [F#m]careful what you do, +and don't [D]go around breaking young girls' [F#m]hearts !" +And [D]mother always told me, "Be [F#m]careful who you love, +and be [D]careful of what you do 'cause the [C#7]lie becomes the truth." Hey!{eoh} +- +{soh}[F#m] Billie [E]Jean is [F#m]not my [E]lover, +[F#m] she's just a [E]girl who [F#m]claims that [E]I am the [Bm]one, +but the [Bm7]kid is not my [F#m]son. [E]- +[F#m] she says [E]I am the [Bm]one, +but the [Bm7]kid is not my [F#m]son. [E] [F#m] [E]{eoh} +- +{soh:#FFFF00}[F#m] For forty [E]days and [F#m]forty nights, +the law was [E]on her side,{eoh} +[F#m] but who can [E]stand, when she's [F#m]in demand, +[E]her schemes and [Bm]plans. +'cause we [Bm7]danced on the floor in the [F#m]round, [E]- +[F#m] so take [E]my strong ad[Bm]vice, +just re[Bm7]member to always think [F#m]twice. {soh}[E] (Do think [F#m]twice) [E]{eoh} +- +{soh:#FFFF00}[F#m] She told [E]my baby that's[F#m] a treat +as she [E]looked at me{eoh} +[F#m] then showed a [E]photo of [F#m]baby cries, +[E]eyes were like [Bm]mine... +who will [Bm7]dance on the floor in the [F#m]round, [E]baby [F#m] [E] +- +{soh:#FFFF00}[D]People always told me, "Be [F#m]careful what you do, +and don't [D]go around breaking young girls' [F#m]hearts !" +But she [D]came and stood right by me, then the [F#m]smell of sweet perfume +this [D]happened much too soon, she [C#7]called me to her room{eoh} +- +{soh}[F#m] Billie [E]Jean is [F#m]not my [E]lover, +[F#m] she's just a [E]girl who [F#m]claims that [E]I am the [Bm]one, +but the [Bm7]kid is not my [F#m]son. [E]- +[F#m] she says [E]I am the [Bm]one, +but the [Bm7]kid is not my [F#m]son. [E]- [F#m]- [E]-{eoh} +- +[F#m] Billie [E]Jean is [F#m]not my [E]lover, +[F#m] Billie [E]Jean is [F#m]not my [E]lover, +[F#m] Billie [E]Jean is [F#m]not my [E]lover, +[F#m]- [E]- [F#m]- [E]- +RIFF (final) +- +- +- +- +- +- +- + diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/TestChordParsing.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestChordParsing.kt new file mode 100644 index 00000000..417c622e --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestChordParsing.kt @@ -0,0 +1,182 @@ +package com.stevenfrew.beatprompter + +import com.stevenfrew.beatprompter.chord.Chord +import com.stevenfrew.beatprompter.chord.InvalidChordException +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class TestChordParsing { + init { + TestUtils.setMocks() + } + + @Test + fun testBasicChords() { + with(parseChord("A")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.A, root) + assertEquals(null, suffix) + assertEquals(null, bass) + assertEquals("A", toDisplayString(alwaysUseSharps = true, useUnicodeAccidentals = true)) + } + with(parseChord("B")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.B, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("C")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.C, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("D")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.D, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("E")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.E, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("F")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.F, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("G")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.G, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + } + + @Test + fun testBasicSharpChords() { + with(parseChord("A♯")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.ASharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + assertEquals("A#", toDisplayString()) + } + with(parseChord("B#")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.BSharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + assertEquals("B♯", toDisplayString(alwaysUseSharps = true, useUnicodeAccidentals = true)) + } + with(parseChord("C♯")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.CSharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("D#")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.DSharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("E#")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.ESharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("F♯")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.FSharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("G#")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.GSharp, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + } + + @Test + fun testBasicFlatChords() { + with(parseChord("Ab")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.AFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + assertEquals("A♭", toDisplayString(alwaysUseSharps = false, useUnicodeAccidentals = true)) + } + with(parseChord("B♭")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.BFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + assertEquals("Bb", toDisplayString()) + } + with(parseChord("Cb")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.CFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("D♭")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.DFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("Eb")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.EFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("F♭")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.FFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + with(parseChord("Gb")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.GFlat, root) + assertEquals(null, suffix) + assertEquals(null, bass) + } + } + + @Test + fun testComplexChords() { + with(parseChord("G♮/D")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.G, root) + assertEquals(null, suffix) + assertEquals(com.stevenfrew.beatprompter.chord.Note.D, bass) + } + with(parseChord("Dbm7b5")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.DFlat, root) + assertEquals("m7b5", suffix) + assertEquals(null, bass) + assertEquals("C♯m7♭5", toDisplayString(alwaysUseSharps = true, useUnicodeAccidentals = true)) + } + with(parseChord("Dm#5")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.D, root) + assertEquals("m#5", suffix) + assertEquals(null, bass) + } + with(parseChord("F#m(M7)")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.FSharp, root) + assertEquals("m(M7)", suffix) + assertEquals(null, bass) + } + with(parseChord("Dbmaj7sus2/F#")) { + assertEquals(com.stevenfrew.beatprompter.chord.Note.DFlat, root) + assertEquals("maj7sus2", suffix) + assertEquals(com.stevenfrew.beatprompter.chord.Note.FSharp, bass) + } + } + + @Test + fun testInvalidChords() { + assertThrows { parseChord("H") } + assertThrows { parseChord("a") } + assertThrows { parseChord("hello") } + assertThrows { parseChord("D##m") } + assertThrows { parseChord("0x3430") } + assertThrows { parseChord("ABACAB") } + } + + private fun parseChord(chord: String): Chord { + val parsedChord = Chord.parse(chord) + assertNotNull(parsedChord) + return parsedChord + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/TestSongParsing.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestSongParsing.kt new file mode 100644 index 00000000..c32c00f6 --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestSongParsing.kt @@ -0,0 +1,67 @@ +package com.stevenfrew.beatprompter + +import com.stevenfrew.beatprompter.cache.parse.InvalidBeatPrompterFileException +import com.stevenfrew.beatprompter.song.ScrollingMode +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals + +class TestSongParsing { + init { + TestUtils.setMocks() + } + + @Test + fun testNoLines() { + val songFile = TestUtils.getTestFile("songs", "001-NoLines.txt") + val exception = assertThrows { TestUtils.parseSong(songFile) } + assertEquals("2131886489", exception.message) + } + + @Test + fun testBarsAndCommasOnSameLine() { + TestUtils.testSongFileEvents("002-BarsAndCommasSameLine.txt") + } + + @Test + fun testTimingTrickery() { + TestUtils.testSongFileEvents("003-TimingTrickery.txt") + } + + @Test + fun testScrollbeatWithBPBChange() { + TestUtils.testSongFileEvents("004-Scrollbeat&BPBChange.txt") + } + + @Test + fun testBPBChange1() { + TestUtils.testSongFileEvents("005-BPBChange1.txt") + } + + @Test + fun testBPBChange2() { + TestUtils.testSongFileEvents("006-BPBChange2.txt") + } + + @Test + fun testScrollbeat() { + TestUtils.testSongFileEvents("007-Scrollbeat.txt") + } + + @Test + fun testBPBReductionWithScrollbeatAdjustments() { + TestUtils.testSongFileEvents("008-BPBReductionWithScrollbeatAdjustments.txt") + } + + @Test + fun testNoTitle() { + val songFile = TestUtils.getTestFile("songs", "009-NoTitle.txt") + val exception = assertThrows { TestUtils.parseSong(songFile) } + assertEquals("2131886485 009-NoTitle.txt", exception.message) + } + + @Test + fun testSmoothScrolling() { + TestUtils.testSongFileEvents("010-SmoothScrolling.txt", ScrollingMode.Smooth) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/TestUtils.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestUtils.kt new file mode 100644 index 00000000..f1fde13b --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/TestUtils.kt @@ -0,0 +1,442 @@ +package com.stevenfrew.beatprompter + +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.graphics.Paint +import com.stevenfrew.beatprompter.cache.AudioFile +import com.stevenfrew.beatprompter.cache.CachedFile +import com.stevenfrew.beatprompter.cache.parse.FileParseError +import com.stevenfrew.beatprompter.cache.parse.SongInfoParser +import com.stevenfrew.beatprompter.cache.parse.SongParser +import com.stevenfrew.beatprompter.comm.midi.message.MidiMessage +import com.stevenfrew.beatprompter.graphics.DisplaySettings +import com.stevenfrew.beatprompter.graphics.Rect +import com.stevenfrew.beatprompter.midi.EventOffset +import com.stevenfrew.beatprompter.midi.EventOffsetType +import com.stevenfrew.beatprompter.mock.MockGlobalAppResources +import com.stevenfrew.beatprompter.mock.MockPlatformUtils +import com.stevenfrew.beatprompter.mock.MockPreferences +import com.stevenfrew.beatprompter.mock.MockSupportFileResolver +import com.stevenfrew.beatprompter.mock.graphics.MockLine +import com.stevenfrew.beatprompter.song.ScrollingMode +import com.stevenfrew.beatprompter.song.Song +import com.stevenfrew.beatprompter.song.event.AudioEvent +import com.stevenfrew.beatprompter.song.event.BaseEvent +import com.stevenfrew.beatprompter.song.event.BeatEvent +import com.stevenfrew.beatprompter.song.event.ClickEvent +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.PauseEvent +import com.stevenfrew.beatprompter.song.event.StartEvent +import com.stevenfrew.beatprompter.song.load.SongLoadInfo +import org.junit.jupiter.api.Assertions.assertEquals +import org.w3c.dom.Document +import org.w3c.dom.Element +import java.io.File +import java.io.StringWriter +import java.nio.file.Paths +import java.util.Date +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import kotlin.io.path.pathString + +object TestUtils { + internal fun setMocks() { + BeatPrompter.appResources = MockGlobalAppResources() + BeatPrompter.preferences = MockPreferences() + BeatPrompter.platformUtils = MockPlatformUtils() + } + + internal fun getTestFile(subfolder: String, filename: String): File { + val testFilePath = Paths.get(TEST_DATA_FOLDER_PATH.pathString, subfolder, filename) + val testFile = File(testFilePath.pathString) + if (testFile.exists() && testFile.isFile) + return testFile + throw UnsupportedOperationException("Requested test file ${testFilePath.pathString} could not found.") + } + + internal fun parseSong( + songFile: File, + scrollingMode: ScrollingMode = ScrollingMode.Beat + ): Pair> { + val cachedFile = + CachedFile( + songFile, + songFile.path, + songFile.name, + Date(songFile.lastModified()), + listOf(songFile.parent ?: "") + ) + val songFileInfoParser = SongInfoParser(cachedFile) + val parsedSongFile = songFileInfoParser.parse() + val songLoadInfo = SongLoadInfo( + parsedSongFile, parsedSongFile.variations.first(), scrollingMode, TestDisplaySettings, + TestDisplaySettings + ) + val songParser = + SongParser(songLoadInfo, MockSupportFileResolver(TEST_DATA_FOLDER_PATH.pathString)) + val song = songParser.parse() + return song to songParser.errors + } + + internal fun testSongFileEvents( + filename: String, + scrollingMode: ScrollingMode = ScrollingMode.Beat + ): Song { + val songFile = getTestFile("songs", filename) + val (song, errors) = parseSong(songFile, scrollingMode) + checkExpectedSongEvents(songFile, song, errors) + return song + } + + private fun checkExpectedSongEvents(songFile: File, song: Song, errors: List) { + val nameWithoutExtension = songFile.nameWithoutExtension + val songEvents = getSongEvents(song) + val expectedEventsPath = + Paths.get(songFile.parent, "${nameWithoutExtension}.expectedEvents.xml").pathString + val parsedEventsPath = + Paths.get(songFile.parent, "${nameWithoutExtension}.parsedEvents.xml").pathString + val expectedErrorsPath = + Paths.get(songFile.parent, "${nameWithoutExtension}.expectedErrors.txt").pathString + val expectedEventsXml = readEventListXml(expectedEventsPath) + val expectedErrors = readErrorList(expectedErrorsPath) + assertEquals(expectedErrors, errors) + val eventsXml = getEventListAsXml(songEvents) + File(parsedEventsPath).writeText(eventsXml) + if (expectedEventsXml != null) + assertEquals( + expectedEventsXml, + eventsXml, + "$nameWithoutExtension did not parse correctly. Compare output XML files for details." + ) + else + File(expectedEventsPath).writeText(eventsXml) + } + + private val TestScreenSize = Rect(0, 0, 2000, 1000) + private val TestDisplaySettings = + DisplaySettings(ORIENTATION_LANDSCAPE, 8.0f, 80.0f, TestScreenSize) + + private val PROJECT_DIR_ABSOLUTE_PATH = Paths.get("").toAbsolutePath().toString() + private val TEST_DATA_FOLDER_PATH = Paths.get(PROJECT_DIR_ABSOLUTE_PATH, "src/test/data") + + private fun getSongEvents(song: Song): List { + val events = mutableListOf() + var event: LinkedEvent? = song.currentEvent + while (event != null) { + events.add(event.event) + event = event.nextEvent + } + return events + } + + private fun readErrorList(filename: String): List { + val errorListFile = File(filename) + if (errorListFile.exists() && errorListFile.isFile) + return errorListFile.readLines() + return listOf() + } + + private fun readEventListXml(filename: String): String? { + val eventListFile = File(filename) + if (eventListFile.exists() && eventListFile.isFile) + return eventListFile.readText() + return null + } + + private fun parseEventListXml(xmlString: String): List { + val xml = DocumentBuilderFactory + .newInstance() + .newDocumentBuilder() + .parse(xmlString) + val childNodes = xml.documentElement.childNodes + val childNodeCount = childNodes.length + val childNodeList = (0..childNodeCount).map { + childNodes.item(it) as? Element + }.filterIsInstance() + return childNodeList.map { parseXmlElement(it) } + } + + private fun getEventListAsXml(events: List): String { + val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val document = docBuilder.newDocument() + val root = document.createElement("events") + document.appendChild(root) + events.forEach { + val eventElement = writeEventToXml(it, document) + eventElement.setAttribute(TIME_ATTRIBUTE, it.eventTime.toString()) + root.appendChild(eventElement) + } + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + val stringWriter = StringWriter() + val output = StreamResult(stringWriter) + val input = DOMSource(document) + transformer.transform(input, output) + return stringWriter.toString() + } + + private fun parseXmlElement(element: Element): BaseEvent = + when (element.tagName) { + AUDIO_EVENT_TAG_NAME -> parseAudioEventXmlElement(element) + BEAT_EVENT_TAG_NAME -> parseBeatEventXmlElement(element) + CLICK_EVENT_TAG_NAME -> parseClickEventXmlElement(element) + COMMENT_EVENT_TAG_NAME -> parseCommentEventXmlElement(element) + END_EVENT_TAG_NAME -> parseEndEventXmlElement(element) + LINE_EVENT_TAG_NAME -> parseLineEventXmlElement(element) + MIDI_EVENT_TAG_NAME -> parseMidiEventXmlElement(element) + PAUSE_EVENT_TAG_NAME -> parsePauseEventXmlElement(element) + START_EVENT_TAG_NAME -> parseStartEventXmlElement(element) + else -> throw UnsupportedOperationException("Events file contained an unknown event element '${element.tagName}'") + } + + private fun writeEventToXml(event: BaseEvent, document: Document): Element = + when (event) { + is AudioEvent -> writeAudioEventToXml(event, document) + is BeatEvent -> writeBeatEventToXml(event, document) + is ClickEvent -> writeClickEventToXml(document) + is CommentEvent -> writeCommentEventToXml(event, document) + is EndEvent -> writeEndEventToXml(document) + is LineEvent -> writeLineEventToXml(event, document) + is MidiEvent -> writeMidiEventToXml(event, document) + is PauseEvent -> writePauseEventToXml(event, document) + is StartEvent -> writeStartEventToXml(document) + else -> throw UnsupportedOperationException("Don't know how to handle this event type.") + } + + private const val AUDIO_EVENT_TAG_NAME = "audio" + private const val BEAT_EVENT_TAG_NAME = "beat" + private const val CLICK_EVENT_TAG_NAME = "click" + private const val COMMENT_EVENT_TAG_NAME = "comment" + private const val END_EVENT_TAG_NAME = "end" + private const val LINE_EVENT_TAG_NAME = "line" + private const val MIDI_EVENT_TAG_NAME = "midi" + private const val PAUSE_EVENT_TAG_NAME = "pause" + private const val START_EVENT_TAG_NAME = "start" + + private const val TIME_ATTRIBUTE = "time" + private const val VOLUME_ATTRIBUTE = "volume" + private const val DURATION_ATTRIBUTE = "duration" + private const val IS_BACKING_TRACK_ATTRIBUTE = "isBackingTrack" + private const val BPM_ATTRIBUTE = "bpm" + private const val BPB_ATTRIBUTE = "bpb" + private const val BEAT_ATTRIBUTE = "beat" + private const val CLICK_ATTRIBUTE = "click" + private const val WILL_SCROLL_ON_BEAT_ATTRIBUTE = "willScrollOnBeat" + private const val TEXT_ATTRIBUTE = "text" + private const val AUDIENCE_ATTRIBUTE = "audience" + private const val COLOR_ATTRIBUTE = "color" + private const val MESSAGES_ATTRIBUTE = "messages" + private const val OFFSET_AMOUNT_ATTRIBUTE = "offsetAmount" + private const val OFFSET_TYPE_ATTRIBUTE = "offsetType" + private const val LINE_NUMBER_ATTRIBUTE = "lineNumber" + private const val BEATS_ATTRIBUTE = "beats" + private const val PATH_ATTRIBUTE = "path" + private const val FILENAME_ATTRIBUTE = "filename" + private const val ID_ATTRIBUTE = "id" + private const val IS_IN_CHORUS_SECTION_ATTRIBUTE = "isInChorusSection" + private const val SCROLLING_MODE_ATTRIBUTE = "scrollingMode" + private const val SONG_PIXEL_POSITION_ATTRIBUTE = "songPixelPosition" + private const val Y_START_SCROLL_TIME_ATTRIBUTE = "yStartScrollTime" + private const val Y_STOP_SCROLL_TIME_ATTRIBUTE = "yStopScrollTime" + private const val LINES_ATTRIBUTE = "lines" + private const val WIDTH_ATTRIBUTE = "width" + private const val HEIGHT_ATTRIBUTE = "height" + private const val JUMP_SCROLL_INTERVALS_ATTRIBUTE = "jumpScrollIntervals" + private const val PIXELS_TO_TIMES_ATTRIBUTE = "pixelsToTimes" + private const val GRAPHIC_HEIGHTS_ATTRIBUTE = "graphicHeights" + + private fun parseAudioEventXmlElement(element: Element): AudioEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val volume = element.getAttribute(VOLUME_ATTRIBUTE).toInt() + val duration = element.getAttribute(DURATION_ATTRIBUTE).toLong() + val isBackingTrack = element.getAttribute(IS_BACKING_TRACK_ATTRIBUTE).toBoolean() + val path = element.getAttribute(PATH_ATTRIBUTE) + val filename = element.getAttribute(FILENAME_ATTRIBUTE) + val id = element.getAttribute(ID_ATTRIBUTE) + val cachedFile = CachedFile(File(path), id, filename, Date(), listOf()) + val audioFile = AudioFile(cachedFile, duration) + return AudioEvent(time, audioFile, volume, isBackingTrack) + } + + private fun writeAudioEventToXml(event: AudioEvent, document: Document): Element { + val element = document.createElement(AUDIO_EVENT_TAG_NAME) + element.setAttribute(VOLUME_ATTRIBUTE, event.volume.toString()) + element.setAttribute(DURATION_ATTRIBUTE, event.audioFile.duration.toString()) + element.setAttribute(IS_BACKING_TRACK_ATTRIBUTE, event.isBackingTrack.toString()) + element.setAttribute(PATH_ATTRIBUTE, event.audioFile.file.path) + element.setAttribute(FILENAME_ATTRIBUTE, event.audioFile.file.name) + element.setAttribute(ID_ATTRIBUTE, event.audioFile.id) + return element + } + + private fun parseBeatEventXmlElement(element: Element): BeatEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val bpm = element.getAttribute(BPM_ATTRIBUTE).toDouble() + val bpb = element.getAttribute(BPB_ATTRIBUTE).toInt() + val beat = element.getAttribute(BEAT_ATTRIBUTE).toInt() + val click = element.getAttribute(CLICK_ATTRIBUTE).toBoolean() + val willScrollOnBeat = element.getAttribute(WILL_SCROLL_ON_BEAT_ATTRIBUTE).toInt() + return BeatEvent(time, bpm, bpb, beat, click, willScrollOnBeat) + } + + private fun writeBeatEventToXml(event: BeatEvent, document: Document): Element { + val element = document.createElement(BEAT_EVENT_TAG_NAME) + element.setAttribute(BPM_ATTRIBUTE, event.bpm.toString()) + element.setAttribute(BPB_ATTRIBUTE, event.bpb.toString()) + element.setAttribute(BEAT_ATTRIBUTE, event.beat.toString()) + element.setAttribute(CLICK_ATTRIBUTE, event.click.toString()) + element.setAttribute(WILL_SCROLL_ON_BEAT_ATTRIBUTE, event.willScrollOnBeat.toString()) + return element + } + + private fun parseClickEventXmlElement(element: Element): ClickEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + return ClickEvent(time) + } + + private fun writeClickEventToXml(document: Document): Element { + val element = document.createElement(CLICK_EVENT_TAG_NAME) + return element + } + + private fun parseCommentEventXmlElement(element: Element): CommentEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val text = element.getAttribute(TEXT_ATTRIBUTE) + val audience = element.getAttribute(AUDIENCE_ATTRIBUTE) + val color = element.getAttribute(COLOR_ATTRIBUTE) + return CommentEvent( + time, + Song.Comment(text, audience.split("***"), color.toInt(), TestScreenSize, Paint()) + ) + } + + private fun writeCommentEventToXml(event: CommentEvent, document: Document): Element { + val element = document.createElement(COMMENT_EVENT_TAG_NAME) + element.setAttribute(TEXT_ATTRIBUTE, event.comment.text) + element.setAttribute(AUDIENCE_ATTRIBUTE, "") + return element + } + + private fun parseEndEventXmlElement(element: Element): EndEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + return EndEvent(time) + } + + private fun writeEndEventToXml(document: Document): Element { + val element = document.createElement(END_EVENT_TAG_NAME) + return element + } + + private fun parseLineEventXmlElement(element: Element): LineEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val duration = element.getAttribute(TIME_ATTRIBUTE).toLong() + val isInChorusSection = element.getAttribute(TIME_ATTRIBUTE).toBoolean() + val scrollingMode = ScrollingMode.valueOf(element.getAttribute(TIME_ATTRIBUTE)) + val songPixelPosition = element.getAttribute(SONG_PIXEL_POSITION_ATTRIBUTE).toInt() + val yStartScrollTime = element.getAttribute(Y_START_SCROLL_TIME_ATTRIBUTE).toLong() + val yStopScrollTime = element.getAttribute(Y_STOP_SCROLL_TIME_ATTRIBUTE).toLong() + + return LineEvent( + time, + MockLine( + time, + duration, + scrollingMode, + songPixelPosition, + isInChorusSection, + yStartScrollTime, + yStopScrollTime, + TestDisplaySettings + ) + ) + } + + private fun writeLineEventToXml(event: LineEvent, document: Document): Element { + val element = document.createElement(LINE_EVENT_TAG_NAME) + element.setAttribute(DURATION_ATTRIBUTE, event.line.lineDuration.toString()) + element.setAttribute(IS_IN_CHORUS_SECTION_ATTRIBUTE, event.line.isInChorusSection.toString()) + element.setAttribute(SCROLLING_MODE_ATTRIBUTE, event.line.scrollMode.toString()) + element.setAttribute(SONG_PIXEL_POSITION_ATTRIBUTE, event.line.songPixelPosition.toString()) + element.setAttribute(Y_START_SCROLL_TIME_ATTRIBUTE, event.line.yStartScrollTime.toString()) + element.setAttribute(Y_STOP_SCROLL_TIME_ATTRIBUTE, event.line.yStopScrollTime.toString()) + element.setAttribute(LINES_ATTRIBUTE, event.line.measurements.lines.toString()) + element.setAttribute(WIDTH_ATTRIBUTE, event.line.measurements.lineWidth.toString()) + element.setAttribute(HEIGHT_ATTRIBUTE, event.line.measurements.lineHeight.toString()) + element.setAttribute( + JUMP_SCROLL_INTERVALS_ATTRIBUTE, + event.line.measurements.jumpScrollIntervals.joinToString(",") + ) + element.setAttribute( + PIXELS_TO_TIMES_ATTRIBUTE, + event.line.measurements.pixelsToTimes.joinToString(",") + ) + element.setAttribute( + GRAPHIC_HEIGHTS_ATTRIBUTE, + event.line.measurements.graphicHeights.joinToString(",") + ) + return element + } + + private fun parseMidiEventXmlElement(element: Element): MidiEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val messages = element.getAttribute(MESSAGES_ATTRIBUTE) + val offsetAmount = element.getAttribute(OFFSET_AMOUNT_ATTRIBUTE).toInt() + val offsetType = EventOffsetType.valueOf(element.getAttribute(OFFSET_TYPE_ATTRIBUTE)) + val offsetLineNumber = element.getAttribute(LINE_NUMBER_ATTRIBUTE).toInt() + return MidiEvent( + time, + messages.split(',').map { messageBytes -> + MidiMessage( + messageBytes.split(' ').map { + it.toByte(16) + }.toByteArray() + ) + }, + EventOffset(offsetAmount, offsetType, offsetLineNumber) + ) + } + + private fun writeMidiEventToXml(event: MidiEvent, document: Document): Element { + val element = document.createElement(LINE_EVENT_TAG_NAME) + element.setAttribute(OFFSET_AMOUNT_ATTRIBUTE, event.offset.amount.toString()) + element.setAttribute(OFFSET_TYPE_ATTRIBUTE, event.offset.offsetType.toString()) + element.setAttribute(LINE_NUMBER_ATTRIBUTE, event.offset.sourceFileLineNumber.toString()) + val messagesString = event.messages.joinToString(",") { message -> + message.bytes.joinToString(" ") { + it.toString(16) + } + } + element.setAttribute(MESSAGES_ATTRIBUTE, messagesString) + return element + } + + private fun parsePauseEventXmlElement(element: Element): PauseEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + val beats = element.getAttribute(BEATS_ATTRIBUTE).toInt() + val beat = element.getAttribute(BEAT_ATTRIBUTE).toInt() + return PauseEvent(time, beats, beat) + } + + private fun writePauseEventToXml(event: PauseEvent, document: Document): Element { + val element = document.createElement(PAUSE_EVENT_TAG_NAME) + element.setAttribute(BEATS_ATTRIBUTE, event.beats.toString()) + element.setAttribute(BEAT_ATTRIBUTE, event.beat.toString()) + return element + } + + private fun parseStartEventXmlElement(element: Element): StartEvent { + val time = element.getAttribute(TIME_ATTRIBUTE).toLong() + return StartEvent(time) + } + + private fun writeStartEventToXml(document: Document): Element { + val element = document.createElement(START_EVENT_TAG_NAME) + return element + } +} diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockGlobalAppResources.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockGlobalAppResources.kt new file mode 100644 index 00000000..39c87941 --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockGlobalAppResources.kt @@ -0,0 +1,17 @@ +package com.stevenfrew.beatprompter.mock + +import android.content.res.AssetManager +import com.stevenfrew.beatprompter.util.GlobalAppResources + +class MockGlobalAppResources : GlobalAppResources { + override fun getString(resID: Int): String = "$resID" + override fun getString(resID: Int, vararg args: Any): String = + "$resID${getResourceStringArgs(*args)}" + + private fun getResourceStringArgs(vararg args: Any): String = + if (args.isEmpty()) "" else " ${args.joinToString(" ")}" + + override fun getStringSet(resID: Int): Set = setOf("$resID") + override val assetManager: AssetManager + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPlatformUtils.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPlatformUtils.kt new file mode 100644 index 00000000..ea8ef30b --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPlatformUtils.kt @@ -0,0 +1,13 @@ +package com.stevenfrew.beatprompter.mock + +import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapFactory +import com.stevenfrew.beatprompter.graphics.fonts.FontManager +import com.stevenfrew.beatprompter.mock.graphics.MockBitmapFactory +import com.stevenfrew.beatprompter.mock.graphics.MockFontManager +import com.stevenfrew.beatprompter.util.PlatformUtils + +class MockPlatformUtils : PlatformUtils { + override val bitmapFactory: BitmapFactory = MockBitmapFactory() + override val fontManager: FontManager = MockFontManager() + override fun parseColor(colorString: String): Int = 0 +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPreferences.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPreferences.kt new file mode 100644 index 00000000..3995ece2 --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockPreferences.kt @@ -0,0 +1,104 @@ +package com.stevenfrew.beatprompter.mock + +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.preferences.Preferences +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 + +class MockPreferences( + override val midiConnectionTypes: Set = setOf(), + override val alwaysDisplaySharpChords: Boolean = true, + override val displayUnicodeAccidentals: Boolean = true, + override val bluetoothMidiDevices: Set = setOf(), + override var darkMode: Boolean = true, + override val defaultTrackVolume: Int = 66, + override val defaultMIDIOutputChannel: Int = 1, + override val defaultHighlightColor: Int = 0x00445566, + override val bandLeaderDevice: String = "", + override val preferredVariation: String = "", + override val bluetoothMode: BluetoothMode = BluetoothMode.None, + override val incomingMIDIChannels: Int = 65535, + override var cloudDisplayPath: String = "/", + override var cloudPath: String = "/", + override val includeSubFolders: Boolean = true, + override var firstRun: Boolean = false, + override val manualMode: Boolean = false, + override val mute: Boolean = false, + override var sorting: Array = arrayOf(SortingPreference.Title), + override val defaultCountIn: Int = 0, + override val audioLatency: Int = 0, + override val sendMIDIClock: Boolean = false, + override val customCommentsUser: String = "", + override val showChords: Boolean = true, + override val showKey: Boolean = false, + override val showBPMContext: ShowBPMContext = ShowBPMContext.No, + override val sendMIDITriggerOnStart: TriggerOutputContext = TriggerOutputContext.Never, + override val metronomeContext: MetronomeContext = MetronomeContext.Off, + override val lyricColor: Int = 0x00000000, + override val chordColor: Int = 0x00ff0000, + override val chorusHighlightColor: Int = 0x00000000, + override val annotationColor: Int = 0x00000000, + override val largePrint: Boolean = false, + override val proximityScroll: Boolean = false, + override val anyOtherKeyPageDown: Boolean = false, + override var storageSystem: StorageType = StorageType.Local, + override val onlyUseBeatFontSizes: Boolean = false, + override val minimumBeatFontSize: Int = 8, + override val maximumBeatFontSize: Int = 80, + override val minimumSmoothFontSize: Int = 8, + override val maximumSmoothFontSize: Int = 80, + override val minimumManualFontSize: Int = 8, + override val maximumManualFontSize: Int = 80, + override val useExternalStorage: Boolean = false, + override val mimicBandLeaderDisplay: Boolean = true, + override val playNextSong: String = "", + override val showBeatStyleIcons: Boolean = true, + override val showKeyInSongList: Boolean = true, + override val showRatingInSongList: Boolean = true, + override val showMusicIcon: Boolean = true, + override val screenAction: SongView.ScreenAction = SongView.ScreenAction.Scroll, + override val audioPlayer: AudioPlayerType = AudioPlayerType.ExoPlayer, + override val showScrollIndicator: Boolean = true, + override val showSongTitle: Boolean = false, + override val commentDisplayTime: Int = 3, + override val midiTriggerSafetyCatch: SongView.TriggerSafetyCatch = SongView.TriggerSafetyCatch.WhenAtTitleScreenOrPausedOrLastLine, + override val highlightCurrentLine: Boolean = true, + override val showPageDownMarker: Boolean = true, + override val clearTagsOnFolderChange: Boolean = true, + override val highlightBeatSectionStart: Boolean = true, + override val beatCounterColor: Int = 0x0000ff00, + override val commentColor: Int = 0x00000000, + override val scrollIndicatorColor: Int = 0x00000000, + override val beatSectionStartHighlightColor: Int = 0x00000000, + override val currentLineHighlightColor: Int = 0x00000000, + override val pageDownMarkerColor: Int = 0x00000000, + override val pulseDisplay: Boolean = true, + override val backgroundColor: Int = 0x00000000, + override val pulseColor: Int = 0x00000000, + override var dropboxAccessToken: String = "", + override var dropboxRefreshToken: String = "", + override var dropboxExpiryTime: Long = Long.MAX_VALUE +) : Preferences { + override fun getStringPreference(key: String, default: String): String { + TODO("Not yet implemented") + } + + override fun getStringSetPreference(key: String, default: Set): Set { + TODO("Not yet implemented") + } + + override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + TODO("Not yet implemented") + } + + override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockSupportFileResolver.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockSupportFileResolver.kt new file mode 100644 index 00000000..f2996b3a --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/MockSupportFileResolver.kt @@ -0,0 +1,40 @@ +package com.stevenfrew.beatprompter.mock + +import android.util.Size +import com.stevenfrew.beatprompter.cache.AudioFile +import com.stevenfrew.beatprompter.cache.CachedFile +import com.stevenfrew.beatprompter.cache.ImageFile +import com.stevenfrew.beatprompter.cache.parse.SupportFileResolver +import java.io.File +import java.nio.file.Paths +import java.util.Date +import kotlin.io.path.pathString + +class MockSupportFileResolver(private val testDataFolderPath: String) : SupportFileResolver { + override fun getMappedAudioFiles(filename: String): List = + listOf( + AudioFile( + CachedFile( + File(Paths.get(testDataFolderPath, "audio", "mock_audio.mp3").pathString), + filename, + filename, + Date(), + listOf() + ), 200000 + ) + ) + + override fun getMappedImageFiles(filename: String): List = + listOf( + ImageFile( + CachedFile( + File(Paths.get(testDataFolderPath, "images", "mock_image.jpg").pathString), + filename, + filename, + Date(), + listOf() + ), + Size(100, 100) + ) + ) +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmap.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmap.kt new file mode 100644 index 00000000..95e01c77 --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmap.kt @@ -0,0 +1,18 @@ +package com.stevenfrew.beatprompter.mock.graphics + +import com.stevenfrew.beatprompter.graphics.bitmaps.Bitmap +import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapCanvas + +class MockBitmap : Bitmap { + override val isRecycled: Boolean = false + override fun recycle() { + // Nothing to do. + } + + override fun toCanvas(): BitmapCanvas = MockBitmapCanvas() + + override val width: Int + get() = TODO("Not yet implemented") + override val height: Int + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapCanvas.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapCanvas.kt new file mode 100644 index 00000000..7aee04d2 --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapCanvas.kt @@ -0,0 +1,41 @@ +package com.stevenfrew.beatprompter.mock.graphics + +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.Rect +import com.stevenfrew.beatprompter.graphics.bitmaps.Bitmap +import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapCanvas + +class MockBitmapCanvas : BitmapCanvas { + override fun drawBitmap(bitmap: Bitmap, x: Float, y: Float, paint: Paint) { + // Do nothing + } + + override fun drawBitmap(bitmap: Bitmap, srcRect: Rect, destRect: Rect, paint: Paint) { + // Do nothing + } + + override fun drawColor(color: Int, mode: PorterDuff.Mode) { + // Do nothing + } + + override fun drawText(text: String, x: Float, y: Float, paint: Paint) { + // Do nothing + } + + override fun clipRect(left: Int, top: Int, right: Int, bottom: Int) { + // Do nothing + } + + override fun drawRect(rect: Rect, paint: Paint) { + // Do nothing + } + + override fun save() { + // Do nothing + } + + override fun restore() { + // Do nothing + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapFactory.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapFactory.kt new file mode 100644 index 00000000..91e3fbbc --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockBitmapFactory.kt @@ -0,0 +1,9 @@ +package com.stevenfrew.beatprompter.mock.graphics + +import com.stevenfrew.beatprompter.graphics.bitmaps.Bitmap +import com.stevenfrew.beatprompter.graphics.bitmaps.BitmapFactory + +class MockBitmapFactory : BitmapFactory { + override fun createBitmap(width: Int, height: Int): Bitmap = MockBitmap() + override fun createBitmap(path: String): Bitmap = MockBitmap() +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockFontManager.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockFontManager.kt new file mode 100644 index 00000000..b9f3feaf --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockFontManager.kt @@ -0,0 +1,53 @@ +package com.stevenfrew.beatprompter.mock.graphics + +import android.graphics.Paint +import com.stevenfrew.beatprompter.graphics.Rect +import com.stevenfrew.beatprompter.graphics.fonts.FontManager +import com.stevenfrew.beatprompter.graphics.fonts.TextMeasurement + +class MockFontManager : FontManager { + override fun getStringWidth( + paint: Paint, + strIn: String, + fontSize: Float, + bold: Boolean + ): Pair = DEFAULT_WIDTH to DEFAULT_RECT + + override fun getBestFontSize( + text: String, + paint: Paint, + maxWidth: Int, + maxHeight: Int, + bold: Boolean, + minimumFontSize: Float?, + maximumFontSize: Float?, + ): Pair = DEFAULT_FONT_SIZE to DEFAULT_RECT + + override fun measure( + text: String, + paint: Paint, + fontSize: Float, + bold: Boolean + ): TextMeasurement = + TextMeasurement(DEFAULT_RECT, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_DESCENDER_OFFSET) + + override fun setTypeface(paint: Paint, bold: Boolean) { + // Do nothing. + } + + override fun setTextSize(paint: Paint, size: Float) { + // Do nothing. + } + + override val maximumFontSize: Float = 8.0f + override val minimumFontSize: Float = 150.0f + override val fontScaling: Float = 1.0f + + companion object { + private const val DEFAULT_FONT_SIZE = 24 + private const val DEFAULT_WIDTH = 600 + private const val DEFAULT_HEIGHT = 100 + private const val DEFAULT_DESCENDER_OFFSET = 20 + private val DEFAULT_RECT = Rect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockLine.kt b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockLine.kt new file mode 100644 index 00000000..798625fb --- /dev/null +++ b/app/src/test/kotlin/com/stevenfrew/beatprompter/mock/graphics/MockLine.kt @@ -0,0 +1,34 @@ +package com.stevenfrew.beatprompter.mock.graphics + +import android.graphics.Paint +import com.stevenfrew.beatprompter.graphics.DisplaySettings +import com.stevenfrew.beatprompter.song.ScrollingMode +import com.stevenfrew.beatprompter.song.line.Line +import com.stevenfrew.beatprompter.song.line.LineMeasurements + +class MockLine( + lineTime: Long, + lineDuration: Long, + scrollingMode: ScrollingMode, + songPixelPosition: Int, + isInChorusSection: Boolean, + yStartScrollTime: Long, + yStopScrollTime: Long, + displaySettings: DisplaySettings +) : Line( + lineTime, + lineDuration, + scrollingMode, + songPixelPosition, + isInChorusSection, + yStartScrollTime, + yStopScrollTime, + displaySettings +) { + override val measurements: LineMeasurements + get() = TODO("Not yet implemented") + + override fun renderGraphics(paint: Paint) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4a70a5a4..d6dbef5d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,23 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.9.23' repositories { mavenCentral() google() } dependencies { - classpath 'com.android.tools.build:gradle:8.5.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath libs.gradle + classpath libs.kotlin.gradle.plugin // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} + allprojects { repositories { mavenCentral() @@ -28,5 +32,5 @@ allprojects { } tasks.register('clean', Delete) { - delete rootProject.buildDir -} + delete rootProject.layout.buildDirectory +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..cb35e087 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,72 @@ +[versions] +adal = "1.16.3" +agp = "8.7.2" +appcompat = "1.7.0" +appintro = "v4.2.2" +browser = "1.8.0" +commonsIo = "2.13.0" +dropboxCoreSdk = "4.0.1" +googleApiClientAndroid = "1.32.1" +googleApiServicesDrive = "v3-rev20211107-1.32.1" +gradle = "8.7.2" +gson = "2.10.1" +hsvAlphaColorPickerAndroid = "3.1.0" +kotlin = "2.0.0" +ioMockk = "1.13.13" +junit = "4.13.2" +kotlinReflect = "2.0.0" +kotlinStdlibJdk7 = "2.0.0" +kotlinxCoroutinesAndroid = "1.9.0" +kotlinxCoroutinesCore = "1.9.0" +legacySupportV4 = "1.0.0" +lifecycleRuntimeKtx = "2.8.6" +listenablefuture = "9999.0-empty-to-avoid-conflict-with-guava" +material = "1.12.0" +media = "1.7.0" +media3Exoplayer = "1.4.1" +multidex = "2.0.1" +playServicesAuth = "21.2.0" +playServicesPlus = "17.0.0" +preferenceKtx = "1.2.1" +rxjava = "2.2.21" +junitJupiter = "5.8.1" +junitVersion = "1.2.1" + +[libraries] +adal = { module = "com.microsoft.aad:adal", version.ref = "adal" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } +androidx-legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacySupportV4" } +androidx-lifecycle-viewmodel-android = { module = "androidx.lifecycle:lifecycle-viewmodel-android", version.ref = "lifecycleRuntimeKtx" } +androidx-media = { module = "androidx.media:media", version.ref = "media" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } +androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } +appintro = { module = "com.github.apl-devs:appintro", version.ref = "appintro" } +commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" } +dropbox-core-sdk = { module = "com.dropbox.core:dropbox-core-sdk", version.ref = "dropboxCoreSdk" } +google-api-client-android = { module = "com.google.api-client:google-api-client-android", version.ref = "googleApiClientAndroid" } +google-api-services-drive = { module = "com.google.apis:google-api-services-drive", version.ref = "googleApiServicesDrive" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +hsv-alpha-color-picker-android = { module = "com.github.martin-stone:hsv-alpha-color-picker-android", version.ref = "hsvAlphaColorPickerAndroid" } +io-mockk = { module = "io.mockk:mockk", version.ref = "ioMockk" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinReflect" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" } +kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlinStdlibJdk7" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } +listenablefuture = { module = "com.google.guava:listenablefuture", version.ref = "listenablefuture" } +material = { module = "com.google.android.material:material", version.ref = "material" } +play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "playServicesAuth" } +play-services-plus = { module = "com.google.android.gms:play-services-plus", version.ref = "playServicesPlus" } +rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" } +junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiter" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8bfdee5a..147283dc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 13 14:08:50 BST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index e7b4def4..a8150f1f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,22 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "beatprompter" include ':app'