diff --git a/.scalafmt.conf b/.scalafmt.conf
index 33178a1..b761370 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -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
diff --git a/docs/README.md b/docs/README.md
index 612086b..45c9c18 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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.
@@ -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
diff --git a/pom.xml b/pom.xml
index d0fc66b..0ccde15 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
monsterjam
jar
MonsterJam
- 8.0-b15
+ 8.0-b16
diff --git a/src/main/scala/com/github/unthingable/MonsterJamControllerDefinition.scala b/src/main/scala/com/github/unthingable/MonsterJamControllerDefinition.scala
index ef28aef..c35b680 100644
--- a/src/main/scala/com/github/unthingable/MonsterJamControllerDefinition.scala
+++ b/src/main/scala/com/github/unthingable/MonsterJamControllerDefinition.scala
@@ -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
diff --git a/src/main/scala/com/github/unthingable/Util.scala b/src/main/scala/com/github/unthingable/Util.scala
index 279d651..0c271f6 100644
--- a/src/main/scala/com/github/unthingable/Util.scala
+++ b/src/main/scala/com/github/unthingable/Util.scala
@@ -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
@@ -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
@@ -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
diff --git a/src/main/scala/com/github/unthingable/framework/mode/ModeGraph.scala b/src/main/scala/com/github/unthingable/framework/mode/ModeGraph.scala
index 66f809f..c977fa0 100644
--- a/src/main/scala/com/github/unthingable/framework/mode/ModeGraph.scala
+++ b/src/main/scala/com/github/unthingable/framework/mode/ModeGraph.scala
@@ -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",
@@ -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
diff --git a/src/main/scala/com/github/unthingable/framework/mode/ModeLayer.scala b/src/main/scala/com/github/unthingable/framework/mode/ModeLayer.scala
index 5d3e04e..655915b 100644
--- a/src/main/scala/com/github/unthingable/framework/mode/ModeLayer.scala
+++ b/src/main/scala/com/github/unthingable/framework/mode/ModeLayer.scala
@@ -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
@@ -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)))
@@ -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
@@ -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
diff --git a/src/main/scala/com/github/unthingable/jam/Jam.scala b/src/main/scala/com/github/unthingable/jam/Jam.scala
index 558eafe..df53348 100644
--- a/src/main/scala/com/github/unthingable/jam/Jam.scala
+++ b/src/main/scala/com/github/unthingable/jam/Jam.scala
@@ -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)*),
diff --git a/src/main/scala/com/github/unthingable/jam/layer/SceneL.scala b/src/main/scala/com/github/unthingable/jam/layer/SceneL.scala
index 45430ac..d05340b 100644
--- a/src/main/scala/com/github/unthingable/jam/layer/SceneL.scala
+++ b/src/main/scala/com/github/unthingable/jam/layer/SceneL.scala
@@ -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
@@ -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 =
@@ -157,7 +157,7 @@ trait SceneL:
JamColorState.empty
),
)
- }
+ } :+ SupBooleanB(j.song.light, () => isOn, BindingBehavior.soft)
end superSceneSub
lazy val pageMatrix = new SimpleModeLayer("pageMatrix"):
@@ -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(
@@ -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()
diff --git a/src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala b/src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala
index a4024a1..091b26b 100644
--- a/src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala
+++ b/src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala
@@ -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
@@ -110,6 +111,7 @@ trait StepSequencer extends BindingDSL:
Vector(
clip.clipLauncherSlot().isPlaying(),
+ clip.clipLauncherSlot().hasContent(),
secondClip.exists,
secondClip.clipLauncherSlot.sceneIndex,
clip.exists,
@@ -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
@@ -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
@@ -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
@@ -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) =
@@ -363,7 +369,7 @@ trait StepSequencer extends BindingDSL:
override val subModes: Vector[ModeLayer] = Vector(
stepMain,
stepPages,
- // stepGate,
+ stepGate,
stepShiftPages,
stepRegular,
patLength,
@@ -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(
@@ -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),
@@ -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
diff --git a/src/main/scala/com/github/unthingable/jam/layer/TransportL.scala b/src/main/scala/com/github/unthingable/jam/layer/TransportL.scala
index 9e58591..2844610 100644
--- a/src/main/scala/com/github/unthingable/jam/layer/TransportL.scala
+++ b/src/main/scala/com/github/unthingable/jam/layer/TransportL.scala
@@ -25,6 +25,7 @@ import com.github.unthingable.jam.surface.JamOnOffButton
import com.github.unthingable.jam.surface.JamRgbButton
import java.time.Instant
+import scala.concurrent.duration.*
trait TransportL extends BindingDSL, Util:
this: Jam =>
@@ -115,7 +116,12 @@ trait TransportL extends BindingDSL, Util:
BB(tracked = false)
),
SupBooleanB(j.noteRepeat.light, ext.transport.isFillModeActive),
- EB(j.record.st.press, "record pressed", () => ext.transport.record(), BB.omni),
+ EB(
+ j.record.st.release,
+ "record released",
+ () => if !record.isOlderThan(500.millis) && !record.hasDirtyBindings() then ext.transport.record(),
+ BB.omni
+ ),
SupBooleanB(j.record.light, ext.transport.isArrangerRecordEnabled),
EB(
j.auto.st.press,
@@ -224,9 +230,10 @@ trait TransportL extends BindingDSL, Util:
JamColorState.empty
)
)
- } ++ Vector(
- EB(j.clear.st.press, "", () => superBank.view.map(prop).foreach(_.set(false)))
- ),
+ } ++
+ Vector(
+ EB(j.clear.st.press, "", () => superBank.view.map(prop).foreach(_.set(false)))
+ ),
gateMode = gateMode,
silent = silent
)
diff --git a/src/main/scala/com/github/unthingable/jam/stepSequencer/ColorManager.scala b/src/main/scala/com/github/unthingable/jam/stepSequencer/ColorManager.scala
index 00cefbd..9a71375 100644
--- a/src/main/scala/com/github/unthingable/jam/stepSequencer/ColorManager.scala
+++ b/src/main/scala/com/github/unthingable/jam/stepSequencer/ColorManager.scala
@@ -29,7 +29,7 @@ class ColorManager(clipColor: => Color)(using ext: MonsterJamExt):
if custom then noteRowRainbow(noteIdx % 3) else clipColor
def padColor(noteIdx: Int, step: NoteStep) =
step.state() match
- case State.NoteOn => if custom then C(WHITE, 3) else C(clipColor, 2)
- case State.Empty => if custom then C(noteColor(noteIdx), 0) else C.empty
+ case State.NoteOn => C(noteColor(noteIdx), 2)
+ case State.Empty => C.empty
case State.NoteSustain => C(WHITE, 0)
end ColorManager