Skip to content

Commit

Permalink
Step sequencer fixes and improvements
Browse files Browse the repository at this point in the history
* Updated color scheme for alternarting note rows: now it's the note itself that gets the color instead of the background, looks much better
* Fixed regression in quick clip selector activation, could break switching between STEP mode and clip matrix
* Fixed regression in pattern page follow while holding steps, condition was flipped
* Fixed quick clip selector colors, was showing empty scene clips as white
* Fixed SHIFT button forgetting its normal bindings in step mode

Other fixes

* Fixed SONG button indicator for superscene mode, was not lighting up
* REC+GROUP to arm multiple tracks won't trigger transport record unless you mean it (with a single short press of REC)
  • Loading branch information
unthingable committed Feb 10, 2023
1 parent 775413a commit 81f4488
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 70 deletions.
1 change: 1 addition & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rewrite.scala3.removeOptionalBraces = yes
# align.tokens = [off]
danglingParentheses.preset = true
indentOperator.preset = spray
newlines.afterInfix = many
maxColumn = 120
rewrite.rules = [RedundantBraces, RedundantParens, SortModifiers]
# rewrite.imports.sort = scalastyle
Expand Down
17 changes: 16 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Chorded buttons are sensitive to order. For example, **SHIFT+CONTROL** is not th
## Global

* **KNOB turn**: Move arranger playhead, jog through the project timeline. Hold **SHIFT** to adjust in finer increments (e.g. in TEMPO mode).
* **SONG**: **SuperScene** mode (see below) or "home" (return to default Clip Launcher view)
* **SONG**: **SuperScene** mode (see below) or "home" (return to default Clip Launcher view if not already there)
* **STEP**: Step Sequencer
* **CLEAR**: Use in combination with other buttons to delete a scene (scene buttons), clip (a pad in session mode) or track (group buttons).
* **DUPLICATE**: Combine with a scene pad (duplicate scene) or a track button (duplicate track). To copy clips in session mode keep the Duplicate button pressed; choose the source clip (it must be a clip with content, you can still select a different clip as the source); select the destination clip (this must be an empty clip, which can also be on a different track); release the Duplicate button.
Expand Down Expand Up @@ -538,6 +538,21 @@ After changing preferences it may be necessary to reinitialize the extension (tu

# Changelog

## 8.0b16

Step sequencer fixes and improvements

* Updated color scheme for alternarting note rows: now it's the note itself that gets the color instead of the background, looks much better
* Fixed regression in quick clip selector activation, could break switching between STEP mode and clip matrix
* Fixed regression in pattern page follow while holding steps, condition was flipped
* Fixed quick clip selector colors, was showing empty scene clips as white
* Fixed SHIFT button forgetting its normal bindings in step mode

Other fixes

* Fixed SONG button indicator for superscene mode, was not lighting up
* REC+GROUP to arm multiple tracks won't trigger transport record unless you mean it (with a single short press of REC)

## 8.0b15

* Note expressions editor
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<artifactId>monsterjam</artifactId>
<packaging>jar</packaging>
<name>MonsterJam</name>
<version>8.0-b15</version>
<version>8.0-b16</version>

<repositories>
<repository>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class MonsterJamExtensionDefinition() extends ControllerExtensionDefinition:

override def getAuthor = "unthingable"

override def getVersion = "8.0-b15"
override def getVersion = "8.0-b16"

override def getId: UUID = MonsterJamExtensionDefinition.DRIVER_ID

Expand Down
13 changes: 9 additions & 4 deletions src/main/scala/com/github/unthingable/Util.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.time.Instant
import javax.swing.Timer
import scala.annotation.targetName
Expand All @@ -44,11 +45,9 @@ transparent trait Util:
case class Timed[A](value: A, instant: Instant)

extension [A <: ObjectProxy](bank: Bank[A])
def view: IndexedSeqView[A] =
(0 until bank.itemCount().get()).view.map(bank.getItemAt)
def view: IndexedSeqView[A] = (0 until bank.itemCount().get()).view.map(bank.getItemAt)

def fullView: IndexedSeqView[A] =
(0 until bank.getCapacityOfBank()).view.map(bank.getItemAt)
def fullView: IndexedSeqView[A] = (0 until bank.getCapacityOfBank()).view.map(bank.getItemAt)

object Util extends Util:
val EIGHT: Vector[Int] = (0 to 7).toVector
Expand Down Expand Up @@ -148,4 +147,10 @@ object Util extends Util:
timers.update(key, timer)
case Some(timer) => timer.restart()

inline def timed[A](msg: String)(f: => A): A =
val now = Instant.now()
val result = f
val delta = Duration.between(now, Instant.now()).toMillis()
println(s"$msg: $delta ms")
result
end Util
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ object Graph:
case x: ListeningLayer => x.loadBindings
case _ => Seq.empty
val causeId = cause.map(c => s"${c.layer.id}->${layer.id}").getOrElse(layer.id)
layerBindings ++ Vector(
layerBindings ++
Vector(
EB(
layer.selfActivateEvent,
s"${causeId} syn act",
Expand Down Expand Up @@ -311,7 +312,9 @@ object Graph:
Util.println(printBumpers(3, 0, node))

// restore base
val baseRestore = node.nodesToRestore.toSeq
val baseRestore = node.nodesToRestore
// .filter(_.isActive)
.toSeq
val toRestore: Seq[ModeNode] = Seq(
(baseRestore ++ baseRestore.flatMap(_.bumpingMe)).distinct.filter(n => n.isActive),
if byUserAction then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.github.unthingable.jam.surface.WithSource
import java.time.Duration
import java.time.Instant
import java.util.function.BooleanSupplier
import scala.concurrent.duration.FiniteDuration

enum ModeState derives CanEqual:
case Inactive, Activating, Active, Deactivating
Expand Down Expand Up @@ -74,6 +75,9 @@ trait ModeLayer extends IntActivatedLayer, HasId derives CanEqual:
final inline def hasDirtyBindings(withBindings: Binding[?, ?, ?]*): Boolean =
activeAt.map(withBindings.hasOperatedAfter).getOrElse(false)

final inline def isOlderThan(inline duration: FiniteDuration): Boolean =
isOlderThan(Duration.ofNanos(duration.toNanos))

final inline def isOlderThan(inline duration: Duration): Boolean =
activeAt.exists(act => Instant.now().isAfter(act.plus(duration)))

Expand Down Expand Up @@ -264,8 +268,7 @@ abstract class ModeCycleLayer(
var selected: Option[Int] = Some(0)

override def subModesToActivate: Vector[ModeLayer] =
val ret = (selected.map(subModes(_)).toVector ++
super.subModesToActivate // in case they were bumped
val ret = (selected.map(subModes(_)).toVector ++ super.subModesToActivate // in case they were bumped
).distinct
Util.println(s"debug: for $id submode activators are $ret")
ret
Expand Down Expand Up @@ -324,8 +327,8 @@ abstract class ModeButtonCycleLayer(
case _ => Vector.empty

// bindings to inspect when unsticking
def operatedBindings: Iterable[Binding[?, ?, ?]] =
(selected.map(subModes) ++ siblingOperatedModes).flatMap(_.modeBindings) ++ extraOperated
def operatedBindings: Iterable[Binding[?, ?, ?]] = (selected.map(subModes) ++ siblingOperatedModes)
.flatMap(_.modeBindings) ++ extraOperated

def stickyRelease: Vector[ModeCommand[?]] =
(isOn, cycleMode: CycleMode) match
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/com/github/unthingable/jam/Jam.scala
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class Jam(implicit val ext: MonsterJamExt)
bottom -> Coexist(globalQuant, shiftTransport, shiftMatrix, shiftPages),
bottom -> Exclusive(GlobalMode.Clear, GlobalMode.Duplicate, GlobalMode.Select),
trackGroup -> Exclusive(solo, mute, record),
bottom -> Coexist(clipMatrix, pageMatrix, stepSequencer, stepSequencer.stepGate),
bottom -> Coexist(clipMatrix, pageMatrix, stepSequencer, stepGateActivator),
bottom -> stripGroup,
bottom -> Coexist(auxGate, deviceSelector, macroLayer),
trackGroup -> Exclusive(EIGHT.map(trackGate)*),
Expand Down
22 changes: 11 additions & 11 deletions src/main/scala/com/github/unthingable/jam/layer/SceneL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.bitwig.extension.controller.api.Setting
import com.bitwig.`extension`.controller.api.Track
import com.github.unthingable.Util
import com.github.unthingable.framework.binding.Binding
import com.github.unthingable.framework.binding.BindingBehavior
import com.github.unthingable.framework.binding.EB
import com.github.unthingable.framework.binding.HB.BindingOps
import com.github.unthingable.framework.binding.SupBooleanB
Expand Down Expand Up @@ -60,10 +61,9 @@ trait SceneL:
object superSceneSub extends SimpleModeLayer("superSceneSub") with Util:
val maxTracks =
superBank.getSizeOfBank // can be up to 256 before serialization needs to be rethought
val maxScenes = superBank.sceneBank().getSizeOfBank
val bufferSize =
((maxTracks * maxScenes * 4) / 3) * 5 // will this be enough with the new serializer? no idea
var pageIndex = 0
val maxScenes = superBank.sceneBank().getSizeOfBank
val bufferSize = ((maxTracks * maxScenes * 4) / 3) * 5 // will this be enough with the new serializer? no idea
var pageIndex = 0
var lastScene: Option[Int] = None

val sceneStore: SettableStringValue =
Expand Down Expand Up @@ -157,7 +157,7 @@ trait SceneL:
JamColorState.empty
),
)
}
} :+ SupBooleanB(j.song.light, () => isOn, BindingBehavior.soft)
end superSceneSub

lazy val pageMatrix = new SimpleModeLayer("pageMatrix"):
Expand All @@ -176,10 +176,11 @@ trait SceneL:
val btn: JamRgbButton = j.matrix(row)(col)

def hasContent = trackLen >= col && sceneLen >= row
def ourPage = Seq(scenePos, scenePos + 7).map(_ / 8).contains(row) && Seq(
trackPos,
trackPos + 7
).map(_ / 8).contains(col)
def ourPage = Seq(scenePos, scenePos + 7).map(_ / 8).contains(row) &&
Seq(
trackPos,
trackPos + 7
).map(_ / 8).contains(col)

Vector(
SupColorStateB(
Expand Down Expand Up @@ -221,8 +222,7 @@ trait SceneL:
if pageMatrix.isOn then ext.events.eval("sceneL release")(pageMatrix.deactivateEvent*)

if pressedAt.exists(instant =>
instant.plusMillis(400).isAfter(Instant.now())
|| pageMatrix.modeBindings.hasOperatedAfter(instant)
instant.plusMillis(400).isAfter(Instant.now()) || pageMatrix.modeBindings.hasOperatedAfter(instant)
)
then cycle()

Expand Down
104 changes: 65 additions & 39 deletions src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.github.unthingable.framework.binding.GlobalEvent
import com.github.unthingable.framework.binding.GlobalEvent.ClipSelected
import com.github.unthingable.framework.binding.HB
import com.github.unthingable.framework.binding.JCB
import com.github.unthingable.framework.binding.ModeCommand
import com.github.unthingable.framework.binding.SupBooleanB
import com.github.unthingable.framework.binding.SupColorB
import com.github.unthingable.framework.binding.SupColorStateB
Expand Down Expand Up @@ -110,6 +111,7 @@ trait StepSequencer extends BindingDSL:

Vector(
clip.clipLauncherSlot().isPlaying(),
clip.clipLauncherSlot().hasContent(),
secondClip.exists,
secondClip.clipLauncherSlot.sceneIndex,
clip.exists,
Expand All @@ -136,8 +138,9 @@ trait StepSequencer extends BindingDSL:
clip
.playingStep()
.addValueObserver(step =>
if isOn && ext.transport.isPlaying().get() && ext.preferences.stepFollow
.get() && localState.stepState.get.steps.nonEmpty
if isOn && ext.transport.isPlaying().get() &&
ext.preferences.stepFollow
.get() && !localState.stepState.get.steps.nonEmpty
then
val currentPage: Int = ts.stepScrollOffset / ts.stepPageSize
val playingPage: Int = step / ts.stepPageSize
Expand All @@ -156,7 +159,8 @@ trait StepSequencer extends BindingDSL:
btn.light,
() =>
if hasContent then
if ext.transport.isPlaying().get() && clip.clipLauncherSlot().isPlaying().get() && clip
if ext.transport.isPlaying().get() && clip.clipLauncherSlot().isPlaying().get() &&
clip
.playingStep()
.get() / ts.stepPageSize == i
then colorManager.stepScene.playing
Expand Down Expand Up @@ -189,8 +193,9 @@ trait StepSequencer extends BindingDSL:
else colorManager.stepScene.empty
),
)

},
} ++
// workaround to keep SHIFT button doing what it does, because as submode it will bump SHIFT bindings for bottom
Vector(shiftMatrix, shiftTransport).map(_.modeBindings).flatten,
GateMode.Gate
)
end stepShiftPages
Expand Down Expand Up @@ -256,10 +261,11 @@ trait StepSequencer extends BindingDSL:
else JamColorState(clipColor, 2)
)
)
} ++ Vector(
HB(j.encoder.touch.pressedAction, "", () => incStepSize(0)),
HB(j.encoder.turn, "", stepTarget(() => incStepSize(1), () => incStepSize(-1)))
),
} ++
Vector(
HB(j.encoder.touch.pressedAction, "", () => incStepSize(0)),
HB(j.encoder.turn, "", stepTarget(() => incStepSize(1), () => incStepSize(-1)))
),
)

def setChannel(ch: Int) =
Expand Down Expand Up @@ -363,7 +369,7 @@ trait StepSequencer extends BindingDSL:
override val subModes: Vector[ModeLayer] = Vector(
stepMain,
stepPages,
// stepGate,
stepGate,
stepShiftPages,
stepRegular,
patLength,
Expand All @@ -384,9 +390,8 @@ trait StepSequencer extends BindingDSL:
end onStepState

override def subModesToActivate =
(Vector(stepRegular, stepMatrix, stepPages, stepEnc, dpadStep, stepMain) ++ (subModes :+ velAndNote).filter(m =>
m.isOn || (m == velAndNote && ts.noteVelVisible)
)).distinct
(Vector(stepRegular, stepMatrix, stepPages, stepEnc, dpadStep, stepMain) ++
(subModes :+ velAndNote).filter(m => m.isOn || (m == velAndNote && ts.noteVelVisible))).distinct

override val modeBindings: Seq[Binding[?, ?, ?]] =
Vector(
Expand Down Expand Up @@ -453,10 +458,12 @@ trait StepSequencer extends BindingDSL:

override lazy val extraOperated = stepGate.modeBindings

/** Clip selector, page follow, CLEAR/DUPLICATE */
lazy val stepGate = ModeButtonLayer(
/** Clip selector, page follow, CLEAR/DUPLICATE
*
* Activated by stepGateActivator.
*/
lazy val stepGate = SimpleModeLayer(
"stepGate",
j.step,
Vector(
EB(j.clear.st.press, "clear steps", () => stepSequencer.clip.clearSteps(), BB.soft),
EB(j.duplicate.st.press, "duplicate pattern", () => stepSequencer.clip.duplicateContent(), BB.soft),
Expand All @@ -470,32 +477,51 @@ trait StepSequencer extends BindingDSL:
else !ext.transport.isPlaying().get(),
BB.soft
)
) ++ (for row <- EIGHT; col <- EIGHT yield
val btn: JamRgbButton = j.matrix(row)(col)
val target: ClipLauncherSlot = trackBank.getItemAt(col).clipLauncherSlotBank().getItemAt(row)
val clipEq: BooleanValue = clip.clipLauncherSlot().createEqualsValue(target)
clipEq.markInterested()
target.isPlaying().markInterested()
) ++
(for row <- EIGHT; col <- EIGHT yield
val btn: JamRgbButton = j.matrix(row)(col)
val target: ClipLauncherSlot = trackBank.getItemAt(col).clipLauncherSlotBank().getItemAt(row)
val clipEq: BooleanValue = clip.clipLauncherSlot().createEqualsValue(target)
clipEq.markInterested()
target.isPlaying().markInterested()
Vector(
EB(btn.st.press, "", () => target.select()),
SupColorStateB(
btn.light,
() =>
if clipEq.get() then JamColorState(JamColorBase.WHITE, 3)
else if clip.clipLauncherSlot().hasContent.get() then
JamColorState(target.color().get(), if target.isPlaying().get() then 3 else 1)
else JamColorState.empty,
behavior = BB.soft
),
)
).flatten ++
Vector(
EB(btn.st.press, "", () => target.select()),
SupColorStateB(
btn.light,
() =>
if clipEq.get() then JamColorState(JamColorBase.WHITE, 3)
else JamColorState(target.color().get(), if target.isPlaying().get() then 3 else 1),
behavior = BB.soft
),
)
).flatten ++ Vector(
EB(j.dpad.left.st.press, "", () => ()),
EB(j.dpad.right.st.press, "", () => ()),
EB(j.dpad.up.st.press, "", () => ()),
EB(j.dpad.down.st.press, "", () => ())
),
gateMode = GateMode.Gate,
silent = true
EB(j.dpad.left.st.press, "", () => (), BB.soft),
EB(j.dpad.right.st.press, "", () => (), BB.soft),
EB(j.dpad.up.st.press, "", () => (), BB.soft),
EB(j.dpad.down.st.press, "", () => (), BB.soft)
),
)
end stepSequencer

/** Sidecar mode to coexist with stepSequencer layer, activates stepGate
*
* Easier to separate them like this because this both clobbers the mode button and bumps submodes.
*/
object stepGateActivator
extends ModeButtonLayer("stepGateActivator", j.step, gateMode = GateMode.Gate, silent = true):
override val modeBindings: Seq[Binding[?, ?, ?]] = Vector()

override def onActivate(): Unit =
super.onActivate()
ext.events.eval("stepGate activator")(stepSequencer.stepGate.activateEvent*)

override def onDeactivate(): Unit =
ext.events.eval("stepGate deactivator")(stepSequencer.stepGate.deactivateEvent*)
super.onDeactivate()

end StepSequencer

/* todos and ideas
Expand Down
Loading

0 comments on commit 81f4488

Please sign in to comment.