From 57d4f46032ef22e7972c70bb2c1ba4c58caf3cbd Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Wed, 20 Dec 2023 21:04:04 +0800 Subject: [PATCH] Update --- .../com/eimsound/daw/api/CommandManager.kt | 2 +- .../com/eimsound/daw/commons/BasicEditor.kt | 1 + .../eimsound/daw/components/EnvelopeEditor.kt | 4 +- .../eimsound/daw/components/FloatingLayer.kt | 4 +- .../com/eimsound/daw/components/Snackbar.kt | 4 +- .../com/eimsound/daw/components/Waveform.kt | 177 ++++++++++-------- .../components/dragdrop/GlobalDragAndDrop.kt | 6 +- .../com/eimsound/daw/commands/EditCommands.kt | 15 +- .../daw/impl/clips/audio/AudioClipEditor.kt | 4 +- .../impl/clips/envelope/EnvelopeClipEditor.kt | 4 +- .../daw/impl/clips/midi/editor/EventEditor.kt | 1 + .../impl/clips/midi/editor/MidiClipEditor.kt | 21 ++- .../clips/midi/editor/NotesEditorCanvas.kt | 4 +- .../com/eimsound/daw/window/panels/Editor.kt | 1 + .../daw/window/panels/FileSystemBrowser.kt | 4 +- .../daw/window/panels/playlist/Playlist.kt | 22 ++- .../com/eimsound/dsp/data/AudioThumbnail.kt | 2 +- .../eimsound/daw/utils/SnapshotStateSet.kt | 8 +- 18 files changed, 168 insertions(+), 116 deletions(-) diff --git a/api/src/commonMain/kotlin/com/eimsound/daw/api/CommandManager.kt b/api/src/commonMain/kotlin/com/eimsound/daw/api/CommandManager.kt index 2c479a86..05ed3289 100644 --- a/api/src/commonMain/kotlin/com/eimsound/daw/api/CommandManager.kt +++ b/api/src/commonMain/kotlin/com/eimsound/daw/api/CommandManager.kt @@ -6,10 +6,10 @@ import androidx.compose.ui.input.key.Key interface Command { val name: String val displayName: String - fun execute() {} val keyBindings: Array val icon: ImageVector? val activeWhen: Array? + fun execute() } abstract class AbstractCommand( diff --git a/commons/src/commonMain/kotlin/com/eimsound/daw/commons/BasicEditor.kt b/commons/src/commonMain/kotlin/com/eimsound/daw/commons/BasicEditor.kt index b7de47e9..46f66dbb 100644 --- a/commons/src/commonMain/kotlin/com/eimsound/daw/commons/BasicEditor.kt +++ b/commons/src/commonMain/kotlin/com/eimsound/daw/commons/BasicEditor.kt @@ -8,6 +8,7 @@ interface BasicEditor { delete() } fun paste() { } + fun duplicate() { } val hasSelected get() = true val canPaste get() = true val canDelete get() = true diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/EnvelopeEditor.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/EnvelopeEditor.kt index 276723fa..5bb89894 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/EnvelopeEditor.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/EnvelopeEditor.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.* -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.drawText @@ -472,7 +472,7 @@ class EnvelopeEditor( // if (hoveredIndex == -1 && action == EditAction.RESIZE) modifier = modifier.pointerHoverIcon(PointerIconDefaults.VerticalResize) - Canvas(Modifier.fillMaxSize().graphicsLayer { }.onGloballyPositioned { positionInRoot = it.positionInRoot() }.run { + Canvas(Modifier.fillMaxSize().graphicsLayer { }.onPlaced { positionInRoot = it.positionInRoot() }.run { if (eventHandler == null) this else pointerInput(Unit) { detectTapGestures(onDoubleTap = { if ( diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/FloatingLayer.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/FloatingLayer.kt index 467638e9..0ac53191 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/FloatingLayer.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/FloatingLayer.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.unit.* import androidx.compose.ui.util.fastForEach @@ -144,7 +144,7 @@ fun FloatingLayer( } } val offset = remember { arrayOf(Offset.Zero) } val size = remember { arrayOf(Size.Zero) } - Box((if (isCentral) modifier else modifier.onGloballyPositioned { + Box((if (isCentral) modifier else modifier.onPlaced { offset[0] = it.positionInRoot() size[0] = it.size.toSize() }).let { if (enabled) it.pointerHoverIcon(PointerIcon.Hand) else it } diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/Snackbar.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/Snackbar.kt index 55fa9170..193bd543 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/Snackbar.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/Snackbar.kt @@ -15,7 +15,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachReversed @@ -58,7 +58,7 @@ private fun SnackbarAnim(snackbar: Snackbar) { spring(stiffness = Spring.StiffnessMediumLow) ) AnimatedVisibility(snackbar.isVisible, - Modifier.onGloballyPositioned { if (it.size.height > height[0]) height[0] = it.size.height } + Modifier.onPlaced { if (it.size.height > height[0]) height[0] = it.size.height } .run { if (height[0] > 0) height(LocalDensity.current.run { (height[0] * anim).toDp() }) else this }, enter = slideInHorizontally { it }, exit = slideOutHorizontally { it } diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt index e5406ed3..2b9a5c2c 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt @@ -2,32 +2,34 @@ package com.eimsound.daw.components -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.unit.IntSize import com.eimsound.audioprocessor.PlayPosition import com.eimsound.audioprocessor.convertPPQToSeconds import com.eimsound.dsp.data.AudioThumbnail import com.eimsound.dsp.data.EnvelopePointList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.math.absoluteValue private const val STEP_IN_PX = 0.5F private const val WAVEFORM_DAMPING = 0.93F -private fun DrawScope.drawMinAndMax( +private fun Canvas.drawMinAndMax( thumbnail: AudioThumbnail, startSeconds: Double, endSeconds: Double, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, - color: Color + paint: Paint, width: Float ) { var min = 0F var max = 0F - thumbnail.query(size.width.toDouble(), startSeconds, endSeconds, STEP_IN_PX) { x, ch, min0, max0 -> + thumbnail.query(width, startSeconds, endSeconds, STEP_IN_PX) { x, ch, min0, max0 -> val y = 2 + channelHeight * ch + halfChannelHeight val curMax = max0.absoluteValue.coerceAtMost(1F) * drawHalfChannelHeight val curMin = min0.absoluteValue.coerceAtMost(1F) * drawHalfChannelHeight @@ -36,35 +38,25 @@ private fun DrawScope.drawMinAndMax( if (curMax > max) max = curMax else max *= WAVEFORM_DAMPING if (min + max < 0.3F) { - drawLine(color, Offset(x, y), Offset(x + STEP_IN_PX, y), STEP_IN_PX) + drawRect(x, y,x + STEP_IN_PX, y, paint) return@query } - drawLine( - color, - Offset(x, y - max), - Offset(x, y + min), - 0.5F - ) + drawRect(x, y - max, x + STEP_IN_PX, y + min, paint) } } -private fun DrawScope.drawDefault( +private fun Canvas.drawDefault( thumbnail: AudioThumbnail, startSeconds: Double, endSeconds: Double, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, - color: Color + paint: Paint, width: Float ) { - thumbnail.query(size.width.toDouble(), startSeconds, endSeconds, STEP_IN_PX) { x, ch, min, max -> + thumbnail.query(width, startSeconds, endSeconds, STEP_IN_PX) { x, ch, min, max -> val v = (if (max.absoluteValue > min.absoluteValue) max else min).coerceIn(-1F, 1F) * drawHalfChannelHeight val y = 2 + channelHeight * ch + halfChannelHeight if (v.absoluteValue < 0.3F) { - drawLine(color, Offset(x, y), Offset(x + STEP_IN_PX, y), STEP_IN_PX) + drawRect(x, y, x + STEP_IN_PX, y, paint) return@query } - drawLine( - color, - Offset(x, y), - Offset(x, y - v), - STEP_IN_PX - ) + drawRect(x, y, x + STEP_IN_PX, y - v, paint) } } @@ -77,33 +69,50 @@ fun Waveform( isDrawMinAndMax: Boolean = true, modifier: Modifier = Modifier ) { - thumbnail.read() - Canvas(modifier.fillMaxSize().graphicsLayer { }) { - val channelHeight = (size.height / thumbnail.channels) - 2 - val halfChannelHeight = channelHeight / 2 - val drawHalfChannelHeight = halfChannelHeight - 1 - if (isDrawMinAndMax) { - drawMinAndMax( - thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, color - ) - } else { - drawDefault( - thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, color - ) + var size: IntSize? by remember { mutableStateOf(null) } + Box(Modifier.fillMaxSize().onPlaced { size = it.size }) { + val image by produceState( + null, size, thumbnail, thumbnail.read(), startSeconds, endSeconds, isDrawMinAndMax + ) { + val curSize = size + if (curSize == null) { + value = null + return@produceState + } + withContext(Dispatchers.Default) { + val bitmap = ImageBitmap(curSize.width, curSize.height) + val paint = Paint() + Canvas(bitmap).apply { + val channelHeight = (curSize.height.toFloat() / thumbnail.channels) - 2 + val halfChannelHeight = channelHeight / 2 + val drawHalfChannelHeight = halfChannelHeight - 1 + if (isDrawMinAndMax) { + drawMinAndMax( + thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, paint, curSize.width.toFloat() + ) + } else { + drawDefault( + thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, paint, curSize.width.toFloat() + ) + } + } + value = bitmap + } } + image?.let { Image(it, "Waveform", modifier, colorFilter = ColorFilter.tint(color)) } } } -private fun DrawScope.drawMinAndMax( +private fun Canvas.drawMinAndMax( thumbnail: AudioThumbnail, startPPQ: Float, startSeconds: Double, endSeconds: Double, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, - stepPPQ: Float, color: Color, volumeEnvelope: EnvelopePointList? + stepPPQ: Float, volumeEnvelope: EnvelopePointList?, paint: Paint, width: Float ) { var min = 0F var max = 0F - thumbnail.query(size.width.toDouble(), startSeconds, endSeconds, STEP_IN_PX) { x, ch, min0, max0 -> + thumbnail.query(width, startSeconds, endSeconds, STEP_IN_PX) { x, ch, min0, max0 -> val y = 2 + channelHeight * ch + halfChannelHeight val volume = volumeEnvelope?.getValue((startPPQ + x * stepPPQ).toInt(), 1F) ?: 1F val curMin = (min0.absoluteValue * volume).coerceAtMost(1F) * drawHalfChannelHeight @@ -113,37 +122,27 @@ private fun DrawScope.drawMinAndMax( if (curMax > max) max = curMax else max *= WAVEFORM_DAMPING if (min + max < 0.3F) { - drawLine(color, Offset(x, y), Offset(x + STEP_IN_PX, y), STEP_IN_PX) + drawRect(x, y, x + STEP_IN_PX, y, paint) return@query } - drawLine( - color, - Offset(x, y - max), - Offset(x, y + min), - 0.5F - ) + drawRect(x, y - max, x + STEP_IN_PX, y + min, paint) } } -private fun DrawScope.drawDefault( +private fun Canvas.drawDefault( thumbnail: AudioThumbnail, startPPQ: Float, startSeconds: Double, endSeconds: Double, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, - stepPPQ: Float, color: Color, volumeEnvelope: EnvelopePointList? + stepPPQ: Float, volumeEnvelope: EnvelopePointList?, paint: Paint, width: Float ) { - thumbnail.query(size.width.toDouble(), startSeconds, endSeconds, STEP_IN_PX) { x, ch, min, max -> + thumbnail.query(width, startSeconds, endSeconds, STEP_IN_PX) { x, ch, min, max -> val v = ((if (max.absoluteValue > min.absoluteValue) max else min) * (volumeEnvelope?.getValue((startPPQ + x * stepPPQ).toInt(), 1F) ?: 1F)) .coerceIn(-1F, 1F) * drawHalfChannelHeight val y = 2 + channelHeight * ch + halfChannelHeight if (v.absoluteValue < 0.3F) { - drawLine(color, Offset(x, y), Offset(x + STEP_IN_PX, y), STEP_IN_PX) + drawRect(x, y, x + STEP_IN_PX, y, paint) return@query } - drawLine( - color, - Offset(x, y), - Offset(x, y - v), - STEP_IN_PX - ) + drawRect(x, y, x + STEP_IN_PX, y - v, paint) } } @@ -156,26 +155,42 @@ fun Waveform( isDrawMinAndMax: Boolean = true, modifier: Modifier = Modifier ) { - thumbnail.read() - val factor = (thumbnail.sampleRate / position.sampleRate) * timeScale - val startSeconds = position.convertPPQToSeconds(startPPQ) / factor - val endSeconds = position.convertPPQToSeconds(startPPQ + widthPPQ) / factor - Canvas(modifier.fillMaxSize().graphicsLayer { }) { - val channelHeight = (size.height / thumbnail.channels) - 2 - val halfChannelHeight = channelHeight / 2 - val drawHalfChannelHeight = halfChannelHeight - 1 - val stepPPQ = widthPPQ / size.width - volumeEnvelope?.read() - if (isDrawMinAndMax) { - drawMinAndMax( - thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, stepPPQ, color, volumeEnvelope - ) - } else { - drawDefault( - thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, stepPPQ, color, volumeEnvelope - ) + var size: IntSize? by remember { mutableStateOf(null) } + Box(modifier.fillMaxSize().onPlaced { size = it.size }) { + val factor = (thumbnail.sampleRate / position.sampleRate) * timeScale + val startSeconds = position.convertPPQToSeconds(startPPQ) / factor + val endSeconds = position.convertPPQToSeconds(startPPQ + widthPPQ) / factor + val image by produceState( + null, size, thumbnail, thumbnail.read(), startSeconds, endSeconds, volumeEnvelope, isDrawMinAndMax + ) { + val curSize = size + if (curSize == null) { + value = null + return@produceState + } + withContext(Dispatchers.Default) { + val bitmap = ImageBitmap(curSize.width, curSize.height) + val paint = Paint() + val channelHeight = (curSize.height / thumbnail.channels) - 2F + val halfChannelHeight = channelHeight / 2 + val drawHalfChannelHeight = halfChannelHeight - 1 + val stepPPQ = widthPPQ / curSize.width + Canvas(bitmap).apply { + if (isDrawMinAndMax) { + drawMinAndMax( + thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, stepPPQ, volumeEnvelope, paint, curSize.width.toFloat() + ) + } else { + drawDefault( + thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, stepPPQ, volumeEnvelope, paint, curSize.width.toFloat() + ) + } + } + value = bitmap + } } + image?.let { Image(it, "Waveform", Modifier.fillMaxSize(), colorFilter = ColorFilter.tint(color)) } } } diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/dragdrop/GlobalDragAndDrop.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/dragdrop/GlobalDragAndDrop.kt index 9bdbc230..6756175e 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/dragdrop/GlobalDragAndDrop.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/dragdrop/GlobalDragAndDrop.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInRoot import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -72,7 +72,7 @@ fun GlobalDraggable( } } val contentColor = LocalContentColor.current - Box(modifier.onGloballyPositioned { currentPos[0] = it.positionInRoot() }.onDrag(onDragStart = { + Box(modifier.onPlaced { currentPos[0] = it.positionInRoot() }.onDrag(onDragStart = { GlobalScope.launch { val data = onDragStart() ?: return@launch isCurrent = true @@ -92,7 +92,7 @@ fun GlobalDraggable( @Composable fun GlobalDropTarget(onDrop: ((Any, Offset) -> Unit)?, modifier: Modifier = Modifier, content: @Composable (Offset?) -> Unit) { var currentPos by remember { mutableStateOf(Rect.Zero) } - Box(modifier.onGloballyPositioned { currentPos = it.boundsInRoot() }) { + Box(modifier.onPlaced { currentPos = it.boundsInRoot() }) { val globalDragAndDrop = LocalGlobalDragAndDrop.current if (globalDragAndDrop.dataTransfer != null && currentPos.contains(globalDragAndDrop.currentPosition)) { globalDragAndDrop.dropCallback = onDrop?.let { diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/commands/EditCommands.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/commands/EditCommands.kt index ae57d3f1..8cd0069d 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/commands/EditCommands.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/commands/EditCommands.kt @@ -17,49 +17,42 @@ import kotlinx.coroutines.launch object DeleteCommand : AbstractCommand("EIM:Delete", "删除", arrayOf(Key.Delete), Icons.Filled.DeleteForever) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is BasicEditor && panel.canDelete) panel.delete() } } object CopyCommand : AbstractCommand("EIM:Copy", "复制", arrayOf(Key.CtrlLeft, Key.C), Icons.Filled.ContentCopy) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is BasicEditor) panel.copy() } } object CopyToClipboard : AbstractCommand("EIM:CopyToClipboard", "复制到剪辑版", arrayOf(Key.CtrlLeft, Key.ShiftLeft, Key.C)) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is SerializableEditor) CLIPBOARD_MANAGER?.setText(AnnotatedString(panel.copyAsString())) } } object CutCommand : AbstractCommand("EIM:Cut", "剪切", arrayOf(Key.CtrlLeft, Key.X), Icons.Filled.ContentCut) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is BasicEditor) panel.cut() } } object PasteCommand : AbstractCommand("EIM:Paste", "粘贴", arrayOf(Key.CtrlLeft, Key.V), Icons.Filled.ContentPaste) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is BasicEditor) panel.paste() } } object PasteFromClipboard : AbstractCommand("EIM:PasteFromClipboard", "从剪辑版粘贴", arrayOf(Key.CtrlLeft, Key.ShiftLeft, Key.V)) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is SerializableEditor) panel.pasteFromString(CLIPBOARD_MANAGER?.getText()?.text ?: return) } } object SelectAllCommand : AbstractCommand("EIM:Select All", "选择全部", arrayOf(Key.CtrlLeft, Key.A), Icons.Filled.SelectAll) { override fun execute() { - super.execute() val panel = EchoInMirror.windowManager.activePanel if (panel is MultiSelectableEditor) panel.selectAll() } @@ -86,6 +79,13 @@ object RedoCommand : AbstractCommand("EIM:Redo", "重做", arrayOf(Key.CtrlLeft, } } +object DuplicateCommand : AbstractCommand("EIM:Duplicate", "复制", arrayOf(Key.CtrlLeft, Key.D), Icons.Filled.ContentCopy) { + override fun execute() { + val panel = EchoInMirror.windowManager.activePanel + if (panel is BasicEditor) panel.duplicate() + } +} + fun CommandManager.registerAllEditCommands() { registerCommand(DeleteCommand) registerCommand(CopyCommand) @@ -97,4 +97,5 @@ fun CommandManager.registerAllEditCommands() { registerCommand(SaveCommand) registerCommand(UndoCommand) registerCommand(RedoCommand) + registerCommand(DuplicateCommand) } diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/audio/AudioClipEditor.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/audio/AudioClipEditor.kt index 83f389d3..0967edfa 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/audio/AudioClipEditor.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/audio/AudioClipEditor.kt @@ -11,7 +11,7 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -73,7 +73,7 @@ class AudioClipEditor(private val clip: TrackClip) : ClipEditor { var contentWidth by remember { mutableStateOf(0) } val density = LocalDensity.current Box( - Modifier.fillMaxSize().onGloballyPositioned { contentWidth = it.size.width } + Modifier.fillMaxSize().onPlaced { contentWidth = it.size.width } .scrollable(horizontalScrollState, Orientation.Horizontal, reverseDirection = true) .scalableNoteWidth(noteWidth, horizontalScrollState) ) { diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/envelope/EnvelopeClipEditor.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/envelope/EnvelopeClipEditor.kt index 972e4bcb..b565e4f7 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/envelope/EnvelopeClipEditor.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/envelope/EnvelopeClipEditor.kt @@ -11,7 +11,7 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -63,7 +63,7 @@ class EnvelopeClipEditor(private val clip: TrackClip) : ClipEditor clip.track?.clips?.update() } var contentWidth by remember { mutableStateOf(0.dp) } - Box(Modifier.fillMaxSize().onGloballyPositioned { contentWidth = it.size.width.dp } + Box(Modifier.fillMaxSize().onPlaced { contentWidth = it.size.width.dp } .scrollable(horizontalScrollState, Orientation.Horizontal, reverseDirection = true) ) { EchoInMirror.currentPosition.apply { diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EventEditor.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EventEditor.kt index 5936c467..656e5f34 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EventEditor.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/EventEditor.kt @@ -147,6 +147,7 @@ class CCEvent(private val editor: MidiClipEditor, eventId: Int, points: Envelope override fun pasteFromString(value: String) { envEditor.pasteFromString(value) } override fun delete() { envEditor.delete() } override fun selectAll() { envEditor.selectAll() } + override fun duplicate() { envEditor.duplicate() } } private val defaultCCEvents = sortedMapOf( 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 94974b92..c730e1e0 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 @@ -9,10 +9,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastMap import androidx.compose.ui.zIndex import com.eimsound.audioprocessor.oneBarPPQ import com.eimsound.audioprocessor.projectDisplayPPQ @@ -60,7 +61,7 @@ private fun EditorContent(editor: DefaultMidiClipEditor) { clip.duration = it.range clip.track?.clips?.update() } - Box(Modifier.weight(1F).onGloballyPositioned { + Box(Modifier.weight(1F).onPlaced { with(localDensity) { contentWidth = it.size.width.toDp() } }) { Row(Modifier.fillMaxSize().zIndex(-1F)) { @@ -170,7 +171,7 @@ class DefaultMidiClipEditor(override val clip: TrackClip) : MidiClipEd private fun copyAsObject(): List { val startTime = selectedNotes.minOf { it.time } - return selectedNotes.map { it.copy(it.time - startTime) } + return selectedNotes.map { it.copy(time = it.time - startTime) } } override fun copy() { @@ -181,9 +182,9 @@ class DefaultMidiClipEditor(override val clip: TrackClip) : MidiClipEd override fun paste() { if (isEventPanelActive) selectedEvent?.paste() else { - if (copiedNotes?.isEmpty() == true) return + if (copiedNotes.isNullOrEmpty()) return val startTime = EchoInMirror.currentPosition.timeInPPQ.fitInUnitCeil(EchoInMirror.editUnit) - val notes = copiedNotes!!.map { it.copy(time = it.time + startTime) } + val notes = copiedNotes!!.fastMap { it.copy(time = it.time + startTime) } clip.doNoteAmountAction(notes, false) selectedNotes.clear() selectedNotes.addAll(notes) @@ -224,6 +225,16 @@ class DefaultMidiClipEditor(override val clip: TrackClip) : MidiClipEd } } + override fun duplicate() { + if (selectedNotes.isEmpty()) return + val startTime = EchoInMirror.editUnit.fitInUnitCeil(selectedNotes.maxOf { it.time + it.duration }) + val firstTime = selectedNotes.minOf { it.time } + val notes = selectedNotes.map { it.copy(time = it.time - firstTime + startTime) } + clip.doNoteAmountAction(notes, false) + selectedNotes.clear() + selectedNotes.addAll(notes) + } + internal var currentX = 0 private var currentNote = 0 internal inline fun Density.getClickedNotes( diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/NotesEditorCanvas.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/NotesEditorCanvas.kt index 42f03dc5..28eb4afa 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/NotesEditorCanvas.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/impl/clips/midi/editor/NotesEditorCanvas.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.* @@ -105,7 +105,7 @@ internal fun NotesEditorCanvas(editor: DefaultMidiClipEditor) { Box( Modifier.fillMaxSize().clipToBounds().background(MaterialTheme.colorScheme.background) .scrollable(verticalScrollState, Orientation.Vertical, reverseDirection = true) - .onGloballyPositioned { offsetOfRoot = it.positionInRoot() } + .onPlaced { offsetOfRoot = it.positionInRoot() } .pointerInput(coroutineScope, editor) { handleMouseEvent(coroutineScope, editor, floatingLayerProvider) } diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/Editor.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/Editor.kt index 6c7dd03c..2ba3a8cb 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/Editor.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/Editor.kt @@ -56,4 +56,5 @@ object Editor: Panel, MultiSelectableEditor { override fun cut() { editor?.cut() } override fun delete() { editor?.delete() } override fun selectAll() { editor?.selectAll() } + override fun duplicate() { editor?.duplicate() } } diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/FileSystemBrowser.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/FileSystemBrowser.kt index f834e15e..065ced67 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/FileSystemBrowser.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/FileSystemBrowser.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -113,7 +113,7 @@ object FileSystemBrowser : Panel { @Composable private fun Previewer() { val width = remember { intArrayOf(1) } - Surface(Modifier.fillMaxWidth().height(40.dp).onGloballyPositioned { width[0] = it.size.width }, tonalElevation = 3.dp) { + Surface(Modifier.fillMaxWidth().height(40.dp).onPlaced { width[0] = it.size.width }, tonalElevation = 3.dp) { @OptIn(ExperimentalFoundationApi::class) DropdownMenu({ close -> MenuItem({ diff --git a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/playlist/Playlist.kt b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/playlist/Playlist.kt index 6720dc9a..b542db4b 100644 --- a/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/playlist/Playlist.kt +++ b/daw/src/jvmMain/kotlin/com/eimsound/daw/window/panels/playlist/Playlist.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.input.pointer.* -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp @@ -158,6 +158,24 @@ class Playlist : Panel, MultiSelectableEditor { override fun selectAll() { EchoInMirror.bus!!.subTracks.forEach(::selectAllClipsInTrack) } + override fun duplicate() { + if (selectedClips.isEmpty()) return + val new = selectedClips.groupBy { it.track }.flatMap { (_, clips0) -> + val clips = clips0.sorted() + val first = clips.first() + val firstStart = first.time + var betweenTime = clips.getOrNull(1)?.time + betweenTime = if (betweenTime == null) 0 else (betweenTime - (firstStart + first.duration)).coerceAtLeast(0) + var startTime = clips.maxOf { it.time + it.duration } + betweenTime + startTime = startTime.fitInUnitCeil(EchoInMirror.editUnit) + val newClips = clips.map { it.copy(time = it.time + startTime - firstStart) } + newClips.doClipsAmountAction(false) + newClips + } + selectedClips.clear() + selectedClips.addAll(new) + } + private fun selectAllClipsInTrack(track: Track) { selectedClips.addAll(track.clips) track.subTracks.forEach(::selectAllClipsInTrack) @@ -177,7 +195,7 @@ class Playlist : Panel, MultiSelectableEditor { val coroutineScope = rememberCoroutineScope() Box(Modifier.weight(1f).pointerInput(coroutineScope) { handleMouseEvent(this@Playlist, coroutineScope) - }.onGloballyPositioned { with(localDensity) { contentWidth = it.size.width.toDp() } }) { + }.onPlaced { with(localDensity) { contentWidth = it.size.width.toDp() } }) { EchoInMirror.currentPosition.apply { EditorGrid(noteWidth, horizontalScrollState, projectRange, ppq, timeSigDenominator, timeSigNumerator) } diff --git a/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt b/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt index 3b742fec..c87e30b3 100644 --- a/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt +++ b/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt @@ -144,7 +144,7 @@ class AudioThumbnail private constructor( } inline fun query( - widthInPx: Double, startTimeSeconds: Double = 0.0, + widthInPx: Float, startTimeSeconds: Double = 0.0, endTimeSeconds: Double = lengthInSamples / sampleRate.toDouble(), stepInPx: Float = 1F, callback: (x: Float, channel: Int, min: Float, max: Float) -> Unit diff --git a/utils/src/commonMain/kotlin/com/eimsound/daw/utils/SnapshotStateSet.kt b/utils/src/commonMain/kotlin/com/eimsound/daw/utils/SnapshotStateSet.kt index cb1139de..f80f0b2d 100644 --- a/utils/src/commonMain/kotlin/com/eimsound/daw/utils/SnapshotStateSet.kt +++ b/utils/src/commonMain/kotlin/com/eimsound/daw/utils/SnapshotStateSet.kt @@ -42,8 +42,12 @@ class SnapshotStateSet private constructor( override fun add(element: T): Boolean = delegateSnapshotStateMap.put(element, Unit) == null - override fun addAll(elements: Collection): Boolean = - elements.map(::add).any() + override fun addAll(elements: Collection): Boolean { + delegateSnapshotStateMap.putAll(mutableMapOf().apply { + elements.forEach { put(it, Unit) } + }) + return true + } } /**