Giter VIP home page Giter VIP logo

jzz's Introduction

JZZ: MIDI library for Node.js and web-browsers

nodejs firefox chrome opera safari msie edge windows macos linux raspberry pi ios android
npm npm jsDelivr build Coverage Try jzz on RunKit

JZZ.js allows sending, receiving and playing MIDI messages in Node.js and all major browsers in Linux, MacOS and Windows. Some features are available on iOS and Android devices.

JZZ.js enables Web MIDI API in Node.js and those browsers that don't support it, and provides additional functionality to make developer's life easier.

For the best user experience, it's highly RECOMMENDED (though not required) to install the latest version of Jazz-Plugin and browser extensions from Chrome Web Store or Mozilla Add-ons or Apple App Store.

Features

  • MIDI In/Out
  • User-defined MIDI nodes
  • MIDI files
  • MPE
  • SMPTE
  • UMP (MIDI 2.0)
  • Additional modules

Install

npm install jzz --save
or yarn add jzz
or get the full development version and minified scripts from Github

Note: in the (unlikely) case you get into trouble installing the midi-test module, that requires special system configuration, you can safely remove it from the devDependencies by running npm remove midi-test --save-dev.

Usage

Plain HTML
<script src="JZZ.js"></script>
//...
CDN (jsdelivr)
<script src="https://cdn.jsdelivr.net/npm/jzz"></script>       // the latest version, or
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script> // any particular version
//...
CDN (unpkg)
<script src="https://unpkg.com/jzz"></script>       // the latest version, or
<script src="https://unpkg.com/[email protected]"></script> // any particular version
//...
CommonJS
var JZZ = require('jzz');
//...
TypeScript / ES6
import { JZZ } from 'jzz';
//...
AMD
require(['JZZ'], function(JZZ) {
  //...
});

Web MIDI API

(Node.js example)
var navigator = require('jzz');
navigator.requestMIDIAccess().then(onSuccess, onFail);
// ...
navigator.close(); // This will close MIDI inputs,
                   // otherwise Node.js will wait for MIDI input forever.
// In browsers the funcion is neither defined nor required.

JZZ API

MIDI Output/Input
JZZ().or('Cannot start MIDI engine!')
     .openMidiOut().or('Cannot open MIDI Out port!')
     .wait(500).send([0x90,60,127]) // note on
     .wait(500).send([0x80,60,0]);  // note off
JZZ().openMidiIn().or('Cannot open MIDI In port!')
     .and(function() { console.log('MIDI-In: ', this.name()); })
     .connect(function(msg) { console.log(msg.toString()); })
     .wait(10000).close();
Connecting MIDI nodes
var input = JZZ().openMidiIn();
var output = JZZ().openMidiOut();
var delay = JZZ.Widget({ _receive: function(msg) { this.wait(500).emit(msg); }});
input.connect(delay);
delay.connect(output);
Helpers and shortcuts
// All calls below will do the same job:
port.send([0x90, 61, 127]).wait(500).send([0x80, 61, 0]);   // arrays
port.send(0x90, 61, 127).wait(500).send(0x80, 61, 0);       // comma-separated
port.send(0x90, 'C#5', 127).wait(500).send(0x80, 'Db5', 0); // note names
port.noteOn(0, 'C#5', 127).wait(500).noteOff(0, 'B##4');    // helper functions
port.note(0, 'C#5', 127, 500);                              // another shortcut
port.ch(0).noteOn('C#5').wait(500).noteOff('C#5');          // using channels
port.ch(0).note('C#5', 127, 500);                           // using channels
Asynchronous calls
// in the environments that support async/await:
async function playNote() {
  var midi = await JZZ();
  var port = await midi.openMidiOut();
  await port.noteOn(0, 'C5', 127);
  await port.wait(500);
  await port.noteOff(0, 'C5');
  await port.close();
  console.log('done!');
}
// or:
async function playAnotherNote() {
  var port = await JZZ().openMidiOut();
  await port.noteOn(0, 'C5', 127).wait(500).noteOff(0, 'C5').close();
  console.log('done!');
}
Virtual MIDI ports
var logger = JZZ.Widget({ _receive: function(msg) { console.log(msg.toString()); }});
JZZ.addMidiOut('Console Logger', logger);

// now it can be used as a port:
var port = JZZ().openMidiOut('Console Logger');
// ...

// substitute the native MIDIAccess
// to make virtual ports visible to the Web MIDI API code:
navigator.requestMIDIAccess = JZZ.requestMIDIAccess;
Frequency / MIDI conversion
JZZ.MIDI.freq('A5'); // => 440
JZZ.MIDI.freq(69);   // => 440
JZZ.MIDI.freq(69.5); // => 452.8929841231365
// from frequency:
JZZ.MIDI.midi(440);  // => 69
JZZ.MIDI.midi(450);  // => 69.38905773230853
// or from name:
JZZ.MIDI.midi('A5'); // => 69

MIDI 2.0

MIDI2() is an adapter that enables MIDI 2.0 in all subsequent chained calls.
MIDI1() returns the operation back to MIDI 1.0.
Note that the downstream MIDI nodes don't require any special actions to receive and transfer MIDI 2.0 messages:

var first = JZZ.Widget();
var second = JZZ.Widget();
var third = JZZ.Widget();
first.connect(second);
second.connect(third);
third.connect(function (msg) { console.log(msg.toString()); });

first
  .send([0x90, 0x3c, 0x7f])       // 90 3c 7f -- Note On
  .MIDI2()                        // enable MIDI 2.0
  .send([0x20, 0x90, 0x3c, 0x7f]) // 20903c7f -- Note On
  .MIDI1()                        // reset to MIDI 1.0
  .send([0x90, 0x3c, 0x7f])       // 90 3c 7f -- Note On

When used with MIDI 2.0, most of MIDI 1.0 helpers require group as an additional first parameter
and produce MIDI 1.0 messages wrapped into UMP packages.
Most of the new MIDI 2.0 helpers don't have corresponding MIDI 1.0 messages.
Use gr(), ch() and sxId() calls to set default group, channel and SysEx ID for the subsequent calls.
MIDI2() and MIDI1() clear off default group, channel, SysEx ID and MPE settings:

first
  .noteOn(5, 'C5', 127)           // 95 3c 7f -- Note On
  .ch(5).noteOn('C5', 127)        // 95 3c 7f -- Note On
  .MIDI2()
  .noteOn(2, 5, 'C5', 127)        // 22953c7f -- Note On
  .gr(2).noteOn(5, 'C5', 127)     // 22953c7f -- Note On
  .ch(5).noteOn('C5', 127)        // 22953c7f -- Note On
  .MIDI2()
  .noteOn(2, 5, 'C5', 127)        // 22953c7f -- Note On
  .ch(5).noteOn(2, 'C5', 127)     // 22953c7f -- Note On
  .MIDI2()
  .umpNoteOn(2, 5, 'C5', 127)     // 42953c00 007f0000 -- Note On
  .gr(2).umpNoteOn(5, 'C5', 127)  // 42953c00 007f0000 -- Note On
  .ch(5).umpNoteOn('C5', 127)     // 42953c00 007f0000 -- Note On

More on MIDI 2.0 support...

Additional modules

Testing your MIDI application

Check the Getting Started page and the API reference for more information...

Thanks for your support!

Stargazers for @jazz-soft/JZZ

jzz's People

Contributors

abudaan avatar jazz-soft avatar justcfx2u avatar michaellavoie avatar pearcemerritt avatar richienb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jzz's Issues

Crash on OS X when virtual ports encountered

Great work on this library - it really is amazing.

Having an a crash when virtual ports have been created (either by Max/MSP or with a C++ JUCE application)

The issue seems to be in the _engine._refresh function (line 663)

    _engine._refresh = function() {
      _engine._outs = [];
      _engine._ins = [];
      var i, x;
-->      for (i = 0; (x = **_engine._main.MidiOutInfo(i)**).length; i++) {
        _engine._outs.push({ type: _engine._type, name: x[0], manufacturer: x[1], version: x[2] });
      }
      for (i = 0; (x = _engine._main.MidiInInfo(i)).length; i++) {
        _engine._ins.push({ type: _engine._type, name: x[0], manufacturer: x[1], version: x[2] });
      }

_engine._main.midiOutList() will list the correct ports - including the virtual ones.

However - when the for loop hits a virtual port and _engine._main.MidiOutInfo(i) is called, the program will crash. Happens in node JS with a simple JZZ() call.

const JZZ = require('jzz');

JZZ();

Any ideas?

Thanks!

OSX 10.12.6
Node 8.11.2

On OSX - you can drag this into a terminal window to create some virtual ports:

MM1UtilityAp.zip

Fails to detect nodeJS environment in the electron main process when using TypeScript and Webpack

When using jzz (and it's dependency jazz-midi) in an electron main process written in Typescript and transpiled with Webpack, a dynamic require found in JZZ.js does not work and the library fails to detect that it is in a NodeJS environment.

It is similar to the issue found at jazz-soft/jazz-midi#6

A workaround would be to apply the patch mentioned here as well as the following:

_initNode(require('jazz-midi')); // https://github.com/jazz-soft/JZZ/blob/master/javascript/JZZ.js#L575

should be changed to

const require_dynamic = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
_initNode(require_dynamic('jazz-midi'));

I'm not sure if typescript/webpack/electron issues are within the scope of this project, but I thought I would mention the fix I'm using.

TypeError with react-scripts 5.0.0 / webpack v5

I've been successfully using JZZ in a project created with Create React App (CRA). However, I just updated the CRA's react-scripts package that bundles a newer Webpack v5 (previously v4) and now I get this error when trying to initialize the engine:

TypeError: (jzz__WEBPACK_IMPORTED_MODULE_2___namespace_cache || (intermediate value)(intermediate value)) is not a function

As a result, the JZZ engine does not work. I noticed an earlier Webpack issue #50 but this is a different case.

Any ideas how to fix this?

JZZ().info().outputs returns empty list on macOS 13 Ventura ?

Tested macOS 10.15: OK return list of MIDI outputs

$ node -v
v19.1.0
$ node
> let JZZ = require('JZZ')
> JZZ().info().ver
'1.5.5'
> JZZ().info().outputs
[{...},{...},{...}]

Tested on macOS 13: NOK returns empty list

$ node -v
v19.1.0
$ node
> let JZZ = require('JZZ')
> JZZ().info().ver
'1.5.5'
> JZZ().info().outputs
[]

Query

I want to use jazz midi in a webview for ios, as I understand it is not supported, I have a query, is there a possibility to pass the list of usb midi devices in swift to jzz midi to process it and then send it to a midi output port in swift?

Erratic slow time on first JZZ() call (Node.js)

Tested on macOS Mojave 10.14.2 with Node.js 11.9.0 and JZZ.js 0.6.6 :

// jzz-test.js
var JZZ = require('JZZ');
JZZ().and('OK');

$ time node jzz-test.js 
OK

real	0m0.169s
user	0m0.119s
sys	0m0.032s

Repeated with similar times. But after a few seconds :

$ time node jzz-test.js 
OK

real	0m4.975s <<< slow time
user	0m0.117s
sys	0m0.032s

When JZZ() is not called, there is no such erratic slow time :

// jzz-test.js
var JZZ = require('JZZ');
// JZZ().and('OK');

$ time node jzz-test.js 

real	0m0.137s
user	0m0.103s
sys	0m0.031s

...
$ time node jzz-test.js 

real	0m0.141s
user	0m0.106s
sys	0m0.031s

Error when testing onChange

Hi sema,

I'm testing the onChange functionality you added for my request from a few months ago (thank you!) and keep getting this error when I unplug my MIDI-USB cable:

Uncaught TypeError: Cannot read property 'name' of undefined
at Object._engine._refresh (JZZ.js:975)
at JZZ.js:1107

This is using JZZ.js v0.4.4 but I have tried the same thing and am seeing the same error while using v0.4.9 and v0.5.0.

Can you please help?

Regards,
Mohsin

Virtual MIDI-In possible?

Hello!

I'm trying to extract data from various DJ software like Serato / Traktor / Mixxx and wanted to explore using MIDI ports to receive data.

Does JZZ support the creation of a MIDI device that these software packages would see as a wired controller, and hence allow me to map functions / scripts which I can eventually squirt through Chrome to a remote web service? The idea of a user having to install zero software - 100% browser based - is very attractive!

`JZZ()` call fails on WSL(1,2) and Ubuntu without sound devices

Reproduction Repository

https://github.com/lhl2617/jzz-wsl2-repro

Issue Summary

JZZ() call fails crashes on WSL2 without any catch-able exceptions.

Reproduction environment

Windows Version

Windows 10 Version 20H2 (OS Build 19042.867)

WSL2 Version

2

WSL Ubuntu Version

$ cat /etc/issue 
Ubuntu 20.04.2 LTS \n \l

$ uname -a
Linux <REDACTED> 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Node version

v12.22.1

Reproduction Steps

  1. Set up reproduction environment.
  2. Clone repository into WSL2.
  3. Run npm i
  4. Run npm start

Expected Behavior

Runs without any logs (succeeded), or fails gracefully with the exception caught.

Obtained Behavior

$ npm run start

> [email protected] start /home/$USER/experimental/jzz
> node index.js

ALSA lib seq_hw.c:466:(snd_seq_hw_open) open /dev/snd/seq failed: No such file or directory
node: seq.c:1880: snd_seq_query_next_client: Assertion `seq && info' failed.
Aborted
npm ERR! code ELIFECYCLE
npm ERR! errno 134
npm ERR! [email protected] start: `node index.js`
npm ERR! Exit status 134
npm ERR! 
npm ERR! Failed at the [email protected] start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/$USER/.npm/_logs/2021-05-14T11_23_57_411Z-debug.log

Node Module Version

The current jazz.node (jazz-midi dependency) is complied for Node Module Version 64 which when used with Electron limits to version 4.0.3.

If possible, provide a version complied for NMV 69 which would cover 4.0.4 and above.

Warning: The AudioContext was not allowed to start

First of all: thank you for this great library. I've been using it successfully both in Node.js and in browser ('webmidi') in a recent project of mine.

Even now everything works as expected but I still get the following warning messages in Google Chrome's developer console (in 'webmidi' mode):

The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu

At this point my code is only calling JZZ to get a list of MIDI inputs.

The three separate messages point to the lines 2206, 2210 and 2219 of JZZ.js. I've read the link in the message but I'm not sure if this is something I can just ignore or is there something I could do to fix it?

Double connections on Windows

Connecting to a Hardware MIDI Port from 2 different applications has always been a limitation of Windows.
For example "Ableton Push 2 Live Port" will always connect to Ableton Live. This means that you get a connection error if you try to access it. On OSX, this isn't an issue at all.

Is this some safety feature that can be toggled off, so JZZ and Live can talk to the same port?
Or is this one of those things that the MMA needs to fix with MIDI 2.0?

Cheers,
Bjorn

MIDI out send not working

using Mac OS 10.11.6 & node v10.1.0
MIDI out does not seem to work.
I even tried the example

var JZZ = require('jzz');
var midiout = JZZ().or('Cannot start MIDI engine!')
.openMidiOut()
console.log(midiout.info())
midiout.or('Cannot open MIDI Out port!')
.wait(500).send([0x90,60,127])
.wait(500).send([0x90,64,127])
.wait(500).send([0x90,67,127])
.wait(500).send([0x90,72,127])
.wait(1000).send([0x90,60,0]).send([0x90,64,0]).send([0x90,67,0]).send([0x90,72,0])
.and('thank you!');

and verified that it logged the correct output device (IAC Driver 1). But nothing shows in MIDITrail.
A browser web app (Multiplayer Piano) connect to the same device, does work though.

chrome os?

trying to get a usb midi keyboard to work on my chrome os and i was wondering if this is possible?

Using web worker to keep playing when window is not focused

Just an idea, maybe it's out of scope for this library, so feel free to close this :)

Both setInterval and requestAnimationFrame have issues for midi playback, they both don't get called often enough when the window is not focused. (requestAnimationFrame doesn't get called at all in chrome, and setInterval gets called only once per second or so.)
This is really annoying when switching from the midi-playing/looping browser window to the DAW where the live midi is being processed, it really disturbs the whole workflow :/

Web workers allow running a loop in a separate thread that runs even when the window isn't focused:
https://mortenson.coffee/blog/making-multi-track-tape-recorder-midi-javascript/

But setInterval is extremely unreliable, especially as users change tabs and reallocate resources. To make things more consistent, I created a new Web Worker just for the timing code so that it runs in its own thread. Web Workers also have more consistent performance when the page doesn't have focus.

In the above blog post, the author had good results with a web worker. But I would write the tick() function differently:
Instead of hoping that the time between calls stays constant, and trying to adjust the next timeout value based on deviation, I would use performance.now() to calculate the number of milliseconds since the last call, and then calculate how many ticks passed based on that, also storing the fractional tick remainder to add it to the number of ticks for the next frame, like this:

pub fn frame(&mut self, timestamp: DOMHighResTimeStamp) {
	let start_timestamp = *self.m_start_timestamp.get_or_insert(timestamp);
	if let Some(prev) = self.m_prev_timestamp && prev < timestamp {
		let elapsed_since_prev_ms = timestamp - prev;

		// Sent midi beat clock 24 times per beat
		let cur_midi_beat_clock_frame = (self.playhead_tick as u64 * 24 / self.smf.ticks_per_beat as u64) as i32;
		if self.clock_enabled {
			// Potentially send multiple clock msgs per step if many ticks passed since last call
			for _ in self.midi_beat_clock_frame .. cur_midi_beat_clock_frame {
				// Send clock msg
				send_live(&self.midi_output, LiveEvent::Realtime(SystemRealtime::TimingClock));
			}
		}
		self.midi_beat_clock_frame = cur_midi_beat_clock_frame;

		let timeframe = self
			.timeframe_fract
			.process(elapsed_since_prev_ms as f64 * self.bpm * (self.smf.ticks_per_beat as f64 / (60. * 1000.)));

		let events = self.smf.events_window(self.playhead_tick, timeframe);
		self.playhead_tick += timeframe;
		self.callback.emit(PlayerStateChange::Playhead(self.playhead_tick));

		for event in events {
			// Send event to midi output
			match event.event {
				PlayerEvent::LiveEvent(e) => {
					send_live(&self.midi_output, e);
				}
				PlayerEvent::Bpm(bpm) => {
					self.bpm = bpm;
				}
			}
		}

		if let Some(last) = self.smf.events.last() && last.time < self.playhead_tick {
			// Finished playing
		}
	}
	self.m_prev_timestamp = Some(timestamp);
}

timeframe_fract is of type TimeframeWithFract to account for fractional tick remainder, defined like this:

// Return int timeframe but add unused fractional part to next frame's timeframe
pub struct TimeframeWithFract {
	fract: f64,
}

impl TimeframeWithFract {
	pub fn process(&mut self, timeframe: f64) -> PlayerTickTime {
		let timeframe_f = timeframe + self.fract;
		let timeframe_i = timeframe_f as PlayerTickTime;
		self.fract = timeframe_f - timeframe_i as f64;
		timeframe_i
	}
}

What do you think? :)

If it's out of scope for this library, would it be possible to let user code handle the timing, so that a user can write their own web worker which sends midi messages to the JZZ player in the main app (currently the Web Midi API is not accessible for web workers, but they can be used for timing, but they have to send messages to the main app which can then send midi out.
If I wanted to do this with JZZ, would it be possible with the JZZ GUI Player? So that I can have its GUI functionality and API, but let the web worker do the timing. Like inversion of control, my app would call JZZ to advance its state, instead of JZZ using setInterval, would that be possible somehow? :)
I'm asking because this issue is very important to me, I really need midi playback continuing when working in the DAW while the browser window is not focused.

Incorrect Octave being triggered

Hi Jazz-Soft,

Just want to thank you for this awesome library!

I have picked up an issue that I can recreate over multiple environments + devices.

Using code suggested on the repo I have done a test as follows (I did not include all the code I used to keep it short):

    await port.noteOn(1, 'C2', 127).wait(500).noteOff(1, 'C2',0).close();

What is returned is the incorrect octave for the note that is being triggered.

You will see in the images that I have console.log the note being sent and the note received by MIDI Monitoring software.

Test 1:

MIDI-OX: -1 Octave.
unknown

Test 2: (Different PC)

Midi Viewer: -2 Octave.

image

Prevent hanging notes when switching ports in the middle of playback

I noticed whenever I switch the JZZ Player between midi and audio output, I get hanging notes.
Using this code, calling toggle_output_audio_midi() to switch ports:

import JZZ from "jzz";
import jzz_midi_smf from "jzz-midi-smf";
import jzz_gui_player from "jzz-gui-player";
import jzz_synth_tiny from "jzz-synth-tiny";

jzz_midi_smf(JZZ);
jzz_gui_player(JZZ);
jzz_synth_tiny(JZZ);

export class JzzPlayer {
	constructor() {
		JZZ.synth.Tiny.register("Web Audio");
		this._player = new JZZ.gui.Player({});
		this._preferred_ports = [
			JZZ().openMidiOut("Web Audio"), 
			JZZ().openMidiOut("loopMIDI Port"),
		];
		this._usingMidiPortInsteadOfAudio = false; // Use Web Audio initially
		this.connect_to_preferred_port(false);
	}
	load(smf_data) {
		this._player.load(new JZZ.MIDI.SMF(smf_data));
	}
	play() {
		this._player.play();
	}
	connect_to_preferred_port(disconnect_other) {
		if(disconnect_other) {
			this._player.disconnect(this._preferred_ports[+!this._usingMidiPortInsteadOfAudio]);
		}
		this._player.connect(this._preferred_ports[+this._usingMidiPortInsteadOfAudio]);
	}
	toggle_output_audio_midi() {
		this._usingMidiPortInsteadOfAudio ^= true;
		this.connect_to_preferred_port(true);
		return this._usingMidiPortInsteadOfAudio;
	}
}

How to prevent hanging notes?

Previously I wrote a midi player myself, and what I did there was:

  • Track how the output port's state changes based on sent midi events. Basically applying outgoing midi events to change the state represented by a data structure like this:
pub struct ChannelState {
	ccs: [u8; 128],
	program: u8,
	pitch_bend: u16,
	notes_on: [u8; 128], // 0 == off
	// TODO: keep track of ChannelAftertouch, PolyphonicAftertouch, RPN settings
}
pub struct PortState {
	channels: [ChannelState; 16],
}

And whenever playback is paused/resumed or the player jumps (e.g. because of looping), the "diff" between the current and desired port state is computed in terms of midi events that would need to be sent to set the output port equal to the desired port state, and the diff is then sent.
When switching ports, in terms of midi diff it's equivalent to pausing (sending the diff(playing_state, default_port_state)), then switching output ports, then resuming (sending the diff(default_port_state, playing_state)).
When jumping, I was sending the diff(playing_state, state_at_jump_target_tick), where state_at_jump_target_tick is computed by "integrating" (applying to a default_port_state) all midi msgs since the beginning of the SMF untiljump_target_tick. (For loops, the state_at_jump_target_tick` gets repeatedly used so in theory it could even be cached but for small midi files it's cheap to recompute on every loop jump.)

I'm curious if this kind of port state management would be possible with JZZ's existing callbacks/hooks.
I can register my own port state tracker (like the "Console Logger" example) but when playing a SMF file, how can I send these diffs as if from the JZZ player to the output port without them going through my custom output AND without messing up JZZ Player's internals?

Ideally, the midi signal flow would be: JZZInternalPlayer -> PortStateTracker -> MidiOut
But currently it's JZZInternalPlayer -> MidiOut and if I register my own midi output, it would additionally be JZZInternalPlayer -> MyMidiOut. So if I have my PortStateTracker in MyMidiOut and then send messages "through" the player, MyMidiOut will receive them, too, which messes up the tracker because it's supposed to track the playing state.
(Also, how to actually send messages "through the player when it's playing a SMF file?)

If there is no good way to add this in user code using the API, it would make sense to have a built-in way of preventing hanging notes and hanging/lingering CCs, Program & pitchbend (!).

The easiest solution would be to just reset the output port like this before disconnecting, although this sends more messages than necessary which takes more time (depends on the midi port/device):

// Reset notes, CCs, program, pitchbend
fn reset_port(midi_output: &MidiOutput) {
	use midly::{num::*, MidiMessage, PitchBend};

	const CC_ALL_SOUND_OFF: u8 = 120;
	const CC_RESET_ALL_CCS: u8 = 121;

	for ch in 0 .. 16 {
		let channel = u4::new(ch);
		send_live(midi_output, LiveEvent::Midi {
			channel,
			message: MidiMessage::ProgramChange { program: u7::new(0) },
		});
		send_live(midi_output, LiveEvent::Midi {
			channel,
			message: MidiMessage::PitchBend { bend: PitchBend(u14::new(0x2000)) },
		});
		send_live(midi_output, LiveEvent::Midi {
			channel,
			message: MidiMessage::Controller { controller: u7::new(CC_ALL_SOUND_OFF), value: u7::new(0) },
		});
		send_live(midi_output, LiveEvent::Midi {
			channel,
			message: MidiMessage::Controller { controller: u7::new(CC_RESET_ALL_CCS), value: u7::new(0) },
		});
	}
}

The port state tracking would have additional advantages because when connecting to a port while notes are on, they would also be sent, not just new NoteOn msgs. (I did that in my midi player, except for drum notes. This is also useful for having a non-full-song loop.)

It would also be useful to have an optional port-reset button on the player (that does something like reset_port() above).

Using with webpack

I get the following error when I try to bundle the JZZ module with latest WebPack:

wp

It seems that it's originating from the node check here

As far as I get it, currently JZZ is not exported as ES module and it fails to do so because of the require.
Any plans to do that?

Issue capturing control change messages

Hello,

First off I want to thank you for your hard work with this library. It has been very easy to use so far and I appreciate the work you've put into the docs. I've stumbled across a problem and I'm not sure if it's my doing, or if there's something missing.

I am using JZZ in NodeJS and am able to capture Midi messages using the JZZ.Widget class connected to an external Midi hardware interface. All of the Note On, Note Off, Timing Clock, Start and Stop messages are captured and logged to my console without a problem.

const input = await JZZ().openMidiIn(0);

input.connect(JZZ.Widget({
  _receive: function(msg) {
    console.log(msg.toString());
  }
});

The problem is when I move knobs or faders on my Midi keyboard. Only a single message is captured for all of the control change messages. For example, here I pressed a few keys and then moved a few different knobs and faders. Notice there's only a single event logged, even though I interacted with multiple CC controls.

96 24 5a -- Note On
86 24 40 -- Note Off
96 25 60 -- Note On
86 25 59 -- Note Off
96 26 55 -- Note On
86 26 4d -- Note Off
96 27 4c -- Note On
86 27 54 -- Note Off
96 28 49 -- Note On
86 28 54 -- Note Off
b1 49 3f -- Sound Controller 4

I would expect to see a large number of messages with multiple CC param and values changing.

I am using Node v12.16.1. This is the info I get from JZZ().info():

{
  name: 'JZZ.js',
  ver: '1.0.3',
  version: '1.5.2',
  inputs: [...truncated...],
  outputs: [...truncated...],
  engine: 'node',
  sysex: true
}

Please let me know if there's some configuration I'm missing, or if there are any more details I can provide you to help troubleshoot.

Best,
Chris

Midi input not working on Chromebook

Hi Sema,

I wanted to go ahead and report this issue. I tested using the latest version of the code and still cannot get input events to work on a Chromebook :(

Regards,
Mohsin

Playing midi files with more than 16 tracks on multiple ports

Hi,
I have a midi file with 32 tracks that i want to play using this library.
Using this code:

const JZZ = require('jzz');
const fs = require('fs');
require('jzz-midi-smf')(JZZ);

const filename = 'END.MID';
const midiout = JZZ().openMidiOut(3);
const data = fs.readFileSync(filename, 'binary');
const smf = new JZZ.MIDI.SMF(data);
const player = smf.player();

player.connect(midiout);
player.play();

only the first 16 tracks are played, which is expected since one midi port only can handle 16 tracks.
Is there any way i can open 2 out ports and play track 1-16 in port1 and track 17-32 in port2?

Thanks!

How to send MIDI messages to other Software?

I want to send MIDI messages to Adobe Lightroom Classic's Plugin "MIDI2LR" (or for debugging just any MIDI logger like "MIDI Monitor" on macOS)

But neither the Lightroom Plugin is reacting to the message I'm sending nor is "a new device" showing up in MIDI Monitor.

I'm intending to use this in a Nuxt 3 Application. Here is my page component:

<template>
  <div class="midi-out-test">
    <button @click="sendMidi()">
      Send some Midi
    </button>
  </div>
</template>

<script>
import * as JZZ from 'jzz';

export default {
  name: 'MidiOutTest',

  mounted() {
    this.output = JZZ().openMidiOut('Frankensteins Controller');
  },

  methods: {
    sendMidi() {
      this.output.control(1, 1, 127);
    },
  },
};
</script>

Receiving MIDI to manipulate my application works already on a different page in this project, so I guess JZZ is working properly.

How can I make messages go out of the browser to said applications?

Thanks!

EDIT: My op1 field (pocket synth connected via bluetooth) just played a note while working on a solution. I crapped myself :D:D – So apparently MIDI is sent. But it's still not picked up by either MIDI2LR or MIDI Monitor. So I have to "create and register a device somehow"?

Deno support

Hi,

I know this is a challenging request. Currently, JZZ.js supports both browsers and Node.js.

Deno is approaching 1.0 version and it may be a good time to migrate JZZ.js to Deno.

Thanks,
Pierre

onChange not triggered after screen refresh

Hi sema,

onChange works perfectly when everything loads afresh but when I disconnect the cable and refresh my page, I can't get the onChange to trigger when I then try to plug the cable in/out.

_openMidiIn() isn't getting called when I call openMidiIn() after the refresh and I can't seem to be able to figure out a way to do that.

My thinking is that if I can get JZZ to be initialized from scratch when I refresh the page and I can start from a known state for input MIDI ports then this should work but maybe you can suggest a better way.

I have tried JZZ().close() before calling JZZ().openMidiIn() but that didn't do the trick.

As always, thank you so much for such a useful tool and for being so quick with your replies. I'm so excited that you've implemented this feature!

Regards,
Mohsin

Problem using ports that have the same name

I have 4 USB/Midi keyboards - 2 using MIO's and 2 using Arduino's. They are all functional with other software.

JZZ sees all the devices, but only connects to the first device of each type no matter with port # I give it.

Below is the list of ports from JZZ().info().inputs

0: {id: "Midi Through Port-0", name: "Midi Through Port-0", manufacturer: "", version: "ALSA library version 1.1.9", engine: "webmidi"}
1: {id: "mio MIDI 1", name: "mio MIDI 1", manufacturer: "iConnectivity", version: "ALSA library version 1.1.9", engine: "webmidi"}
2: {id: "Arduino Due MIDI 1", name: "Arduino Due MIDI 1", manufacturer: "Arduino LLC", version: "ALSA library version 1.1.9", engine: "webmidi"}
3: {id: "Arduino Due MIDI 1", name: "Arduino Due MIDI 1", manufacturer: "Arduino LLC", version: "ALSA library version 1.1.9", engine: "webmidi"}
4: {id: "mio MIDI 1", name: "mio MIDI 1", manufacturer: "iConnectivity", version: "ALSA library version 1.1.9", engine: "webmidi"}

I can successfully use the below code to display midi events from ports 1 or 2. But when I try port 3 or 4, they get the events from 1 or 2.

function onRecord() {
console.log("Recording on port 3");
JZZ().openMidiIn(3).or('Cannot open MIDI In port!')
.and(function() { console.log('MIDI-In: ', this.name()); })
.connect(function(msg) { console.log(msg.toString()); })
.wait(10000).close();
}

If I physically unplug devices 1 & 2 then I can get midi events from devices 3 & 4.
Again, when using them with other midi programs they all work together fine.

I suspect that this may have something to do with all of them being assigned the same "id".

Environment:
Linux Ubuntu 20.04
JZZ current version from CDN (jsdelivr)
Google Chrome: Version 89.0.4389.72 (Official Build) (64-bit)
Jazz-MIDI plugin for Chrome: Version 1.0.2.0

How to temporarily stop midi input?

Hi,

I am using jzz Web Midi API in node.js, however I can't find a way to stop midi input temporarily.

Closing the midi input ports does change the port.connection state to 'closed' but still keeps passing midi input messages to port.onmidimessage.
Calling navigator.close() stops the entire midi engine, but after that I cannot get it to work again: a new navigator.requestMIDIAccess() fails from then on.

Any ideas?

JZZ and WebMidi not working on iOS

Hi Sema,

I’ve been trying to get midi hooked up to Chrome on iOS and have not had any luck so far. Before I waste your time and money again like I did for the Chromebook (I feel terrible about that) I’d like to get you as much information as I can to make sure it’s not just something I’m missing.

Here’s what I see on your webmidi test page:

Cannot start WebMIDI: TypeError: navigator.requestMIDIAccess is not a function. (In 'navigator.requestMIDIAccess()', 'navigator.requestMIDIAccess' is undefined)

Is there something else I can try?

"Await"ing JZZ fails to enumerate MIDI devices

I tried to use JZZ in TypeScript by the documentation,
Trying to call JZZ() yields This expression is not callable.

import * as JZZ from 'jzz';

Experimentally, I changed the import to the following:

import JZZ from 'jzz';

// ...

let x = await JZZ().or('oh no!').and('Everything is good!');

Written like this, I am able to use JZZ().

However, x.info().outputs and x.info().inputs are empty even though "Everything is good!" is logged into the console. It seems that there are some internal errors inside JZZ.

screenshot 1

The above failure was caused while in useEffect of a React functional component.

const App: React.FC = () => {
  useEffect(() => {
    async function initJZZ() {
      let x = await JZZ().or('oh no!').and('Everything is good!');
      console.dir(x.info());
      console.dir(x);
    }
    initJZZ();
  }, []);

  return (<p>JSX stuff here</p>);
}

However, referencing issue #6, I put in some raw WebMIDI code in useEffect() and it seems to have worked.

  useEffect(() => {
    let webmidi:any;

    function refresh() {
      console.log(["wew", webmidi.inputs, webmidi.outputs]);

      console.log('---------');
      console.log('OUTPUTS');
      webmidi.outputs.forEach(function(port:any) {
        console.log('    ' + port.name +
          '; manufacturer: ' + port.manufacturer +
          '; version: ' + port.version +
          '; state: ' + port.state);
      });
      console.log('---------');
      console.log('INPUTS');
      webmidi.inputs.forEach(function(port:any) {
        console.log('    ' + port.name +
          '; manufacturer: ' + port.manufacturer +
          '; version: ' + port.version +
          '; state: ' + port.state);
      });

    }
    function fail(err:any) {
      var s = 'Cannot start WebMIDI';
      if (err) s += ': ' + err;
      console.log(s);
    }
    
    function success(midiaccess:any) {
      webmidi = midiaccess;
      // webmidi.onstatechange = refresh;
      refresh();
    }
    
    try {
      navigator.requestMIDIAccess().then(success, fail);
    }
    catch (err) {
      console.log('Cannot start WebMIDI: ' + err);
    }
  }, []);

screenshot 2

I can only assume that something in JZZ doesn't like TypeScript or how it's referenced within React.

The end goal is either a PWA (Progressive Web Application) hosted on Firebase and/or an Electron app written in React to configure a specific MIDI device using SysEx.

use Gamepad peripherals for Jazz midi

Hello! What do you think of the idea of implementing gamecontroller.js or something similar, to use certain features of a midi gamepad, such as the Analog for pitch bend, just an idea

#<DOMException> when setting onmidimessage

Hello,

In a node.js environment when I set onmidimessage I get:
(node:59882) UnhandledPromiseRejectionWarning: #< DOMException >

var navigator = require('jzz');

const onSuccess = (midiAccess) => {
  for (const input of midiAccess.inputs.values()) {
    input.onmidimessage = console.log;
  }
};

const onFail = console.log;
navigator.requestMIDIAccess().then(onSuccess, onFail);

Tested on win and mac.
node: v12.18.0

@jazz-soft

Uncaught (in promise) TypeError in 1.2.9

After updating to version 1.2.9 (from 1.2.8) I get this message at the console:

JZZ.js:899 Uncaught (in promise) TypeError: self._resume is not a function at onGood (JZZ.js:899)

JZZ on iOS

Hi Sema,

This is more of a question than a problem. Have you been able to successfully test JZZ on iOS? What kind of hardware did you use to connect an instrument to the device?

Regards,
Mohsin

Typo in JZZ.MIDI manual (smfTimeSignature)

Thank you for your useful libraries!

I found a typo in the manual for smfTimeSignature(): https://jazz-soft.net/doc/JZZ/jzzmidi.html

Original:
..., where nn is a time signature numerator, dd - time signature numerator, where nn is a time signature denominator expressed as a power of 2 ...

Fixed:
..., where nn is a time signature numerator, dd - time signature denominator, where dd is a time signature denominator expressed as a power of 2 ...

Correct way to enable Midi sysex permission?

Hi. I am enabling the security for sysex, but still getting an error.
Verified in electron fiddle and built with electron react:

main process:
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})

// and load the index.html of the app.
mainWindow.loadFile('index.html')

const { session } = require('electron');
session
  .fromPartition('persist:cmx800')
  .setPermissionRequestHandler((webContents, permission, callback) => {
	if (permission === 'midiSysex') {
	  // Approves the permissions request
	  callback(true);
	}
});

})
}

renderer:

MIDIengine = require('jzz');
MIDIengine.requestMIDIAccess( { sysex: true } ).then(onMIDISuccess, onMIDIFailure);

then later in my success function:

if (port.name == strCMX800) {
	output = MIDIengine().openMidiOut(strCMX800)
						 .or(function(){ alert('Cannot open MIDI output\n' + this.err()); })
						 .wait(500).sxIdRequest();
}

Error:

Uncaught DOMException: Failed to execute 'send' on 'MIDIOutput': System exclusive message is not allowed at index 0 (240).

Sending synchronized midi clock to midi port

Hi, thanks for this great library! I'm using it to send midi to VCV Rack, and I need to send clock messages in sync with the midi playback at 24 clocks per beat, using the Impromptu clocked module as slave in VCV like this:

image

But JZZ Player currently doesn't send any clock. Is there a way to register a custom callback/hook for every time the midi player advances its state, so that I can pass a function that sends clock signals when each next clock frame is reached?

Before switching to JZZ, I was using my own code (Rust compiled to wasm) where I was successfully sending clock like this (called in my player's step function, which accounts for any number of ticks being passed since the last call):

// Sent midi beat clock 24 times per beat
let cur_midi_beat_clock_frame = (self.playhead_tick as u64 * 24 / self.smf.ticks_per_beat as u64) as i32;
// Potentially send multiple clock msgs per step if many ticks passed since last call
for _ in self.midi_beat_clock_frame .. cur_midi_beat_clock_frame {
	// Send clock msg
	send_live(&self.midi_output, LiveEvent::Realtime(SystemRealtime::TimingClock));
}
self.midi_beat_clock_frame = cur_midi_beat_clock_frame;

(The initial value of midi_beat_clock_frame is -1 so that it already sends the first clock on tick 0.)
That way, I can have my other modules (e.g. delay or rhythmic stuff) in VCV synced to my midi file's grid.
JZZ seems great, it even properly takes into account tempo change meta messages, which other JS midi players don't do, and it seems it also responds to RPNs for setting pitchbend range, which is great. Also I like that it supports all GM instruments, timidity doesn't.
Just the one thing missing is the midi clock :)
I'm not suggesting sending clock by default, but it could be either built-in/optional (with constructor option flag) or via a custom onStep callback (such that the callback can access the player instance and send a msg to it that will get sent still in this current frame!).

Btw, I also like that JZZ Player supports jumping to a tick position, other js midi players don't.
In my use case, I set a custom loop (not looping the full song but custom section) so I need this jumping functionality.
Although I noticed if the loop len is not a beat/bar multiple, it messes up the clock sync in VCV.
So I need to figure out how to reset the clock properly on a jump. Maybe by sending a clock stop message when the playhead reaches the loop end and sending a clock start message when after jumping the playhead reaches a beat or bar boundary (in most cases, the jump target tick is such a boundary because loop len will be quantized to beats/bars).
If you know anything about midi clock, I'd appreciate if you could tell me how to handle this situation properly :)
But this is only my use case, I don't expect JZZ to support non-full-song looping because I can implement it on top of its API.
I just mention this use case because it affects how the clock messages need to be sent.

How do I output to OmniMIDI?

I am trying to output to a synth called OmniMIDI, doing
JZZ().openMidiOut('OmniMIDI') results in no sound, and so does JZZ().openMidiOut(1)...
Any ideas on how to get around this?

Using plugins with ES6 import syntax?

I'm using JZZ with ES6 syntax (import JZZ from "jzz";) and would like to use some plugins like JZZ-synth-Tiny, however it only lists a CommonJS import statement.

Is it possible to import and use these plugins using ES6 syntax?

JZZ not seeing any MIDI devices on Linux

using Ubuntu 18.04
with a console.log(JZZ().info())
no MIDI devices are listed
even though aplaymidi -l shows all MIDI devices
including the TiMidity port 0 which I want to connect to.
image

😢

edit: also ran npm test on jazz-midi

$ npm test

> [email protected] test /home/op/mppaudio2discordvoice/node_modules/jazz-midi
> node test.js

Node.js version: 8.11.3
Package version: 1.6.4
Jazz-MIDI version: 1.5
isJazz: true
midi.out: true
midi.in: true
Supported functions:
[ 'ClearMidiIn',
  'MidiInClose',
  'MidiInInfo',
  'MidiInList',
  'MidiInOpen',
  'MidiOut',
  'MidiOutClose',
  'MidiOutInfo',
  'MidiOutList',
  'MidiOutLong',
  'MidiOutOpen',
  'MidiOutRaw',
  'OnConnectMidiIn',
  'OnConnectMidiOut',
  'OnDisconnectMidiIn',
  'OnDisconnectMidiOut',
  'QueryMidiIn',
  'Support',
  'Time' ]
MIDI-Out ports:
[]
MIDI-In ports:
[]

=== MIDI-Out test ===
No MIDI-Out ports found.

=== MIDI-In test ===
No MIDI-In ports found.

Thank you for using Jazz-MIDI!

edit: updated to node v10.7.0, ditto

Segmentation fault: 11 when closing other MIDI-Applications (like Koala-Sampler or DLS-Midi Synth)

Node.js exits with a "segmentation fault: 11"-Error when closing a midi-application whose (virtual) midi-ports where already recognized by JZZ's watcher.

This happens when JZZ.js runs within my node application, then I open e.g. the DLS-Midi Synth App on Mac and the port 'DLS-MIDI-Synth Virtual In' shows up in JZZ's list of available ports. Now, when I close the DLS-Midi Synth App, my node application exits with the segmentation fault.

There seems to be a problem with the watcher and the MidiOutInfo method calling the compiled jazz-midi/bin/11_15/macos64/jazz.node gyp. And it seems to be related to virtual ports that weren't created by JZZ.

Reinstalling all node_modules didn't fix it.

Running node.js v16.1.0 on Mac 10.14.6. Here the log:

PID 15144 received SIGSEGV for address: 0x0
0   segfault-handler.node               0x000000010e172006 _ZL16segfault_handleriP9__siginfoPv + 310
1   libsystem_platform.dylib            0x00007fff5b9a5b5d _sigtramp + 29
2   CoreFoundation                      0x00007fff8a9618f0 __kCFAllocatorSystemDefault + 0
3   jazz.node                           0x000000010e1a4d15 _ZN11CMidiMacOSX11MidiOutInfoEPKw + 245
4   jazz.node                           0x000000010e198c0d _Z11MidiOutInfoP10napi_env__P20napi_callback_info__ + 541
5   node                                0x000000010923168a _ZN6v8impl12_GLOBAL__N_123FunctionCallbackWrapper6InvokeERKN2v820FunctionCallbackInfoINS2_5ValueEEE + 122
6   node                                0x0000000109463275 _ZN2v88internal25FunctionCallbackArguments4CallENS0_15CallHandlerInfoE + 613
7   node                                0x0000000109462848 _ZN2v88internal12_GLOBAL__N_119HandleApiCallHelperILb0EEENS0_11MaybeHandleINS0_6ObjectEEEPNS0_7IsolateENS0_6HandleINS0_10HeapObjectEEESA_NS8_INS0_20FunctionTemplateInfoEEENS8_IS4_EENS0_16BuiltinArgumentsE + 824
8   node                                0x0000000109461dff _ZN2v88internalL26Builtin_Impl_HandleApiCallENS0_16BuiltinArgumentsEPNS0_7IsolateE + 255
9   node                                0x0000000109ceb7b9 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit + 57
10  node                                0x0000000109c861e5 Builtins_InterpreterEntryTrampoline + 197
Segmentation fault: 11

TypeScript type access

Hello,
I'm currently working on a TypeScript project, but I have a problem: I can't export the types.
This causes a problem during compilation: I can't specify a type to which I don't have access.
My goal is simply to store an instance of a port in a class variable.
Here is an example :

import jzz from 'jzz'

class Midi {
  private _port = jzz().openMidiOut();
  private _midiOut: typeof _port; // <= This is the only solution I could find to get around the problem

  ...

  public get out() { // <= And now it's here that I have an error because of the type.
    return this._midiOut;
  }

I can indeed specify the any type to allow me to override this error, but it bothers me for the sake of clarity.
Thanks in advance for your help!

Safari Compatibility?

Hi

JZZ.js does not work in my Safari (im on BigSour 11.4).
Did I miss something? There is a Safari icon in the readme, but I always get Cannot open MIDI In port!

Is there a way to receive MIDI messages on Safari?
Or is there something else I can try?

Audio rendering can be wrong (web browser)

Hello,

When playing a midi file from https://jazz-soft.net/demo/ReadMidiFile.html (which throws a 406 error these last days) and then playing another one without reloading the page, the audio rendering can sometimes be wrong. This does not happen for every midi file and even for a given file thiw will not happen every time (so maybe the issue comes out when the engine plays a specific note and the user, at the same time, loads another file).

By "wrong audio rendering", I mean the following items can happen:

  • the notes being played are not the ones you would expect but they are all in a "consistent" way - eg. all A notes become B notes, B notes become A, etc., so actually the music is still recognizable, just odd.
  • an extra instrument seems to be playing.

I was able to reproduce the last case (tried in in Firefox and Chrome) every time so I give the steps to do so:

  • Get the 2 midi files attached to this report (midiFiles.zip).
  • On the ReadMidiFile page, play from file "musicB.mid" (a short sound effect from Mario game), remember the sound.
  • Now (you can reload or not the page), play from file "musicA.mid" (music from LBA2 game). Wait about 25 seconds until many instruments are being played.
  • Finally (do not reload the page), play again from file "musicB.mid" and notice the rendering not what you've heard before.

Thank you.
midiFiles.zip

The argument `dd` of `smfTimeSignature()` doesn't seem to be a power of 2

The document (https://jazz-soft.net/doc/JZZ/jzzmidi.html) says:
smfTimeSignature(nn, dd, cc, bb) - time signature; returns FF58 04 nn dd cc bb, where nn is a time signature numerator, and dd - time signature denominator expressed as a power of 2 (e.g. nn=6, dd=3 would mean 6/8). Optional parameters: ...

However, the following code (nn=4, dd=4) outputs 4/4 (this should be 4/16 if dd is a power of 2):

console.log(JZZ.MIDI.smfTimeSignature(4, 4).toString()); // => ff58 -- Time Signature: 4/4 24 8

Also, the following code (nn=6, dd=3) throws RangeError exception:

console.log(JZZ.MIDI.smfTimeSignature(6, 3).toString()); // => Uncaught RangeError: Wrong time signature: : 6
                                                         //    at smfTimeSignature (JZZ.js:1933)

The version of JZZ: 1.2.5

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.