Skip to content

Commit

Permalink
- More averaging for tap in
Browse files Browse the repository at this point in the history
- Extended circle animations
  • Loading branch information
thetwom committed Feb 15, 2023
1 parent acac112 commit 37167aa
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class MetronomeFragment : Fragment() {
private var speedLimiter: SpeedLimiter? = null

private var tapInEvaluator = TapInEvaluator(
5,
50,
Utilities.bpm2millis(InitialValues.maximumBpm),
Utilities.bpm2millis(InitialValues.minimumBpm)
)
Expand Down Expand Up @@ -351,15 +351,15 @@ class MetronomeFragment : Fragment() {
"minimumspeed" -> {
sharedPreferences.getString("minimumspeed", InitialValues.minimumBpm.toString())?.toFloatOrNull()?.let {
tapInEvaluator = TapInEvaluator(
tapInEvaluator.numHistoryValues,
tapInEvaluator.maxNumHistoryValues,
tapInEvaluator.minimumAllowedDtInMillis,
Utilities.bpm2millis(it))
}
}
"maximumspeed" -> {
sharedPreferences.getString("maximumspeed", InitialValues.maximumBpm.toString())?.toFloatOrNull()?.let {
tapInEvaluator = TapInEvaluator(
tapInEvaluator.numHistoryValues,
tapInEvaluator.maxNumHistoryValues,
Utilities.bpm2millis(it),
tapInEvaluator.maximumAllowedDtInMillis,
)
Expand All @@ -378,16 +378,20 @@ class MetronomeFragment : Fragment() {
vibratingNote?.strength = sharedPreferences.getInt("vibratestrength", 50)
}
"tickvisualization" -> {
when (sharedPreferences.getString("tickvisualization", "leftright")) {
"leftright" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.LeftRight
"bounce" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.Bounce
"fade" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.Fade
val type = when (sharedPreferences.getString("tickvisualization", "leftright")) {
"leftright" -> TickVisualizerSync.VisualizationType.LeftRight
"bounce" -> TickVisualizerSync.VisualizationType.Bounce
"fade" -> TickVisualizerSync.VisualizationType.Fade
else -> TickVisualizerSync.VisualizationType.Fade
}
tickVisualizer?.visualizationType = type
speedPanel?.visualizationType = type
}
"tickingcircle" -> {
tickingCircle = sharedPreferences.getBoolean("tickingcircle", false)
if (!tickingCircle)
speedPanel?.stopTicking()
tickVisualizer?.visibility = if (tickingCircle) View.INVISIBLE else View.VISIBLE
}
}
}
Expand All @@ -402,7 +406,7 @@ class MetronomeFragment : Fragment() {
val minimumSpeed = sharedPreferences.getString("minimumspeed", InitialValues.minimumBpm.toString())?.toFloatOrNull()
val maximumSpeed = sharedPreferences.getString("maximumspeed", InitialValues.maximumBpm.toString())?.toFloatOrNull()
if (minimumSpeed != null && maximumSpeed != null)
tapInEvaluator = TapInEvaluator(tapInEvaluator.numHistoryValues, Utilities.bpm2millis(maximumSpeed), Utilities.bpm2millis(minimumSpeed))
tapInEvaluator = TapInEvaluator(tapInEvaluator.maxNumHistoryValues, Utilities.bpm2millis(maximumSpeed), Utilities.bpm2millis(minimumSpeed))

val bpmSensitivity = sharedPreferences.getInt("speedsensitivity", (Utilities.sensitivity2percentage(
InitialValues.bpmPerCm
Expand All @@ -412,12 +416,17 @@ class MetronomeFragment : Fragment() {
vibrate = sharedPreferences.getBoolean("vibrate", false)
vibratingNote?.strength = sharedPreferences.getInt("vibratestrength", 50)

when (sharedPreferences.getString("tickvisualization", "leftright")) {
"leftright" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.LeftRight
"bounce" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.Bounce
"fade" -> tickVisualizer?.visualizationType = TickVisualizerSync.VisualizationType.Fade
val type = when (sharedPreferences.getString("tickvisualization", "leftright")) {
"leftright" -> TickVisualizerSync.VisualizationType.LeftRight
"bounce" -> TickVisualizerSync.VisualizationType.Bounce
"fade" -> TickVisualizerSync.VisualizationType.Fade
else -> TickVisualizerSync.VisualizationType.Fade
}
tickVisualizer?.visualizationType = type
speedPanel?.visualizationType = type

tickingCircle = sharedPreferences.getBoolean("tickingcircle", false)
tickVisualizer?.visibility = if (tickingCircle) View.INVISIBLE else View.VISIBLE

// register all observers
viewModel.bpm.observe(viewLifecycleOwner) { bpm ->
Expand Down
110 changes: 92 additions & 18 deletions app/src/main/java/de/moekadu/metronome/misc/TapInEvaluator.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
package de.moekadu.metronome.misc

import android.util.Log
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToLong

class TapInEvaluator(val numHistoryValues: Int, val minimumAllowedDtInMillis: Long, val maximumAllowedDtInMillis: Long) {
class TapInEvaluator(val maxNumHistoryValues: Int, val minimumAllowedDtInMillis: Long, val maximumAllowedDtInMillis: Long) {
/** List of tap-in values obtained withe System.nanoTime(), the first one is the oldest, the
* last one the newest. The actual number of values, which have valid values is not the size
* of this array but saved by the property numValues.
*/
private val values = LongArray(numHistoryValues)
private val values = LongArray(maxNumHistoryValues)

/** Number of valid values within the values-array. */
private var numValues = 0

/** Max relative deviation between two succeeding values, to include them in the averaging. */
private val maxDeviation = 0.5f

private val maxRelativeRatioToBeSimilar = 0.3f

/** Additional delay in millis for predicted next tap for more natural feel. */
private val predictionDelayInMillis = 0L

Expand Down Expand Up @@ -48,23 +51,94 @@ class TapInEvaluator(val numHistoryValues: Int, val minimumAllowedDtInMillis: Lo
++numValues
}

// "delete" all values if the last two taps are too far apart
// increase the maximumAllowedDt a bit (use 1500_000L instead of 1000_000L as millis to nanos
// conversion), such that we can safely reach this value.
// (otherwise it would be hard to reach the maximum value since either we are below or
// or the values will be killed due to exceeding it.)
if (numValues >= 2 && values[numValues - 1] - values[numValues - 2] > 1500_000L * maximumAllowedDtInMillis) {
values[0] = timeInNanos
numValues = 1
// make sure, that if numValues > 1 we never have dtNanos == NOT_AVAILABLE
// to avoid such checks lateron
if (numValues == 2) {
val dt = values[1] - values[0]
if (dt > maximumAllowedDtInMillis * 1000_000L) {
values[0] = values[1]
numValues = 1
dtNanos = NOT_AVAILABLE
} else {
dtNanos = dt
}
}

val iBegin = determineFirstValidValue()
val iEnd = numValues
val numDts = max(0, iEnd - iBegin - 1)
dtNanos = if (numDts == 0)
NOT_AVAILABLE
else
(values[iEnd - 1] - values[iBegin]) / numDts
// delete all values but the last one, if the last dt is very big
if (numValues > 1) {
val lastDt = values[numValues-1] - values[numValues-2]
if (lastDt > 2.5f * dtNanos || lastDt > maximumAllowedDtInMillis * 1000_000L) {
dtNanos = NOT_AVAILABLE
values[0] = values[numValues - 1]
numValues = 1
}
}

if (numValues > 2) {
val lastDt = values[numValues-1] - values[numValues-2]
val dtBeforeLastDt = values[numValues-2] - values[numValues-3]
val dtDifference = (lastDt - dtBeforeLastDt).absoluteValue
val dtAverage = (lastDt + dtBeforeLastDt) / 2

// last to dts are similar to each other, but very different to the dt, which
// is currently used -> reset and keep just the three values
if (dtDifference.toDouble() / dtAverage < maxRelativeRatioToBeSimilar &&
((dtAverage - dtNanos).absoluteValue / dtNanos.toDouble() > maxRelativeRatioToBeSimilar)) {
values.copyInto(values, 0, numValues - 3, numValues)
numValues = 3
dtNanos = dtAverage
}
// the last to dts are not similar and both different the current dt -> keep only the last two values
else if ((lastDt - dtNanos).absoluteValue / dtNanos.toDouble() > maxRelativeRatioToBeSimilar &&
(dtBeforeLastDt - dtNanos).absoluteValue / dtNanos.toDouble() > maxRelativeRatioToBeSimilar) {
values.copyInto(values, 0, numValues - 2, numValues)
numValues = 2
dtNanos = lastDt
}
// we have now treated:
// - last two dts similar but different to current dt
// - last two dts different and also different to current dt
// -> remaining case is that one of the last two dts is similar to current dt, which
// is fine for us -> no else needed.
}

if (dtNanos == NOT_AVAILABLE) {
predictedNextTapNanos = NOT_AVAILABLE
return
}
// Log.v("Metronome", "TapInEvaluator.tap: numValues = $numValues")
var indexLower = 0
var indexUpper = numValues - 1

var weightSum = 0L
var dtSum = 0L
// now compute a nice dt ...
while (indexLower < indexUpper) {
val numTicks = ((values[indexUpper] - values[indexLower]) / dtNanos.toDouble()).roundToLong()
weightSum += numTicks
dtSum += (values[indexUpper] - values[indexLower])
indexLower += 1
indexUpper -= 1
}
dtNanos = dtSum / weightSum

// // "delete" all values if the last two taps are too far apart
// // increase the maximumAllowedDt a bit (use 1500_000L instead of 1000_000L as millis to nanos
// // conversion), such that we can safely reach this value.
// // (otherwise it would be hard to reach the maximum value since either we are below or
// // or the values will be killed due to exceeding it.)
// if (numValues >= 2 && values[numValues - 1] - values[numValues - 2] > 1500_000L * maximumAllowedDtInMillis) {
// values[0] = timeInNanos
// numValues = 1
// }
//
// val iBegin = determineFirstValidValue()
// val iEnd = numValues
// val numDts = max(0, iEnd - iBegin - 1)
// dtNanos = if (numDts == 0)
// NOT_AVAILABLE
// else
// (values[iEnd - 1] - values[iBegin]) / numDts

predictedNextTapNanos = if (dtNanos == NOT_AVAILABLE) {
NOT_AVAILABLE
Expand All @@ -85,7 +159,7 @@ class TapInEvaluator(val numHistoryValues: Int, val minimumAllowedDtInMillis: Lo
// }
// // values[numValues - 1] + dt + predictionDelayInMillis
// sum / weightSum + 1000_000L * predictionDelayInMillis
values[iEnd - 1] + dtNanos
values[numValues - 1] + dtNanos
}
}

Expand Down
81 changes: 78 additions & 3 deletions app/src/main/java/de/moekadu/metronome/views/SpeedPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class SpeedPanel(context : Context, attrs : AttributeSet?, defStyleAttr: Int)
color = Color.RED
}

var visualizationType = TickVisualizerSync.VisualizationType.Fade

constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs,
R.attr.controlPanelStyle
)
Expand Down Expand Up @@ -161,8 +163,13 @@ class SpeedPanel(context : Context, attrs : AttributeSet?, defStyleAttr: Int)
canvas.drawPath(pathOuterCircle, circlePaint)
pathOuterCircle.rewind()

if (currentNoteStartNanos >= 0L)
drawTickVisualization(canvas)
if (currentNoteStartNanos >= 0L) {
when (visualizationType) {
TickVisualizerSync.VisualizationType.Fade -> drawTickVisualizationFade(canvas)
TickVisualizerSync.VisualizationType.Bounce -> drawTickVisualizationBounce(canvas)
TickVisualizerSync.VisualizationType.LeftRight -> drawTickVisualizationLeftRight(canvas)
}
}

if(changingSpeed) {
circlePaint.color = highlightColor
Expand Down Expand Up @@ -360,7 +367,7 @@ class SpeedPanel(context : Context, attrs : AttributeSet?, defStyleAttr: Int)
invalidate()
}

private fun drawTickVisualization(canvas: Canvas) {
private fun drawTickVisualizationFade(canvas: Canvas) {
val amplitude = currentVolume
val fadeDurationNanos = min(currentNoteEndNanos - currentNoteStartNanos, 150 * 1000_000L)
val positionSinceStartNanos = System.nanoTime() - currentNoteStartNanos
Expand All @@ -371,4 +378,72 @@ class SpeedPanel(context : Context, attrs : AttributeSet?, defStyleAttr: Int)

canvas.drawCircle(centerX.toFloat(), centerY.toFloat(), radius.toFloat(), tickPaint)
}

private fun drawTickVisualizationBounce(canvas: Canvas) {
val sweepMin = 35f
val sweepMax = 110f
val durationRef = Utilities.bpm2nanos(70f)
val sizeFraction = min(currentNoteEndNanos - currentNoteStartNanos, durationRef) / durationRef.toFloat()
val amp = 140 * sizeFraction
val sweep = sweepMin * sizeFraction + sweepMax * (1-sizeFraction)
val alpha = (255 * max(0.3f, currentVolume)).toInt()

// Log.v("Metronome", "SpeedPanel.drawTickVisualizationBounce: amp=$amp, durationRef=$durationRef")
val positionSinceStartNanos = System.nanoTime() - currentNoteStartNanos
val fraction = positionSinceStartNanos / (currentNoteEndNanos - currentNoteStartNanos).toFloat()

val shift = amp * sin(PI.toFloat() * fraction)
//Log.v("Metronome", "TickVisualizerSync.onDraw: amp=$amp, ampMax=$ampMax, blockWidth = $blockWidth, shift=$shift")
if (currentNoteCount % 2L == 0L) {
tickPaint.alpha = 120
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
90f, sweep, true, tickPaint
)

tickPaint.alpha = alpha
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
90f-shift-sweep, sweep, true, tickPaint
)
} else {
tickPaint.alpha = 120
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
90f-sweep, sweep, true, tickPaint
)

tickPaint.alpha = alpha
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
90f+shift, sweep, true, tickPaint
)
}
}

private fun drawTickVisualizationLeftRight(canvas: Canvas) {
val sweep = 180f
val sweepHalf = 0.5f * sweep
val alpha = (255 * max(0.1f, currentVolume)).toInt()
//Log.v("Metronome", "TickVisualizerSync.onDraw: amp=$amp, ampMax=$ampMax, blockWidth = $blockWidth, shift=$shift")
if (currentNoteCount % 2L == 0L) {
tickPaint.alpha = alpha
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
180f-sweepHalf, sweep, true, tickPaint
)
} else {
tickPaint.alpha = alpha
canvas.drawArc(
centerX-radius.toFloat(), centerY-radius.toFloat(),
centerX+radius.toFloat(), centerY+radius.toFloat(),
-sweepHalf, sweep, true, tickPaint
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.util.Log
import android.view.View
import de.moekadu.metronome.*
import de.moekadu.metronome.metronomeproperties.*
Expand Down Expand Up @@ -169,12 +168,16 @@ class TickVisualizerSync(context : Context, attrs : AttributeSet?, defStyleAttr:
canvas?.drawRect(0.5f * width, 0f, width.toFloat(), height.toFloat(), paint)
}
VisualizationType.Fade -> {
paint.alpha = (255 * (1 - fraction)).toInt()
val fadeDurationNanos = min(currentTickEndTimeNanos - currentTickStartTimeNanos, 150 * 1000_000L)
val positionSinceStartNanos = System.nanoTime() - currentTickStartTimeNanos
val reducedFraction = (positionSinceStartNanos.coerceIn(0L, fadeDurationNanos).toFloat()
/ fadeDurationNanos)
paint.alpha = (255 * (1 - reducedFraction)).toInt()
canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
VisualizationType.Bounce -> {
val duration250Nanos = Utilities.bpm2nanos(200f)
var amp = Utilities.dp2px(20f) * max(currentTickEndTimeNanos - currentTickStartTimeNanos, duration250Nanos) / duration250Nanos
val durationRef = Utilities.bpm2nanos(200f)
var amp = Utilities.dp2px(20f) * max(currentTickEndTimeNanos - currentTickStartTimeNanos, durationRef) / durationRef
// Log.v("Metronome", "TickVisualizerSync.onDraw: amp=$amp, delta=${currentTickEndTimeNanos - currentTickStartTimeNanos}, duration250Nanos=$duration250Nanos")
val ampMax = 0.5f * width * (1 - 0.2f)
amp = min(amp, ampMax)
Expand Down

0 comments on commit 37167aa

Please sign in to comment.