Skip to content

Commit

Permalink
Merge pull request #83 from nnirror/clientside
Browse files Browse the repository at this point in the history
v1.4.0
  • Loading branch information
nnirror committed Mar 9, 2024
2 parents c6ebaad + 8414276 commit 9a5545e
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 27 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,65 @@ You need to connect the MIDI device you want to use before starting Facet.
- _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.
- example:
- `$('example').sine(1).size(128).pitchbend();`
---
- **savemidi** (_midifilename_ = Date.now(), _velocityPattern_ = 64, _durationPattern_ = 16, _wraps_ = 1, _tick_mode_ = false_)
- creates a MIDI file of MIDI notes named `midifilename` in the `midi` directory, with the FacetPattern's data.
- `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.
- The `velocityPattern` expects values between 1 and 100. (This range is set by the midi-writer-js npm package).
- 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.
- 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.
- When `tick_mode` is set to false or excluded from the command, the following values are the only valid `durationPattern` argument values:
```javascript
1: whole note
2: half note
d2: dotted half note
dd2: double dotted half note
4: quarter note
4t: quarter triplet note
d4: dotted quarter note
dd4: double dotted quarter note
8: eighth note
8t: eighth triplet note
d8: dotted eighth note
dd8: double dotted eighth note
16: sixteenth note
16t: sixteenth triplet note
32: thirty-second note
64: sixty-fourth note
```
- example:
- `$('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`
- `$('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`
- `$('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`
---
- **savemidi2d()** (_midifilename_ = Date.now(), _velocityPattern_ = 64, _durationPattern_ = 16, _tick_mode_ = false_, min_note = 0, max_note = 127)
- 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.
- `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.
- The `velocityPattern` expects values between 1 and 100. (This range is set by the midi-writer-js npm package).
- The `min_note` and `max_note` values control the range of notes that the corresponding 2d pattern will be generated between.
- 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.
- When `tick_mode` is set to false or excluded from the command, the following values are the only valid `durationPattern` argument values:
```javascript
1: whole note
2: half note
d2: dotted half note
dd2: double dotted half note
4: quarter note
4t: quarter triplet note
d4: dotted quarter note
dd4: double dotted quarter note
8: eighth note
8t: eighth triplet note
d8: dotted eighth note
dd8: double dotted eighth note
16: sixteenth note
16t: sixteenth triplet note
32: thirty-second note
64: sixty-fourth note
```
- example:
- `$('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`
- `$('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`

### Methods for controlling transport BPM
- **bpm** ( )
Expand Down
82 changes: 80 additions & 2 deletions js/FacetPattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const wav = require('node-wav');
const fft = require('fft-js').fft;
const ifft = require('fft-js').ifft;
const fftUtil = require('fft-js').util;
const MidiWriter = require('midi-writer-js');
const WaveFile = require('wavefile').WaveFile;
const FacetConfig = require('./config.js');
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
Expand All @@ -15,6 +16,7 @@ const curve_calc = require('./lib/curve_calc.js');
const KarplusStrongString = require('./lib/KarplusStrongString.js').KarplusStrongString;
const Complex = require('./lib/Complex.js');
const { Scale } = require('tonal');
const { Midi } = require("tonal");
const PNG = require('pngjs').PNG;
let cross_platform_slash = process.platform == 'win32' ? '\\' : '/';

Expand Down Expand Up @@ -4537,7 +4539,83 @@ ffilter (minFreqs, maxFreqs, invertMode = false) {
}
png.pack().pipe(fs.createWriteStream(`img/${filename}.png`));
return this;
}
}

savemidi2d (midifilename = Date.now(), velocityPattern = 64, durationPattern = 16, tick_mode = false, minNote = 0, maxNote = 127) {
if ( typeof velocityPattern == 'number' || Array.isArray(velocityPattern) === true ) {
velocityPattern = new FacetPattern().from(velocityPattern);
}
velocityPattern.size(this.data.length);
velocityPattern.clip(1,100); // for some reason the MIDI writer library only accepts velocities within this range

if ( typeof durationPattern == 'number' || Array.isArray(durationPattern) === true ) {
durationPattern = new FacetPattern().from(durationPattern);
}
durationPattern.size(this.data.length).round();

const sliceSize = Math.sqrt(this.data.length);
const track = new MidiWriter.Track();

const minIndex = 0;
const maxIndex = this.data.length - 1;

for (let i = 0; i < sliceSize; i++) {
const pitches = [];
for (let j = 0; j < sliceSize; j++) {
const index = j * sliceSize + i;
if (this.data[index] !== 0) {
const midiNoteNumber = Math.round((index - minIndex) / (maxIndex - minIndex) * (maxNote - minNote) + minNote);
pitches.push(Midi.midiToNoteName(midiNoteNumber));
}
}
let duration_str = durationPattern.data[i];
if ( tick_mode ) {
duration_str = 'T' + duration_str; // convert to tick syntax
}
if (pitches.length > 0) {
track.addEvent(new MidiWriter.NoteEvent({pitch: pitches, velocity: velocityPattern.data[i], sequential: false, duration: duration_str}));
}
}

const write = new MidiWriter.Writer([track]);
fs.writeFileSync(`midi/${midifilename}.mid`, write.buildFile(), 'binary');
return this;
}

savemidi (midifilename = Date.now(), velocityPattern = 64, durationPattern = 16, wraps = 1, tick_mode = false) {
if ( typeof velocityPattern == 'number' || Array.isArray(velocityPattern) === true ) {
velocityPattern = new FacetPattern().from(velocityPattern);
}
velocityPattern.size(this.data.length);
velocityPattern.clip(1,100); // for some reason the MIDI writer library only accepts velocities within this range

if ( typeof durationPattern == 'number' || Array.isArray(durationPattern) === true ) {
durationPattern = new FacetPattern().from(durationPattern);
}
durationPattern.size(this.data.length).round();

const sliceSize = Math.ceil(this.data.length / wraps);
const track = new MidiWriter.Track();

for (let i = 0; i < sliceSize; i++) {
const pitches = [];
for (let j = 0; j < wraps; j++) {
const index = j * sliceSize + i;
if (index < this.data.length) {
pitches.push(Midi.midiToNoteName(this.data[index]));
}
}
let duration_str = durationPattern.data[i];
if ( tick_mode ) {
duration_str = 'T' + duration_str; // convert to tick syntax
}
track.addEvent(new MidiWriter.NoteEvent({pitch: pitches, velocity: velocityPattern.data[i], sequential: false, duration: duration_str}));
}

const write = new MidiWriter.Writer([track]);
fs.writeFileSync(`midi/${midifilename}.mid`, write.buildFile(), 'binary');
return this;
}

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

// if mode is 0, only draw the outline by checking if the distance is close to the radius
// if mode is 1, fill the circle by checking if the distance is less than or equal to the radius
if ((mode === 0 && Math.abs(distance - radius) < 1) || (mode === 1 && distance <= radius)) {
if ((mode === 0 && Math.abs(distance - radius) < 0.5) || (mode === 1 && distance <= radius)) {
this.data[i] = value;
}
}
Expand Down
3 changes: 2 additions & 1 deletion js/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let configSettings = {
"OSC_OUTPORT": 5813,
"SAMPLE_RATE": 44100,
"EVENT_RESOLUTION_MS": 10
"EVENT_RESOLUTION_MS": 10,
"HOST": "127.0.0.1",
}

if ( typeof module !== 'undefined' && typeof module.exports !== 'undefined' ) {
Expand Down
34 changes: 17 additions & 17 deletions js/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ function getLastLineOfBlock(initial_line) {
}

$(document).keydown(function(e) {
// [ctrl + enter] or [ctrl + r] to select text and send to pattern server (127.0.0.1:1123)
// [ctrl + enter] or [ctrl + r] to select text and send to pattern server :1123
if ( e.ctrlKey && ( e.keyCode == 13 || e.keyCode == 82 ) ) {
runFacet();
}
else if ( e.ctrlKey && e.keyCode == 188 ) {
// clear hooks: [ctrl + ","]
$.post('http://127.0.0.1:1123/hooks/clear', {}).done(function( data, status ) {});
$.post(`http://${configSettings.HOST}:1123/hooks/clear`, {}).done(function( data, status ) {});
$.growl.notice({ message: 'regeneration stopped' });
}
else if ( e.ctrlKey && (e.keyCode == 190 || e.keyCode == 191) ) {
// clear hooks and mute everything: [ctrl + "."] or [ctrl + "?"]
$.post('http://127.0.0.1:1123/stop', {}).done(function( data, status ) {});
$.post(`http://${configSettings.HOST}:1123/stop`, {}).done(function( data, status ) {});
$.growl.notice({ message: 'system muted' });
}
else if ( e.ctrlKey && (e.keyCode == 222 ) ) {
Expand All @@ -102,7 +102,7 @@ $(document).keydown(function(e) {

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

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

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

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

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

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

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

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

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

Expand All @@ -324,7 +324,7 @@ checkStatus();

function checkStatus() {
setInterval( () => {
$.post('http://127.0.0.1:1123/status', {
$.post(`http://${configSettings.HOST}:1123/status`, {
mousex:mousex,
mousey:mousey
}).done(function( data, status ) {
Expand Down Expand Up @@ -365,7 +365,7 @@ setInterval(()=>{
bpm = $('#bpm').val();
// send change on increment/decrement by 1
if ( !isNaN(bpm) && bpm >= 1 && $('#bpm').is(':focus') && ( Math.abs(bpm-prev_bpm) == 1 ) ) {
$.post('http://127.0.0.1:3211/bpm', {bpm:bpm}).done(function( data, status ) {}).fail(function(data) {
$.post(`http://${configSettings.HOST}:3211/bpm`, {bpm:bpm}).done(function( data, status ) {}).fail(function(data) {
$.growl.error({ message: 'no connection to the Facet server' });
});
}
Expand All @@ -382,7 +382,7 @@ let ac;
ac = new AudioContext();

// connect to the server
const socket = io.connect('http://localhost:3000', {
const socket = io.connect(`http://${configSettings.HOST}:3000`, {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
Expand Down
9 changes: 5 additions & 4 deletions js/pattern_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const WaveFile = require('wavefile').WaveFile;
const FacetPattern = require('./FacetPattern.js');
const FacetConfig = require('./config.js');
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
const HOST = FacetConfig.settings.HOST;
let bpm = 90;
let bars_elapsed = 0;
let reruns = {};
Expand Down Expand Up @@ -205,7 +206,7 @@ app.post('/hooks/clear', (req, res) => {
app.post('/stop', (req, res) => {
reruns = {};
terminateAllWorkers();
axios.post('http://localhost:3211/stop',{})
axios.post(`http://${HOST}:3211/stop`,{})
.catch(function (error) {
console.log(`error stopping transport server: ${error}`);
});
Expand Down Expand Up @@ -337,7 +338,7 @@ function postToTransport (fp) {
// remove this.data as it's not needed in the transport and is potentially huge
let fpCopy = { ...fp };
delete fpCopy.data;
axios.post('http://localhost:3211/update',
axios.post(`http://${HOST}:3211/update`,
{
pattern: JSON.stringify(fpCopy)
}
Expand All @@ -348,14 +349,14 @@ function postToTransport (fp) {
}

function startTransport () {
axios.post('http://localhost:3211/play',{})
axios.post(`http://${HOST}:3211/play`,{})
.catch(function (error) {
console.log(`error starting transport server: ${error}`);
});
}

function postMetaDataToTransport (fp,data_type) {
axios.post('http://localhost:3211/meta',
axios.post(`http://${HOST}:3211/meta`,
{
pattern: JSON.stringify(fp),
type: data_type
Expand Down
6 changes: 3 additions & 3 deletions js/transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const cors = require('cors');
const app = express();
const axios = require('axios');
const FacetConfig = require('./config.js');
const SAMPLE_RATE = FacetConfig.settings.SAMPLE_RATE;
const HOST = FacetConfig.settings.HOST;
const OSC = require('osc-js')
const udp_osc_server = new OSC({ plugin: new OSC.DatagramPlugin({ send: { port: FacetConfig.settings.OSC_OUTPORT } }) })
udp_osc_server.open({ port: 2134 });
Expand Down Expand Up @@ -334,7 +334,7 @@ tick();
function reportTransportMetaData() {
// pass along the current bpm and bars elapsed, if the transport is running
if ( transport_on === true ) {
axios.post('http://localhost:1123/meta',
axios.post(`http://${HOST}:1123/meta`,
{
bpm: JSON.stringify(bpm),
bars_elapsed: JSON.stringify(bars_elapsed)
Expand Down Expand Up @@ -462,7 +462,7 @@ function applyNextPatterns () {
function requestNewPatterns () {
if ( bars_elapsed > 0 ) {
// tell server to generate any new patterns
axios.get('http://localhost:1123/update');
axios.get(`http://${HOST}:1123/update`);
}
}

Expand Down
Empty file added midi/.gitignore
Empty file.
Loading

0 comments on commit 9a5545e

Please sign in to comment.