'<' '>' to transpose audio & chords; "Transpose to C": to convert note to C scale
This commit is contained in:
parent
88b734cdc2
commit
78876d7ec0
8 changed files with 156 additions and 38 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -264,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 }))
|
||||
|
|
@ -276,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)
|
||||
}
|
||||
|
|
@ -2086,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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -468,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
|
||||
|
|
@ -476,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
|
||||
|
|
@ -489,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,
|
||||
|
|
@ -507,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)
|
||||
|
|
@ -517,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,
|
||||
|
|
@ -549,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(),
|
||||
|
|
@ -568,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
|
||||
}
|
||||
) {
|
||||
|
|
@ -613,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),
|
||||
|
|
@ -642,7 +665,7 @@ fun MainScreenWithDrawer(
|
|||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Transposer en C",
|
||||
text = buttonText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
|
@ -799,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) {
|
||||
|
|
@ -1138,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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue