Skip to content

Commit

Permalink
lichen-community-systemsGH-8: Added demos for sending/receiving MTC q…
Browse files Browse the repository at this point in the history
…uarter frame messages.
  • Loading branch information
duhrer committed Feb 7, 2023
1 parent 4fad637 commit 1db62d2
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 17 deletions.
23 changes: 23 additions & 0 deletions demos/css/quarterFrame.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2023, Tony Atkins
*
* Licensed under the MIT license, see LICENSE for details.
*/

.timestamp {
align-items: stretch;
column-gap: 2rem;
display: flex;
flex-direction: row;
font-size: 10rem;
padding: 1rem;
}

.timestamp > * {
align-content: center;
border: 1px solid #969696;
border-radius: 0.5rem;
font-family: monospace;
text-align: center;
width: 100%;
}
38 changes: 22 additions & 16 deletions demos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@
<link type="text/css" rel="stylesheet" href="../src/css/youme.css"/>
</head>
<body>
<h3>"Youme" Demos</h3>
<h3>"Youme" Demos</h3>

<p>The pages below demonstrate various components provided by this package.</p>
<p>The pages below demonstrate various components provided by this package.</p>

<ul>
<li>
<a href="./passthrough.html">"Passthrough": Relay messages from a single input to a single output.</a>
</li>
<li>
<a href="./multi-passthrough.html">"Multi-Passthrough": Relay messages from one or more inputs to one or more outputs.</a>
</li>
<li>
<a href="./message-monitor.html">"Message Monitor": Display MIDI messages received from one or more inputs.</a>
</li>
<li>
<a href="./midi-console.html">"MIDI Console": Convert MIDI messages between hex and JSON, send them to an output.</a>
</li>
</ul>
<ul>
<li>
<a href="./passthrough.html">"Passthrough": Relay messages from a single input to a single output.</a>
</li>
<li>
<a href="./multi-passthrough.html">"Multi-Passthrough": Relay messages from one or more inputs to one or more outputs.</a>
</li>
<li>
<a href="./message-monitor.html">"Message Monitor": Display MIDI messages received from one or more inputs.</a>
</li>
<li>
<a href="./midi-console.html">"MIDI Console": Convert MIDI messages between hex and JSON, send them to an output.</a>
</li>
<li>
<a href="./receive-quarter-frame.html">"Quarter Frame Receiver": Update a local clock based on external quarter frame MTC messages.</a>
</li>
<li>
<a href="./receive-quarter-frame.html">"Quarter Frame Sender": Send quarter frame MTC messages to an external receiver to control its clock.</a>
</li>
</ul>
</body>
</html>
63 changes: 63 additions & 0 deletions demos/js/quarterFrameTimestamp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2023, Tony Atkins
*
* Licensed under the MIT license, see LICENSE for details.
*/
(function (fluid) {
"use strict";

var youme = fluid.registerNamespace("youme");

fluid.defaults("youme.demos.quarterFrame.timestamp", {
gradeNames: ["youme.templateRenderer"],
model: {
hour: 0,
minute: 0,
second: 0,
frame: 0
},
selectors: {
hour: ".timestamp-hour",
minute: ".timestamp-minute",
second: ".timestamp-second",
frame: ".timestamp-frame"
},
markup: {
container: "<div class='timestamp'><div class='timestamp-hour'></div><div class='timestamp-minute'></div><div class='timestamp-second'></div><div class='timestamp-frame'></div></div>"
},
modelRelay: {
hour: {
singleTransform: {
input: "{that}.model.hour",
type: "youme.demos.quarterFrame.timestamp.padStart"
},
target: "{that}.model.dom.hour.text"
},
minute: {
singleTransform: {
input: "{that}.model.minute",
type: "youme.demos.quarterFrame.timestamp.padStart"
},
target: "{that}.model.dom.minute.text"
},
second: {
singleTransform: {
input: "{that}.model.second",
type: "youme.demos.quarterFrame.timestamp.padStart"
},
target: "{that}.model.dom.second.text"
},
frame: {
singleTransform: {
input: "{that}.model.frame",
type: "youme.demos.quarterFrame.timestamp.padStart"
},
target: "{that}.model.dom.frame.text"
}
}
});

youme.demos.quarterFrame.timestamp.padStart = function (number) {
return (number).toString().padStart(2, "0");
};
})(fluid);
104 changes: 104 additions & 0 deletions demos/js/receiveQuarterFrame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2023, Tony Atkins
*
* Licensed under the MIT license, see LICENSE for details.
*/
(function (fluid) {
"use strict";
var youme = fluid.registerNamespace("youme");
fluid.defaults("youme.demos.quarterFrame.receive", {
gradeNames: ["youme.templateRenderer"],
markup: {
container: "<div class='quarter-frame'><div class='timestamp'></div><div class='input'></div></div>"
},
selectors: {
input: ".input",
timestamp: ".timestamp"
},
model: {
rate: 3,
hour: 0,
minute: 0,
second: 0,
frame: 0
},

components: {
timestamp: {
type: "youme.demos.quarterFrame.timestamp",
container: "{that}.dom.timestamp",
options: {
model: {
hour: "{youme.demos.quarterFrame.receive}.model.hour",
minute: "{youme.demos.quarterFrame.receive}.model.minute",
second: "{youme.demos.quarterFrame.receive}.model.second",
frame: "{youme.demos.quarterFrame.receive}.model.frame"
}
}
},
input: {
type: "youme.portSelectorView.input",
container: "{that}.dom.input",
options: {
desiredPortSpec: { name: "IAC Driver Bus.+" },
listeners: {
"onMessage.handleMessage": {
funcName: "youme.demos.quarterFrame.receive.handleMessage",
args: ["{youme.demos.quarterFrame.receive}", "{arguments}.0"] // message
}
}
}
}
}
});

youme.demos.quarterFrame.receive.handleMessage = function (that, message) {
if (message.type === "quarterFrameMTC") {
switch (message.piece) {
case 0:
var oldHighFrameNibble = that.model.frame & 16;
var frameValueWithNewLowNibble = oldHighFrameNibble + (message.frame & 15);
that.applier.change("frame", frameValueWithNewLowNibble);
break;
case 1:
var oldLowFrameNibble = that.model.frame & 15;
var frameValueWithNewHighNibble = (message.frame & 16) + oldLowFrameNibble;
that.applier.change("frame", frameValueWithNewHighNibble);
break;
case 2:
var oldHighSecondNibble = that.model.second & 48;
var secondValueWithNewLowNibble = oldHighSecondNibble + (message.second & 15);
that.applier.change("second", secondValueWithNewLowNibble);
break;
case 3:
var oldLowSecondNibble = that.model.second & 15;
var secondValueWithNewHighNibble = (message.second & 48) + oldLowSecondNibble;
that.applier.change("second", secondValueWithNewHighNibble);
break;
case 4:
var oldHighMinuteNibble = that.model.minute & 48;
var minuteValueWithNewLowNibble = oldHighMinuteNibble + (message.minute & 15);
that.applier.change("minute", minuteValueWithNewLowNibble);
break;
case 5:
var oldLowMinuteNibble = that.model.minute & 15;
var minuteValueWithNewHighNibble = (message.minute & 48) + oldLowMinuteNibble;
that.applier.change("minute", minuteValueWithNewHighNibble);
break;
case 6:
var oldHighHourNibble = that.model.hour & 16;
var hourValueWithNewLowNibble = oldHighHourNibble + (message.hour & 15);
that.applier.change("hour", hourValueWithNewLowNibble);
break;
case 7:
var oldLowHourNibble = that.model.hour & 15;
var hourValueWithNewHighNibble = (message.hour & 16) + oldLowHourNibble;
that.applier.change("hour", hourValueWithNewHighNibble);
that.applier.change("rate", message.rate);
default:
fluid.log(fluid.logLevel.ERROR, "Invalid piece number: " + message.piece);
}

}
};
})(fluid);
139 changes: 139 additions & 0 deletions demos/js/sendQuarterFrame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2023, Tony Atkins
*
* Licensed under the MIT license, see LICENSE for details.
*/
(function (fluid) {
"use strict";
var youme = fluid.registerNamespace("youme");
fluid.defaults("youme.demos.quarterFrame.send", {
gradeNames: ["youme.templateRenderer", "youme.messageSender"],
markup: {
container: "<div class='quarter-frame'><div class='timestamp'></div><div class='outputs'></div></div>"
},
selectors: {
outputs: ".outputs",
timestamp: ".timestamp"
},
model: {
timestamp: 0,
direction: 1,
piece: 0,
rate: 3,
hour: 0,
minute: 0,
second: 0,
frame: 0
},

invokers: {
handleQuarterFrame: {
funcName: "youme.demos.quarterFrame.send.handleQuarterFrame",
args: ["{that}"]
}
},

listeners: {
"onCreate.startScheduler": {
funcName: "youme.demos.quarterFrame.send.startScheduler",
args: ["{that}"]
},
"sendMessage.sendToOutputs": "{outputs}.events.sendMessage.fire"
},

components: {
// TODO: Something to control direction.
timestamp: {
type: "youme.demos.quarterFrame.timestamp",
container: "{that}.dom.timestamp",
options: {
model: {
timestamp: "{youme.demos.quarterFrame.send}.model.timestamp",
hour: "{youme.demos.quarterFrame.send}.model.hour",
minute: "{youme.demos.quarterFrame.send}.model.minute",
second: "{youme.demos.quarterFrame.send}.model.second",
frame: "{youme.demos.quarterFrame.send}.model.frame"
}
}
},
outputs: {
type: "youme.multiPortSelectorView.outputs",
container: "{that}.dom.outputs",
options: {
desiredPortSpecs: ["Arturia BeatStep"]
}
},
scheduler: {
type: "berg.scheduler",
options: {
components: {
clock: {
type: "berg.clock.raf",
options: {
freq: 60 // times per second
}
}
}
}
}
}
});

// I am choosing not to properly deal with drop frame, and pretend it's just 30 FPS.
youme.demos.quarterFrame.send.rates = [24, 25, 30, 30];

youme.demos.quarterFrame.send.startScheduler = function (that) {
that.scheduler.schedule({
type: "repeat",
// These are both about the best I can get the RAF scheduler to do.
// A `freq` higher than 100 hangs the window.
freq: 100,
// An `interval` less than 0.01 hangs the window.
// interval: 0.01,
callback: that.handleQuarterFrame
});

that.scheduler.start();
};

youme.demos.quarterFrame.send.handleQuarterFrame = function (that) {
// Set the time values to match the current moment.
var quarterFrameMessage = {
type: "quarterFrameMTC",
piece: that.model.piece,
rate: that.model.rate,
hour: that.model.hour,
minute: that.model.minute,
second: that.model.second,
frame: that.model.frame
};

// Send the quarter-frame MTC to any connected devices.
that.events.sendMessage.fire(quarterFrameMessage);

// Batch all model updates, as we'll be making them quite frequently.
var transaction = that.applier.initiate();

// Track the elapsed time internally rather than peeking at the current time.
var fps = youme.demos.quarterFrame.send.rates[that.model.rate];
var msPerPiece = (1000 / (fps * 4));
var newTimestamp = that.model.timestamp + (that.model.direction * msPerPiece);
var newSecond = (60 + Math.round(newTimestamp / 1000)) % 60;
var newMinute = (60 + Math.floor( newTimestamp / 60000)) % 60;
var newHour = (24 + Math.floor(newTimestamp / 3600000)) % 24;
transaction.fireChangeRequest({ path: "timestamp", value: newTimestamp});
transaction.fireChangeRequest({ path: "second", value: newSecond});
transaction.fireChangeRequest({ path: "minute", value: newMinute});
transaction.fireChangeRequest({ path: "hour", value: newHour});

// Update the frame in whichever direction we're going.
if ((that.model.direction === 1 && that.model.piece === 7) || (that.model.direction === -1 && that.model.piece === 0)) {
var nextFrame = (fps + that.model.frame + (that.model.direction * 2)) % fps;
transaction.fireChangeRequest({ path: "frame", value: nextFrame});
}
// Update the piece in whichever direction we're going.
var nextPiece = (8 + that.model.piece + that.model.direction) % 8;
transaction.fireChangeRequest({ path: "piece", value: nextPiece});
transaction.commit();
};
})(fluid);
Loading

0 comments on commit 1db62d2

Please sign in to comment.