Git Product home page Git Product logo

mellowd's People

Contributors

spencerpark avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

mellowd's Issues

Patterns

A possible new language feature in which the composer can specify a sequence of transpositions to apply to the following melody.

Overview

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.

Proposed syntax

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.

Design Considerations (Needs deciding)

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?

Improve internal structure of concatenation statements

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.

Range indexing

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]

MIDI Soundfonts

Support injecting soundfonts with the CLI and expose it in the compiler API.

Branches

This language feature would support diverging executions within a block.

Proposed syntax

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.

Considerations

The branches would share memory. This raises the same concerns raised in #12, we have concurrency issues with the shared memory.

Bring loop variables into the repetition scope

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
}

Decisions

  1. Decide on an iteration index name (maybe it or rep).
  2. Should there be convenience variables isLastRep, isFirstRep

API and JavaDocs

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.

Better error messages

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.

Run time chord builders

Supply a set of functions that take a root note a build a chord with the same name as the function.

Native interface and plugins

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

Possible implementation solutions

  1. In the declaration files there could be a direct link to a class file that can be resolved.
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.

  1. Another option is with annotations in the injected classes
@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.

  1. Plugins could register themselves with the existing interpreter API.
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.

  1. A little bit of both with a namespace and interface configuration file packed into the plugin jar. This would map imports to classes and then a custom source resolver that looks for plugins would be used to preform the lookup. Unfortunately it wouldn't resolve source, but rather implementation, and so the import mechanism would need to be fixed to account for this.

Recursive indexing

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

Generators

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.

Possible syntax

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>

Future plans

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.

Local function declarations

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.

Scope considerations

  1. Are functions defined within a block visible from other blocks? The namespace for the function is simply the block name so the fully qualified name is well defined and deterministic. From a user perspective this may be natural but from the compiler perspective this is a lot of overhead and complications that might never be worth it.
    • Constant functions with empty closure environments would be no problem to share
    • Non-empty closure environments with non-constant variable introduce the obvious race conditions with the implicit shared memory
    • These definitions may depend on the block's state and therefore must be defined at a certain point in time. Simply invoking a function has a race condition on the definedness of the function unless there is an implicit synchronization which is most likely too unnatural to impose.
  2. Should function definitions be more like methods that have a super memory referencing the block it is defined in?
  3. Functions defined in blocks as closures perhaps? This may be restricted to only allowed to reference constants in the super scope or it could take a snapshot as the initial value of a local variable in the function environment with the same name.

Other considerations

  1. Are the forward references allowed as with the fields? Leaning towards no to keep things consistent with local variables. Similarly this would feel more like jumping around in time with the sequential nature of the block bodies.
  2. Do they need to be constant?

Plugin loader functionality and CLI

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.

Controller blocks

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.

Proposed syntax

Definition

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

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>
        }
    }
}
Multi-performance

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.

Difference from branches (#19)

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.

Song Parameters

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.

Interpreter and intermediate format

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.

Landmarks

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.

Proposed Syntax

@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.

Constants and global variables

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.

Proposed Syntax

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

Mellow D truth

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

Comparisons

Only supported between like types. Otherwise an exception is raised.

  • beat: longer duration is larger than shorter duration
  • Rhythm: radix comparison of beats
  • note: higher pitch is larger than lower pitch, rest pitch is absolute lowest
  • chord: radix comparison of notes
  • melody: radix comparison of elements. If different a chord is always larger than a note

Nested comments

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 */
*/

Plugin injected sources for the import mechanism

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.

boolean expression parser

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 !=

If-Statements

TODO

  • Decide on syntax
  • Implement a boolean expression parser #1
  • Fill in the truth table #2

Some options for syntax include:

Traditional procedural if-statements.

if (condition) {
    code
} else if (other) {
    code
} else {
    code
}

Guarded statements.

{ code } when (condition)
or { code } when (condition)
or { code }

More verbose guarded statements.

only do { <code> } when <condition>
or { <code> } when <condition>
or { <code> }

Syntax

do { <code> } if <condition>
else do { <code> } if <condition>
else do { <code> }

Index resolution bug

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()));
    }

support references to numbers

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.

Add examples and get-started section

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:

  • The langRef.md seems to be good as a lookup table for people who already know the language but it cannot replace a tutorial.
  • The website doesn't work for me. When pressing compile, I only get "HTTP Error"

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:

  • add at least one simple .mlod example file (with a lot of comments explaining the source)
  • add a get started section in the Readme file, covering the necessary steps how to compile an .mlod file to MIDI

I think that's not too much of work but it would enhance usibility a lot.

Function call syntax

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.

Return default

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].

Possible call improvements

<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}

REPL

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:

  • imports
  • block definitions
  • variable definitions
  • block performances
  • function definitions

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.

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.