Skip to content

Commit 9a5545e

Browse files
authored
Merge pull request #83 from nnirror/clientside
v1.4.0
2 parents c6ebaad + 8414276 commit 9a5545e

File tree

9 files changed

+175
-27
lines changed

9 files changed

+175
-27
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,65 @@ You need to connect the MIDI device you want to use before starting Facet.
258258
- _Note_: This method is automatically scaled into the expected range for MIDI pitchbend data. It expects a FacetPattern of values between -1 and 1, with 0 meaning no pitchbend.
259259
- example:
260260
- `$('example').sine(1).size(128).pitchbend();`
261+
---
262+
- **savemidi** (_midifilename_ = Date.now(), _velocityPattern_ = 64, _durationPattern_ = 16, _wraps_ = 1, _tick_mode_ = false_)
263+
- creates a MIDI file of MIDI notes named `midifilename` in the `midi` directory, with the FacetPattern's data.
264+
- `VelocityPattern` and `DurationPattern` will automatically scale to match the note pattern. This allows you to modulate MIDI velocity and duration over the course of the whole note.
265+
- The `velocityPattern` expects values between 1 and 100. (This range is set by the midi-writer-js npm package).
266+
- The `wraps` parameter controls how many times to wrap the data back around onto itself, allowing for MIDI polyphony. For example, if your FacetPattern has 8 different 128-note patterns appended together in a sequence, a `wraps` of 8 would superpose all of those patterns on top of each other, while a `wraps` value of 4 would produce four patterns on top of each other, followed by four more patterns on top of each other.
267+
- When `tick_mode` is set to a truthy value, the numbers in `durationPattern` represent the number of ticks to last, rather than a whole-note divisions. 1 tick = 1/256th note. This allows for durations smaller and larger than the valid duration values for when `tick_mode` is set to false.
268+
- When `tick_mode` is set to false or excluded from the command, the following values are the only valid `durationPattern` argument values:
269+
```javascript
270+
1: whole note
271+
2: half note
272+
d2: dotted half note
273+
dd2: double dotted half note
274+
4: quarter note
275+
4t: quarter triplet note
276+
d4: dotted quarter note
277+
dd4: double dotted quarter note
278+
8: eighth note
279+
8t: eighth triplet note
280+
d8: dotted eighth note
281+
dd8: double dotted eighth note
282+
16: sixteenth note
283+
16t: sixteenth triplet note
284+
32: thirty-second note
285+
64: sixty-fourth note
286+
```
287+
- example:
288+
- `$('example').noise(64).scale(20,90).key('c major').savemidi(ts(),64,16).once(); // 64 random notes in c major at 64 velocity, each lasting a 16th note`
289+
- `$('example').noise(64).scale(20,90).key('c major').savemidi(ts(),_.noise(64).scale(1,100),4,1,true).once(); // 64 random notes in c major, each with a random velocity between 1 - 100, each lasting 4 ticks`
290+
- `$('example').iter(8,()=>{this.append(_.sine(choose([1,2,3,4])).size(128).scale(ri(30,50),ri(60,90)).key('c major'))}).savemidi(ts(),64,16,8).once(); // 8 sine wave patterns playing notes in c major, superposed on top of each other. try changing the wraps argument to values other than 8`
291+
---
292+
- **savemidi2d()** (_midifilename_ = Date.now(), _velocityPattern_ = 64, _durationPattern_ = 16, _tick_mode_ = false_, min_note = 0, max_note = 127)
293+
- creates a MIDI file of MIDI notes named `midifilename` in the `midi` directory, with the FacetPattern's data, assuming that the data was created using the 2d methods for image generation and processing. All nonzero "pixel" values will be translated into a MIDI note. Go to the [methods for image generation and processing](#methods-for-image-generation-and-processing) section for more details on these methods.
294+
- `VelocityPattern` and `DurationPattern` will automatically scale to match the note pattern. This allows you to modulate MIDI velocity and duration over the course of the whole note.
295+
- The `velocityPattern` expects values between 1 and 100. (This range is set by the midi-writer-js npm package).
296+
- The `min_note` and `max_note` values control the range of notes that the corresponding 2d pattern will be generated between.
297+
- When `tick_mode` is set to a truthy value, the numbers in `durationPattern` represent the number of ticks to last, rather than a whole-note divisions. 1 tick = 1/256th note. This allows for durations smaller and larger than the valid duration values for when `tick_mode` is set to false.
298+
- When `tick_mode` is set to false or excluded from the command, the following values are the only valid `durationPattern` argument values:
299+
```javascript
300+
1: whole note
301+
2: half note
302+
d2: dotted half note
303+
dd2: double dotted half note
304+
4: quarter note
305+
4t: quarter triplet note
306+
d4: dotted quarter note
307+
dd4: double dotted quarter note
308+
8: eighth note
309+
8t: eighth triplet note
310+
d8: dotted eighth note
311+
dd8: double dotted eighth note
312+
16: sixteenth note
313+
16t: sixteenth triplet note
314+
32: thirty-second note
315+
64: sixty-fourth note
316+
```
317+
- example:
318+
- `$('example').silence(2500).iter(8,()=>{this.tri2d(ri(0,49),ri(0,49),ri(0,49),ri(0,49),ri(0,49),ri(0,49),1,0)}).savemidi2d(ts(), 64, 16).once(); // 8 randomly sized triangles in 2d space, all at velocity 64, 16th note durations`
319+
- `$('example').silence(2500).iter(10,()=>{this.circle2d(ri(10,40), ri(10,40), 10, 1, 0)}).savemidi2d(ts(), 64, 64).once(); // 10 randomly sized circles in 2d space, all at velocity 64, 64th note durations`
261320

262321
### Methods for controlling transport BPM
263322
- **bpm** ( )

js/FacetPattern.js

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const wav = require('node-wav');
77
const fft = require('fft-js').fft;
88
const ifft = require('fft-js').ifft;
99
const fftUtil = require('fft-js').util;
10+
const MidiWriter = require('midi-writer-js');
1011
const WaveFile = require('wavefile').WaveFile;
1112
const FacetConfig = require('./config.js');
1213
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
@@ -15,6 +16,7 @@ const curve_calc = require('./lib/curve_calc.js');
1516
const KarplusStrongString = require('./lib/KarplusStrongString.js').KarplusStrongString;
1617
const Complex = require('./lib/Complex.js');
1718
const { Scale } = require('tonal');
19+
const { Midi } = require("tonal");
1820
const PNG = require('pngjs').PNG;
1921
let cross_platform_slash = process.platform == 'win32' ? '\\' : '/';
2022

@@ -4537,7 +4539,83 @@ ffilter (minFreqs, maxFreqs, invertMode = false) {
45374539
}
45384540
png.pack().pipe(fs.createWriteStream(`img/${filename}.png`));
45394541
return this;
4540-
}
4542+
}
4543+
4544+
savemidi2d (midifilename = Date.now(), velocityPattern = 64, durationPattern = 16, tick_mode = false, minNote = 0, maxNote = 127) {
4545+
if ( typeof velocityPattern == 'number' || Array.isArray(velocityPattern) === true ) {
4546+
velocityPattern = new FacetPattern().from(velocityPattern);
4547+
}
4548+
velocityPattern.size(this.data.length);
4549+
velocityPattern.clip(1,100); // for some reason the MIDI writer library only accepts velocities within this range
4550+
4551+
if ( typeof durationPattern == 'number' || Array.isArray(durationPattern) === true ) {
4552+
durationPattern = new FacetPattern().from(durationPattern);
4553+
}
4554+
durationPattern.size(this.data.length).round();
4555+
4556+
const sliceSize = Math.sqrt(this.data.length);
4557+
const track = new MidiWriter.Track();
4558+
4559+
const minIndex = 0;
4560+
const maxIndex = this.data.length - 1;
4561+
4562+
for (let i = 0; i < sliceSize; i++) {
4563+
const pitches = [];
4564+
for (let j = 0; j < sliceSize; j++) {
4565+
const index = j * sliceSize + i;
4566+
if (this.data[index] !== 0) {
4567+
const midiNoteNumber = Math.round((index - minIndex) / (maxIndex - minIndex) * (maxNote - minNote) + minNote);
4568+
pitches.push(Midi.midiToNoteName(midiNoteNumber));
4569+
}
4570+
}
4571+
let duration_str = durationPattern.data[i];
4572+
if ( tick_mode ) {
4573+
duration_str = 'T' + duration_str; // convert to tick syntax
4574+
}
4575+
if (pitches.length > 0) {
4576+
track.addEvent(new MidiWriter.NoteEvent({pitch: pitches, velocity: velocityPattern.data[i], sequential: false, duration: duration_str}));
4577+
}
4578+
}
4579+
4580+
const write = new MidiWriter.Writer([track]);
4581+
fs.writeFileSync(`midi/${midifilename}.mid`, write.buildFile(), 'binary');
4582+
return this;
4583+
}
4584+
4585+
savemidi (midifilename = Date.now(), velocityPattern = 64, durationPattern = 16, wraps = 1, tick_mode = false) {
4586+
if ( typeof velocityPattern == 'number' || Array.isArray(velocityPattern) === true ) {
4587+
velocityPattern = new FacetPattern().from(velocityPattern);
4588+
}
4589+
velocityPattern.size(this.data.length);
4590+
velocityPattern.clip(1,100); // for some reason the MIDI writer library only accepts velocities within this range
4591+
4592+
if ( typeof durationPattern == 'number' || Array.isArray(durationPattern) === true ) {
4593+
durationPattern = new FacetPattern().from(durationPattern);
4594+
}
4595+
durationPattern.size(this.data.length).round();
4596+
4597+
const sliceSize = Math.ceil(this.data.length / wraps);
4598+
const track = new MidiWriter.Track();
4599+
4600+
for (let i = 0; i < sliceSize; i++) {
4601+
const pitches = [];
4602+
for (let j = 0; j < wraps; j++) {
4603+
const index = j * sliceSize + i;
4604+
if (index < this.data.length) {
4605+
pitches.push(Midi.midiToNoteName(this.data[index]));
4606+
}
4607+
}
4608+
let duration_str = durationPattern.data[i];
4609+
if ( tick_mode ) {
4610+
duration_str = 'T' + duration_str; // convert to tick syntax
4611+
}
4612+
track.addEvent(new MidiWriter.NoteEvent({pitch: pitches, velocity: velocityPattern.data[i], sequential: false, duration: duration_str}));
4613+
}
4614+
4615+
const write = new MidiWriter.Writer([track]);
4616+
fs.writeFileSync(`midi/${midifilename}.mid`, write.buildFile(), 'binary');
4617+
return this;
4618+
}
45414619

45424620
saveimg (filename = Date.now(), rgbData, width = Math.round(Math.sqrt(this.data.length)), height = Math.round(Math.sqrt(this.data.length))) {
45434621
if (typeof filename !== 'string') {
@@ -4774,7 +4852,7 @@ circle2d(centerX, centerY, radius, value, mode = 0) {
47744852

47754853
// if mode is 0, only draw the outline by checking if the distance is close to the radius
47764854
// if mode is 1, fill the circle by checking if the distance is less than or equal to the radius
4777-
if ((mode === 0 && Math.abs(distance - radius) < 1) || (mode === 1 && distance <= radius)) {
4855+
if ((mode === 0 && Math.abs(distance - radius) < 0.5) || (mode === 1 && distance <= radius)) {
47784856
this.data[i] = value;
47794857
}
47804858
}

js/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
let configSettings = {
22
"OSC_OUTPORT": 5813,
33
"SAMPLE_RATE": 44100,
4-
"EVENT_RESOLUTION_MS": 10
4+
"EVENT_RESOLUTION_MS": 10,
5+
"HOST": "127.0.0.1",
56
}
67

78
if ( typeof module !== 'undefined' && typeof module.exports !== 'undefined' ) {

js/editor.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,18 @@ function getLastLineOfBlock(initial_line) {
7070
}
7171

7272
$(document).keydown(function(e) {
73-
// [ctrl + enter] or [ctrl + r] to select text and send to pattern server (127.0.0.1:1123)
73+
// [ctrl + enter] or [ctrl + r] to select text and send to pattern server :1123
7474
if ( e.ctrlKey && ( e.keyCode == 13 || e.keyCode == 82 ) ) {
7575
runFacet();
7676
}
7777
else if ( e.ctrlKey && e.keyCode == 188 ) {
7878
// clear hooks: [ctrl + ","]
79-
$.post('http://127.0.0.1:1123/hooks/clear', {}).done(function( data, status ) {});
79+
$.post(`http://${configSettings.HOST}:1123/hooks/clear`, {}).done(function( data, status ) {});
8080
$.growl.notice({ message: 'regeneration stopped' });
8181
}
8282
else if ( e.ctrlKey && (e.keyCode == 190 || e.keyCode == 191) ) {
8383
// clear hooks and mute everything: [ctrl + "."] or [ctrl + "?"]
84-
$.post('http://127.0.0.1:1123/stop', {}).done(function( data, status ) {});
84+
$.post(`http://${configSettings.HOST}:1123/stop`, {}).done(function( data, status ) {});
8585
$.growl.notice({ message: 'system muted' });
8686
}
8787
else if ( e.ctrlKey && (e.keyCode == 222 ) ) {
@@ -102,7 +102,7 @@ $(document).keydown(function(e) {
102102

103103
// set bpm & unfocus the #bpm input when user hits enter while focused on it
104104
if ( $('#bpm').is(':focus') && e.keyCode == 13 ) {
105-
$.post('http://127.0.0.1:3211/bpm', {bpm:$('#bpm').val()}).done(function( data, status ) {}).fail(function(data) {
105+
$.post(`http://${configSettings.HOST}:3211/bpm`, {bpm:$('#bpm').val()}).done(function( data, status ) {}).fail(function(data) {
106106
$.growl.error({ message: 'no connection to the Facet server' });
107107
});
108108
$('#bpm').blur();
@@ -124,7 +124,7 @@ $(document).keydown(function(e) {
124124
}
125125

126126
if ( e.ctrlKey && e.code === 'Space' ) {
127-
$.post('http://127.0.0.1:1123/autocomplete', {}).done(function( data, status ) {
127+
$.post(`http://${configSettings.HOST}:1123/autocomplete`, {}).done(function( data, status ) {
128128
facet_methods = data.data.methods;
129129
// forked custom hinting from: https://stackoverflow.com/a/39973139
130130
var options = {
@@ -173,11 +173,11 @@ function runFacet(mode = 'run') {
173173
setTimeout(function(){ cm.setCursor({line: line, ch: cursor.ch }); }, 100);
174174
setStatus(`processing`);
175175
let code = cm.getSelection();
176-
$.post('http://127.0.0.1:1123', {code:code,mode:mode});
176+
$.post(`http://${configSettings.HOST}:1123`, {code:code,mode:mode});
177177
}
178178

179179
let midi_outs;
180-
$.post('http://127.0.0.1:3211/midi', {}).done(function( data, status ) {
180+
$.post(`http://${configSettings.HOST}:3211/midi`, {}).done(function( data, status ) {
181181
// create <select> dropdown with this -- check every 2 seconds, store
182182
// in memory, if changed update select #midi_outs add option
183183
if (data.data != midi_outs) {
@@ -192,11 +192,11 @@ $.post('http://127.0.0.1:3211/midi', {}).done(function( data, status ) {
192192

193193
$('body').on('change', '#midi_outs', function() {
194194
localStorage.setItem('midi_outs_value', this.value);
195-
$.post('http://127.0.0.1:3211/midi_select', {output:this.value}).done(function( data, status ) {});
195+
$.post(`http://${configSettings.HOST}:3211/midi_select`, {output:this.value}).done(function( data, status ) {});
196196
});
197197

198198
$('body').on('click', '#midi_refresh', function() {
199-
$.post('http://127.0.0.1:3211/midi', {}).done(function( data, status ) {
199+
$.post(`http://${configSettings.HOST}:3211/midi`, {}).done(function( data, status ) {
200200
$('#midi_outs').html('');
201201
for (var i = 0; i < data.data.length; i++) {
202202
let midi_out = data.data[i];
@@ -231,7 +231,7 @@ $('body').on('click', '#sound', function() {
231231
});
232232

233233
$('body').on('click', '#stop', function() {
234-
$.post('http://127.0.0.1:1123/stop', {}).done(function( data, status ) {
234+
$.post(`http://${configSettings.HOST}:1123/stop`, {}).done(function( data, status ) {
235235
$.growl.notice({ message: 'system muted' });
236236
})
237237
.fail(function(data) {
@@ -242,7 +242,7 @@ $('body').on('click', '#stop', function() {
242242
});
243243

244244
$('body').on('click', '#clear', function() {
245-
$.post('http://127.0.0.1:1123/hooks/clear', {}).done(function( data, status ) {
245+
$.post(`http://${configSettings.HOST}:1123/hooks/clear`, {}).done(function( data, status ) {
246246
$.growl.notice({ message: 'regeneration stopped' });
247247
});
248248
});
@@ -252,7 +252,7 @@ $('body').on('click', '#rerun', function() {
252252
});
253253

254254
$('body').on('click', '#restart', function() {
255-
$.post('http://127.0.0.1:5831/restart', {}).done(function( data, status ) {
255+
$.post(`http://${configSettings.HOST}:5831/restart`, {}).done(function( data, status ) {
256256
if (status == 'success') {
257257
$.growl.notice({ message: 'Facet restarted successfully'});
258258
}
@@ -296,7 +296,7 @@ function setBrowserSound(true_or_false_local_storage_string) {
296296
$('#sound').css('background',"url('../spkr.png') no-repeat");
297297
$('#sound').css('background-size',"100% 200%");
298298
}
299-
$.post('http://127.0.0.1:3211/browser_sound', {browser_sound_output:browser_sound_output}).done(function( data, status ) {});
299+
$.post(`http://${configSettings.HOST}:3211/browser_sound`, {browser_sound_output:browser_sound_output}).done(function( data, status ) {});
300300
}
301301

302302
function initializeMIDISelection () {
@@ -305,7 +305,7 @@ function initializeMIDISelection () {
305305
if (storedValue) {
306306
// reset the most recently used MIDI out destination
307307
$('#midi_outs').val(storedValue);
308-
$.post('http://127.0.0.1:3211/midi_select', {output:storedValue}).done(function( data, status ) {});
308+
$.post(`http://${configSettings.HOST}:3211/midi_select`, {output:storedValue}).done(function( data, status ) {});
309309
}
310310
}
311311

@@ -324,7 +324,7 @@ checkStatus();
324324

325325
function checkStatus() {
326326
setInterval( () => {
327-
$.post('http://127.0.0.1:1123/status', {
327+
$.post(`http://${configSettings.HOST}:1123/status`, {
328328
mousex:mousex,
329329
mousey:mousey
330330
}).done(function( data, status ) {
@@ -365,7 +365,7 @@ setInterval(()=>{
365365
bpm = $('#bpm').val();
366366
// send change on increment/decrement by 1
367367
if ( !isNaN(bpm) && bpm >= 1 && $('#bpm').is(':focus') && ( Math.abs(bpm-prev_bpm) == 1 ) ) {
368-
$.post('http://127.0.0.1:3211/bpm', {bpm:bpm}).done(function( data, status ) {}).fail(function(data) {
368+
$.post(`http://${configSettings.HOST}:3211/bpm`, {bpm:bpm}).done(function( data, status ) {}).fail(function(data) {
369369
$.growl.error({ message: 'no connection to the Facet server' });
370370
});
371371
}
@@ -382,7 +382,7 @@ let ac;
382382
ac = new AudioContext();
383383

384384
// connect to the server
385-
const socket = io.connect('http://localhost:3000', {
385+
const socket = io.connect(`http://${configSettings.HOST}:3000`, {
386386
reconnection: true,
387387
reconnectionAttempts: Infinity,
388388
reconnectionDelay: 1000,

js/pattern_generator.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const WaveFile = require('wavefile').WaveFile;
1313
const FacetPattern = require('./FacetPattern.js');
1414
const FacetConfig = require('./config.js');
1515
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
16+
const HOST = FacetConfig.settings.HOST;
1617
let bpm = 90;
1718
let bars_elapsed = 0;
1819
let reruns = {};
@@ -205,7 +206,7 @@ app.post('/hooks/clear', (req, res) => {
205206
app.post('/stop', (req, res) => {
206207
reruns = {};
207208
terminateAllWorkers();
208-
axios.post('http://localhost:3211/stop',{})
209+
axios.post(`http://${HOST}:3211/stop`,{})
209210
.catch(function (error) {
210211
console.log(`error stopping transport server: ${error}`);
211212
});
@@ -337,7 +338,7 @@ function postToTransport (fp) {
337338
// remove this.data as it's not needed in the transport and is potentially huge
338339
let fpCopy = { ...fp };
339340
delete fpCopy.data;
340-
axios.post('http://localhost:3211/update',
341+
axios.post(`http://${HOST}:3211/update`,
341342
{
342343
pattern: JSON.stringify(fpCopy)
343344
}
@@ -348,14 +349,14 @@ function postToTransport (fp) {
348349
}
349350

350351
function startTransport () {
351-
axios.post('http://localhost:3211/play',{})
352+
axios.post(`http://${HOST}:3211/play`,{})
352353
.catch(function (error) {
353354
console.log(`error starting transport server: ${error}`);
354355
});
355356
}
356357

357358
function postMetaDataToTransport (fp,data_type) {
358-
axios.post('http://localhost:3211/meta',
359+
axios.post(`http://${HOST}:3211/meta`,
359360
{
360361
pattern: JSON.stringify(fp),
361362
type: data_type

js/transport.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const cors = require('cors');
1010
const app = express();
1111
const axios = require('axios');
1212
const FacetConfig = require('./config.js');
13-
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
13+
const HOST = FacetConfig.settings.HOST;
1414
const OSC = require('osc-js')
1515
const udp_osc_server = new OSC({ plugin: new OSC.DatagramPlugin({ send: { port: FacetConfig.settings.OSC_OUTPORT } }) })
1616
udp_osc_server.open({ port: 2134 });
@@ -334,7 +334,7 @@ tick();
334334
function reportTransportMetaData() {
335335
// pass along the current bpm and bars elapsed, if the transport is running
336336
if ( transport_on === true ) {
337-
axios.post('http://localhost:1123/meta',
337+
axios.post(`http://${HOST}:1123/meta`,
338338
{
339339
bpm: JSON.stringify(bpm),
340340
bars_elapsed: JSON.stringify(bars_elapsed)
@@ -462,7 +462,7 @@ function applyNextPatterns () {
462462
function requestNewPatterns () {
463463
if ( bars_elapsed > 0 ) {
464464
// tell server to generate any new patterns
465-
axios.get('http://localhost:1123/update');
465+
axios.get(`http://${HOST}:1123/update`);
466466
}
467467
}
468468

midi/.gitignore

Whitespace-only changes.

0 commit comments

Comments
 (0)