Skip to content
This repository was archived by the owner on May 31, 2025. It is now read-only.

Commit 5e72992

Browse files
committed
Initial Commit
0 parents  commit 5e72992

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package-lock.json
2+
node_modules
3+
.node-version

LICENSE.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright (c) 2018, Jan Krutisch <jan@krutisch.de>
2+
3+
Permission to use, copy, modify, and/or distribute this software for any
4+
purpose with or without fee is hereby granted, provided that the above
5+
copyright notice and this permission notice appear in all copies.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ableton-push-canvas-display
2+
3+
Ableton Push 2 has a high resolution color display that can be updated via libusb very easily.
4+
The hardest part is drawing pretty graphics. Usually people use frameworks like JUCE for that, but
5+
with node and the web, we have something simple, that should also work: The canvas.
6+
7+
This package does that hard work of converting the image data from a canvas to the slightly weird
8+
push framebuffer format and sending it via USB.
9+
10+
## How to use
11+
12+
Since this uses node libusb bindings, you need to have a node environment. This means that it
13+
actually works in electron, even in the renderer process. That also means that you can send a canvas
14+
that is actually rendered in a web page.
15+
16+
Here's a minimal example that assumes a set up canvas. The canvas should be 960x160 (the display
17+
size of Push 2).
18+
19+
```JavaScript
20+
const { initPush, sendFrame } = require('ableton-push-canvas-display')
21+
22+
function render() {
23+
ctx.clearRect(...)
24+
ctx.strokeLine(...)
25+
sendFrame(ctx, (err) => {
26+
render() // you probably want to do something like requestAnimationFrame or so.
27+
})
28+
}
29+
30+
initPush((err) => {
31+
if (!err) {
32+
render()
33+
}
34+
})
35+
```
36+
37+
If you can't or don't want to use a web canvas, there's a npm package called "canvas" that
38+
replicates the API using libcairo which works as an image source as well.
39+
40+
## Pitfalls
41+
42+
If you don't clear the canvas with a solid background, the results will look weird, as the
43+
conversion process doesn't take the alpha channel into account. As soon as you render to a solid
44+
background, the alpha masks (for antia aliasing for example) are baked and it should look ok.
45+
46+
For performance reasons I'm using direct access to typed arrays which means that things can go wrong
47+
in terms of endianness if you are not on a little endian machine. (which is VERY rare nowadays)
48+
49+
The libusb bulk transfer process takes up almost all of the 16 ms we have to make this a 60fps
50+
operation on my iMac, so there could be some jank sometimes. I have to test this on other hardware
51+
to see if it needs additional optimisations.
52+
53+
## Status
54+
55+
This needs tests (sort of hard to test without actual hardware, but I guess I could at least test
56+
the conversion process properly).
57+
58+
It is not handling multiple push devices. Not sure that this is actually a valid usecase for me.

examples/simple-anim.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const Canvas = require('canvas')
2+
const path = require('path')
3+
const { initPush, sendFrame } = require(path.join(__dirname, '..', 'index.js'))
4+
5+
const canvas = new Canvas(960,160)
6+
const ctx = canvas.getContext('2d')
7+
8+
function drawFrame(ctx, frameNum) {
9+
ctx.strokeStyle = "#ff0"
10+
ctx.fillStyle = "#000"
11+
ctx.fillRect(0, 0, 960, 160)
12+
ctx.fillStyle = "hsl(" + frameNum % 360 +",100%,50%)"
13+
ctx.lineWidth = 4
14+
ctx.fillRect((frameNum * 2) % 960, (frameNum * 2) % 160, 20, 20)
15+
ctx.beginPath()
16+
ctx.arc(100, 100, 50, 0, (frameNum / 20.0) % (2 * Math.PI))
17+
ctx.lineTo(100, 100)
18+
ctx.stroke()
19+
20+
ctx.font = '800 20px "SF Pro Display"';
21+
ctx.fillStyle = '#fff'
22+
ctx.fillText("Awesome!", 50, 100);
23+
}
24+
25+
let frameNum = 0
26+
27+
function nextFrame() {
28+
drawFrame(ctx, frameNum)
29+
frameNum++
30+
sendFrame(ctx, function() {
31+
process.nextTick(function() {
32+
nextFrame()
33+
})
34+
})
35+
}
36+
37+
initPush(function() {
38+
nextFrame()
39+
})

index.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const USB = require('usb')
2+
const PUSH_VID = 0x2982
3+
const PUSH_PID = 0x1967
4+
const PUSH_FRAME_SIZE = 327680
5+
const PUSH_LINE_SIZE = 2048
6+
const PUSH_BUFFER_SIZE = PUSH_FRAME_SIZE
7+
const PUSH_XOR_PATTERN = 0xFFE7F3E7
8+
const PUSH_BUFFER_COUNT = PUSH_FRAME_SIZE / PUSH_BUFFER_SIZE
9+
const PUSH_FRAME_HEADER = [0xFF, 0xCC, 0xAA, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]
10+
11+
var endpoint;
12+
var interface;
13+
14+
const frameData = new ArrayBuffer(327680)
15+
16+
/* The actual conversion function */
17+
function convertImage(image, frame) {
18+
var y,h,x,w;
19+
for(y=0;y<160;y++) {
20+
const frameLineStart = PUSH_LINE_SIZE * y
21+
const frameLine = new Uint16Array(frame, frameLineStart, PUSH_LINE_SIZE / 2)
22+
for(x=0;x<1024;x++) {
23+
var word = 0
24+
if (x < 960) {
25+
const off = (y * 3840) + x * 4
26+
const r = image[off + 0] >> 3
27+
const g = image[off + 1] >> 2
28+
const b = image[off + 2] >> 3
29+
word = (r & 0b11111) | ((g & 0b111111) << 5) | ((b & 0b11111) << 11)
30+
}
31+
var mask = 0xF3E7
32+
if (x & 1 > 0) {
33+
mask = 0xFFE7
34+
}
35+
frameLine[x] = word ^ mask
36+
}
37+
}
38+
}
39+
/* this is thi actual device init */
40+
function _init(push) {
41+
if (push == null) { return; }
42+
push.open()
43+
const pushInterface = push.interface(0)
44+
pushInterface.claim()
45+
endpoint = pushInterface.endpoint(1)
46+
}
47+
48+
/**
49+
* initialize the push device. his will open the push device and claim the display interface.
50+
* This function also sets up callbacks for later device addition, so that the push gets
51+
* initialized if added to the system later
52+
* @param { function(error) } callback - callback will be called after init. Follows node's "error first" async convention
53+
*/
54+
function initPush(callback) {
55+
var push = USB.findByIds(PUSH_VID, PUSH_PID)
56+
USB.on('attach', function(device) {
57+
if (device.deviceDescriptor.idVendor === PUSH_VID && device.deviceDescriptor.idProduct === PUSH_PID) {
58+
_init(device)
59+
}
60+
})
61+
USB.on('detach', function(device) {
62+
if (device.deviceDescriptor.idVendor === PUSH_VID && device.deviceDescriptor.idProduct === PUSH_PID) {
63+
endpoint = null
64+
}
65+
})
66+
if (push == null) {
67+
callback(new Error("Push not found"))
68+
}
69+
_init(push)
70+
callback()
71+
}
72+
73+
/**
74+
* sends the current content of the given canvas context to the push.
75+
* The function assumes the canvas has the correct dimenstions.
76+
* It's fine to call this function even if initPush returned an error, as Push may have
77+
* been added to the system at a later time.
78+
* @param { Context2D } ctx - the canvas context. Both a native web canvas and node-canvas work.
79+
* @param { function(error) } callback - callback will be called after init. Follows node's "error first" async convention
80+
*/
81+
function sendFrame(ctx, callback) {
82+
if (!endpoint) { callback(new Error("No Push available")); return; } // push not available
83+
convertImage(ctx.getImageData(0,0,960,160).data, frameData)
84+
endpoint.transfer(Buffer.from(PUSH_FRAME_HEADER), function (error) {
85+
if (!error) {
86+
endpoint.transfer(Buffer.from(frameData), function (error) {
87+
if (!error) {
88+
callback()
89+
} else {
90+
callback(error)
91+
}
92+
})
93+
} else {
94+
callback(error)
95+
}
96+
})
97+
}
98+
99+
module.exports = {
100+
initPush: initPush,
101+
sendFrame: sendFrame
102+
}

package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "ableton-push-canvas-display",
3+
"version": "1.0.0",
4+
"description": "send contents of a canvas 2d context to the Ableton Push 2 display",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/halfbyte/ableton-push-canvas-display.git"
12+
},
13+
"engines": {
14+
"nodejs": ">=8.0.0"
15+
},
16+
"keywords": [
17+
"music",
18+
"hardware",
19+
"usb"
20+
],
21+
"author": "Jan Krutisch <jan@krutisch.de>",
22+
"license": "ISC",
23+
"bugs": {
24+
"url": "https://github.com/halfbyte/ableton-push-canvas-display/issues"
25+
},
26+
"homepage": "https://github.com/halfbyte/ableton-push-canvas-display#readme",
27+
"dependencies": {
28+
"usb": "^1.3.1"
29+
},
30+
"devDependencies": {
31+
"canvas": "^1.6.11"
32+
}
33+
}

0 commit comments

Comments
 (0)