Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78876d7ec0 | |||
| 88b734cdc2 | |||
| b8b9eda836 | |||
| ec9194ce14 | |||
| 8b171fceaf | |||
| 640be8edb5 | |||
| b935c7d8f0 | |||
| df72c0b755 | |||
| 0eb3da71a0 | |||
| 2f45098c28 | |||
| 4d79ec5fff |
23 changed files with 329 additions and 112 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
||||
import java.util.Properties
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
|
|
@ -13,7 +12,9 @@ plugins {
|
|||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
val appVersion = "1.0.0"
|
||||
val gitVersionCode = getGitCommitCount()
|
||||
val gitVersionName = getGVN()
|
||||
val desktopVersionName = getCleanDesktopVersion(gitVersionName)
|
||||
kotlin {
|
||||
/*linuxX64 {
|
||||
binaries {
|
||||
|
|
@ -114,8 +115,8 @@ android {
|
|||
applicationId = "mg.dot.feufaro"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = appVersion
|
||||
versionCode = gitVersionCode
|
||||
versionName = gitVersionName
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
|
|
@ -144,8 +145,8 @@ compose.desktop {
|
|||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Exe, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "Feufaro"
|
||||
packageVersion = appVersion
|
||||
jvmArgs("-Dapp.version=$appVersion")
|
||||
packageVersion = desktopVersionName
|
||||
jvmArgs("-Dapp.version=$gitVersionName")
|
||||
modules("java.base", "java.desktop", "jdk.unsupported", "java.naming")
|
||||
|
||||
windows {
|
||||
|
|
@ -231,3 +232,34 @@ tasks.register<Copy>("exportLibs") {
|
|||
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
fun getGitCommitCount(): Int {
|
||||
return try {
|
||||
val execResult = providers.exec {
|
||||
commandLine("git", "rev-list", "--count", "HEAD")
|
||||
}
|
||||
val countStr = execResult.standardOutput.asText.get().trim()
|
||||
val count = countStr.toIntOrNull() ?: 1
|
||||
if (count > 0) count else 1
|
||||
} catch (e: Exception) {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fun getGVN(): String {
|
||||
val execResult = providers.exec {
|
||||
commandLine("git", "describe", "--tags", "--always", "--abbrev=0")
|
||||
}
|
||||
val rawVersion = execResult.standardOutput.asText.get().trim()
|
||||
|
||||
return if (rawVersion.isNotEmpty()) {
|
||||
rawVersion
|
||||
} else {
|
||||
"v1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
fun getCleanDesktopVersion(versionName: String): String {
|
||||
val regex = Regex("""^\d+(\.\d+)*""")
|
||||
return regex.find(versionName)?.value ?: "1.0.0"
|
||||
}
|
||||
|
|
@ -125,9 +125,14 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
|||
} else {
|
||||
val shortDisplayPath = longPathToShort(displayPath)
|
||||
|
||||
val message = if (shortDisplayPath.endsWith(".json", ignoreCase = true)) {
|
||||
"Playlist mise à jour"
|
||||
} else {
|
||||
"Sauvegarde réussie dans : $shortDisplayPath"
|
||||
}
|
||||
Toast.makeText(
|
||||
context.applicationContext,
|
||||
"Sauvegarde réussie dans : $shortDisplayPath",
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ actual class FMediaPlayer actual constructor(
|
|||
private val navigationSteps = mutableListOf<NavigationStep>()
|
||||
private val prefs: Settings = provideSettings()
|
||||
|
||||
|
||||
@Volatile private var userCustomTempo: Boolean = false
|
||||
init {
|
||||
midiDriver.start()
|
||||
loadVoiceVolumes()
|
||||
|
|
@ -196,7 +198,9 @@ actual class FMediaPlayer actual constructor(
|
|||
val usPerQuarter = ((md[0].toInt() and 0xFF) shl 16) or
|
||||
((md[1].toInt() and 0xFF) shl 8) or
|
||||
(md[2].toInt() and 0xFF)
|
||||
targetBpm = 60_000_000f / usPerQuarter
|
||||
val parsedBpm = 60_000_000f / usPerQuarter
|
||||
targetBpm = parsedBpm
|
||||
initialBpm = parsedBpm
|
||||
}
|
||||
|
||||
usPerTick = (60_000_000.0 / targetBpm) / resolution
|
||||
|
|
@ -292,8 +296,10 @@ actual class FMediaPlayer actual constructor(
|
|||
private fun resetTempoToInitial() {
|
||||
tempoChangeJob?.cancel()
|
||||
isInTempoChange = false
|
||||
targetBpm = initialBpm
|
||||
boundModel?.setBpmFlow(initialBpm)
|
||||
//targetBpm = initialBpm
|
||||
userCustomTempo = false
|
||||
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
||||
boundModel?.setBpmFlow(targetBpm)
|
||||
}
|
||||
|
||||
private fun extractDynamic(marker: String) = Dynamic.entries.firstOrNull {
|
||||
|
|
@ -382,6 +388,7 @@ actual class FMediaPlayer actual constructor(
|
|||
} else {
|
||||
yield()
|
||||
}
|
||||
if (!isRunning || !isActive) break
|
||||
|
||||
currentTickPos = ev.tickAbsolute
|
||||
lastEventNano = System.nanoTime()
|
||||
|
|
@ -424,11 +431,11 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentGrid = (currentTickPos / resolution).toLong()
|
||||
if (boundModel?.activeIndex?.value != currentGrid.toInt()) {
|
||||
boundModel?.updateActiveIndex(currentGrid)
|
||||
boundModel?.updateActiveIndexByIndex(currentGrid.toInt())
|
||||
}
|
||||
//println("C= ${currentGrid.toInt()} et shrd ${boundModel?.activeIndex?.value}")
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
|
@ -438,9 +445,13 @@ actual class FMediaPlayer actual constructor(
|
|||
currentTickPos = gridIndex.toLong() * resolution
|
||||
lastEventNano = System.nanoTime()
|
||||
|
||||
val initialOrPreviousBpm = navigationSteps
|
||||
val initialOrPreviousBpm = if (userCustomTempo) {
|
||||
targetBpm
|
||||
} else {
|
||||
navigationSteps
|
||||
.filter { it.newBpm != null && it.gridIndex <= gridIndex }
|
||||
.maxByOrNull { it.gridIndex }?.newBpm ?: initialBpm
|
||||
}
|
||||
|
||||
targetBpm = initialOrPreviousBpm
|
||||
sharedScreenModel.setBpmFlow(initialOrPreviousBpm)
|
||||
|
|
@ -451,6 +462,9 @@ actual class FMediaPlayer actual constructor(
|
|||
navigationSteps.forEach { step ->
|
||||
if (step.gridIndex > gridIndex) {
|
||||
step.alreadyDone = false
|
||||
if (step.isFarany) {
|
||||
step.faranyActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
applyVoiceStates()
|
||||
|
|
@ -470,7 +484,10 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
|
||||
private fun resetNavigationFlags() {
|
||||
navigationSteps.forEach { it.alreadyDone = false }
|
||||
navigationSteps.forEach {
|
||||
it.alreadyDone = false
|
||||
it.faranyActive = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareNavigation(sharedScreenModel: SharedScreenModel) {
|
||||
|
|
@ -595,10 +612,12 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
// ── Farany ────────────────────────────────
|
||||
finReg.containsMatchIn(mTrim) -> {
|
||||
val hasDcAfter = metadataList.any { m ->
|
||||
(m.gridIndex ?: 0) > ci && m.marker.any { mk -> dcR.containsMatchIn(mk) }
|
||||
val hasDcOrDsAfter = metadataList.any { m ->
|
||||
(m.gridIndex ?: 0) > ci && m.marker.any { mk ->
|
||||
dcR.containsMatchIn(mk) || dsR.containsMatchIn(mk)
|
||||
}
|
||||
if (hasDcAfter) {
|
||||
}
|
||||
if (hasDcOrDsAfter) {
|
||||
navigationSteps.add(
|
||||
NavigationStep(
|
||||
marker,
|
||||
|
|
@ -691,10 +710,15 @@ actual class FMediaPlayer actual constructor(
|
|||
//♩
|
||||
step.newBpm != null -> {
|
||||
step.alreadyDone = true
|
||||
if (step.gridIndex == 0 && userCustomTempo) {
|
||||
|
||||
}
|
||||
else {
|
||||
println("Exécution changement de tempo : ${step.newBpm} BPM à la grille $currentIndex")
|
||||
targetBpm = step.newBpm
|
||||
sharedScreenModel.setBpmFlow(step.newBpm)
|
||||
}
|
||||
}
|
||||
|
||||
// Modulation
|
||||
step.newKey != null -> {
|
||||
|
|
@ -755,9 +779,13 @@ actual class FMediaPlayer actual constructor(
|
|||
step.isFarany -> {
|
||||
println("C Farany ok $step")
|
||||
if (pendingFin || step.faranyActive) {
|
||||
println("Farany activé → STOP à grille $currentIndex")
|
||||
step.alreadyDone = true
|
||||
isRunning = false
|
||||
allNotesOff()
|
||||
playJob?.cancel()
|
||||
seekToGrid(0)
|
||||
boundModel?.updateActiveIndexByIndex(0)
|
||||
println("Farany activé → STOP à grille $currentIndex")
|
||||
onFinished()
|
||||
return@collect
|
||||
} else {
|
||||
|
|
@ -802,13 +830,18 @@ actual class FMediaPlayer actual constructor(
|
|||
private fun startSyncLoop(sharedScreenModel: SharedScreenModel) {
|
||||
syncJob?.cancel()
|
||||
syncJob = playerScope.launch(Dispatchers.Default) {
|
||||
var lastSentGrid = -1
|
||||
while (isActive) {
|
||||
if (isRunning && !isHolding) {
|
||||
val elapsedNano = System.nanoTime() - lastEventNano
|
||||
val extraTicks = (elapsedNano / (usPerTick * 1000)).toLong().coerceAtLeast(0L)
|
||||
val interpolated = (currentTickPos + extraTicks) / resolution
|
||||
val rawGrid = interpolated.toInt().coerceAtLeast(0)
|
||||
sharedScreenModel.updateActiveIndexByIndex(rawGrid)
|
||||
//sharedScreenModel.updateActiveIndexByIndex(rawGrid)
|
||||
if (interpolated.toInt() != lastSentGrid) {
|
||||
lastSentGrid = interpolated.toInt()
|
||||
sharedScreenModel.updateActiveIndexByIndex(interpolated.toInt())
|
||||
}
|
||||
}
|
||||
delay(16)
|
||||
}
|
||||
|
|
@ -917,7 +950,9 @@ actual class FMediaPlayer actual constructor(
|
|||
allNotesOff(); currentTickPos = 0L
|
||||
resetNavigationFlags(); navigationSteps.clear(); clearLoop()
|
||||
currentDynamicFactor = Dynamic.MF.factor
|
||||
resetTempoToNormal()
|
||||
//resetTempoToNormal()
|
||||
currentPlaybackBpm = targetBpm
|
||||
usPerTick = (60_000_000.0 / targetBpm.toDouble()) / resolution.toDouble()
|
||||
resetTempoToInitial()
|
||||
}
|
||||
actual fun release() {
|
||||
|
|
@ -938,7 +973,9 @@ actual class FMediaPlayer actual constructor(
|
|||
actual fun setTempo(bpm: Float) {
|
||||
targetBpm = bpm
|
||||
currentPlaybackBpm = bpm
|
||||
userCustomTempo = true
|
||||
usPerTick = (60_000_000.0 / bpm.toDouble()) / resolution.toDouble()
|
||||
boundModel?.setBpmFlow(bpm)
|
||||
boundModel?.let { prepareNavigation(it) }
|
||||
needsClockSync = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
M0:|t:EWS 103 Go, and search the tomb of Jesus|a:Robert Cleaver Chapman 1803–1902|r:8.7.8.7.4.7|h:DISMISSAL William Litton Viner 1790-1867|c:A|m:4/4|u:https://hymnary.org/hymn/SAM1900/page/162
|
||||
U0:z0:${mf}4(22)44 (22)(22)44/ 4(22)(22)(22) 4(22)8/ ${f}4(22)44 4444/ 4(22)44 44(4${>}t)${=}/ ${mf}4(22)44 (22)(22)44/ 4(22)(22)(22) 4(22)8
|
||||
U0:z0:${mf}4(22)44 (22)(22)44/ 4(22)(22)(22) 4(22)8/ ${f}4(22)44 4444/ 4(22)44 44(4${>}t)/${=} ${mf}4(22)44 (22)(22)44/ 4(22)(22)(22) 4(22)8
|
||||
N1:#s, dtlsm,l-d-sm,mrdl-r-dt-d mmfs'mfrmd mmfs'mfmrzzz dtlsm,l-d-sm, mrdl-r-dt-d
|
||||
N2:#m, msfmmf-f-mm ss-f-l-s-fm ss-ssssss ss-s sssslsf msfmm f-f-mm ss-f-l-s-fm
|
||||
N3:#s, dd-ddd-d-dd dtdr-lfmr-d ddrmdrrdm ddrm drdtzzz dd-dd d-d-dd dtdr-lfmr-d
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
M0:|c:C|m:4/4|a:Nils J. Holm (1778-1845); tr (st. 1): Carl Døving|h:Ludvig Mathias Lindeman (1812-1887)|t:EWS 351 How blessed is the little flock|r:86868886
|
||||
U0:zC:46244624/44(22)(22)(22)8z4/46244624/4444(22)8z4/44444444/44444444/46244624/444448z4
|
||||
N1:d'slsfmrmsld'-t-l-s/d'slsfmrmsld'tl-s/sltd'mfslfrmflsfmsld'ttd'm'r'md'lr'td'
|
||||
N2:msfrrdrdt,mm-r-r-r/drrmddt,drmdrr-r/ddrdddddrt,Drmrt,dmfffmmslSmflss
|
||||
N3:sd'd'ttssssssFs-Fd't/ssfmfsssssssFd't/ssfmssrSSsslmsssd'd'd'r'r'd'd'ltd'd'f'r'm'
|
||||
N4:dmfssdt,dmdl,-t,dr-s/mt,t,dl,s,s,dt,dmrr-s/mfrl,T,l,mf/rsmrDt,s,ddfllSlmfmllfsd
|
||||
E1:How bles_sed is the lit_tle flock,/Whom Je_sus calls His own!/He is their Sa_vior and their rock,/They trust in Him a_lone;/They walk by faith and hope and love,/But they shall dwell with Him a_bove,/When hope and faith shall pass a_way,/And love shall last for aye.
|
||||
E2:My Je_sus, am I in that band,/And wilt Thou call me Thine?/Do I a_mong the cho_sen stand/Whose lamps so bright_ly shine?/O let me not lie down to rest/Till this I know, my Sa_vior blest,/Till I can say, by grace re_stored:/Thou know'st I love Thee, Lord!
|
||||
E3:And e_ven if with tears it be,/That this to Thee I say,/Yet Thou in grace wilt look on me/And wipe my tears a_way;/Yea, when but Thou who all dost know,/In me canst find Thy love be_low/And own me Thine, then well is me,--/My all I have in Thee.
|
||||
|
|
@ -5,5 +5,5 @@ N2:#s,smmrdssstssTllldddddllttr4mddt4ddt4dmd5tsmmmrddTldffmdD4z-DdrrmffmsstddTdd
|
|||
N3:#rsdtlsmrmfrmmfffmffF4frf4smmrrrfmmrfffmssF4ssddtlssmffltdsl4z-Tlsssr'r'dsmfsls-drrmsfm
|
||||
N4:#f,sd4ls4ddffld#s,ffrrrrss6dds4dds#l,sssd'ddllrr#f,ssd7f4ddD4z-Drs5df'mrdlm-f'r,s4d
|
||||
Y1:Inty a\ho Jesoa!/Mi_antso a\hy Hianao/Na dia tsy mendrika\ e/Manolo-tena\ izao/Hiana_o no nifidy/Tsy haiko ny handà/‘Ty antso sarobidy/Ry Tompo, mamelà/Ny heloko rehetra/Di_ovy a\ho, havaozy/Ho olom-ba_ovao/Hanao ny sitrakao/Ho mendrika toko_a/Ny antsonao Jesoa.
|
||||
Y2:Inty a\ho Jeso mi_antso a\hy Hianao/Ny fa\halemeko\ anie/Ry Tompo fantatrao/Hiana_o no nifidy/Sy he maniraka\ a\hy/Avy\ Ami_2nao Jesoa/Ny herin’ny fana\hy/Ho entiko mana_o/Ny asa atolotrao/Fa sarotra\ e ny dia/Ry Tompo manampia/Ho mendrika toko_a/Ny antsonao Jesoa
|
||||
Y2:Inty a\ho Jeso mi_antso a\hy Hianao/Ny fa\halemeko\ anie/Ry Tompo fantatrao/Hiana_o no nifidy/Sy he maniraka\ a\hy/Avy\ Ami_2nao Jesoa/Ny herin’ny fana\hy/Ho entiko mana_o/Ny asa\ atolotra_o/Fa sarotra\ e ny dia/Ry Tompo manampia/Ho mendrika toko_a/Ny antsonao Jesoa
|
||||
Y3:Inty a\ho Jesoa/Mi_antso a\hy Hianao/Mba ho fahazava\n’/Izao tontolo\ izao/Hiana_o no nifidy ahy\Ho fanasina/Hanefa ny adidy/Le\hibe sy masina/Koa hamasino, Tompo/Fa vonona\ a\ho hanompo/Hombay ny herinao/Hitondra aim-baovao/Ho mendrika toko_a/Ny antsonao Jesoa
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
M0:|c:G|m:6/4|r:P.M|t:FF 7 Tsy hainay ny hangina|a:Ramarolahy Aina Nirina Jaona|h:
|
||||
U0:zK:4 C444 C84 444444 C84 C444 C84 444444 C8${$}4 ${x7} 444444${x0} K4 444444 (444)84 444444 C84 444444 (444)84 444444 84${D.S}8
|
||||
U0:zK:4C444C84444444C84C444C84444444C8${$}${Fiverenana}4444444444444444444444444444444444444444444${Farany}K4444444(444)84444444C84444444(444)8444444484${D.S.Fin}8
|
||||
N1:s,mmfsls smsmfmrrd mrFmrsr #s,tmrdrtlss sm-3f#t,ls-4 ssrmfsls-4 s,m-3fls-ssfrdsmfmrd d4msmf--d d4mrdsf r4Fmrs--r trrrd#s,tls--
|
||||
N2:#f,sddrmRm sdddttsss sFdddtt sF6r,f sdstdrRmdrmddt--t-- dmdtdrdsdt-- dmrrdlsddttss T7llTl T7ll d7ttdt sl3F3r,m,f
|
||||
N3:#l,s5Fs ssmdrsffm dr7d4rdtt ssmfs--smfsmmr--f-- msmrmfsmmf-- msT'T'l'fmmsrsfm m4smsddrd m4sfmdd F4l'Fl'rrmr rF3rrdtdr
|
||||
N4:#r,sd6 s7dd ddllFss sr6ss sdmsd-- dmsddls--s-- ddmslt d3s--dmmfrs7d, d7f4 d7ff r'7s4 r'4r3slt
|
||||
Y1:Tsy hainay ny hangina/Ka tsy hankalaza ny Avo/Nameno fitahiana/Ka tsy mba namela ho lavo.${R=}Ka de_4ra ry Ray!_5/Sy saotra no\ atolotra\ Anao_5/ny fo_4tena fa_2ly loatra no entinay midera\ Anao./Ny sainay tsy ampy\ hahalala/Ny fatran’ny fitiavana_o/Ka raiso ry Tompo Malala/Ny fankasitrahana\ Anao (ry Ray).
|
||||
Y1:Tsy hainay ny hangina/Ka tsy hankalaza ny Avo/Nameno fitahiana/Ka tsy mba namela ho lavo.${R=}Ka de_4ra ry Ray!_4/Sy saotra no\ atolotra\ Anao_4/ny fo_4tena fa_2ly loatra no entinay midera\ Anao./Ny sainay tsy ampy\ hahalala/Ny fatran’ny fitiavana_o/Ka raiso ry Tompo Malala/Ny fankasitrahana\ Anao (ry Ray).
|
||||
Y2:Tsy hainay hadinoina/‘Lay fara-fitiavanao, Ray o!/Fa maty novonoina/Ny Zanakao hamonjy anay./
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ N2:#t, drd8mff mdtddttt/drd8mf fmdddtd /d7/ d7/ ddtrddt/ d7/ d7/ ddtrddt
|
|||
N3:#r mfs4fmsd4sfmssrs/ mfs4fmsd4mfsfm/ mmf5/ lfm5/ fmrs4/ mmf5/ lfm5/ fmrtlls
|
||||
N4:#s, d11ffd5s3/ d11ffdllssd/ ddf5/ ffd5/ ddrtdms/ ddf5/ ffd5/ ddr4s
|
||||
Y1:Misy maro ao an-tsaha mitaraina,/Ka ento ny Jiro mba hankao!/Tsy mba misy anto_andro na maraina/Fa maizina re ny ao;/Indro olo-maro be/No maniry mafy hoe:/Mba midina mankaty,/Itondray Baiboly re/Izy\ ireo fa ta handre/An'i Kristy Tompo soa !/${R=}${D:"Mandehana", hoy ny Tompo, "mandehana/Ho amin'izao tontolo i\zao!"/Mitsangàna ary, eny, mitsangàna/Fa Izy no homba\ anao!}
|
||||
Y2:Mandehana mba hitarika ny jamba,/Mba ampifalio ny ory fo;/Ny mahantra ilazao fa misy lamba/Tsy miova, tsy mety lo;/Jeso Mofon'aina soa/Sady Ranon'aina koa,/Asehoy ho hitany!/Tena Andriamanitra,/Tompo tao an-danitra,/Ambarao ho fantany!${D:"Mandehana", hoy ny Tompo, "mandehana/Ho amin'izao tontolo i\zao!"/Mitsangàna ary, eny, mitsangàna/Fa Izy no homba\ anao!}
|
||||
Y3:Mandehana hanatanjaka ny osa,/Omeo toky re ny ketraka;/Ny anjaranao, ataovy ho tontosa/Ka aza dia reraka!/Mandehana i_anao/Ka vitao ny asanao/Fa ny tambinao arỳ;/Mbola any\ ihany re/ny handraisan-tamby be,/Dia rehefa tsy etỳ!${D:"Mandehana", hoy ny Tompo, "mandehana/Ho amin'izao tontolo i\zao!"/Mitsangàna ary, eny, mitsangàna/Fa Izy no homba\ anao!}
|
||||
Y2:Mandehana mba hitarika ny jamba,/Mba ampifalio ny ory fo;/Ny mahantra ilazao fa misy lamba/Tsy miova, tsy mety lo;/Jeso Mofon'aina soa/Sady Ranon'aina koa,/Asehoy ho hitany!/Tena Andriamanitra,/Tompo tao an-danitra,/Ambarao ho fantany!
|
||||
Y3:Mandehana hanatanjaka ny osa,/Omeo toky re ny ketraka;/Ny anjaranao, ataovy ho tontosa/Ka aza dia reraka!/Mandehana i_anao/Ka vitao ny asanao/Fa ny tambinao arỳ;/Mbola any\ ihany re/ny handraisan-tamby be,/Dia rehefa tsy etỳ!
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
M0:|c:G|m:6/4|r:86868886|t:FFPM 332 Finaritra ny olonao|a:N.J. Holm 1778-1845 Nad. L. Dahle 1843-1925|h:APPROACH George A. Minor 1845-1904|u:https://hymnary.org/tune/approach_my_soul_the_mercy_seat_55553
|
||||
Q0:${N:New}U0:zK:4 844(44) 84(44)/4 84(44)4 K/4 844(44) 84(44)/4 84(44)4 K/4 84(44)4 84(44)/4 84(44)4 848/4 84(44)4 84(44)/4 84(44)4 K
|
||||
U0:zK:4 844(44) 84(44)/4 84(44)4 Gz4/4 844(44) 84(44)/4 84(44)4 Cz8/4 84(44)4 84(44)/4 84(44)4 848/4 84(44)4 84(44)/4 84(44)4 Cz8
|
||||
N1:#m,msssmsdddm'dllldls/msssmsdr#l,msfmdrdtd/mffflfmmms/mrrrmddls/#m, msssmsdrm'sf'm'drdtd
|
||||
N1:#m,msssmsdddm'dllldls/msssmsdr#l,msfmdrdtd/mfffl'fmmms/mrrrmddl#m,s/ msssmsdrm's'f'm'drdtd
|
||||
N2:#d, mmmmdmsss-sffflfm/mmmmdmsss-lsmfmrm/slll-lsss-/ssss-slfm/mmmmdmsss-/lsmfmrm
|
||||
N3:#s, sddd-dddd-dddd-dd/sddd-ddtd-ddds-ss/ddddfddddm/dtttd5/sddd-ddtd-/ddds-ss
|
||||
N4:#d, dddd-dmmm-mfff-fd/dddd-dmrdmfsls-sd/dfff-fddd-dsss-mffd/dddd-dmrdmfsls-sd
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ M0:|c:F|m:3/4|a:Ch. Rajoelisolo 1896-1969|r:7777D|h:MARTYN Simeon Butler Marsh 1
|
|||
U0:z0:${x8}848484C/${x0}
|
||||
N1:mmmdrrr mmsfmrd mmmdrrr mmsfmrd s4lls s4lls mmmdrrr mmsfmrd
|
||||
N2:#t, d4t3 ddmrdtd 5t3 ddmrdtd 19t3 d3rdtd
|
||||
N3:s3ms6lsfm s3ms6lsfm mfdmffm mfsmffm s3ms6lsfm
|
||||
N3:s3ms6lsfm s3ms6lsfm mfsmffm mfsmffm s3ms6lsfm
|
||||
N4:#f,d4s3 d3fssd 5s3 d3fssd drmdfld drmdfld 5s3ddmf's'sd
|
||||
Y1:Zanahariko Tsito\ha,/Tompo, Hery sy Fitia,/Ray hatramin’ny talo\ha,/A\hy anio, ka ho doria./Manjakà mandrakizay/Ao am-poko, Tompo soa;/Dera, laza,\ Anao ry Ray!/Zanaka, Fanahy koa.
|
||||
Y2:Ny fino_ako Anao,/Hatanjaho, mba hamoa/Hi_asako ny\ asanao,/Hamafazako ny voa./Aoka ny Fanahinao/Jiro mba hitarika\ a\hy/Ka apetrako\ aminao/Na ny tena na fana\hy.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
M0:|t:FFPM 564 Tsy hita izay androko sisa ety|c:Bb|m:6/4 Miadana 2/|r:10.10.12.8.8.8.12.8|a:J. Andrianaivoravelona 1835-1897|h:James McGranahan 1840-1907|u:https://hymnary.org/tune/i_know_not_the_hour_when_my_mcgranahan
|
||||
U0:zK:4 444444 (44)48/(22) 4448(22) 4448/22 444444 444$Q8/4 444444 K/4 444444 444444 444444 (4448)/22 444444 444$Q8/4 44444 4K
|
||||
N1:#f, ssFsmrddtls sssfm,f lllsFs sssFsmrddrmf' fm4rdr sd4rml-4 lr4dls-3 sssFsmrddrmf' f'mmms'f'rd
|
||||
N1:#f, ssFsmrddtls sssfm,f lllsFs sssFsmrddrmf' f'm4rdr sd4rml-4 lr4dls-3 sssFsmrddrmf' f'mmms'f'rd
|
||||
N2:#d, mmRmsfmlsfm mmmrDr fffmRm mmmRmsfmmfsl ls4fmf fm4fsf-4 fF6srmf ffmRmsfmmfsl ls5fm
|
||||
N3:#m, sslsd--d--d d-t--t--d-ls d-slsd--d--d dd--dtlt z6d7 rl4dt-dr rrd14ttts
|
||||
N4:#d, dd--d--d--d dms--s--d--- d-d--d--d--f fs--s--s z6df6 r6s--- ssd9f fs6d
|
||||
Y1:Tsy hita\ izay androko sisa\ atỳ,/Ahiko fandrao tsy tonga arỳ,/Ry fanahiko! Ao ny Mpamonjy anao,/He! Jeso, Mpamonjy anao./He! Jeso, Mpamonjy anao._4/He! Jeso, Mpamonjy anao./Ry fanahiko! Ao ny Mpamonjy anao,/He! Jeso Mpamonjy anao.
|
||||
Y2:Izaho anefa tamàna\ atỳ;/Tsy hiomana va, fa\ hifindra arỳ?/Fa\ efa fantatro i\zao, fa havela ity,/He! Lanitra so_a arỳ./He! Lanitra so_a arỳ._4/He! Lanitra so_a arỳ./Fa\ efa fantatro i\zao fa havela ity,/He! Lanitra soa arỳ.
|
||||
Y3:Ilaozy ity, hadinoy, ry fo!/Afoizo izao, ry fanahiko\ ô!/Fa\ efa reko ny Tompo/Mi_antso anao,/He! Feo mahafaly izao.He! Feo mahafaly izao._3//He! Feo mahafaly izao./Fa\ efa reko ny Tompo/Mi_antso anao,/He! Feo mahafaly izao.
|
||||
O1:v34:_6He! Jeso, Mpamonjy
|
||||
O2:v34:_6He! Lanitra so_a
|
||||
O3:v34:_6He! Feo mahafaly
|
||||
Y1:Tsy hita\ izay androko sisa\ atỳ,/Ahiko fandrao tsy tonga arỳ,/Ry fanahiko! Ao ny Mpamonjy anao,/He! Jeso, Mpamonjy anao./${O:1}He! Jeso, Mpamonjy anao._4/He! Jeso, Mpamonjy anao./Ry fanahiko! Ao ny Mpamonjy anao,/He! Jeso Mpamonjy anao.
|
||||
Y2:Izaho anefa tamàna\ atỳ;/Tsy hiomana va, fa\ hifindra arỳ?/Fa\ efa fantatro i\zao, fa havela ity,/He! Lanitra so_a arỳ./${O:2}He! Lanitra so_a arỳ._4/He! Lanitra so_a arỳ./Fa\ efa fantatro i\zao fa havela ity,/He! Lanitra so_a arỳ.
|
||||
Y3:Ilaozy ity, hadinoy, ry fo!/Afoizo izao, ry fanahiko\ ô!/Fa\ efa reko ny Tompo/Mi_antso anao,/He! Feo mahafaly izao./${O:3}He! Feo mahafaly izao._3//He! Feo mahafaly izao./Fa\ efa reko ny Tompo/Mi_antso anao,/He! Feo mahafaly izao.
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ object ScreenSolfa : Screen {
|
|||
var showContextualMenu = false
|
||||
var viewportHeight by remember { mutableStateOf(0) }
|
||||
|
||||
var isScanning by remember { mutableStateOf(false) }
|
||||
var qrCodeResult by remember { mutableStateOf("Aucun code scanné") }
|
||||
LaunchedEffect(Unit) {
|
||||
DeepLinkHandler.onSongReceived = { content, filePath ->
|
||||
|
|
@ -88,9 +87,6 @@ object ScreenSolfa : Screen {
|
|||
solfaScreenModel,
|
||||
sharedScreenModel,
|
||||
qrCodeResult = qrCodeResult,
|
||||
onScannerButtonClick = {
|
||||
isScanning = true
|
||||
},
|
||||
solfaScrollState = scrollState
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ data class MidiPitch (
|
|||
metaType = type
|
||||
}
|
||||
fun setNote(note: String, theDuration: Int) {
|
||||
val midiPitch = Transpose.transposeToMidi(note, key, "C")
|
||||
val midiPitch = Transpose.transposeToMidi(note, "C", key)
|
||||
velocity = if (midiPitch < 0) {
|
||||
0
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -102,17 +102,19 @@ object HarmonicAnalyzer {
|
|||
return degreeLine
|
||||
}
|
||||
|
||||
fun analyzeChordName(notes: Map<Int, String>, songKey: String = "C"): String {
|
||||
fun analyzeChordName(notes: Map<Int, String>, songKey: String = "C", transposeInt: Int = 0): String {
|
||||
val keyOffset = keyOffsets[songKey] ?: 0
|
||||
val totalOffset = ((keyOffset + transposeInt) % 12 + 12) % 12
|
||||
val slices = splitSlices(notes)
|
||||
|
||||
val names = slices.map { slice ->
|
||||
val cleaned = cleanNotes(slice)
|
||||
val match = findChordMatch(cleaned) ?: return@map ""
|
||||
val absoluteRoot = (keyOffset + match.root) % 12
|
||||
|
||||
val absoluteRoot = ((totalOffset + match.root) % 12 + 12) % 12
|
||||
val chordName = noteNames[absoluteRoot] + match.quality
|
||||
if (match.bass != -1 && match.bass != match.root) {
|
||||
val absoluteBass = (keyOffset + match.bass) % 12
|
||||
val absoluteBass = ((totalOffset + match.bass) % 12 + 12) % 12
|
||||
"$chordName/${noteNames[absoluteBass]}"
|
||||
} else {
|
||||
chordName
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ import kotlin.math.min
|
|||
//@todo: ffpm-99-2.txt triolet.mid + triolet (m--)--
|
||||
//@todo: ffpm-657-1.txt ${m:4/4 Miadana kely}
|
||||
//@todo: ews-80.txt ${m:12/4}
|
||||
//@todo: ews-81.txt ${R=}${D:....}
|
||||
//@todo: bug ffpm-629.txt sssszd au lieu de ssss-d(-rrrrtdr)
|
||||
//@todo: bug ews-103.txt Triolet adds one blank
|
||||
//@todo: bug ffpm-167-1.txt shoud start with : and not |
|
||||
|
||||
class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository: FileRepository) {
|
||||
|
|
@ -36,6 +34,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
private var refrainMaxStanza = -1
|
||||
private var smartLyricsType = "L"
|
||||
private val pitches = mutableListOf<MidiPitch>()
|
||||
private val globalDComments = mutableListOf<String>()
|
||||
|
||||
companion object {
|
||||
var currentFile = ""
|
||||
|
|
@ -62,6 +61,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
val REGEX_NOTE_TOADD = Regex("([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)>([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)")
|
||||
val REGEX_NOTE_TODROP = Regex("([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)<")
|
||||
val REGEX_NOTE_SPLIT = Regex("\\$\\{[Ss]:([^\\}]+)\\}([drmfsltDRFSTz][,']*)")
|
||||
val REGEX_DC_REFRAIN = Regex("""\$\{R=\}\$\{D:(.*?)\}""")
|
||||
}
|
||||
|
||||
fun parseNoteSplit(input: String): String {
|
||||
|
|
@ -218,6 +218,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
T.clear()
|
||||
N.clear()
|
||||
L.clear()
|
||||
globalDComments.clear()
|
||||
if (pitches.isNotEmpty()) {
|
||||
pitches[0].reset()
|
||||
}
|
||||
|
|
@ -263,7 +264,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
|
||||
private val prefs: Settings = provideSettings()
|
||||
|
||||
fun generateMidiFile(bpm: Float, outputPath: String = "whawyd3.mid") {
|
||||
fun generateMidiFile(bpm: Float, outputPath: String = "whawyd3.mid", transpI: Int = 0) {
|
||||
val parseScope = CoroutineScope(Dispatchers.Default)
|
||||
parseScope.launch {
|
||||
val pitchesSorted = pitches.sortedWith(compareBy({ it.tick }, { it.voiceNumber }))
|
||||
|
|
@ -275,12 +276,14 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
3 to prefs.getInt("voice_instrument", 0),
|
||||
4 to prefs.getInt("voice_instrument", 0),
|
||||
)
|
||||
val originalKey = sharedScreenModel.songKey.value
|
||||
val transposedKey = Transpose.transposeKey(baseKey = originalKey, interval = transpI)
|
||||
midiWriter.process(
|
||||
pitchesSorted,
|
||||
sharedScreenModel.getFullMarkers(),
|
||||
initialBpm = bpm,
|
||||
instruments,
|
||||
initialKey = sharedScreenModel.songKey.value
|
||||
initialKey = transposedKey
|
||||
)
|
||||
midiWriter.save(outputPath)
|
||||
}
|
||||
|
|
@ -301,6 +304,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
T.clear()
|
||||
N.clear()
|
||||
L.clear()
|
||||
globalDComments.clear()
|
||||
if (pitches.isNotEmpty()) {
|
||||
pitches[0].reset()
|
||||
}
|
||||
|
|
@ -2084,7 +2088,12 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
|
||||
midiPitch.currentVoiceNumber(voiceNumber)
|
||||
if (voiceNumber == 1) {
|
||||
midiPitch.initKey(meta["C"] ?: "C", meta["C"]?.endsWith("m") ?: false)
|
||||
val rawKey = sharedScreenModel.songKey.value.ifEmpty { meta["C"] ?: "C" }
|
||||
val interval = sharedScreenModel.transpositionInterval.value
|
||||
val isMinor = rawKey.endsWith("m", ignoreCase = true)
|
||||
|
||||
val transposedKey = Transpose.transposeKey(rawKey, interval)
|
||||
midiPitch.initKey(transposedKey, isMinor)
|
||||
pushMidi("meta")
|
||||
if (midiPitch.initMeasure(meta["m"] ?: "4/4")) {
|
||||
pushMidi("measure")
|
||||
|
|
@ -2358,12 +2367,24 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
meta[subKey] = line.substring(2)
|
||||
}
|
||||
if ("c" == subKey) {
|
||||
val tonality = line.uppercaseFirstMeta()
|
||||
val validKeys = Transpose.keyToNumber.sortedByDescending { it.length }
|
||||
val keyRegex = Regex("\\b(${validKeys.joinToString("|")})(m)?\\b", RegexOption.IGNORE_CASE)
|
||||
val rawText = line.substringAfter("c:")
|
||||
val matchResult = keyRegex.find(rawText)
|
||||
|
||||
if (matchResult != null) {
|
||||
val rawBaseKey = matchResult.groupValues[1]
|
||||
val hasMinor = matchResult.groupValues[2].isNotEmpty()
|
||||
val baseTonality = Transpose.keyToNumber.firstOrNull { it.equals(rawBaseKey, ignoreCase = true) }
|
||||
if (baseTonality != null) {
|
||||
val tonality = if (hasMinor) "${baseTonality}m" else baseTonality
|
||||
meta["C"] = tonality
|
||||
sharedScreenModel.setSongKey(tonality)
|
||||
sharedScreenModel.setTransposeAsIf(tonality)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.uppercaseFirstMeta(): String {
|
||||
if (this.length < 2) {
|
||||
|
|
@ -2465,8 +2486,9 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
copyRefrainToStanza(stanzaNumber)
|
||||
}
|
||||
|
||||
if (lyricsComment.isNotEmpty()) {
|
||||
val lyricsIterator = lyricsComment.iterator()
|
||||
val commentsToProcess = if (lyricsComment.isNotEmpty()) lyricsComment else globalDComments
|
||||
if (commentsToProcess.isNotEmpty()) {
|
||||
val lyricsIterator = commentsToProcess.toList().iterator()
|
||||
while (lyricsIterator.hasNext()) {
|
||||
var item = lyricsIterator.next()
|
||||
// @todo: if item == ${O:...}
|
||||
|
|
@ -2490,7 +2512,6 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
.forEachIndexed { i, xval ->
|
||||
appendLyricsItem(stanzaNumber, i + pDS, xval)
|
||||
}
|
||||
lyricsIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2549,12 +2570,19 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
val smartLyrics = smartELyrics(lyrics)
|
||||
loadL(intKey, smartLyrics)
|
||||
}
|
||||
|
||||
private fun isDCRefrain(text: String): Boolean {
|
||||
return text.startsWith("\${R=}\${D:") && text.endsWith("}")
|
||||
}
|
||||
private fun getLyricsComments(lyrics: String): String {
|
||||
val matchResult = REGEX_LYRICS_COMMENT.findAll(lyrics)
|
||||
.map { it.value }.toList()
|
||||
if (matchResult.isNotEmpty()) {
|
||||
lyricsComment.addAll(matchResult)
|
||||
matchResult.filter { isDCRefrain(it) || REGEX_DC_REFRAIN.containsMatchIn(it) }.forEach { refrain ->
|
||||
if (!globalDComments.contains(refrain)) {
|
||||
globalDComments.add(refrain)
|
||||
}
|
||||
}
|
||||
}
|
||||
return REGEX_STRIP_DC.replace(lyrics, $$"\\${x:}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -959,8 +959,10 @@ fun LazyVerticalGridTUO(
|
|||
val degreeName = remember(chordMap) {
|
||||
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null
|
||||
}
|
||||
val chordName = remember(chordMap) {
|
||||
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null
|
||||
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||
val songKey = sharedScreenModel.songKey.value
|
||||
val chordName = remember(chordMap, songKey, appliedInterval) {
|
||||
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, songKey, appliedInterval) else null
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
@ -1104,7 +1106,7 @@ fun LazyVerticalGridTUO(
|
|||
)
|
||||
}
|
||||
) {
|
||||
val currentInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||
val currentInterval by sharedScreenModel.transpToCInt.collectAsState()
|
||||
TimeUnitComposable(
|
||||
tuo = oneTUO,
|
||||
stanzaNumber = currentStanza,
|
||||
|
|
|
|||
|
|
@ -200,22 +200,81 @@ class Transpose {
|
|||
return note
|
||||
}
|
||||
val noteNaked = regexFound.groupValues[1]
|
||||
val originalOctaveStr = regexFound.groupValues[2]
|
||||
val rest = regexFound.groupValues[3]
|
||||
val noteNum = noteToNumber.indexOf(noteNaked)
|
||||
val oldKey = keyToNumber.indexOf(fromKey)
|
||||
val newKey = keyToNumber.indexOf(toKey)
|
||||
var newNoteNum = noteNum - newKey + oldKey + alter
|
||||
var suffix = ""
|
||||
if (oldKey == -1 || newKey == -1) return note
|
||||
|
||||
var keyShift = newKey - oldKey
|
||||
if (keyShift > 6) {
|
||||
keyShift -= 12
|
||||
} else if (keyShift < -6) {
|
||||
keyShift += 12
|
||||
}
|
||||
|
||||
var newNoteNum = noteNum + keyShift + alter
|
||||
var octaveShift = 0
|
||||
val noteSize = noteToNumber.size
|
||||
|
||||
while (newNoteNum < 0) {
|
||||
newNoteNum += noteSize
|
||||
suffix += ","
|
||||
octaveShift -= 1
|
||||
}
|
||||
while (newNoteNum >= noteSize) {
|
||||
newNoteNum -= noteSize
|
||||
suffix += "'"
|
||||
octaveShift += 1
|
||||
}
|
||||
val newNote = noteToNumber[newNoteNum] + regexFound.groupValues[2] + suffix
|
||||
return simplifyNote(newNote) + regexFound.groupValues[3]
|
||||
|
||||
val originalOctaveVal = octaveToInt(originalOctaveStr)
|
||||
val finalOctaveVal = originalOctaveVal + octaveShift
|
||||
val finalOctaveStr = intToOctave(finalOctaveVal)
|
||||
|
||||
val transposedNote = noteToNumber[newNoteNum]
|
||||
return transposedNote + finalOctaveStr + rest
|
||||
}
|
||||
fun octaveToInt(octaveStr: String): Int {
|
||||
var valOctave = 0
|
||||
for (char in octaveStr) {
|
||||
when (char) {
|
||||
'\'', '¹' -> valOctave += 1
|
||||
',', '₁' -> valOctave -= 1
|
||||
'²' -> valOctave += 2
|
||||
'₂' -> valOctave -= 2
|
||||
'³' -> valOctave += 3
|
||||
'₃' -> valOctave -= 3
|
||||
'⁴' -> valOctave += 4
|
||||
'₄' -> valOctave -= 4
|
||||
}
|
||||
}
|
||||
return valOctave
|
||||
}
|
||||
|
||||
fun intToOctave(octaveVal: Int): String {
|
||||
val index = octaveVal + 4
|
||||
return if (index in octaveSigns.indices) {
|
||||
octaveSigns[index]
|
||||
} else if (octaveVal > 0) {
|
||||
"'".repeat(octaveVal)
|
||||
} else if (octaveVal < 0) {
|
||||
",".repeat(-octaveVal)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun transposeKey(baseKey: String, interval: Int): String {
|
||||
if (baseKey.isBlank()) return "C"
|
||||
|
||||
val isMinor = baseKey.endsWith("m", ignoreCase = true)
|
||||
val rootKey = if (isMinor) baseKey.dropLast(1) else baseKey
|
||||
val baseIndex = keyToNumber.indexOfFirst { it.equals(rootKey, ignoreCase = true) }
|
||||
if (baseIndex == -1) return baseKey
|
||||
val newIndex = (baseIndex + interval).mod(12)
|
||||
val newRootKey = keyToNumber[newIndex]
|
||||
|
||||
return if (isMinor) "${newRootKey}m" else newRootKey
|
||||
}
|
||||
@OptIn(ExperimentalMaterial3Api::class) // Ajoutez cette annotation pour utiliser ExposedDropdownMenuBox
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ fun AboutDialog(onDismiss: () -> Unit) {
|
|||
modifier = Modifier.padding(top = 4.dp, bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Version $appVersion",
|
||||
text = appVersion,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||
import cafe.adriel.voyager.navigator.internal.BackHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -53,13 +55,13 @@ import mg.dot.feufaro.solfa.Transpose
|
|||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, InternalVoyagerApi::class)
|
||||
@Composable
|
||||
fun MainScreenWithDrawer(
|
||||
solfaScreenModel: SolfaScreenModel,
|
||||
sharedScreenModel: SharedScreenModel,
|
||||
qrCodeResult: String,
|
||||
onScannerButtonClick: () -> Unit,
|
||||
solfaScrollState: ScrollState,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
|
|
@ -199,6 +201,22 @@ fun MainScreenWithDrawer(
|
|||
val density = LocalDensity.current
|
||||
var keyboardHeightDp by remember { mutableStateOf(0.dp) }
|
||||
|
||||
BackHandler(enabled = isSearchActive) {
|
||||
sharedScreenModel.showSearchMenu(false)
|
||||
}
|
||||
BackHandler(enabled = showMidiCtrl) {
|
||||
sharedScreenModel.setMidiCtrl(false)
|
||||
}
|
||||
BackHandler(enabled = isQrVisible) {
|
||||
sharedScreenModel.toggleQRCodeVisibility()
|
||||
}
|
||||
BackHandler(enabled = (!showMidiCtrl && isExpanded)) {
|
||||
sharedScreenModel.setExpandedFAB(!isExpanded)
|
||||
}
|
||||
BackHandler(enabled = drawerState.isOpen) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||
SimpleDrawerContent(
|
||||
items,
|
||||
|
|
@ -207,10 +225,6 @@ fun MainScreenWithDrawer(
|
|||
currentActivePath,
|
||||
drawerState,
|
||||
scope,
|
||||
onScannerButtonClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
onScannerButtonClick()
|
||||
},
|
||||
onSongSelected = { newSong ->
|
||||
sharedScreenModel.loadNewSong("$midiFile")
|
||||
},
|
||||
|
|
@ -454,7 +468,8 @@ fun MainScreenWithDrawer(
|
|||
}
|
||||
}
|
||||
}, actions = {
|
||||
var tempInterval by remember(fileContent) { mutableStateOf(0) }
|
||||
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||
var tempInterval by remember(fileContent, appliedInterval) { mutableStateOf(appliedInterval) }
|
||||
var isEyeVisible by remember { mutableStateOf(false) }
|
||||
val isMinor = songKey.endsWith("m", ignoreCase = true)
|
||||
val cleanSongKey = if (isMinor) songKey.dropLast(1) else songKey
|
||||
|
|
@ -462,10 +477,9 @@ fun MainScreenWithDrawer(
|
|||
val songKeyIndex = keysOrder.indexOf(cleanSongKey).takeIf { it != -1 } ?: 0
|
||||
val rawKeyIndex = (songKeyIndex + tempInterval) % 12
|
||||
val tempUiKey = keysOrder[if (rawKeyIndex < 0) rawKeyIndex + 12 else rawKeyIndex]
|
||||
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
|
||||
val isPendingChange = tempInterval != appliedInterval
|
||||
val isCurrentlyTransposed = appliedInterval != 0
|
||||
|
||||
val formattedKey = if (isMinor) "${tempUiKey}m" else tempUiKey
|
||||
Box(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
|
|
@ -475,11 +489,11 @@ fun MainScreenWithDrawer(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if(isMinor) "${tempUiKey}m" else tempUiKey,
|
||||
text = formattedKey,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.Black,
|
||||
textAlign = TextAlign.Center,
|
||||
color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White,
|
||||
color = if (isCurrentlyTransposed || isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = sharedInteractionSource,
|
||||
|
|
@ -493,7 +507,10 @@ fun MainScreenWithDrawer(
|
|||
|
||||
DropdownMenu(
|
||||
expanded = isEyeVisible,
|
||||
onDismissRequest = { isEyeVisible = false },
|
||||
onDismissRequest = {
|
||||
tempInterval = appliedInterval
|
||||
isEyeVisible = false
|
||||
},
|
||||
containerColor = Color(0xFF2C3135).copy(alpha = 0.75f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = Modifier.padding(12.dp)
|
||||
|
|
@ -503,6 +520,8 @@ fun MainScreenWithDrawer(
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
/* < = > */
|
||||
val isOriginalKeyC = cleanSongKey.equals("C", ignoreCase = true)
|
||||
val isTransposedToC = tempUiKey.equals("C", ignoreCase = true) && !isOriginalKeyC
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
|
|
@ -535,11 +554,11 @@ fun MainScreenWithDrawer(
|
|||
}
|
||||
|
||||
val centralIcon =
|
||||
if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz
|
||||
if (isPendingChange && !isTransposedToC) Icons.Filled.Check else Icons.Filled.SwapHoriz
|
||||
val centralTint =
|
||||
if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White
|
||||
val tooltipText =
|
||||
if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer"
|
||||
if (isPendingChange) "Écoutez en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else ""
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
|
|
@ -554,13 +573,18 @@ fun MainScreenWithDrawer(
|
|||
IconButton(
|
||||
modifier = Modifier.size(36.dp),
|
||||
onClick = {
|
||||
if (!isPendingChange && isCurrentlyTransposed) {
|
||||
if (isPendingChange) {
|
||||
sharedScreenModel.setTranspositionInterval(tempInterval)
|
||||
sharedScreenModel.stopMidi()
|
||||
sharedScreenModel.seekTo(0f)
|
||||
solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value)
|
||||
} else if (isCurrentlyTransposed) {
|
||||
tempInterval = 0
|
||||
sharedScreenModel.setTranspositionInterval(0)
|
||||
} else {
|
||||
sharedScreenModel.setTranspositionInterval(tempInterval)
|
||||
}
|
||||
sharedScreenModel.stopMidi()
|
||||
sharedScreenModel.seekTo(0f)
|
||||
solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value)
|
||||
}
|
||||
isEyeVisible = false
|
||||
}
|
||||
) {
|
||||
|
|
@ -599,22 +623,35 @@ fun MainScreenWithDrawer(
|
|||
|
||||
val cIndex = keysOrder.indexOf("C")
|
||||
val intervalToC = if (cIndex != -1) cIndex - songKeyIndex else 0
|
||||
|
||||
val buttonText = if (isTransposedToC) "Revenir en $songKey" else "Transposer en C"
|
||||
val tooltipTextC = if (isTransposedToC) "Revenir en clé $songKey" else "Écrire les notes en Do (C)"
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
containerColor = Color.DarkGray,
|
||||
contentColor = Color.White
|
||||
) { Text(text = "Transposer en Do (C)", fontSize = 12.sp) }
|
||||
) { Text(text = tooltipTextC, fontSize = 12.sp) }
|
||||
},
|
||||
state = rememberTooltipState()
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
tempInterval = intervalToC
|
||||
sharedScreenModel.setTranspositionInterval(intervalToC)
|
||||
solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value)
|
||||
if (isTransposedToC) {
|
||||
tempInterval = 0
|
||||
sharedScreenModel.setTranspositionInterval(0)
|
||||
sharedScreenModel.setTranspToC(0)
|
||||
sharedScreenModel.stopMidi()
|
||||
sharedScreenModel.seekTo(0f)
|
||||
} else {
|
||||
val currentKey = sharedScreenModel.songKey.value
|
||||
val intervalC = getIntervalToC(currentKey)
|
||||
|
||||
tempInterval = -intervalC
|
||||
sharedScreenModel.setTranspToC(intervalC)
|
||||
sharedScreenModel.stopMidi()
|
||||
sharedScreenModel.seekTo(0f)
|
||||
}
|
||||
isEyeVisible = false
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(36.dp),
|
||||
|
|
@ -628,7 +665,7 @@ fun MainScreenWithDrawer(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Transposer en C",
|
||||
text = buttonText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
|
@ -785,9 +822,10 @@ fun MainScreenWithDrawer(
|
|||
)
|
||||
|
||||
if (!savedPath.isNullOrEmpty()) {
|
||||
val transpositionInterval = sharedScreenModel.transpositionInterval.value
|
||||
val bpm = sharedScreenModel.getBpmFlow().value ?: 120f
|
||||
|
||||
solfaScreenModel.generateMidiFile(bpm, savedPath)
|
||||
solfaScreenModel.generateMidiFile(bpm, savedPath, transpositionInterval)
|
||||
println("Fichier MIDI enregistré avec succès sous : $savedPath")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -1124,6 +1162,24 @@ fun MainScreenWithDrawer(
|
|||
}
|
||||
})
|
||||
}
|
||||
private fun getIntervalToC(key: String): Int {
|
||||
val cleanKey = key.removeSuffix("m").trim().uppercase()
|
||||
return when (cleanKey) {
|
||||
"C" -> 0
|
||||
"DB" -> 1
|
||||
"D" -> 2
|
||||
"EB" -> 3
|
||||
"E" -> 4
|
||||
"F" -> 5
|
||||
"GB" -> 6
|
||||
"G" -> 7
|
||||
"AB" -> 8
|
||||
"A" -> 9
|
||||
"BB" -> 10
|
||||
"B" -> 11
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MyFAB(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ fun SimpleDrawerContent(
|
|||
activePath: String = Solfa.currentFile,
|
||||
drawerState: DrawerState,
|
||||
scope: CoroutineScope,
|
||||
onScannerButtonClick: () -> Unit,
|
||||
onSongSelected: (String) -> Unit,
|
||||
onSettingsCheck: () -> Unit,
|
||||
onAboutCheck: () -> Unit,
|
||||
|
|
@ -195,7 +194,7 @@ fun SimpleDrawerContent(
|
|||
|
||||
stickyHeader {
|
||||
DrawerHeaderSticky(
|
||||
title = "Personnel",
|
||||
title = "Fichiers locaux",
|
||||
icon = Icons.Default.LibraryMusic,
|
||||
isExpanded = externalExpanded,
|
||||
onToggle = { externalExpanded = !externalExpanded },
|
||||
|
|
|
|||
|
|
@ -290,6 +290,11 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
transpositionInterval.value = interval
|
||||
}
|
||||
|
||||
val transpToCInt = MutableStateFlow(0)
|
||||
fun setTranspToC(count: Int) {
|
||||
transpToCInt.value = count
|
||||
}
|
||||
|
||||
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
|
||||
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
|
||||
|
||||
|
|
@ -728,6 +733,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_sourceModeState.value = false
|
||||
_harmonyView.value = false
|
||||
setTranspositionInterval(0)
|
||||
setTranspToC(0)
|
||||
setSilentDurBefr(0)
|
||||
_midiMarkersList.value = emptyList()
|
||||
_tuoTimestamps.value = emptyList()
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class SolfaScreenModel(
|
|||
solfa.createNewSolfa(metadata)
|
||||
}
|
||||
}
|
||||
fun generateMidiFile(bpm: Float, path: String) {
|
||||
solfa.generateMidiFile(bpm, path)
|
||||
fun generateMidiFile(bpm: Float, path: String, transpI: Int) {
|
||||
solfa.generateMidiFile(bpm, path, transpI)
|
||||
}
|
||||
}
|
||||
|
|
@ -405,7 +405,7 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
// Farany
|
||||
finReg.containsMatchIn(mTrim) -> {
|
||||
val hasDcAfter = metadataList.any { m ->
|
||||
val hasDcOrDsAfter = metadataList.any { m ->
|
||||
val after = (m.gridIndex ?: 0) > currentIndex
|
||||
|
||||
println(
|
||||
|
|
@ -413,14 +413,15 @@ actual class FMediaPlayer actual constructor(
|
|||
)
|
||||
|
||||
val hasMarker = m.marker.any { mk ->
|
||||
dcRegex.matches(mk.trim())
|
||||
dcRegex.containsMatchIn(mk) || dsRegex.containsMatchIn(mk)
|
||||
}
|
||||
|
||||
println("after=$after, hasMarker=$hasMarker, result=${after && hasMarker}")
|
||||
|
||||
after && hasMarker
|
||||
}
|
||||
if (hasDcAfter) {
|
||||
|
||||
if (hasDcOrDsAfter) {
|
||||
navigationSteps.add(
|
||||
NavigationStep(
|
||||
marker = marker,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue