From 094faf6739274a40ed7e18b7f75241ee512c77e8 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Mon, 1 Jan 2024 22:50:06 +0800 Subject: [PATCH] Update --- builtin/build.gradle.kts | 4 +- builtin/chord-analyzer/build.gradle.kts | 1 + .../builtin/chordanalyzer/ChordAnalyzer.kt | 62 +++++++++++ .../chordanalyzer/GlobalChordAnalyzer.kt | 13 +++ builtin/settings.gradle.kts | 2 +- daw/.gitignore | 3 + daw/build.gradle.kts | 22 +++- .../kotlin/com/eimsound/daw/Configuration.kt | 3 + .../jvmMain/kotlin/com/eimsound/daw/Main.kt | 3 + .../impl/clips/midi/editor/EditorControls.kt | 100 ++++++++++-------- .../impl/clips/midi/editor/MidiClipEditor.kt | 26 +++++ gradle.properties | 2 +- settings.gradle.kts | 4 +- 13 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/ChordAnalyzer.kt create mode 100644 builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/GlobalChordAnalyzer.kt diff --git a/builtin/build.gradle.kts b/builtin/build.gradle.kts index 33d454ca..c4d86fea 100644 --- a/builtin/build.gradle.kts +++ b/builtin/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + kotlin("jvm") version "1.9.21" } repositories { @@ -7,5 +7,5 @@ repositories { } dependencies { - implementation(project(":chord-analyzer")) + api(project(":chord-analyzer")) } \ No newline at end of file diff --git a/builtin/chord-analyzer/build.gradle.kts b/builtin/chord-analyzer/build.gradle.kts index 87b7c672..0926fb0a 100644 --- a/builtin/chord-analyzer/build.gradle.kts +++ b/builtin/chord-analyzer/build.gradle.kts @@ -7,4 +7,5 @@ repositories { } dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2") } diff --git a/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/ChordAnalyzer.kt b/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/ChordAnalyzer.kt new file mode 100644 index 00000000..96ce6a72 --- /dev/null +++ b/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/ChordAnalyzer.kt @@ -0,0 +1,62 @@ +package com.eimsound.daw.builtin.chordanalyzer + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.BufferedWriter +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +data class Chords( + val chords: List, + val durations: List +) + +interface ChordAnalyzer : AutoCloseable { + suspend fun analyze(chord: List, starts: List, durations: List): Chords +} + +class ChordAnalyzerImpl(file: Path) : ChordAnalyzer { + private val mutex = Mutex() + private val input: BufferedReader + private val output: BufferedWriter + private val process = ProcessBuilder(file.absolutePathString(), "chords").run { + redirectError() + start().apply { + input = inputStream.bufferedReader() + output = outputStream.bufferedWriter() + } + } + + override suspend fun analyze(chord: List, starts: List, durations: List) = withContext(Dispatchers.IO) { + mutex.withLock { +// val gg = StringWriter() +// val output = BufferedWriter(gg) + output.write("chords_detect\n") + output.write(chord.joinToString(",")) + output.write("\n") + starts.forEachIndexed { i, it -> + if (i != 0) output.write(",") + output.write(it.toString()) + } + output.write("\n") + durations.forEachIndexed { i, it -> + if (i != 0) output.write(",") + output.write(it.toString()) + } + output.write("\n") + output.flush() +// println(gg.buffer.toString()) +// gg.close() + + Chords( + input.readLine().split(","), + input.readLine().split(",").map { it.toFloatOrNull() ?: Float.MAX_VALUE } + ) + } + } + + override fun close() { process.destroy() } +} diff --git a/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/GlobalChordAnalyzer.kt b/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/GlobalChordAnalyzer.kt new file mode 100644 index 00000000..8a26f098 --- /dev/null +++ b/builtin/chord-analyzer/src/main/kotlin/com/eimsound/daw/builtin/chordanalyzer/GlobalChordAnalyzer.kt @@ -0,0 +1,13 @@ +package com.eimsound.daw.builtin.chordanalyzer + +import kotlin.io.path.Path + +var chordAnalyzerInitialized = false + private set +val chordAnalyzer: ChordAnalyzer by lazy { + val fallback = if (System.getProperty("os.name").contains("Windows")) "EIMUtils/EIMUtils.exe" + else "EIMUtils/EIMUtils" + ChordAnalyzerImpl(Path(System.getProperty("eim.eimutils.file", fallback))).apply { + chordAnalyzerInitialized = true + } +} diff --git a/builtin/settings.gradle.kts b/builtin/settings.gradle.kts index d305d324..6ae64adf 100644 --- a/builtin/settings.gradle.kts +++ b/builtin/settings.gradle.kts @@ -1,3 +1,3 @@ include( ":chord-analyzer" -) \ No newline at end of file +) diff --git a/daw/.gitignore b/daw/.gitignore index bc39858c..0bdeac91 100644 --- a/daw/.gitignore +++ b/daw/.gitignore @@ -1,3 +1,6 @@ /EIMHost-MacOS.zip /EIMHost.app /EIMHost + +/EIMUtils-MacOS.zip +/EIMUtils diff --git a/daw/build.gradle.kts b/daw/build.gradle.kts index 0e817e84..2d67b106 100644 --- a/daw/build.gradle.kts +++ b/daw/build.gradle.kts @@ -85,8 +85,8 @@ fun downloadFileFromGithub(repo: String, destName: String, sourceName: String = val isArm by lazy { System.getProperty("os.arch") == "aarch64" } project(":daw") { + val os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem() task("downloadEIMHost") { - val os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem() if (os.isWindows) { downloadFileFromGithub("EIMHost", "EIMHost.exe", "EIMHost-x64.exe") // downloadFileFromGithub("EIMHost", "EIMHost-x86.exe", "EIMHost-x86.exe") @@ -99,6 +99,24 @@ project(":daw") { downloadFileFromGithub("EIMHost", "EIMHost", "EIMHost-Linux") } } + + task("downloadEIMUtils") { + if (os.isWindows) { + if (downloadFileFromGithub("EIMUtils", "EIMUtils-Windows.zip", "EIMUtils-Windows.zip")) { + from(zipTree(File("EIMUtils-Windows.zip"))).into(".") + } + } else if (os.isMacOsX) { + if (downloadFileFromGithub("EIMUtils", "EIMUtils-MacOS.zip", + if (isArm) "EIMUtils-MacOS.zip" else "EIMUtils-MacOS-x86_64.zip")) { + from(zipTree(File("EIMUtils-MacOS.zip"))).into(".") + println(233333) + } + } else { + if (downloadFileFromGithub("EIMUtils", "EIMUtils", "EIMUtils-Linux.zip")) { + from(zipTree(File("EIMUtils-Linux.zip"))).into(".") + } + } + } } tasks.withType { @@ -135,5 +153,5 @@ tasks.withType { // Run before build tasks.withType { dependsOn(":downloadEIMHost") - dependsOn(":downloadTimeStretcher") + dependsOn(":downloadEIMUtils") } diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/Configuration.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/Configuration.kt index 702cd327..cd2418ae 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/Configuration.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/Configuration.kt @@ -88,7 +88,9 @@ object Configuration : JsonSerializable { else "EIMHost" ) } + var execExt = "" val x86Host = if (SystemUtils.IS_OS_WINDOWS) { + execExt = ".exe" nativeHostPath.absolute().parent.resolve(nativeHostPath.name.removeSuffix(".exe") + "-x86.exe") } else nativeHostPath @@ -98,6 +100,7 @@ object Configuration : JsonSerializable { System.setProperty("eim.dsp.nativeaudioplugins.host", nativeHostPath.absolutePathString()) System.setProperty("eim.dsp.nativeaudioplugins.host.x86", (if (Files.exists(x86Host)) x86Host else nativeHostPath).absolutePathString()) System.setProperty("eim.dsp.nativeaudioplayer.file", nativeHostPath.absolutePathString()) + System.setProperty("eim.eimutils.file", Path("EIMUtils/EIMUtils$execExt").absolutePathString()) System.setProperty("eim.tempfiles.prefix", "EchoInMirror") val libraryExt = if (SystemUtils.IS_OS_WINDOWS) "dll" else if (SystemUtils.IS_OS_MAC) "dylib" else "so" diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/Main.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/Main.kt index d98948c5..51020616 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/Main.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/Main.kt @@ -14,6 +14,8 @@ import com.eimsound.daw.api.clips.ClipManager import com.eimsound.daw.api.EchoInMirror import com.eimsound.daw.api.controllers.DefaultParameterControllerFactory import com.eimsound.daw.api.clips.defaultEnvelopeClipFactory +import com.eimsound.daw.builtin.chordanalyzer.chordAnalyzer +import com.eimsound.daw.builtin.chordanalyzer.chordAnalyzerInitialized import com.eimsound.daw.commons.ExperimentalEIMApi import com.eimsound.daw.components.app.EIMTray import com.eimsound.daw.components.controllers.parameterControllerCreateClipHandler @@ -65,6 +67,7 @@ fun main() { } val windowManager = EchoInMirror.windowManager Runtime.getRuntime().addShutdownHook(thread(false) { + if (chordAnalyzerInitialized) chordAnalyzer.close() androidApplication?.close() EchoInMirror.close() }) diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EditorControls.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EditorControls.kt index 38cf0bfe..834aefaf 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EditorControls.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EditorControls.kt @@ -67,7 +67,39 @@ private fun TrackItem(track: Track, backingTracks: IManualStateValue + val v = str.toIntOrNull()?.coerceIn(0, 127) ?: return@CustomOutlinedTextField + if (editor.selectedNotes.isEmpty()) DefaultMidiClipEditor.defaultVelocity = v + else editor.clip.clip.doNoteVelocityAction(editor.selectedNotes.toTypedArray(), v - velocity) + }, + Modifier.width(60.dp).padding(end = 10.dp), + label = { Text("力度") }, + singleLine = true, + ) + Slider(trueValue.toFloat() / 127, + { + if (editor.selectedNotes.isEmpty()) DefaultMidiClipEditor.defaultVelocity = (it * 127).roundToInt() + else delta = (it * 127).roundToInt() - velocity + }, Modifier.weight(1f), onValueChangeFinished = { + if (editor.selectedNotes.isNotEmpty()) editor.clip.clip + .doNoteVelocityAction(editor.selectedNotes.toTypedArray(), delta) + delta = 0 + } + ) + } +} + +@Composable +private fun TrackSelector(editor: DefaultMidiClipEditor) { val track = EchoInMirror.selectedTrack FloatingLayer({ size, _ -> Surface(Modifier.width(IntrinsicSize.Max).widthIn(min = size.width), shape = MaterialTheme.shapes.extraSmall, @@ -99,54 +131,34 @@ internal fun EditorControls(editor: DefaultMidiClipEditor) { } } } +} + +@Composable +internal fun EditorControls(editor: DefaultMidiClipEditor) { + TrackSelector(editor) Column(Modifier.padding(10.dp)) { NoteWidthSlider(editor.noteWidth) - editor.apply { - Row(verticalAlignment = Alignment.CenterVertically) { - Text("试听音符", Modifier.weight(1f), style = MaterialTheme.typography.labelLarge) - Checkbox(DefaultMidiClipEditor.playOnEdit, { DefaultMidiClipEditor.playOnEdit = !DefaultMidiClipEditor.playOnEdit }) - } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("试听音符", Modifier.weight(1f), style = MaterialTheme.typography.labelLarge) + Checkbox(DefaultMidiClipEditor.playOnEdit, { DefaultMidiClipEditor.playOnEdit = !DefaultMidiClipEditor.playOnEdit }) + } - Row(verticalAlignment = Alignment.CenterVertically) { - editor.clip.clip.notes.read() - var delta by remember { mutableStateOf(0) } - val cur = currentSelectedNote - val velocity = if (editor.selectedNotes.isEmpty()) DefaultMidiClipEditor.defaultVelocity else - (cur ?: editor.selectedNotes.first()).velocity - val trueValue = velocity + (if (editor.selectedNotes.isEmpty()) 0 else delta) - CustomOutlinedTextField( - trueValue.toString(), { str -> - val v = str.toIntOrNull()?.coerceIn(0, 127) ?: return@CustomOutlinedTextField - if (editor.selectedNotes.isEmpty()) DefaultMidiClipEditor.defaultVelocity = v - else editor.clip.clip.doNoteVelocityAction(editor.selectedNotes.toTypedArray(), v - velocity) - }, - Modifier.width(60.dp).padding(end = 10.dp), - label = { Text("力度") }, - singleLine = true, - ) - Slider(trueValue.toFloat() / 127, - { - if (editor.selectedNotes.isEmpty()) DefaultMidiClipEditor.defaultVelocity = (it * 127).roundToInt() - else delta = (it * 127).roundToInt() - velocity - }, Modifier.weight(1f), onValueChangeFinished = { - if (editor.selectedNotes.isNotEmpty()) editor.clip.clip - .doNoteVelocityAction(editor.selectedNotes.toTypedArray(), delta) - delta = 0 - } - ) - } + NoteVelocityComponets(editor) - Row(verticalAlignment = Alignment.CenterVertically) { - editor.clip.clip.notes.read() - Text("启用", Modifier.weight(1f), style = MaterialTheme.typography.labelLarge) - val cur = currentSelectedNote ?: editor.selectedNotes.firstOrNull() - val curState = cur?.isDisabled ?: false - Checkbox(!curState, { - if (cur == null) return@Checkbox - editor.clip.clip.doNoteDisabledAction(editor.selectedNotes.toList(), !curState) - }) - } + Row(verticalAlignment = Alignment.CenterVertically) { + editor.clip.clip.notes.read() + Text("启用", Modifier.weight(1f), style = MaterialTheme.typography.labelLarge) + val cur = editor.currentSelectedNote ?: editor.selectedNotes.firstOrNull() + val curState = cur?.isDisabled ?: false + Checkbox(!curState, { + if (cur == null) return@Checkbox + editor.clip.clip.doNoteDisabledAction(editor.selectedNotes.toList(), !curState) + }) + } + + Button({ editor.detectChords() }) { + Text("分析和弦") } } } diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/MidiClipEditor.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/MidiClipEditor.kt index c730e1e0..c5114824 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/MidiClipEditor.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/MidiClipEditor.kt @@ -24,6 +24,8 @@ import com.eimsound.daw.api.clips.MidiClipEditor import com.eimsound.daw.api.clips.TrackClip import com.eimsound.daw.api.processor.Track import com.eimsound.daw.api.window.EditorExtension +import com.eimsound.daw.builtin.chordanalyzer.Chords +import com.eimsound.daw.builtin.chordanalyzer.chordAnalyzer import com.eimsound.daw.commons.IManualStateValue import com.eimsound.daw.commons.ManualStateValue import com.eimsound.daw.commons.json.JsonIgnoreDefaults @@ -37,9 +39,14 @@ import com.eimsound.daw.utils.* import com.eimsound.daw.window.panels.playlist.playlistTrackControllerMinWidth import com.eimsound.daw.window.panels.playlist.Playlist import com.eimsound.dsp.data.midi.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.util.* +import kotlin.collections.ArrayList import kotlin.math.roundToInt // Focusable @@ -268,4 +275,23 @@ class DefaultMidiClipEditor(override val clip: TrackClip) : MidiClipEd selectedNotes.add(newNote) return newNote } + + var chords by mutableStateOf(Chords(emptyList(), emptyList())) + @OptIn(DelicateCoroutinesApi::class) + fun detectChords() { + val list = ArrayList(clip.clip.notes) + GlobalScope.launch(Dispatchers.IO) { + list.sortBy { it.time } + var pre = 0 + chords = chordAnalyzer.analyze( + list.map { getNoteName(it.note) }, + list.map { it.duration }, + list.map { + val result = it.time - pre + pre = it.time + result + } + ) + } + } } diff --git a/gradle.properties b/gradle.properties index 8ccd5162..5e974780 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ org.gradle.caching=true org.gradle.configureondemand=true eim.dependencies.kotlin.logging=5.1.0 -eim.dependencies.kotlinx.coroutines=1.7.3 +eim.dependencies.kotlinx.coroutines=1.8.0-RC2 eim.dependencies.kotlinx.serialization=1.6.0 eim.dependencies.commons.lang=3.13.0 eim.dependencies.slf4j=2.0.9 diff --git a/settings.gradle.kts b/settings.gradle.kts index 7f175627..833ce419 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,6 +29,4 @@ include( ":daw" ) -includeBuild( - "builtin" -) +includeBuild("builtin")