Skip to content

Commit bd29724

Browse files
committed
* Step: note selector jumps in banks
* Hold REC to arm multiple tracks * Clear REC/MUTE/SOLO on all tracks with CLEAR * Clip queued for recording will flash red Fixes: * No more collision warnings that TrackTracker complained about * Adding new step would display wrong velocity in NOTES mode * Step sequencer was mapped to wrong notes and not aligned with note pads
1 parent 73a9331 commit bd29724

File tree

15 files changed

+302
-189
lines changed

15 files changed

+302
-189
lines changed

docs/README.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Chorded buttons are sensitive to order. For example, **SHIFT+CONTROL** is not th
6666

6767
* **PLAY**: Start/Stop playback
6868
* **REC**: Toggle recording
69+
* **REC (hold)**: Arm tracks via track buttons
6970
* **SHIFT+PLAY** Restart or pause/continue (see **Settings**)
7071
* **SHIFT+RECORD**: Toggle launcher overdub
7172
* **SHIFT+PAGE LEFT**: Toggle the metronome
@@ -212,7 +213,7 @@ Scene button color guide:
212213
### Pad matrix
213214

214215
* Press empty step (dark button) to create a step
215-
* Press an existing step to clear
216+
* Press an existing step to clear (and release quickly)
216217
* Press and hold step to select it for editing (long press will not delete it)
217218
* **SHIFT+NOTES** to toggle alternating note row colors
218219

@@ -256,28 +257,30 @@ Velocity selector works with two different velocity values:
256257
* Default velocity: newly entered note steps will use this
257258
* Velocity of a specific note step: hold one or more note steps, then you can see/edit its velocity via the selector. If multiple steps are held, it's the last pressed step that takes precedence, and adjustments are made to all of them simultaneously.
258259

259-
### Note selector (WIP)
260+
### Note selector
260261

261262
Play notes and focus step editor.
262263

263-
The 128 notes are divided in 8 pages of 16 (or fewer) notes each. The note selector is always showing one of those pages (think of the Chain scroller in Drum Maschine), which one depends on which page the bottommost visible note falls into.
264+
The lower right quadrant (numbered pads) correspond to 16 consecutive notes (in scale), starting with the bottom of the current note page (see below). The "bank" of white buttons are notes currently visible in the step grid.
264265

265266
Pressing a pad:
266267
* Plays the note
267-
* Scrolls the step editor to that note (so that it's at the top row)
268+
* Scrolls the step editor to that note "bank" (hold SELECT to ignore banking and scroll to that note directly, or use encoder)
268269

269-
The note selector button corresponding to the bottommost visible note is bright white. If more notes are visible in the step view, their buttons will be dim white. Note that because the step grid is laid out top-down vertically (high notes on top, low on bottom, matching the clip view in the app) and the note buttons are left-right and down-up (as labeled on the Jam), this can feel backwards.
270+
The note selector button corresponding to the bottommost visible note is bright white. If more notes are visible in the step view, their buttons will be dim white.
270271

271-
Currently playing notes will flash, letting you see activity outside of the visible step grid.
272+
Currently playing non-white notes will flash, letting you see activity outside of the visible step grid.
272273

273274
#### Note pages
274275

275-
**Press and hold NOTE** to see the page selector (SCENE buttons), higher notes are on the right like on a piano (if your brain isn't hurting yet, this is opposite of how scenes work with clips). Unlike Note selector that is fixed to a page, page selector will indicate if current note window straddles two pages, pages with visible content will be in dim white. Pressing a button always scrolls bottom note row to the bottom of that page. If the grid is aligned exactly to the beginning of a page, its page button will be bright white.
276+
**Press and hold NOTE** to see the page selector (SCENE buttons), higher notes are on the right like on a piano. Unlike Note selector that is fixed to a page, page selector will indicate if current note window straddles two pages, pages with visible content will be in dim white. Pressing a button always scrolls bottom note row to the bottom of that page. If the grid is aligned exactly to the beginning of a page, its page button will be bright white.
276277

277-
Seeing how the Drum Machine lays out its banks, in chromatic scale the first page is 4 notes while the rest are 16. This is a compromise where the page layout matches Drum Machines (but you cannot access the last page with Scene buttons because there are only 8 — can still scroll though). In non-chromatic mode all pages are 16 notes.
278+
Seeing how the Drum Machine lays out its banks, in chromatic scale the first page is 4 notes while the rest are 16. This is a compromise where the page layout matches Drum Machine's (but you cannot access the last page with Scene buttons because there are only 8 — can still scroll though). In non-chromatic mode all pages are 16 notes.
278279

279280
Use note/page selectors together with knob scrolling for maximum nagivation.
280281

282+
Note: note page selector always displays pages for the full 8x8 grid, regardless of whether NOTES mode is active.
283+
281284
## Channel and Scale
282285

283286
Hold **PERFORM** to access channel and scale selectors.
@@ -353,7 +356,7 @@ You can quickly drop into a mode, do something and drop back out with no additio
353356
Some specific modes use an inverted variant of this behavior (long press leaves mode on).
354357

355358
Some modes that are like this:
356-
* SOLO, MUTE, TEMPO
359+
* SOLO, MUTE, TEMPO, RECORD
357360
* User control pages
358361
* Control slice selectors
359362
* etc. — if you think it should be auto-gating it probably is, and if not then let me know.
@@ -398,6 +401,14 @@ is entered, SuperScene will not be able to launch or stop it directly.
398401
If a group track containing SuperScene clips in its inner tracks was folded,
399402
SuperScene will launch the _entire_ last (bottom-most) scene of that group track that has a playing clip.
400403

404+
**NOTE**: SuperScenes are experimental and are built using undocumented implementation details of Bitwig API to derive unique track IDs. There is a chance things will randomly stop working with a new release.
405+
406+
### ID collision warnings
407+
408+
Due to experimental nature of the underlying track ID mechanism, MonsterJam monitors track IDs for uniqueness. If a duplicate ID is detected, a notification will pop up with the names of tracks.
409+
410+
If this happens, try duplicating the offending track and deleting the original, and definitely let me know.
411+
401412
## Device Selector
402413

403414
Note: there is currently a bug in the API that will cause Device Selector display to freak out when adding new devices. In the meantime, scroll the track bank back and forth to clear.
@@ -501,6 +512,22 @@ After changing preferences it may be necessary to reinitialize the extension (tu
501512

502513
# Changelog
503514

515+
## 8.0b12
516+
(changelog since b9)
517+
### Features
518+
* Step: note selector jumps in banks
519+
* Hold REC to arm multiple tracks
520+
* Clear REC/MUTE/SOLO on all tracks with CLEAR
521+
* Clip queued for recording will flash red
522+
### Fixes
523+
* No more collision warnings that TrackTracker complained about
524+
* Adding new step would display wrong velocity in NOTES mode
525+
* Step sequencer was mapped to wrong notes and not aligned with note pads
526+
527+
### Known issues
528+
* NOTES mode in sequencer does not reactivate automatically
529+
* After using TUNE, previous strip mode will not reactivate
530+
504531
## 8.0
505532

506533
### New features

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<artifactId>monsterjam</artifactId>
77
<packaging>jar</packaging>
88
<name>MonsterJam</name>
9-
<version>8.0-b9</version>
9+
<version>8.0-b12</version>
1010

1111
<repositories>
1212
<repository>

src/main/scala/com/github/unthingable/MonsterJamControllerDefinition.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class MonsterJamExtensionDefinition() extends ControllerExtensionDefinition {
1616

1717
override def getAuthor = "unthingable"
1818

19-
override def getVersion = "8.0-b9"
19+
override def getVersion = "8.0-b12"
2020

2121
override def getId: UUID = MonsterJamExtensionDefinition.DRIVER_ID
2222

src/main/scala/com/github/unthingable/MonsterJamExtension.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,26 @@ case class MonsterJamExt(
4343
binder: Binder = new Binder(),
4444
events: EventBus[Event] = new EventBus(),
4545
) {
46-
type Schedulable = (Int, () => Boolean, () => Unit)
47-
4846
lazy val xmlMap = loadMap(host)
47+
48+
type Schedulable = (Int, () => Boolean, () => Unit) | (Int, () => Unit)
4949

50-
final def run(tasks: Schedulable*): Unit = {
50+
final def sequence(tasks: List[Schedulable]): Unit = {
5151
tasks match {
5252
case Nil => ()
53+
case (wait, action) :: tt =>
54+
host.scheduleTask(() =>
55+
action()
56+
sequence(tt*), wait)
5357
case (wait, condition, action) :: tt =>
5458
host.scheduleTask(() => if (condition()) {
5559
action()
56-
run(tt: _*)
60+
sequence(tt*)
5761
}, wait)
5862
}
5963
}
64+
65+
final inline def sequence(tasks: Schedulable*): Unit = sequence(tasks.toList)
6066

6167
// for when you need a quick action
6268
def a(f: => Unit): HardwareActionBindable = host.createAction(() => f, () => "")

src/main/scala/com/github/unthingable/Util.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.github.unthingable
22

33
import com.bitwig.extension.api.Color
44
import com.bitwig.extension.controller.api.{
5+
Bank,
56
CursorRemoteControlsPage,
7+
ObjectProxy,
68
Preferences,
79
SettableBooleanValue,
810
SettableEnumValue,
@@ -30,6 +32,7 @@ import java.io.ObjectOutputStream
3032
import scala.util.Try
3133
import java.nio.charset.StandardCharsets
3234
import scala.annotation.targetName
35+
import scala.collection.IndexedSeqView
3336

3437
transparent trait Util {
3538
implicit class SeqOps[A, S[B] <: Iterable[B]](seq: S[A]) {
@@ -43,6 +46,13 @@ transparent trait Util {
4346
Color.fromRGBA(color.getRed, color.getGreen, color.getBlue, color.getAlpha)
4447

4548
case class Timed[A](value: A, instant: Instant)
49+
50+
extension [A <: ObjectProxy](bank: Bank[A])
51+
def view: IndexedSeqView[A] =
52+
(0 until bank.itemCount().get()).view.map(bank.getItemAt)
53+
54+
def fullView: IndexedSeqView[A] =
55+
(0 until bank.getCapacityOfBank()).view.map(bank.getItemAt)
4656
}
4757
object Util extends Util {
4858
val EIGHT: Vector[Int] = (0 to 7).toVector
@@ -98,7 +108,7 @@ object Util extends Util {
98108
ois.close()
99109
obj.asInstanceOf[A]
100110
}.toEither
101-
111+
102112
def comparator[A, B](a: A, b: A)(f: A => B): Boolean =
103113
f(a) == f(b)
104114
}

src/main/scala/com/github/unthingable/jam/Jam.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class Jam(implicit val ext: MonsterJamExt)
122122
sceneCycle -> top,
123123
bottom -> Coexist(globalQuant, shiftTransport, shiftMatrix, shiftPages),
124124
bottom -> Exclusive(GlobalMode.Clear, GlobalMode.Duplicate, GlobalMode.Select),
125-
trackGroup -> Exclusive(solo, mute),
125+
trackGroup -> Exclusive(solo, mute, record),
126126
bottom -> Coexist(clipMatrix, pageMatrix, stepSequencer),
127127
bottom -> stripGroup,
128128
bottom -> Coexist(auxGate, deviceSelector, macroLayer),

src/main/scala/com/github/unthingable/jam/TrackTracker.scala

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ trait TrackTracker {
3333
/** Track ephemeral IDs as hashcodes of Track objects under proxies.
3434
* Unlike SmartTracker which was stable, UnsafeTracker does not preserve IDs between host restarts.
3535
*/
36-
class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends TrackTracker {
36+
class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends TrackTracker, Util {
3737

38-
private val bankRange = 0 until bank.getCapacityOfBank()
3938
private type MRef = Ref[Option[Method]]
4039
private var idM: MRef = Ref(None)
4140
private var targetM: MRef = Ref(None)
4241

43-
bankRange
44-
.map(bank.getItemAt)
42+
bank
43+
.fullView
4544
.zipWithIndex
4645
.foreach { case (t, idx) =>
4746
t.position().markInterested()
@@ -50,9 +49,18 @@ class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends Track
5049

5150
bank.itemCount().addValueObserver(_ =>
5251
// check for collisions
53-
val hashes = bankRange.map(bank.getItemAt).flatMap(idForBankTrack)
52+
val hashes = bank.view.flatMap(idForBankTrack).toVector
5453
if (hashes.distinct.size != hashes.size) // && ext.preferences.smartTracker.get())
5554
ext.host.showPopupNotification("Track ID hash collision detected, superscenes might not work")
55+
val dups = hashes.zipWithIndex.map((id, idx) =>
56+
val track = bank.getItemAt(idx)
57+
Util.println(s"$idx $id ${track.name().get}")
58+
(id, track)
59+
).groupBy(_._1)
60+
.values.filter(_.length > 1)
61+
// .tapEach(v => Util.println(v.toString()))
62+
val names = dups.flatten.map(_._2.name().get()).toVector.distinct.mkString(",")
63+
ext.host.showPopupNotification(s"Track ID hash collision: $names")
5664
)
5765

5866
val ids = mutable.ArraySeq.from(0 until bank.getCapacityOfBank())
@@ -68,7 +76,7 @@ class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends Track
6876
else None
6977

7078
override inline def getItemAt(id: TrackId): Option[Track] =
71-
LazyList.from(bankRange).map(bank.getItemAt).find(t => trackId(t).contains(id))
79+
bank.view.find(t => trackId(t).contains(id))
7280

7381
// cache method references
7482
private def getOr(mref: MRef, method: => Option[Method]): Option[Method] =
@@ -88,7 +96,8 @@ class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends Track
8896
idMethod: Method <- getOr(idM,
8997
tObj.getClass().getMethods()
9098
.filter(_.getReturnType().equals(classOf[UUID]))
91-
.headOption) // let's hope there is just one
99+
// .tapEach(m => Util.println(s"$st $m"))
100+
.lastOption) // we can only hope it's the right one, no way to know for sure
92101
} yield idMethod.invoke(tObj).asInstanceOf[UUID]
93102

94103
val id = uid.map(_.hashCode())
@@ -97,9 +106,9 @@ class UnsafeTracker(val bank: TrackBank)(using ext: MonsterJamExt) extends Track
97106
id.map(TrackId.apply)
98107

99108
inline def idList: Seq[Option[TrackId]] =
100-
LazyList.from(bankRange).map(idx => idForBankTrack(bank.getItemAt(idx)))
109+
bank.view.map(idForBankTrack).toVector
101110

102111
inline def idMap: Seq[(TrackId, Int)] =
103-
idList.zipWithIndex.map{case (a, b) => a.map((_, b))}.flatten
112+
idList.zipWithIndex.map{(a, b) => a.map((_, b))}.flatten
104113
}
105114

src/main/scala/com/github/unthingable/jam/layer/ClipMatrix.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ trait ClipMatrix { this: Jam =>
4242
clip.color().markInterested()
4343
clip.hasContent.markInterested()
4444
clip.isPlaying.markInterested()
45+
clip.isRecording().markInterested()
46+
clip.isRecordingQueued().markInterested()
4547
clip.isSelected.markInterested()
4648
clip.isPlaybackQueued.markInterested()
4749
clip.isStopQueued.markInterested()
@@ -144,13 +146,15 @@ trait ClipMatrix { this: Jam =>
144146
JamColorState(
145147
if (clip.hasContent.get())
146148
JamColorState.toColorIndex(clip.color().get())
149+
else if clip.isRecordingQueued().get() then
150+
JamColorBase.RED
147151
else
148152
JamColorBase.OFF,
149153
brightness =
150154
if (clip.isPlaying.get())
151155
if (track.isQueuedForStop.get()) if (j.Mod.blink) 3 else -1
152156
else 3
153-
else if (clip.isPlaybackQueued.get()) if (j.Mod.blink) 0 else 3
157+
else if (clip.isPlaybackQueued.get() || clip.isRecordingQueued().get()) if (j.Mod.blink) 0 else 3
154158
else 0
155159
)
156160
}

src/main/scala/com/github/unthingable/jam/layer/StepSequencer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ trait StepSequencer extends BindingDSL { this: Jam =>
8181
clip.addNoteStepObserver(ns => steps(ns.channel())(ns.x()).update(ns.y(), ns))
8282

8383
devices.itemCount().addValueObserver(v => Util.println(v.toString))
84-
clip.getPlayStop.addValueObserver(v => Util.println(s"beats $v"))
84+
// clip.getPlayStop.addValueObserver(v => Util.println(s"beats $v"))
8585
// clip.playingStep().addValueObserver(v => Util.println(s"playing step $v"))
8686

8787
// follow track selection

src/main/scala/com/github/unthingable/jam/layer/TransportL.scala

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.github.unthingable.jam.layer
22

33
import com.bitwig.extension.api.Color
4-
import com.bitwig.extension.controller.api.{Channel, HardwareActionBindable, SettableBooleanValue}
4+
import com.bitwig.extension.controller.api.{Channel, HardwareActionBindable, SettableBooleanValue, Track}
55
import com.github.unthingable.framework.mode.{GateMode, ModeButtonLayer, SimpleModeLayer}
66
import com.github.unthingable.framework.binding.HB.action
77
import com.github.unthingable.framework.binding.{
@@ -18,8 +18,9 @@ import com.github.unthingable.jam.Jam
1818
import com.github.unthingable.framework.binding.BindingDSL
1919
import java.time.Instant
2020
import com.github.unthingable.JamSettings
21+
import com.github.unthingable.Util
2122

22-
trait TransportL extends BindingDSL { this: Jam =>
23+
trait TransportL extends BindingDSL, Util { this: Jam =>
2324
lazy val position = SimpleModeLayer(
2425
"position",
2526
Vector(
@@ -110,7 +111,7 @@ trait TransportL extends BindingDSL { this: Jam =>
110111
BB(tracked = false)
111112
),
112113
SupBooleanB(j.noteRepeat.light.isOn, ext.transport.isFillModeActive),
113-
EB(j.record.st.press, "record pressed", () => ext.transport.record()),
114+
EB(j.record.st.press, "record pressed", () => ext.transport.record(), BB(exclusive = false)),
114115
SupBooleanB(j.record.light.isOn, ext.transport.isArrangerRecordEnabled),
115116
EB(
116117
j.auto.st.press,
@@ -196,8 +197,10 @@ trait TransportL extends BindingDSL { this: Jam =>
196197
name: String,
197198
modeButton: JamOnOffButton,
198199
group: Seq[JamRgbButton],
199-
prop: Channel => SettableBooleanValue,
200-
color: Int // Jam's color index
200+
prop: Track => SettableBooleanValue,
201+
color: Int, // Jam's color index
202+
gateMode: GateMode = GateMode.Auto,
203+
silent: Boolean = false
201204
): ModeButtonLayer = ModeButtonLayer(
202205
name,
203206
modeButton,
@@ -223,11 +226,17 @@ trait TransportL extends BindingDSL { this: Jam =>
223226
JamColorState.empty
224227
)
225228
)
226-
}
229+
} ++ Vector(
230+
EB(j.clear.st.press, "", () => superBank.view.map(prop).foreach(_.set(false)))
231+
),
232+
gateMode = gateMode,
233+
silent = silent
227234
)
228235

229236
lazy val solo =
230237
buttonGroupChannelMode("solo", j.solo, j.groupButtons, _.solo(), JamColorBase.YELLOW)
231238
lazy val mute =
232239
buttonGroupChannelMode("mute", j.mute, j.groupButtons, _.mute(), JamColorBase.ORANGE)
240+
lazy val record =
241+
buttonGroupChannelMode("record", j.record, j.groupButtons, _.arm(), JamColorBase.RED, GateMode.Gate, true)
233242
}

0 commit comments

Comments
 (0)