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 a3c70d2..e692e86 100644 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt +++ b/components/src/commonMain/kotlin/com/eimsound/daw/components/Waveform.kt @@ -2,36 +2,94 @@ package com.eimsound.daw.components +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb import com.eimsound.audioprocessor.PlayPosition import com.eimsound.audioprocessor.convertPPQToSeconds -import com.eimsound.daw.components.utils.NativePainter -import com.eimsound.daw.components.utils.drawVerticesNative import com.eimsound.dsp.data.AudioThumbnail import com.eimsound.dsp.data.EnvelopePointList import org.jetbrains.skia.* import kotlin.math.absoluteValue import kotlin.math.ceil -import kotlin.time.measureTime private const val STEP_IN_PX = 1F private const val HALF_STEP_IN_PX = STEP_IN_PX / 2 private const val WAVEFORM_DAMPING = 0.94F +private const val REDUNDANT = 50 -private var buffer = FloatArray(0) -private fun checkBufferSize(size: Int) { if (buffer.size < size) buffer = FloatArray(size) } +private fun drawZero(x: Float, y: Float, buffer: FloatArray, offset: Int) { + var i = offset + buffer[i++] = x + buffer[i++] = y + HALF_STEP_IN_PX + buffer[i++] = x + HALF_STEP_IN_PX + buffer[i++] = y - HALF_STEP_IN_PX + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y + HALF_STEP_IN_PX + + buffer[i++] = x + buffer[i++] = y + HALF_STEP_IN_PX + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y + HALF_STEP_IN_PX + buffer[i++] = x + HALF_STEP_IN_PX + buffer[i] = y - HALF_STEP_IN_PX +} + +private fun drawMinAndMax(x: Float, y: Float, min: Float, max: Float, buffer: FloatArray, offset: Int) { + if (min + max < STEP_IN_PX) { + drawZero(x, y, buffer, offset) + return + } + var i = offset + buffer[i++] = x + buffer[i++] = y - max + buffer[i++] = x + buffer[i++] = y + min + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y + min + + buffer[i++] = x + buffer[i++] = y - max + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y - max + buffer[i++] = x + STEP_IN_PX + buffer[i] = y + min +} + +private fun drawDefault(x: Float, y: Float, v: Float, buffer: FloatArray, offset: Int) { + if (v.absoluteValue < STEP_IN_PX) { + drawZero(x, y, buffer, offset) + return + } + var i = offset + buffer[i++] = x + buffer[i++] = y - v + buffer[i++] = x + buffer[i++] = y + HALF_STEP_IN_PX + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y + HALF_STEP_IN_PX + + buffer[i++] = x + buffer[i++] = y - v + buffer[i++] = x + STEP_IN_PX + buffer[i++] = y + HALF_STEP_IN_PX + buffer[i++] = x + STEP_IN_PX + buffer[i] = y - v +} private fun Canvas.drawMinAndMax( thumbnail: AudioThumbnail, startSeconds: Float, endSeconds: Float, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, paint: Paint, width: Float ) { - checkBufferSize(ceil(width / STEP_IN_PX + 100).toInt() * 12) + val buffer = FloatArray(ceil(width / STEP_IN_PX + REDUNDANT).toInt() * 12) repeat(thumbnail.channels) { ch -> var min = 0F var max = 0F @@ -44,30 +102,10 @@ private fun Canvas.drawMinAndMax( else min *= WAVEFORM_DAMPING if (curMax > max) max = curMax else max *= WAVEFORM_DAMPING - if (min + max < 0.3F) { - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + HALF_STEP_IN_PX - buffer[i++] = y - HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - return@query - } - buffer[i++] = x - buffer[i++] = y - max - buffer[i++] = x - buffer[i++] = y + min - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + min - - buffer[i++] = x - buffer[i++] = y - max - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y - max - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + min + drawMinAndMax(x, y, min, max, buffer, i) + i += 12 } - drawVerticesNative(buffer, paint, i / 2) + drawVertices(VertexMode.TRIANGLES, buffer, null, null, null, BlendMode.SRC, paint) } } private fun Canvas.drawDefault( @@ -75,36 +113,16 @@ private fun Canvas.drawDefault( channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, paint: Paint, width: Float ) { - checkBufferSize(ceil(width / STEP_IN_PX + 100).toInt() * 12) + val buffer = FloatArray(ceil(width / STEP_IN_PX + REDUNDANT).toInt() * 12) repeat(thumbnail.channels) { ch -> var i = 0 thumbnail.query(ch, width, startSeconds, endSeconds, STEP_IN_PX) { x, 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) { - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + HALF_STEP_IN_PX - buffer[i++] = y - HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - return@query - } - buffer[i++] = x - buffer[i++] = y - v - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - - buffer[i++] = x - buffer[i++] = y - v - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y - v + drawDefault(x, y, v, buffer, i) + i += 12 } - drawVerticesNative(buffer, paint, i / 2) + drawVertices(VertexMode.TRIANGLES, buffer, null, null, null, BlendMode.SRC, paint) } } @@ -120,22 +138,22 @@ fun Waveform( thumbnail.read() val paint = remember { Paint() } remember(color) { paint.colorFilter = ColorFilter.makeBlend(color.toArgb(), BlendMode.SRC_IN) } - NativePainter(modifier.fillMaxSize()) { size -> + Spacer(modifier.fillMaxSize().drawBehind { val channelHeight = (size.height / thumbnail.channels) - 2 val halfChannelHeight = channelHeight / 2 val drawHalfChannelHeight = halfChannelHeight - 1 if (isDrawMinAndMax) { - drawMinAndMax( + drawContext.canvas.nativeCanvas.drawMinAndMax( thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, drawHalfChannelHeight, paint, size.width ) } else { - drawDefault( + drawContext.canvas.nativeCanvas.drawDefault( thumbnail, startSeconds, endSeconds, channelHeight, halfChannelHeight, drawHalfChannelHeight, paint, size.width ) } - } + }.graphicsLayer { }) } private fun Canvas.drawMinAndMax( @@ -143,7 +161,7 @@ private fun Canvas.drawMinAndMax( endSeconds: Float, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, stepPPQ: Float, volumeEnvelope: EnvelopePointList?, width: Float, paint: Paint ) { - checkBufferSize(ceil(width / STEP_IN_PX + 100).toInt() * 12) + val buffer = FloatArray(ceil(width / STEP_IN_PX + REDUNDANT).toInt() * 12) repeat(thumbnail.channels) { ch -> var min = 0F var max = 0F @@ -157,30 +175,10 @@ private fun Canvas.drawMinAndMax( else min *= WAVEFORM_DAMPING if (curMax > max) max = curMax else max *= WAVEFORM_DAMPING - if (min + max < 0.3F) { - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + HALF_STEP_IN_PX - buffer[i++] = y - HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - return@query - } - buffer[i++] = x - buffer[i++] = y - max - buffer[i++] = x - buffer[i++] = y + min - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + min - - buffer[i++] = x - buffer[i++] = y - max - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y - max - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + min + drawMinAndMax(x, y, min, max, buffer, i) + i += 12 } - drawVerticesNative(buffer, paint, i / 2) + drawVertices(VertexMode.TRIANGLES, buffer, null, null, null, BlendMode.SRC, paint) } } private fun Canvas.drawDefault( @@ -188,7 +186,7 @@ private fun Canvas.drawDefault( endSeconds: Float, channelHeight: Float, halfChannelHeight: Float, drawHalfChannelHeight: Float, stepPPQ: Float, volumeEnvelope: EnvelopePointList?, width: Float, paint: Paint ) { - checkBufferSize(ceil(width / STEP_IN_PX + 100).toInt() * 12) + val buffer = FloatArray(ceil(width / STEP_IN_PX + REDUNDANT).toInt() * 12) repeat(thumbnail.channels) { ch -> var i = 0 thumbnail.query(ch, width, startSeconds, endSeconds, STEP_IN_PX) { x, min, max -> @@ -196,30 +194,10 @@ private fun Canvas.drawDefault( (volumeEnvelope?.getValue((startPPQ + x * stepPPQ).toInt(), 1F) ?: 1F)) .coerceIn(-1F, 1F) * drawHalfChannelHeight val y = 2 + channelHeight * ch + halfChannelHeight - if (v.absoluteValue < 0.3F) { - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + HALF_STEP_IN_PX - buffer[i++] = y - HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - return@query - } - buffer[i++] = x - buffer[i++] = y - v - buffer[i++] = x - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - - buffer[i++] = x - buffer[i++] = y - v - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y + HALF_STEP_IN_PX - buffer[i++] = x + STEP_IN_PX - buffer[i++] = y - v + drawDefault(x, y, v, buffer, i) + i += 12 } - drawVerticesNative(buffer, paint, i / 2) + drawVertices(VertexMode.TRIANGLES, buffer, null, null, null, BlendMode.SRC, paint) } } @@ -236,7 +214,7 @@ fun Waveform( thumbnail.read() val paint = remember { Paint() } remember(color) { paint.colorFilter = ColorFilter.makeBlend(color.toArgb(), BlendMode.SRC_IN) } - NativePainter(modifier.fillMaxSize()) { size -> + Spacer(modifier.fillMaxSize().drawBehind { val channelHeight = (size.height / thumbnail.channels) - 2F val halfChannelHeight = channelHeight / 2 val drawHalfChannelHeight = halfChannelHeight - 1 @@ -244,18 +222,16 @@ fun Waveform( val factor = (thumbnail.sampleRate / position.sampleRate) * timeScale val startSeconds = (position.convertPPQToSeconds(startPPQ) / factor).toFloat() val endSeconds = (position.convertPPQToSeconds(startPPQ + widthPPQ) / factor).toFloat() - println(measureTime { - if (isDrawMinAndMax) { - drawMinAndMax( - thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, stepPPQ, volumeEnvelope, size.width, paint - ) - } else { - drawDefault( - thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, - drawHalfChannelHeight, stepPPQ, volumeEnvelope, size.width, paint - ) - } - }) - } + if (isDrawMinAndMax) { + drawContext.canvas.nativeCanvas.drawMinAndMax( + thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, stepPPQ, volumeEnvelope, size.width, paint + ) + } else { + drawContext.canvas.nativeCanvas.drawDefault( + thumbnail, startPPQ, startSeconds, endSeconds, channelHeight, halfChannelHeight, + drawHalfChannelHeight, stepPPQ, volumeEnvelope, size.width, paint + ) + } + }.graphicsLayer { }) } diff --git a/components/src/commonMain/kotlin/com/eimsound/daw/components/utils/NativeCall.kt b/components/src/commonMain/kotlin/com/eimsound/daw/components/utils/NativeCall.kt deleted file mode 100644 index bebcfbd..0000000 --- a/components/src/commonMain/kotlin/com/eimsound/daw/components/utils/NativeCall.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.eimsound.daw.components.utils - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposeWindow -import androidx.compose.ui.awt.* -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.* -import org.jetbrains.skia.* -import org.jetbrains.skia.BlendMode -import org.jetbrains.skia.Canvas -import org.jetbrains.skia.Paint -import org.jetbrains.skia.VertexMode -import org.jetbrains.skiko.context.* -import org.jetbrains.skia.impl.NativePointer -import java.awt.Window -import java.lang.invoke.MethodHandle -import java.lang.invoke.MethodHandles -import java.lang.invoke.MethodType -import java.lang.reflect.Field -import java.util.WeakHashMap - -private val nDrawRect: MethodHandle? = try { - MethodHandles.lookup().unreflect(Class.forName("org.jetbrains.skia.CanvasKt") - .getDeclaredMethod("_nDrawRect", NativePointer::class.java, Float::class.java, - Float::class.java, Float::class.java, Float::class.java, NativePointer::class.java) - .apply { isAccessible = true }) -} catch (e: Exception) { - e.printStackTrace() - null -} - -private val nDrawVertices: MethodHandle? = try { - MethodHandles.lookup().unreflect(Class.forName("org.jetbrains.skia.CanvasKt") - .getDeclaredMethod("_nDrawVertices", NativePointer::class.java, Int::class.java, - Int::class.java, Any::class.java, Any::class.java, Any::class.java, Int::class.java, - Any::class.java, Int::class.java, NativePointer::class.java) - .apply { isAccessible = true }) -} catch (e: Exception) { - e.printStackTrace() - null -} - -@Suppress("unused") -fun Canvas.drawRectNative(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) { - val f = nDrawRect - if (f == null) drawRect(Rect(left, top, right, bottom), paint) - else { - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - f.invokeExact(_ptr, left, top, right, bottom, paint._ptr) - } -} - -fun Canvas.drawVerticesNative( - positions: FloatArray, paint: Paint, pointCount: Int = positions.size / 2 -) { - val f = nDrawVertices - if (f == null) drawVertices(VertexMode.TRIANGLES, positions.let { - if (it.size == pointCount * 2) it - else it.copyOf(pointCount * 2) - }, null, null, null, BlendMode.SRC, paint) - else { - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - f.invokeExact(_ptr, 0, pointCount, positions as Any, null as Any?, null as Any?, 0, null as Any?, 1, paint._ptr) - } -} - -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -private object SurfaceUtil { - private lateinit var ComposeWindow_delegate: Field - private lateinit var ComposeWindowDelegate_bridge: Field - private val ContextHandlerSurface: MethodHandle - private var redrawerCache: MutableMap? = null - - init { - var contextHandlerSurface: MethodHandle? = null - try { - ComposeWindow_delegate = ComposeWindow::class.java.getDeclaredField("delegate").apply { isAccessible = true } - ComposeWindowDelegate_bridge = ComposeWindowDelegate::class.java.getDeclaredField("_bridge").apply { isAccessible = true } - contextHandlerSurface = MethodHandles.lookup().unreflectGetter(ContextHandler::class.java.getDeclaredField("surface").apply { isAccessible = true }) - redrawerCache = WeakHashMap() - } catch (e: Exception) { - e.printStackTrace() - } - ContextHandlerSurface = contextHandlerSurface ?: MethodHandles.empty(MethodType.methodType(Surface::class.java)) - } - - fun getSurface(window: Window): Surface? { - val cache = redrawerCache ?: return null - var ctx = cache[window] - if (null == ctx) try { - val redrawer = (ComposeWindowDelegate_bridge.get(ComposeWindow_delegate.get(window)) as WindowComposeBridge).component.redrawer!! - ctx = redrawer::class.java.getDeclaredField("contextHandler").apply { isAccessible = true }.get(redrawer) as ContextHandler - cache[window] = ctx - } catch (e: Exception) { - e.printStackTrace() - } - if (null == ctx) return null - val surface = ContextHandlerSurface.invokeExact(ctx) as Surface - return if (surface.isClosed) null else surface - } -} - -@Composable -fun NativePainter(modifier: Modifier, block: Canvas.(size: Size) -> Unit) { - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - val window = androidx.compose.ui.window.LocalWindow.current - Spacer(modifier.drawWithCache { - var image: Image? = null - if (window != null && size.width != 0F && size.height != 0F) { - SurfaceUtil.getSurface(window)?.makeSurface(size.width.toInt(), size.height.toInt())?.apply { - canvas.block(size) - image = makeImageSnapshot() - close() - } - } - onDrawBehind { - val img = image - if (img == null || img.isClosed) { - if (size.width != 0F && size.height != 0F) drawContext.canvas.nativeCanvas.block(size) - } else { - drawContext.canvas.nativeCanvas.drawImage(img, 0F, 0F) - img.close() - } - } - }) -} 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 866af96..7a35dec 100644 --- a/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt +++ b/dsp/src/commonMain/kotlin/com/eimsound/dsp/data/AudioThumbnail.kt @@ -37,6 +37,10 @@ class AudioThumbnail private constructor( private val maxTree = Array(channels) { ByteArray(this.size * 4 + 1) } private val tempArray = FloatArray(channels * 2) private var modification by mutableStateOf(0) + @Deprecated("Use query(x, y) instead") + var tmpMax = 0F + @Deprecated("Use query(x, y) instead") + var tmpMin = 0F constructor( channels: Int, lengthInSamples: Long, sampleRate: Float, samplesPerThumbSample: Int = DEFAULT_SAMPLES_PRE_THUMB_SAMPLE @@ -108,8 +112,8 @@ class AudioThumbnail private constructor( fun read() { modification } - @Suppress("DuplicatedCode") - fun query(channel:Int, x: Int, y: Int): FloatArray { + @Suppress("DuplicatedCode", "DEPRECATION") + fun query(channel: Int, x: Int, y: Int) { @Suppress("NAME_SHADOWING") var y = y var min: Byte = 127 var max: Byte = -128 @@ -123,9 +127,8 @@ class AudioThumbnail private constructor( y -= y.takeLowestOneBit() } } - tempArray[0] = min / 127F - tempArray[1] = max / 127F - return tempArray + tmpMin = min / 127F + tmpMax = max / 127F } @Suppress("DuplicatedCode") @@ -187,7 +190,7 @@ class AudioThumbnail private constructor( } } - @Suppress("DuplicatedCode") + @Suppress("DuplicatedCode", "DEPRECATION") inline fun query( channel: Int, widthInPx: Float, startTimeSeconds: Float = 0F, endTimeSeconds: Float = lengthInSamples / sampleRate, @@ -201,8 +204,9 @@ class AudioThumbnail private constructor( var i = 0 while (x <= end) { val y = x + step - val minMax = query(channel, x.roundToInt(), y.roundToInt()) - callback(i * stepInPx, minMax[0], minMax[1]) + if (y > end) return + query(channel, x.roundToInt(), y.roundToInt()) + callback(i * stepInPx, tmpMin, tmpMax) i++ x = y }