Last active
October 10, 2015 04:57
-
-
Save kn0ll/3636640 to your computer and use it in GitHub Desktop.
a directed tick-based "streaming" graph. used for an audio processing graph, for example.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// this is a directed graph data structure- with the unique property that it is | |
// intended to handle the flow of data upstream in a clock based system. | |
// streaming audio through a graph of nodes with variable inputs and outputs, for instance. | |
// an `Edge` behaves as a relation between two nodes. | |
// the edge is saved on both connected nodes for reference. | |
var Edge = function(fromNode, fromIndex, toNode, toIndex) { | |
this.fromNode = fromNode; | |
this.fromIndex = fromIndex; | |
this.toNode = toNode; | |
this.toIndex = toIndex; | |
}; | |
// a `Node` is a node in a graph with x inputs and y outputs. | |
// - `inputs` is an object whose keys are input indexes, and values are edges. | |
// - `outputs` is an object whose keys are output indexes, and values are edges. | |
// - `data` is an object whose keys are input indexes, and values are written data. | |
var Node = function() { | |
this.inputs = {}; | |
this.outputs = {}; | |
this.data = {}; | |
}; | |
// `connect` stores a new `Edge` in the current node's outputs, | |
// and saves the same edge on the next node's inputs. | |
Node.prototype.connect = function(fromIndex, toIndex, node) { | |
var edge = new Edge(this, fromIndex, node, toIndex); | |
this.outputs[fromIndex] = edge; | |
node.inputs[toIndex] = edge; | |
}; | |
// `inputsLength` returns the number of connected inputs. | |
Node.prototype.inputsLength = function() { | |
var foundInputs = 0; | |
for (var i in this.inputs) { | |
foundInputs++; | |
} | |
return foundInputs; | |
}; | |
// `outputsLength` returns the number of connected outputs. | |
Node.prototype.outputsLength = function() { | |
var foundOutputs = 0; | |
for (var i in this.outputs) { | |
foundOutputs++; | |
} | |
return foundOutputs; | |
}; | |
// `dataLength` returns the number of the inputs | |
// which have data written to them. | |
Node.prototype.dataLength = function() { | |
var foundData = 0; | |
for (var i in this.read()) { | |
foundData++; | |
} | |
return foundData; | |
}; | |
// `findRootNodes` finds all nodes in the graph before | |
// this one which have no connected inputs. these are essentially the | |
// beginning nodes of the directed graph. | |
Node.prototype.findRootNodes = function() { | |
var nodes = []; | |
for (var i in this.inputs) { | |
var node = this.inputs[i].fromNode; | |
if (node.inputsLength()) { | |
nodes = nodes.concat(node.findRootNodes()); | |
} else { | |
nodes.push(node); | |
} | |
} | |
return nodes; | |
}; | |
// `send` takes an outputIndex and writes data | |
// to the input connected to the output associated with outputIndex | |
Node.prototype.send = function(outputIndex, data) { | |
var output = this.outputs[outputIndex]; | |
output.toNode.write(output.toIndex, data); | |
}; | |
// `write` stores input data on the node. | |
// once all connected nodes have written data to the inputs, | |
// we do one of two things: if the current node is an "end" node in the graph, | |
// we call `generate` is responsible for handling the final data. if the current node | |
// has outputs and is not and "end" node, it's responsibility is starting a new `tick` | |
// to send upstream. | |
Node.prototype.write = function(inputIndex, data) { | |
this.data[inputIndex] = data; | |
if (this.dataLength() == this.inputsLength()) { | |
if (this.outputsLength()) { | |
this.tick(); | |
} else { | |
this.generate(); | |
} | |
this.data = {}; | |
} | |
}; | |
// `read` returns the data currently written to the node's inputs | |
Node.prototype.read = function() { | |
return this.data; | |
}; | |
// `tick` handles two explicit scenarios. if the current node | |
// has no connected outputs, and is thus an "end" node in the graph, it's | |
// job is to find all "beginning" nodes in the graph and trigger their `tick`- | |
// which will begin the flow of data upstream. however if the node has connected outputs, | |
// thus data to be sent, we call `generate` which will process and write the data to the following nodes. | |
Node.prototype.tick = function() { | |
if (!this.outputsLength()) { | |
var rootNodes = this.findRootNodes(); | |
for (var i = 0; i < rootNodes.length; i++) { | |
rootNodes[i].tick(); | |
} | |
} else { | |
for (var index in this.outputs) { | |
this.generate(); | |
} | |
} | |
}; | |
// `generate` is called on each node once all it's inputs have had data written to them. | |
// if the node is an "end" node, it should handle the data in some way. if it's not, | |
// it should process data and write it to the next node. | |
Node.prototype.generate = function() { | |
if (this.outputsLength()) { | |
for (var index in this.outputs) { | |
this.send(index, 'lol'); | |
} | |
} else { | |
console.log('End node received data', this.read()); | |
} | |
}; | |
// a simple graph: [a] => [b] => [c] | |
var nodeA = new Node(), | |
nodeB = new Node(), | |
nodeC = new Node(); | |
nodeA.connect(0, 0, nodeB); | |
nodeB.connect(0, 0, nodeC); | |
setInterval(function() { | |
console.log('triggering tick'); | |
nodeC.tick(); | |
}, 3000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// a simple graph: [a] => [b] => [c] | |
var nodeA = new Node(), | |
nodeB = new Node(), | |
nodeC = new Node(); | |
nodeA.connect(0, 0, nodeB); | |
nodeB.connect(0, 0, nodeC); | |
setInterval(function() { | |
console.log('triggering tick'); | |
nodeC.tick(); | |
}, 3000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Sine = Node.extend({ | |
generate: function() { | |
var sin = Math.sin, | |
two_pi = 2 * Math.PI, | |
sampleRate = 44100; | |
var frequency = 440, | |
phase = this.phase || 0, | |
nextPhase = phase + (two_pi * frequency / sampleRate), | |
sine = sin(phase); | |
if (nextPhase > two_pi) { | |
this.phase = nextPhase % two_pi; | |
} else { | |
this.phase = nextPhase; | |
} | |
this.send(0, [sine, sine]); | |
} | |
}); | |
var Gain = Node.extend({ | |
generate: function() { | |
var level = 0.8, | |
samples = this.read()[0], | |
new_samples = []; | |
for (var i = 0; i < samples.length; i++) { | |
new_samples.push(samples[i] * level); | |
} | |
this.send(0, new_samples); | |
} | |
}); | |
var Speaker = Node.extend({ | |
generate: function() { | |
console.log('play noise', this.read()[0]); | |
} | |
}); | |
// a simple graph: [a] => [b] => [c] | |
var sine = new Sine(), | |
gain = new Gain(), | |
speaker = new Speaker(); | |
sine.connect(0, 0, gain); | |
gain.connect(0, 0, speaker); | |
setInterval(function() { | |
speaker.tick(); | |
}, 100); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment