spencerpark / mellowd Goto Github PK
View Code? Open in Web Editor NEWProgrammable music! A compiler and high level language.
Home Page: https://spencerpark.github.io/mellowd-site/editor.html
Programmable music! A compiler and high level language.
Home Page: https://spencerpark.github.io/mellowd-site/editor.html
The project does function with an internal API but a proper one should be exposed and documented. The JavaDocs for that API should be published.
Add boolean expression parsing to the compiler.
Support the general boolean operators: and
, or
, and not
and comparison via:
lt
for <
leq
for <=
gt
for >
geq
for >=
eq
for ==
neq
for !=
Support injecting soundfonts with the CLI and expose it in the compiler API.
Decide on truth values for the types in the language. Document these in some sort of table.
type | true | false |
---|---|---|
note | not a rest | rest |
number | > 0 | 0 |
boolean | true | false |
reference | defined | undefined |
Only supported between like types. Otherwise an exception is raised.
Indexing a melody may produce a chord which can then be further indexed to produce a pitch. Similarly range queries may be further index (for whatever reason, maybe there isn't a reasonable use case).
Ex:
melodyVar -> [(a, b, c)]
melodyVar:0:1 //Would be 'b'
melodyVar:0~0:0~0 //Would still just be melodyVar
if (condition) {
code
} else if (other) {
code
} else {
code
}
{ code } when (condition)
or { code } when (condition)
or { code }
only do { <code> } when <condition>
or { <code> } when <condition>
or { <code> }
do { <code> } if <condition>
else do { <code> } if <condition>
else do { <code> }
Expose an interface for plugins to create their own source finder to be used for defining their functions or constants without directly injecting them into the script. This way the source file consuming the plugin would have an explicit import at the top to better describe where the functions come from as well as get the option to only import specific things to avoid polluting the namespace.
Functions don't really need to be restricted to the top level but they were to distinguish the fact that they do in their own namespace (a function and variable can share the same name). This is due to the very verbose syntax for function calls and the primitive like nature of all values.
The functions currently require the def
keyword also to signify that they cannot be changed (but can be overloaded). Therefor they can be imported/exported very easily. In addition the complete separation from any context makes them trivial to pluck out of the scope they are defined in.
On the contrary, common functions like chorus
or similar could benefit from being defined a couple times in various blocks instead of multiple top level chorus_piano
, chorus_guitar
, etc.
In some instances such as piano
specifically, there are multiple performances happening simultaneously. This may be both hands playing the same instrument. In these scenarios both blocks are tightly coupled and could be controlled by a higher order block, the controller block.
This block would not have any output of its own but would have a namespace and scope. It also has access to the output streams of the performers it controls.
Defined similar to regular blocks but have the additional control
clause which specifies the blocks the controller needs access to. In this clause there is an option to provide a local alias for the block name.
def block PianoLH
def block PianoRH
def block Piano controls PianoLH as Left, PianoRH as Right {
// ... config
}
Note that the original PianoLH
and PianoRH
are still able to perform on their own but it is unlikely they will need to in this scenario.
Performance looks very similar to the standard performance with the exception that the performances are nested inside the controller's scope allowing access to it's local memory space.
Piano {
playChord -> C
Left {
[playChord]*<w>
}
Right {
4 * {
[playChord:it]*<q>
}
}
}
Similar to the multi-block performance statement where the same definitions are evaluated for all blocks, the same may occur at the top level of a controller block. To improve support for this feature an implicit boolean should be defined for each of the blocks the controller controls. This may allow for similar but slightly diverging executions based on a test for which block is executing it.
Branches are anonymous and share the memory space of the controller (a plain block). They therefor must synchronize at the end of the branching since the performer cannot diverge in time after the branch is complete.
Landmarks are a proposed feature that allow the composer to mark important parts of a song. Landmarks could then be synchronized on to allow for blocks to be defined in separate files and imported. They could also be used by the CLI to start playback at a certain time in the composition.
@chorus
{ "chorus" } => landmark
Syncing would then be done with a regular sync call but using blockName.landmark
instead. The landmark is only required for imported blocks and if not given will sync with the start of the block.
Support comment nesting so that commenting out a block of code doesn't require uncommenting inner blocks to avoid breaking the outer comment.
Note the bottom */
is evicted from the comment as the first */
closes it.
/* outer
/* inner */
*/
The project's scope looks promising.
The scope seems to be very similar to my own project: https://github.com/truj/midica
However I have problems to even try out MellowD.
Reasons:
I know, issue #9 already addresses documentation but a good documentation is a lot of work, taking a lot of time.
So I suggest the following:
I think that's not too much of work but it would enhance usibility a lot.
The current plugin metadata looks like the following:
# Required: the id of the plugin. Same as in the file name
name=bjorklund
# Required: the fully qualified class name of the class that
# implements MellowDPlugin
implementation-class=io.github.spencerpark.mellowd.plugin.defaults.Bjorklund
# Required: the version of the plugin
version=0.1.0
# Required: the version of MellowD that the plugin is compatible with
mellowd-version=2.+
# Optional: the name of the author of the plugin
author=Spencer Park
# Optional: a short description of the plugin
description=Generate Euclidian rhythms based on Bjorklund pulse spreading algorithm
# Optional: a website such as a github repository associated with the plugin
website=https://github.com/SpencerPark/MellowD
The only field currently used is implementation-class
as it is used to load the class but many fields are required.
The name
field should be verified to match the file name<plugin-id>.mellowd-plugin.properties
.
The mellowd-version
field should do a check against the compiler importing it to warn about it.
The other meta-data should be printed by a verbose compiler when the plugin is loaded. In the future if a REPL or compiler server are implemented, this meta data could be queried after being loaded.
Things like chord indexing and loops currently only support literal numbers. These should support references to numbers.
Tuplets and octave shifts should remain as literals.
Supply a set of functions that take a root note a build a chord with the same name as the function.
This language feature would support diverging executions within a block.
play {
[a, b, c]*<e, e, e>
}{
[d, e, f]*<e, q, q>
}
The above example would preform the equivalent of
blockA, blockB {} //sync
blockA {
[a, b, c]*<e, e, e>
}
blockB {
[d, e, f]*<e, q, q>
}
blockA, blockB {} //sync
This feature's use would mainly pop up in concurrent instruments such as piano which can have 2 hands playing different things, or percussion blocks. Percussion blocks are really a collection of many instruments (each different sound can be visualized as a stream of beats) and making a fresh, named block for each is just too bulky.
The branches would share memory. This raises the same concerns raised in #12, we have concurrency issues with the shared memory.
The compiler has a pseudo intermediate format currently which is just java objects. The compiler generates intermediate code by creating these "Expression" objects which are then executed afterwards. It would make sense to add an intermediate, language agnostic, format that can be interpreted on it's own.
Completely separating an interpreter from the compiler would better support a plugin API and native interface. These things would depend only on the interpreter and could detach from the compiler entirely. Conceptually this is a better place for them anyways.
To get this done there needs to be an explicit, well defined, intermediate standard. Diverging the 2 projects will make implementing new features a bit more extensive but would better encourage backwards compatibility hopefully resulting in a more stable language.
The parser accepts index parameters as identifiers but the compiler still treats them as only being constants.
@Override
public Expression<Integer> visitIndex(MellowDParser.IndexContext ctx) {
return new Constant<>(visitNumber(ctx.number()));
}
@Override
public Expression<Integer> visitUpperIndex(MellowDParser.UpperIndexContext ctx) {
return new Constant<>(visitNumber(ctx.number()));
}
There are some things that would be nice to have in MellowD but the language is not the right place to implement them. Just like plugins for sound editors it would make sense to allow plugins to be written in Java and added to the "MellowD VM". These things need an interface then and it could actually just be inferred from the injected function classes or explicitly declared in an interface declaration.
This could take a form like TypeScript's .d.ts files with declarations something like Java's native
keyword.
def native function myFunction => chord arg1, arg2
def native function myFunction => chord arg1, arg2 {
invoke my.java.package.ClassName::methodImplemtingMyFunction
//or
implementation my.java.package.ClassName::methodImplemtingMyFunction
}
The problem with the above is that this tightly couples the language with a Java interpreter. It also requires an implementation to be provided to the interpreter as well as the declaration in the right folder with the source. The namespace is then also decided by the developer including the library and not the author.
@MellowDFunction("my.namespace.myFunction")
public void myFunction(Chord arg1, Object arg2) {
...
}
@MellowDConstant("my.namespace.MyValue")
public static final Rhythm MY_VALUE = new Rhythm(...);
@MellowDConstant("my.namespace.MyValue2")
public static Rhythm getRhythm() { ... }
Where the getRhythm
call would be invoked once to construct the rhythm and stored in the namespace.
This approach would require a fresh plugin API which might be a bit more work but also decouples the injection from the interpreter a bit. Although the Rhythm, Chord, etc. classes are still exposed anyways.
public class MyPlugin {
public MyPlugin(MellowD mellowd) {
final Parameter<Chord> param1 = Parameter.newRequiredParameter("param1", Chord.class);
FunctionSignature sig = new FunctionSignature("myFunction", param1);
mellowd.getFunctionBank("my", "namespace").addFunction(new Function(sig, false, (env, out) -> {
Chord arg1 = param1.getReference().dereference(env.getMemory());
out.put(...);
}));
}
}
This is exactly what the default functions do and essentially it is already all implemented. It would just be beneficial to formalize the interface and maybe clean it up a bit (#10). This approach would reject an import syntax though as a loaded plugin would immediately inject itself into the function bank whether it is needed or not.
The sync link has changed the code execution order and there is no longer the top down intuition for execution. There is no way to reliably predict when each block will change a global variable value.
To solve this problem I think there should be the introduction of a constant. All global variables must be constants. This also opens up the possibility of exporting variables for consumption by other programs.
Reuse the def
keyword used for defining blocks and functions. This would define a value.
def myChord -> (a, b, c) //A constant
myOtherChord -> (a, b, c) //A variable
Assignment to constants would then throw an exception
myChord -> (d, e, f) //Error, myChord is constant
The compiler should also ensure that the current behavior of manipulation operations making fresh objects as everything in the object should be immutable. i.e.
myRoot -> (a)
def myChord -> (myRoot, b, c)
myRoot -> (b) //Must not change myChord
Put whitespace on a HIDDEN channel so that references to source don't print out something ugly when pointing back at a sequence of tokens.
The documentation is very outdated and desperately needs an update
The concatenation component internal API is all over the place with some unnatural interfaces. This needs some reworking before work can start on #6 as it will need to attach itself onto the same API.
A more general builder pattern with some method references for the types of components that can be appended looks like the best approach.
A REPL would be an extremely useful tool for getting start with MellowD and playing around with composition ideas.
The REPL should accept all of the top level statements:
Additionally there should be some method of executing single statements and expressions within a specific block scope. Possibly with the help of a REPL command to select a default block or just use the last defined block.
After executing something with output the output should be played back live.
New index syntax for indexing a range from something index-able.
myMelody -> [a, b, c]
[myMelody:0~1]*<e, e> //[a, b]
[myMelody:-1~0]<e, e> //[c, a]
With the planning of if statements (See #3) it would be useful for the repetition code block to know where it is at in it's iteration.
Ex:
3 * {
//code that can reference implicit loop variables
}
it
or rep
).isLastRep
, isFirstRep
Random generation should be built in at the language level. I don't think a function would be too pleasant to work with.
Generators would be block local and seeded with a string. Repeating a good sounding execution should be possible and hence a seed would support this.
A random variable that is indexed to describe the bounds of the generation. May be missleading as subsequent calls of rand:4
won't necessarily return the same value but this fits so comfortablly in the language.
rand:4 //random int between [0, 4)
rand:4~6 //random int between [4, 6)
A possible integration for this could be implicit bounds for indexing melodies, chords, rhythms.
mel -> [a, b, c]
[mel:rand]*<e, e, e> //plays the same random note from mel 3 times
[mel:rand, mel:rand, mel:rand]*<e> //plays 3 random notes from mel
Another options is fresh range-like syntax
0..4 //random int between [0, 4)
4..6 //random in between [4, 6)
Or simply
save { 4, 6 } => rand
[a+rand]*<e>
MellowD is moving to support more tools for composing original music. As such generators for melodies, chords, rhythms are predicted to be used very often and imported by name. If we can implement the syntax for random in a clean way that extends to generators of other names that would be nice.
With the MellowD gradle plugin, compilation can be nicely integrated into the build cycle. Something nice to support would be the option of adding song-defined parameters to the compiler. A song would declare these similarly to function parameters and this way the parameters can be called by the song builder.
This feature would be similar to C's macros and the ability to define them with the -D flag when compiling.
A possible new language feature in which the composer can specify a sequence of transpositions to apply to the following melody.
This would provide a more natural options for composing with scales as well as better abstract a melody for designing functions that cover a broader scope. The idea is that a root can be specified and a phrase can be developed on top of it.
This would also make scales and chord progressions into library level features that can be written in MellowD and not the compiler.
The pattern length would not need to match the mapping source (melody or chord) as it would simply modulo extend the shorter operand. This behavior is then consistent with melody/chord and rhythm composition.
This is no where close to set in stone, but the general context that it would appear in is as follows:
/1, 2, 3/*[a]*<e, e, e>
Where the pattern is /1, 2, 3/
that would result in the melody [a#, b, c]
.
The pattern elements would then be literal numbers or references in favor of #4. Both positive and negative would be acceptable.
The syntax needs reworking to naturally support transposing as well as chord root transpositions and type (minor, major, etc). Possibly consider using the opposite slashes for chord vs. melody patterns?
The function call syntax is very nice to read
{ arg1, arg2 } => funcName
save { arg1, arg2 } => funcName
but is not realistic for using return values in expressions.
Similar to javascript's export construct, MellowD could specify a default return value that is stored in the root of the saved object.
def function func => {
return.val -> [a, b, c]
return.default -> [a, b, c]
}
that is called like this
Block {
save { } => func
func*<e> //which is equivalent to
func.val*<e>
}
It could also just support assigning a value to return
since blocks of memory and values are actually stored separately. For example the above would be return -> [a, b, c]
instead of return.default -> [a, b, c]
.
<1, 3 => func> //return.defualt casted to an rhythm
(1, 3 => func) //cast to chord
[1, 3 => func] //melody
What do we do for numbers, beats, notes? A generic
{1, 3 => func}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.