Skip to content

Commit

Permalink
Merge pull request #3 from pansong291/dev/sky_studio_convert_20241015
Browse files Browse the repository at this point in the history
增加转换 sky studio 乐谱功能
  • Loading branch information
pansong291 authored Oct 18, 2024
2 parents 264ff94 + a1db8ac commit 49404df
Show file tree
Hide file tree
Showing 40 changed files with 666 additions and 146 deletions.
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {
applicationId = "pansong291.piano.wizard"
minSdk = 24
targetSdk = 34
versionCode = 20240928
versionName = "1.0.2-beta"
versionCode = 20241018
versionName = "1.1.0-beta"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -55,6 +55,7 @@ dependencies {
implementation(libs.getactivity.easywindow)
implementation(libs.google.gson)
implementation(libs.simplecityapps.recyclerview.fastscroll)
implementation(libs.googlecode.juniversalchardet)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
70 changes: 60 additions & 10 deletions app/src/main/kotlin/pansong291/piano/wizard/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,44 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import pansong291.piano.wizard.consts.ColorConst
import pansong291.piano.wizard.consts.StringConst
import pansong291.piano.wizard.coroutine.SkyStudioFileConvertor
import pansong291.piano.wizard.dialog.ConfirmDialog
import pansong291.piano.wizard.dialog.LoadingDialog
import pansong291.piano.wizard.dialog.MessageDialog
import pansong291.piano.wizard.dialog.SkyStudioSheetChooseDialog
import pansong291.piano.wizard.services.ClickAccessibilityService
import pansong291.piano.wizard.services.MainService

import java.io.File

class MainActivity : AppCompatActivity() {
private lateinit var btnFilePerm: Button
private lateinit var btnWinPerm: Button
private lateinit var btnAccessibilityPerm: Button
private lateinit var btnAbout: Button
private lateinit var btnConvertSkyStudio: Button
private lateinit var btnStart: Button
private lateinit var btnStop: Button
private lateinit var btnAbout: Button

// 创建一个与 Activity 生命周期绑定的 CoroutineScope
private val activityScope = CoroutineScope(Dispatchers.IO + Job())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
btnFilePerm = findViewById(R.id.btn_main_file_perm)
btnAccessibilityPerm = findViewById(R.id.btn_main_accessibility_perm)
btnWinPerm = findViewById(R.id.btn_main_win_perm)
btnAccessibilityPerm = findViewById(R.id.btn_main_accessibility_perm)
btnAbout = findViewById(R.id.btn_main_about)
btnConvertSkyStudio = findViewById(R.id.btn_main_convert_sky_studio)
btnStart = findViewById(R.id.btn_main_start)
btnStop = findViewById(R.id.btn_main_stop)
btnAbout = findViewById(R.id.btn_main_about)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
Expand All @@ -62,12 +76,6 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
}
btnStart.setOnClickListener {
startService(Intent(this, MainService::class.java))
}
btnStop.setOnClickListener {
stopService(Intent(this, MainService::class.java))
}
btnAbout.setOnClickListener {
AlertDialog.Builder(this)
.setIcon(R.drawable.outline_info_32)
Expand Down Expand Up @@ -97,13 +105,55 @@ class MainActivity : AppCompatActivity() {
}
.show()
}
btnConvertSkyStudio.setOnClickListener {
val ssscd = SkyStudioSheetChooseDialog(this)
ssscd.onFileChose = { path, file ->
showLoadingAndConvertSkyStudioFile(File(path, file), ssscd::reload)
}
ssscd.onFolderChose = { path ->
val cd = ConfirmDialog(this)
cd.setText(R.string.convert_current_folder_confirm_message)
cd.onOk = {
showLoadingAndConvertSkyStudioFile(File(path), ssscd::reload)
cd.destroy()
}
cd.show()
}
ssscd.show()
}
btnStart.setOnClickListener {
startService(Intent(this, MainService::class.java))
}
btnStop.setOnClickListener {
stopService(Intent(this, MainService::class.java))
}
}

override fun onStart() {
super.onStart()
updatePermState(7)
}

override fun onDestroy() {
activityScope.cancel()
super.onDestroy()
}

private fun showLoadingAndConvertSkyStudioFile(file: File, onSuccess: () -> Unit) {
val ld = LoadingDialog(this)
ld.show()
SkyStudioFileConvertor.onFinished = ld::destroy
SkyStudioFileConvertor.onResult = {
onSuccess()
MessageDialog(this).apply {
setTitle(R.string.convert_result)
setText(it)
show()
}
}
SkyStudioFileConvertor.convert(application, activityScope, file)
}

private fun updatePermState(flags: Int, success: Boolean? = null) {
if (flags and 1 == 1) {
val s = success ?: XXPermissions.isGranted(this, Permission.MANAGE_EXTERNAL_STORAGE)
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/kotlin/pansong291/piano/wizard/consts/StringConst.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ object StringConst {
* 乐谱后缀
*/
const val MUSIC_NOTATION_FILE_EXT = ".yp.txt"

/**
* 默认数据存储文件名
*/
const val SHARED_PREFERENCES_NAME = "piano_wizard"

/**
Expand All @@ -22,6 +26,16 @@ object StringConst {
*/
const val SP_DATA_KEY_DEFAULT_FOLDER = "default_folder"

/**
* SkyStudio 乐谱的上次选择目录
*/
const val SP_DATA_KEY_SKY_STUDIO_SHEET_LAST_FOLDER = "sky_studio_sheet_last_folder"

/**
* SkyStudio 乐谱的文件后缀
*/
const val SKY_STUDIO_SHEET_FILE_EXT = ".txt"

const val ABOUT_QQ_GROUP_NUMBER = "906654380"
const val ABOUT_REPOSITORY_LINK = "https://github.com/pansong291/PianoWizard"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pansong291.piano.wizard
package pansong291.piano.wizard.coroutine

import android.graphics.Point
import android.os.Handler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package pansong291.piano.wizard.coroutine

import android.app.Application
import android.os.Environment
import android.os.Handler
import android.os.Looper
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import pansong291.piano.wizard.R
import pansong291.piano.wizard.consts.StringConst
import pansong291.piano.wizard.entity.SkyStudioSheet
import pansong291.piano.wizard.exceptions.ServiceException
import pansong291.piano.wizard.utils.FileUtil
import pansong291.piano.wizard.utils.LangUtil
import pansong291.piano.wizard.utils.MusicUtil
import java.io.File
import java.io.FileFilter
import java.nio.charset.Charset

object SkyStudioFileConvertor {
private val handler = Handler(Looper.getMainLooper())
private lateinit var application: Application
var onResult: ((message: String) -> Unit)? = null
var onFinished: (() -> Unit)? = null

fun convert(application: Application, scope: CoroutineScope, file: File) {
this.application = application
scope.launch {
val result = tryResult {
val messages = mutableListOf<String>()
val gson = Gson()
if (file.isDirectory) {
file.listFiles(FileFilter {
it.isFile && it.name.endsWith(StringConst.SKY_STUDIO_SHEET_FILE_EXT)
})?.forEach {
messages.add(convert(it, gson))
}
} else {
messages.add(convert(file, gson))
}
messages.joinToString("\n\n")
}
onResult?.also { handler.post { it(result) } }
}.invokeOnCompletion {
onFinished?.also { handler.post(it) }
}
}

private fun convert(file: File, gson: Gson): String {
return file.name + " ->\n" + tryResult {
val text = file.readText(FileUtil.detectFileEncoding(file)?.let {
Charset.forName(it)
} ?: Charsets.UTF_8)
val sheets = gson.fromJson<List<SkyStudioSheet>>(
text,
object : TypeToken<List<SkyStudioSheet>>() {}.type
)
sheets.joinToString("\n") {
" " + convert(it, file.parent ?: Environment.getExternalStorageDirectory().path)
}
}
}

private fun convert(sheet: SkyStudioSheet, path: String): String {
return tryResult {
val name = sheet.name ?: application.getString(R.string.unknow_music)
val bpm = sheet.bpm?.takeIf { it > 0 }
?: throw ServiceException(R.string.target_must_gt_zero_message, "bpm")
val pitchLevel = (sheet.pitchLevel?.toInt() ?: 0).takeIf { it >= 0 }
?: throw ServiceException(R.string.target_must_gte_zero_message, "pitchLevel")
val baseTime = 60_000.0 / bpm
val songNotes = sheet.songNotes
if (songNotes.isNullOrEmpty())
throw ServiceException(R.string.target_cannot_empty_message, "songNotes")
val notesList = songNotes.groupByTo(LinkedHashMap()) {
// 按时间分组,相同时间的 key 构成和弦
(it.time ?: .0).takeIf { it >= 0 }
?: throw ServiceException(R.string.target_must_gte_zero_message, "time")
}.mapTo(mutableListOf()) {
it.key to it.value.map {
it.key?.split("Key")?.getOrNull(1)?.toIntOrNull()
?: throw ServiceException(R.string.target_format_incorrect_message, "key")
}
}
// 排序
LangUtil.insertionSort(notesList) { p1, p2 -> p1.first < p2.first }
val isSemi = MusicUtil.isSemitone(pitchLevel)
val basePitch = MusicUtil.naturals.indexOf(if (isSemi) pitchLevel - 1 else pitchLevel)
val baseNote = if (basePitch < 5) 'C' + basePitch else 'A' + basePitch - 5
val strBuilder = StringBuilder("[1=").apply {
append(baseNote)
if (isSemi) append('#')
// FIXME 节拍待填入
append(",4/4,")
append(bpm.toLong())
append(']')
}
var last = .0 to "0"
for (i in 0..notesList.size) {
if (i == notesList.size) {
strBuilder.append(last.second).append(',')
break
}
val note = notesList[i]
var rateA = (note.first - last.first).toLong()
var rateB = baseTime.toLong()
val gcd = LangUtil.gcd(rateA, rateB)
rateA /= gcd
rateB /= gcd
if (rateA > 0) strBuilder.append(last.second).apply {
if (rateA != 1L) append('*').append(rateA)
if (rateB != 1L) append('/').append(rateB)
append(',')
}
last = note.first to note.second.joinToString("&") {
MusicUtil.compileNote(MusicUtil.basicNoteTo12TET(it))
}
}
val filename =
FileUtil.findAvailableFileName(path, name, StringConst.MUSIC_NOTATION_FILE_EXT)
File(path, filename).writeText(strBuilder.toString())
filename
}
}

private fun tryResult(block: () -> String): String {
return try {
block()
} catch (e: Throwable) {
val cause = e.cause ?: e
if (cause is ServiceException)
"${application.getString(R.string.error)}: ${cause.getI18NMessage(application)}"
else "${cause.javaClass.simpleName}: ${cause.message}"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package pansong291.piano.wizard.dialog

import android.app.Application
import android.content.Context
import android.widget.TextView
import androidx.annotation.StringRes
import pansong291.piano.wizard.dialog.actions.DialogConfirmActions
import pansong291.piano.wizard.dialog.base.BaseDialog
import pansong291.piano.wizard.dialog.contents.DialogMessageContent

class ConfirmDialog(application: Application) : BaseDialog(application) {
class ConfirmDialog(context: Context) : BaseDialog(context) {
private val textView: TextView = DialogMessageContent.loadIn(this)
var onOk: (() -> Unit)? = null
var onCancel: (() -> Unit)? = { destroy() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package pansong291.piano.wizard.dialog

import android.app.Application
import android.content.Context
import android.view.View
import androidx.appcompat.widget.AppCompatImageButton
import pansong291.piano.wizard.R
import pansong291.piano.wizard.dialog.base.BaseDialog
import pansong291.piano.wizard.dialog.contents.DialogRadioListContent
import pansong291.piano.wizard.entity.KeyLayout

class KeyLayoutListDialog(
application: Application,
context: Context,
data: List<KeyLayout>,
default: Int
) : BaseDialog(application) {
) : BaseDialog(context) {
var onAction: ((index: Int, actionId: Int) -> Unit)? = null
private val setSpecialActionEnabled: (b: Boolean) -> Unit
private val adapter: DialogRadioListContent.Adapter =
Expand All @@ -21,7 +22,7 @@ class KeyLayoutListDialog(
setIcon(R.drawable.outline_layout_32)
setTitle(R.string.select_layout)
val actions = View.inflate(
application,
context,
R.layout.dialog_actions_key_layout,
findActionsWrapper()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package pansong291.piano.wizard.dialog

import android.content.Context
import pansong291.piano.wizard.R
import pansong291.piano.wizard.dialog.base.BaseDialog
import pansong291.piano.wizard.dialog.contents.DialogSpinnerContent

class LoadingDialog(context: Context) : BaseDialog(context) {
init {
DialogSpinnerContent.loadIn(this)
setTitle(R.string.processing)
setMaskCloseable(false)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package pansong291.piano.wizard.dialog

import android.app.Application
import android.content.Context
import android.widget.TextView
import androidx.annotation.StringRes
import pansong291.piano.wizard.dialog.actions.DialogCommonActions
import pansong291.piano.wizard.dialog.base.BaseDialog
import pansong291.piano.wizard.dialog.contents.DialogMessageContent

class MessageDialog(application: Application) : BaseDialog(application) {
class MessageDialog(context: Context) : BaseDialog(context) {
private val textView: TextView = DialogMessageContent.loadIn(this)
var onOkClick: (() -> Unit)? = { destroy() }

Expand Down
Loading

0 comments on commit 49404df

Please sign in to comment.