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