diff --git a/README.md b/README.md new file mode 100644 index 0000000..4272157 --- /dev/null +++ b/README.md @@ -0,0 +1,2366 @@ +# Simple Music Notation Processor + +## What is it? +SMNP is a command line tool aimed on doing music stuff using custom domain-specific language. +You are able to create music (both monophonic and polyphonic) using simple notation +and then synthesize it with custom settings, plot the signal, evaluate DFT on it and so on. +Music tools can be used not only to create music but also to prepare kind of scenarios +of ear trainings like recognizing intervals etc. +Apart of that developed domain-specific language offers you tools known from most popular +programming languages like conditional statements, loops, variable mechanism, +functions (including defining custom ones) etc. + + +## For what? +You might ask whether such kind of tool including design of custom language isn't +over-engineering of ear-training problem. There are a lot of ear-training tools developed even +on mobile platforms which can make them more convenient to use because of their portability feature. + +The reasons for this tool are: +* I'm Java developer and I just wanted to get know more Python, which I used to use at the college. +And well, I'm starting from the assumption, that te best thing that you can do to learn new programming +language is... creating a new one with it. ;-) +Besides, I'm kind of interested in technical side of programming languages that I'm using at work, +so designing and implementing a custom language from scratch would be a nice experience. +* I'm musician also and no one of available tools is suitable for me. +I'm church organist and most of my work is based on dialogue between me and priest. He can +sing melodies in different keys and it requires me to answer keeping the same key. +My tool allows me to create scenarios that can pick one key randomly and play melody +waiting for my answer (basing on input from microphone). +* As a musician I'm also keen on physic nature of sounds and relations between them. +All audio stuff (except hardware layer) implemented to this tool is created from scratch. +It means that the tool synthesises sound by itself and makes use of 3rd party library +only to send compiled wave to speakers. + +## Disclaimers +1. Readability of the code and its structure is one of most important things +related to educational aspect of the project. And in spite of having huge negative impact +on efficiency of the tool, according to one of the assumptions +it has much higher priority. So don't be surprised +if the tool turns out to be extremely slow or ineffective. +2. I try writing consistent code and sticking with some convention of programming of course, +however, as a Java developer I like Java guidelines and I really don't +like PEP8 (especially snakecased identifiers). +Just don't be surprised if you see Java-like code - it has been intentionally written. + +## SMNP language +As mentioned before, SMNP introduces new language. All language evaluation engine (tokenizer, parser and evaluator) +is implemented without any 3rd party libraries and frameworks, just using vanilla Python. + +### About language +SMNP language is interpreted, high-level, formal language with syntax +mostly inspired by Java language and - according to Chomsky's hierarchy - *context-free* grammar. + +#### Type system +SMNP is dynamic language, because there is no any static analyser between +parsing and evaluating stages. It means that any type-mismatching +will be raised on runtime, when control flow reacheas the point of error. + +For example: +``` +function foo(integer n) { + println(n); +} + +x = false; + +if (x) { + foo("hello"); +} +``` +As long as `x == false`, the code will be executed without any errors, +even though `foo` function expects `integer` argument and is being called +with `string`. +However if you switch `x` value to `true`, error will be raised when +control flow reaches `foo` function invocation with wrong argument's type. + +Even though there is no real definition of *strongly-typed language*, +we can say SMNP is strongly-typed, because there are no any implicit type +conversions. You always have to provide correct type, even for +string concatenation which accepts only `string` values: +``` +# Incorrect +"My number is: " + 14; + +# Correct +"My number is: " + 14.toString(); +``` + +### Comments +SMNP language allows you to make comments in the code. +It can be done with `#` character, like: +``` +# This is is a comment +``` +There is no syntax for multiline comment, but you can of course do something like this: +``` +# This is +# a multiline +# comment +``` +Note that because of hash-beginning comments you can +put a *shebang* at the very first of your code making it more +convenient to execute: +``` +#!/usr/bin/smnp +println("Hello, world!"); + +# And now add executable flag (chmod +x) +# to the file and execute it like any other +# script/program from the shell +``` + +### Delimiter +SMNP language doesn't require you to delimit instructions, however it is still possible +and highly recommended, because it helps you to get rid of code ambiguity. + +Example: +``` +size = [1, 2, 3].size +(size - 1) as i ^ print(i) +``` +Execution of this code is interrupted with error, because SMNP parser +tries to interpret `size` property as method `size(size - 1)`. +As long as lists don't have `size` method (but they have `size` property), +error will be raised and you will be able to fix problem. However ambiguity could be +a less obvious and you can stick with debugging code having no idea what is going wrong. + +To remove ambiguity you can end each instruction with semicolon `;`: +``` +size = [1, 2, 3].size; +(size - 1) as i ^ print(i); # 01 +``` + +All code snippets of present document follows the convention of ending each +instruction with semicolon. + +### Basic types +SMNP language introduces 9 data types and *void* which is a *quasi-type* because of it special meaning. +Most of the types is known from other popular programming languages, like Java etc. + +#### integer +`integer` is numeric data type and is suitable to signed integers, like -6, 0, 1, 14, 23164 etc. + +Example: +``` +a = 1; +b = -15; + +sum = a + b; # Produces also an integer +println(sum); # -14 +``` + +###### Methods: +* `toString()` [string] - returns a string representation of integer: `14.toString() == "14"` + +###### Unary operators: +* `-` - negates value + +###### Binary operators: +* `+` - sum +* `-` - difference +* `*` - product +* `/` - quotient +* `**` - power +* `==` - equals +* `!=` - not equals +* `>` - greater than +* `>=` - greater or equals +* `<` - less than +* `<=` - less or equal + +#### float +`float` is numeric data type suitable for non-integer values, like: -3.4, 0.01, 3.14, 12.0043 etc. + +Example: +``` +pi = 3.14; +r = 12.5; + +area = pi*r**2; +println(area); # 490.625 +``` + +###### Methods: +* `toString()` [string] - returns a string representation of float: `1.4.toString() == "1.4"` + +###### Unary operators: +see `integer` + +###### Binary operators: +see `integer` + +#### string +`string` type is suitable to any sort of texts enabling you +to create string of any characters, including Unicode. +For now, strings can be delimited only with double quote (`"`). + +Example: +``` +text = "Hello, world!"; + +println(text); # Hello, world! +println(-text); # !dlrow ,olleH +``` + +###### Properties +* `length` [integer] - length of string: `"hello".length == 5` + +###### Methods +* `join(list l)` [string] - join all elements of list `l` with +given string as delimiter: `":".join(["1", "2", "3", "4"]) == "1:2:3:4"` +* `toString()` [string] - returns itself ;-) + +###### Unary operators +* `-` - reverse string: `-"Hey!" == "!yeH"` (why not? ;-)) + +###### Binary operators +* `+` - concatenate strings: `"he" + "llo" == "hello"` +* `==` - equals +* `!=` - not equals + +#### bool +`bool` data type allows you to perform basic Boolean logic +operations introducing two constant values: `true` and `false`. + +Example: +``` +_2b = false; + +println(_2b or not _2b); # true +``` + +###### Methods: +* `toString()` [string] - returns a string representation of bool: `true.toString() == "true"` + +###### Unary operators +* `not` - negate value: `not false == true` + +###### Binary operators +* `and` - logical conjunction: `true and false == false` +* `or` - logical alternative: `true or false == true` +* `==` - equals +* `!=` - not equals + +#### note +`note` is basic data type allowing you to compose music sheets. +It is a base music unit that represents a sound, i.e. its pitch and duration. + +Note literal is written with the following syntax: +``` +'@' PITCH [OCTAVE] [:DURATION [d]] + +; where + +PITCH := (c|d|e|f|g|a) [b|#] | h# | b + | (C|D|E|F|G|A) [B|#] | H# | B +OCTAVE := 1-9 +DURATION := /non-negative integer/ +``` +Duration number means the denominator (`n`) of fraction `1/n`, i.e.: +* `1` stands for whole note +* `2` stands for half note +* `4` stands for quarter note +* `8` stands for eighth note +* `16` stands for sixteenth note +* and so on + +You can also put a `d` character after duration number to add a ***d**ot* to note, like: +* `2d` stands for half note and quarter note (dotted half note) +* `4d` stands for quarter note and eighth note (dotted quarter note) +* `16d` stands for sixteenth note and thirty-second note (dotted sixteenth note) +* and so on + +Default octave is `4` (1 Line) and default duration is `4` (quarter note). +Examples (note that pitch is case-insensitive): +* `@c` is quarter note with pitch *c'* +* `@F5:2` is half note with pitch *f''* +* `@g#3:4d` is dotted quarter note with pitch *g♯* +* `@Ab6:16` is sixteenth note with pitch *a♭'''* +* `@b2:1` is whole note with pitch *B* (*H♯*) +* `@C#1:32d` is dotted thirty-second note with pitch *C♯,* + +**Note:** note literal syntax cannot include any whitespace character. + +###### Properties +* `pitch` [string] - pitch of note: `@d#5:2d.pitch == "DIS"` +* `octave` [integer] - octave of note: `@d#5:2d.octave == 5` +* `duration` [integer] - duration of note: `@d#5:2d.duration == 2` +* `dot` [bool] - does note is dotted: `@d#5:2d.dot == true` + +###### Methods +* `withOctave(integer octave)` [note] - factory method that copies +note with new `octave` value: `@c.withOctave(5) == @c5` +* `withDuration(integer duration)` [note] - factory method that copies +note with new `duration` value: `@c.withDuration(2) == @c:2` +* `withDot(bool dot)` [note] - factory method that copies +note with new `dot` value: `@c.withDot(true) == @c:4d` +* `toIntRepr()` [integer] - convert note's pitch and octave to unique integer value +* `transpose(integer semitones)` [note] - copy note and transpose +it with given number of semitones: `@c.transpose(2) == @d` +* `toString()` [string] - returns a string representation of note: `@g#.toString() == "G#"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### sound +`sound` is a wrapper for external music file, like `ogg`, `mp3` etc. +This is the only type that isn't possible to create syntactically. +Instead of that it can be instantiated with constructor-like function, +as at the example below: +``` +myMusic = Sound("Music/Piano/Chopin/NocturneOp9No2.ogg"); +myMusic.play(); +``` + +###### Properties +* `file` [string] - a sound source file +* `fs` [integer] - sampling rate + +###### Methods +* `play()` [void] - play a loaded sound file +* `toString()` [string] - returns a string representation of sound: `Sound("/../../music.ogg").toString == "/../../music.ogg"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### list +`list` is an ordered container able to store objects with different types. +List is created within square brackets (`[` and `]`) with items separated by comma (`,`). +Lists can be nested, which means they can contain another +lists that can contain yet another lists and so on. + +Example: +``` +myList = [1, "hello", @Ab:2d, true, 14.0, [ "even", "other", list!"], [], {}]; +println(myList.size); # 9 +println([14].size); # 1 +println([].size); # 0 +println(myList.contains(1)); # true +println(myList.contains(2)); # false +``` + +###### Properties +* `size` [integer] - a number of elements in list + +###### Methods +* `get(integer index`) [?] - returns item of list with given index (**note:** indices start from **0**) +* `contains(element)` [bool] - test if list does contain given element +* `toString()` [string] - returns a string representation of list: `[1, 2].toString() == "[1, 2]` + +###### Unary operators +* `-` - reverse lists (just because!): `-[1, 2, 3, 4] == [4, 3, 2, 1]` + +###### Binary operators +* `+` - join lists: `[1, 2] + [3, 4] == [1, 2, 3, 4]` +* `==` - equals +* `!=` - not equals + + +#### map +`map` is unordered container able to store pairs `key-value` with different +types of both key and value. +Syntactically map is a set of pairs `key-value` separated with comma (`,`) and placed +between braces (`{` and `}`). Single `key-value` pair is created from two items +separated with arrow operator `->`. Similarly to lists, maps can also be nested. +Keys of course must be unique for single map object. + +Even though value of pair can be arbitrary expression, key +should be explicit literal, like integer, note, bool value, type and string. +Lists and other maps can't be used as keys. Note that if string +key doesn't have any whitespaces, there is no need to use quotes around it. + +Example: +``` +myMap = { + 1 -> "hello", + @c -> "world", + true -> false, + "hey" -> 14, + hey2 -> "key without quotes!", + empty -> {}, + theList -> [1, 2, [], { inside -> ":-)" }] +}; + +println(myMap.size); # 4 +println(myMap.get(@c)); # world +println(myMap.get("hey")); # 14 +println(myMap.get("hey2")); # key without quotes! +``` + +###### Properties +* `size` [integer] - number of map entries (pairs) +* `keys` [list] - list of keys: `{ a -> 1, b -> 2 }.keys == ["a", "b"]` +* `values` [list] - list of values: `{ a -> 1, b -> 2}.values == [1, 2]` + +###### Methods +* `containsKey(key)` [bool] - test if map does contain pair with given key +* `containsValue(value)` [bool] - test if map does contain pair with given value +* `contains(key, value)` [bool] - test if map does contain pair with given key **and** given value +* `toString()` [string] - returns a string representation of map: `{ a -> 1, b -> 2}.toString() == "{'a' -> '1', 'b' -> '2'}"` + +###### Binary operators +* `+` - join maps: `{ a -> 1 } + { b -> 2 } == { a -> 1, b -> 2 }` +* `==` - equals +* `!=` - not equals + +#### type +`type` represents all available data types including itself. +It is mostly used to get meta information about other values in code. + +Example: +``` +myNumber = 14; +println(typeOf(myNumber) == integer); # true +println(typeOf(myNumber) == bool); # false +println(typeOf(myNumber) == note); # false +println(typeOf(typeOf(myNumber)) == type); # true +println(typeOf(type)); # type +``` + +###### Methods +* `toString()` [string] - returns a string representation of type: `integer.toString() == "integer"` + +###### Binary operators +* `==` - equals +* `!=` - not equals + +#### void +`void` is special data type introduced to distinguish functions returning a value from +functions that don't return anything. It can't be used in any way and the only possibility +to do something with that is getting the type of it or getting a string representation of it. + +Example: +``` +void; # It actually does nothing +println(void.toString()); # void +println(typeOf(void)); # type +``` + +### Variables +SMNP language's variable mechanism allows you to store some data in memory which could be +used in the later stages of code. Even though SMNP language has a data types, declaring +new variables doesn't require you to explicitly put a type before declaration as in the case +of most popular languages, like Java etc. Hence it is not possible to declare variable +without initialization. You always have to initialize your variable when you are declaring it. +Initialization can be done with assignment operator (`=`). The right hand side should be +expression (value, function call or even other variable) but the left hand side can be only +variable identifier. + +``` +myVar; # It is not a declaration +myVar = 2; # It is a declaration. From now you are able to use variable myVar +3 = 2; # error! +var2 = foo(); # Correct +var2 = myVar; # Also correct +``` + +Variables are nothing but references to objects (values) and their don't have any type. +In other words you are able to assign value of different type to already initialized variable. + +``` +myVar = 2; +println(myVar); # 2 +myVar = "Hello, world!"; +println(myVar); # Hello, world! +``` + +Because assignment is an expression which returns a value being assigned it is possible +to perform multiple initialization. + +``` +a = b = c = 10; # It is like a = (b = (c = 10)) +println(a == b and b == c); # true +``` + +SMNP language doesn't have any equivalent of Python's `del` instructions, so you aren't +able to delete already created variable. The only way is using scopes. + +#### Scopes +Scopes are strongly related to blocks. +Block is a set of statements and expressions bounded on both sides with `{` and respectively `}`. +Even though it is not required and you can write whole code in one line, it is highly recommend +to use any kind of indentations for each block, including blocks nested inside. +``` +println("I'm outside any block"); + +{ + println("I'm in the first-level block"); + { + println("I'm in second-level block"); + } + + println("Greetings from first-level block again"); +} + +``` + +In context of variable mechanism it is important to know that variables declared inside +block are available to each instructions following declaration **in the same block** +and for each nested block placed after variable declaration. + +``` +var1 = "top-level"; +{ + var2 = "first-level"; + println(var1); # "top-level" + println(var3); # error! + { + var3 = "second-level"; + println(var1); # "top-level" + println(var2); # "first-level" + println(var3); # "second-level" + } + println(var2); # "first-level" + println(var3); # error! +} + +println(var2); # error! +println(var1); # "top-level" +``` + +#### Identifiers +It is also important to know what is identifier in context of SMNP language. +Identifier is a string that contains only letters (both lowercase and uppercase), numbers and `_` character. +Identifier must not start with number and must not be any of reserved words (keywords). + +List of keywords: +* `and` +* `or` +* `not` +* `integer` +* `string` +* `float` +* `note` +* `bool` +* `type` +* `list` +* `map` +* `function` +* `return` +* `extend` +* `import` +* `throw` +* `from` +* `with` +* `if` +* `else` +* `as` + + +``` +# Valid identifiers +i +var +myVar +my_var +var20 +x_YZ +_vArIaBlE +_if +_return +returns + +# Invalid identifiers +19i +foo[ +bar@ +return +!@#%$ +if +as +``` + +#### Immutability +It's a good place to say that all values in SMNP code are **immutable**. +That means you are not able to e.g. change pitch of existing note, change arbitrary letter +of string, put object to list and so on. + +You are able to produce note basing on given one with new pitch or join two or more lists +but these operations produces new objects and leave values unmodified. + +``` +a = [1, 2]; +b = [3, 4]; +c = a + b; +println(a == [1, 2]); # true +println(b == [3, 4]); # true +println(c == [1, 2, 3, 4]); # true + +a = @c; +b = a.withDuration(2); +println(a == @c); # true +println(b == @c:2); # true +``` + +### Operators precedence +SMNLP language's operators have their unique priorities which determine operations' order +in case of ambiguity. + +| Operator(s) | Precedence | +|----------------:|:----------:| +| `-` (unary) | 1 | +| `.` | 2 | +| `**` | 3 | +| `not` | 4 | +| `*` `/` | 5 | +| `+` `-` (binary)| 6 | +| `==` `!=` | | +| `<=` `>=` | 7 | +| `<` `>` | | +| `and` | 8 | +| `or` | 9 | +| `^` | 10 | + +Remember, that in spite of operators precedence you can always force priority using parentheses: +``` +a = -2+2; +b = -(2+2); + +println(a); # 0 +println(b); # -4 +``` + +### Methods and functions +Function is a code snippet that takes some arguments and produces result. +Both arguments and result can be optionals. +Method in turn is a special function that is called in behalf of some +object. +Function (and method of course) can be invoked using its name and optional arguments +separated with commas (`,`) bounded on both sides with parentheses (`(` and `)`). +Even if no arguments are passed you have to put empty arguments list: `()`. + +Some functions are available out-of-the-box as they are implemented +to SMNP tool. Set of such functions is called *standard library* (you can +read more about standard library in later section). You are able also +to create custom functions and methods (it'll be covered in next section). + +As you can see on below examples implemented functions and methods mechanism +is really similar to other popular languages, like Java or Python. + +Examples: +``` +# Invoking function 'println' without any arguments +println(); + +# Invoking function 'println' with 1 argument +println("Hello, world!"); # Hello, world + +# Invoking function 'println' with 2 argument +println(1, 2); # 12 + +# Invoking function 'println' with 2 argument +# Invoked function produces also result which +# is being assigned to variable 'newNote1' +newNote1 = transpose(2, @c); + +# Method equivalent of function above +newNote2 = @c.transpose(2); + +# Another example of method +firstElementOfList = [14, 3, 20, -4].get(0); + + +println(newNote1 == newNote2); # true +``` + +Note, that some functions or methods don't return anything. +In this case expecting any returned value can throw an exception. +``` +# 'println' is example of function, that doesn't return anything +# in this case following instruction will raise an error +println(println()); # error + +# other examples +x = println(); # error + +println([1, 2, 3, println(), 5, 6]); # error +``` + +#### Function/method signature +Signature is feature of both functions and methods that makes them unique. +Signature consists of function/method name, function/method arguments +and applicable type in case of method. +It is a good place to say, that methods technically are just simple functions +with one additional argument placed at the beginning of arguments list. +Because of that we can say that signature consists only of name and arguments +list. This is also the reason of a little bit meaningless errors +that you can get when you are trying to call method with invalid arguments' types: +``` +# correct +x = [1, 2, 3].get(0); + +# invalid +x = [1, 2, 3].get(@c); +``` +in this case you'll get following error (note first argument type +of expected signatures): +``` +Invocation Error +(...) + +Expected signature: +get(list, integer) +or +get(map, ) + +Found: +get(list, note) +``` + +### Custom methods and functions +SMNP language introduces possibility to define custom functions and methods. +They can be invoked later in the same way as regular functions/methods +provided by SMNP standard library. + +Functions and methods can be defined only at top level of the code, which means +they cannot be nested in any blocks. + +#### Function definition +Functions can be defined with `function` keyword, like it is shown on following example: +``` +function multipleBy2(number) { + return 2*number; +} + +# Correct invocations +x = multipleBy2(2); # x = 4 +x = multipleBy2(14); # x = 28 +x = multipleBy2("hey"); # in spite of correctness it will cause an error + # because string argument doesn't support '*' operator + +# Incorrect invocations +x = multipleBy2(1, 2); # it'll cause Invocation Error because of signatures mismatch +x = multipleBy2(); # as above +``` +Thanks to `return` keyword you can produce an output value basing on e.g. passed arguments. +Example above takes one arbitrary argument, multiplies it by 2 and then returns a result. + +##### Explicit argument types +Because arbitrary argument can be passed, the `multipleBy2` function can throw an error +in case of trying to multiple e.g. note or string, that don't support `*` operator. +In this case you can put a constraint to the argument which accepts only chosen types: +``` +function multipleBy2(integer number) { + return 2*number; +} + +# Correct invocations +x = multipleBy2(2); # x = 4 +x = multipleBy2(14); # x = 28 + +# Incorrect invocations +x = multipleBy2(1, 2); # it'll cause Invocation Error because of signatures mismatch +x = multipleBy2(); # as above +x = multipleBy2("hey"); # as above +``` +Function `multipleBy2` will work only with integer argument now. Any attempt to invoke it +with no-integer values will cause Invocation Error related to mismatched signatures. + +Of course you are still able to create functions without any arguments or mix their types: +``` +function noArgs() { + println("Hello, I don't accept any arguments."); + println("Also see that I don't return anything!"); +} + +function mixedArguments(integer a1, note a2, a3) { + println("See, " + a1.toString() + " is an integer!"); + println("And " + a2.toString() + " is a note."); + println("Type of third argument is " + typeOf(a3).toString()); +} + +# Correct invocations +mixedArgument(1, @c, "hey"); +mixedArguments(14, @Gb:4d, true); + +# Incorrect invocations +mixedArgument(@c, @c, @c); +mixedArgument(1, 1, 1); + +``` +##### Functions returning nothing +Notice also that functions presented in previous example don'tactually return anything. +Technically they **do** return a `void` type and it is transparent to SMNP user. You should remember, that +you can expect value produced by function only and only if `return` statement +is declared in invoked function and the statement **has been called**. Otherwise +function being invoked will return `void` type which cannot act as expression. + +##### Ambiguous arguments +SMNP language allows you also to declare more than one argument that functions can accept. +Accepted types are separated with commas (`,`) and are bounded on both sides with `<` and `>` characters. + +Example: +``` +function foo( x, sound y, z) { + # 'x' can be either string, bool, integer or float + # whereas 'y' can be only a sound + # and 'z' can be of any available type +} + +# Correct invocations +foo("hey", Sound("..."), [1, 2, true, [@c], { a -> 1, b -> 2 }]); +foo(true, Sound("..."), integer); +foo(14, Sound("..."), @c#:16d); +foo(1.4, Sound("..."), 3.14); + +# Incorrect invocations +foo(integer, Sound("..."), 10); +foo("hey", 20, 10); +foo([1, 2, 3], Sound("..."), 10); +``` + +##### Specified lists +SMNP language allows you to specify what objects can be contained in lists. +It can be done with construction similar to previous one. +Accepted types are separated with commas (`,`), bounded on both sides with `<` and `>` characters and placed +next to `list` keyword: +``` +function foo(list x) { + # 'x' can be only list of integers +} + +# Correct invocations +foo([1, -2, 3]); +foo([0]); +foo([]); + +# Incorrect invocations +foo(1); +foo(2, 3); +foo([ 1, 2, @c ]); +``` + +You are still able to use multiple type as constraints: +``` +function foo(list x) { + # 'x' can be only list of integers and notes, nothing else +} + +# Correct invocations +foo([1, @c, 4]); +foo([]); +foo([0]); +foo([@d#]); + +# Incorrect invocations +foo([1, @c, true]); +foo(@c, 1); +foo({ @c -> 1 }); +``` + +List specifier can be empty or even be absent. In this case list can contain +any available types: +``` +function foo(list<> x) { + # 'x' can be only a list containing arbitrary objects +} + +# the same function can be implemented as + +function foo(list x) { + # ... +} + +# Correct invocations +foo([1, @c, true]); +foo([]); +foo([integer, float, @g#:32d, false, [[[]]], { a -> 1, @d -> 2, true -> {} }]); + +# Incorrect invocations +foo({}); +foo([1, @c, true], 1); +foo(true); +``` + +Specifiers can be nested - you are able to create e.g. list of lists of lists of integers etc. +``` +function foo(list>> x) { +} + +# Correct invocations +foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]); + +# Incorrect invocations +foo([[[1, 2], [3, 4]], [[5, 6], [7, 8]], 9]); +``` + +Of course ambiguous arguments can include specified lists: +``` +function foo( x) { + # 'x' can be integer, note or list containing only integers and notes +} + +# Correct invocations +foo(@c); +foo(1); +foo([]); +foo([2, @G]); +foo([@h]); + +# Incorrect invocations +foo(1.0); +foo([true]); +``` + +##### Specified maps +Maps can be also specified. All rules related to specified lists are also applicable for maps. +The only difference is map supports **2** specifiers: first one for keys and second one for values. + +Example: +``` +function foo(map x) { + # 'x' can be only a map with strings as keys and notes as values +} + +function bar(map<> x) { + # 'x' can be only a map with strings as keys and arbitrary values +} + +function xyz(map x) { + # 'x' can be only a map with strings and booleans as keys and integers and notes as values +} + +function abc(map<> x) { + # 'x' can be only a map with arbitrary keys and integers with booleans as values +} + +# Correct invocations +foo({ c -> @c, d -> @d }); +bar({ a -> @c, b -> 1, c -> true, d -> map, e -> { x -> [] }); +xyz({ a -> 1, true -> @c, false -> 2, b -> @d }); +abc({ a -> true, 1 -> false, @c -> 10, true -> 14 }); + +# Incorrect invocations +foo({ c -> @c, @d -> @d, @c -> "hey" }); +bar({ a -> @c, @d -> @c }); +xyz({ integer -> @c, 2 -> 3, float -> bool }); +abc({ a -> true, false -> @c }); +``` + +##### Optional arguments +SMNP language allows you also to use default arguments. +Optional arguments are special kind of arguments that can have default value and are +totally optional to pass during invocation. Syntax of optional arguments is similar +to variable assignment syntax. + +Example: +``` +function foo(x = 10) { + # 'x' can be of any available type. + # You can override default value but you don't have to. + # In this case default value will be used. + + return x; +} + +# Correct invocations +y = foo(); # y = 10 +y = foo(10); # y = 10 +y = foo(true); # y = true +``` + +All kinds of arguments mentioned so far can be optional: +``` +function foo(x = 1, integer y = 14, > z = [[1, @c], [@d]]) { +} + +# Correct invocations +foo(); +foo(2); +foo(10, 11); +foo(-2, 33, @c); +foo(-2, 33, [[1, @d, @f#], [4, 3], [@c, @d, @e]); +foo(0, 0, []); +foo(0, 0, [[]]); + +# Incorrect invocations +foo(true); +foo(1, 0.5); +foo(10, 2, 13); +foo(0, 0, [], 3); +``` + +As you can see, you can have as many optional arguments as you want. +Just remember, that after first declared optional argument you can't use regular +arguments in the same signature anymore. +``` +# Correct signatures +function foo(a, b, c = 0) {} +function bar(a, b = "hello", c = true) {} +function xyz(a = integer, b = [12.5], c = @c) {} + +# Incorrect signatures (will throw an error) +function abc(a = 0, b) {} +function def(a, b, c = 0, d = true, e, f = @G#) {} +``` +In other words, optional arguments should be declared at the tail of function signature. + +##### Varargs +SMNP language also supports functions' signatures with arbitrary length. +This is exactly how `println` or `print` functions work. +You can pass as many arguments as you want and they still are able to handle it. +SMNP language provides a special operator `...` that enables you to have function +with variable number of arguments. + +Remember that: +* you can have only one vararg at the signature +* vararg should be placed at the very end of signature +* you can't mix varargs with optional arguments in one signature. + +After invocation, all matched arguments are collected into one list which is passed +as vararg. + +Example: +``` +function foo(a, b, c...) { + # 'c' is a list of arguments passed after 'a' and 'b' + + return c; +} + +# Correct invocations: +x = foo(0, 1); # x = [] +x = foo(1, 2, 3, 4); # x = [3, 4] +x = foo(true, false, @c, [3.14, 5, integer], float); # x = [@c, [3.14, 5, integer], float] + +# Incorrect invocations: +foo(); +foo(true); +``` + +Same as optional arguments, vararg can work with any previously mentioned kinds +of arguments (except optional arguments of course). +``` +function foo(a, note b, map> c...) { + # after two arguments you can pass any number of + # maps with strings as keys and list of integers and notes as values +} + +# Correct invocations: +foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> [@e] }); +foo(1, @c, {}, {}, {}, {}, {}, {}, {}, {}); + +# Incorrect invocations: +foo(); +foo(1, @c, { a -> [@d], b -> [@c, @g] }, { a -> [], b -> @e }); +``` + +#### Method definition +All rules related to defining custom functions are applicable to methods either. +The difference is method can be invoked in a context of some object. +That's why methods can be defined only as a part of some type. + +##### `extend` statement +SMNP language introduces `extend` statement which allows you to add custom +methods to existing types. All you need is to precise desired type and define function +which will be working as new method. +Following listing presents a syntax of `extend` statement: +``` +extend integer as i with function multipleBy(integer x) { + return i*x; +} + +# From now you can use new method as follows: +x = 10.multipleBy(2); # x = 20 +y = x.multipleBy(4); # y = 80 +``` + +As mentioned before, all rules related to functions are applicable here, so there +is no need to cover `function multipleBy(...) {...}` statement again. + +Code from listing above defines new method of `integer` type. +Note, that thanks to `integer as i` instruction, object that holds the context of method +is available through variable with name `i`. This works similar to arguments +passed to function and actually there are no differences between +using them in function's body. + +Even though type actually works as additional argument passed to function, there +is a slight difference between them. As mentioned before, you can use such features +in function signature as optional arguments, ambiguous arguments, varargs etc. +For obvious reasons these are not available for declaring type. +Type being extended must be declared only as a simple data type or map/list with optional +specifier. Also you have always provide an identifier by which you can refer to the type (`as` part). + +Examples: +``` +# Correct +extend integer as x with function a() {} +extend list as x with function b() {} +extend list<> as x with function c() {} +extend list> as x with function d() {} +extend map as x with function e() {} +extend map<><> as x with function f() {} +extend map<>>>> as x with function g() {} + +# Incorrect +extend as x with function h() {} +extend as x with function i() {} +extend <> as x with function j() {} +extend integer with function k() {} +``` + +Because extending statement supports list/map specifiers, you can define +methods that are applicable only for list/map with specified content: +``` +# Extends all lists, no matter of content +extend list as l with function foo() { + println("foo()"); +} + +# Extends only list +extend list as l with function bar() { + println("bar()"); +} + +l1 = [1, 2, 3, 4]; +l2 = [1, 2, @c, 4]; +l3 = ["a", "b", "c", "d"]; + +l1.foo(); # foo() +l2.foo(); # foo() +l3.foo(); # foo() + +l1.bar(); # bar() +l2.bar(); # error! +l3.bar(); # error! +``` + +Because SMNP doesn't allow you to extend multiple types in one instruction, if you want +extend for example `list` and `list` (but not `list`) you have +to do it separately: +``` +# This won't work at all +extend , list> as l with function foo() { + # ... +} + +# Instead of that you can do: +extend list as l with function foo() { + # ... +} + +extend list as l with function foo() { + # ... +} +``` +Note that there is no signature collision problem, because technically first argument +is different in both methods. + +##### `extend` block +In case of extending type with more than one function you can use block of methods. + +Example: +``` +extend integer { + function multipleBy2() { + return 2*i; + } + + function multipleBy4() { + return 4*i; + } +} + +# Construction above is equivalent of following code: + +extend integer as i with function multipleBy2() { + return 2*i; +} + +extend integer as i with function multipleBy4() { + return 4*i; +} +``` + +#### Overloading function/methods +Unfortunately functions/methods overloading +is not supported in custom functions/methods so far. +However you can notice that some functions and methods coming +from SMNP standard library core are overloaded, like `get()` method, +some constructor-like methods, `wait()` function and so on. +That's because these functions are implemented right in Python +and there still lacks function/method overloading mechanism directly in SMNP language. + +However you can still use ambiguous arguments and just check in function's body +their types by hand using `typeOf()` function: +``` +function myFunction( arg) { + if (typeOf(arg) == integer) { + println("You have passed integer"); + } else { + println("You have passed note!"); + } +} + +myFunction(1); # You have passed integer +myFunction(@c); # You have passed note +myFunction("hey"); # error! +``` + +### Imports +SMNP language allows you to split code into multiple files and then join them together +using `import` statement. +The statement loads file, executes its code and appends newly created context into +main file. It means that all imperative code (code that produces immediately result, like function calls) +will be executed just once, whereas declarative part (code that appends something to context, like +variable assignments or function definitions) will be appended to main context, so that +you will be able to take advantage of defined functions and variables. + +Example: +``` +#################### file_a.mus #################### +function foo() { + println("Hello, world!"); +} + +var = 14; + +println("I'm a file_a.mus!"); +``` +``` +##################### main.mus ##################### +import "file_a.mus"; # I'am a file_a.mus! + +foo(); # Hello, world! +println(var); # 14 +``` + +It is important to know, that `import` is just simple statement that loads external file and executes its code. +Thus there is no any protection from some known dependency problems, like circular dependencies +or repeatedly imported files. + +### `if` and `if-else` statement +SMNP language has support for good old condition statement +and syntax is inspired by Java language. + +#### `if` statement +The simplest form consists only of `if` keyword followed +by condition bounded on both sides with +parentheses (`(` and `)`) and statement +(or block of code): +``` +if (2 > 3) println("It gonna never be displayed!"); + +# or (using block of code) + +if (2 > 3) { + println("It gonna never be displayed!"); +} +``` + +Just like in Java condition **must be** of `bool` type. +``` +# Correct examples +if (true) println(); +if (false) println(); +if (not false) println(); +if (0 == 0) println(); +if ("hello".length > 0) println(); +if (true and not true or not false and true) println(); +if (true and (not true or not false) and true) println(); + +# Incorrect examples +if () println(); +if (0) println(); +if (1) println(); +if ("hello") println(); +if (true.toString()) println(); +``` + +#### `if-else` statement +You can use additional `else` clause which is always executed +when condition is resolved to `false`. + +Example: +``` +if (2 > 3) println("It gonna never be displayed!"); +else println("but this always will be displayed"); + +# or you can use blocks of code to increase readability + +if (2 > 3) { + println("It gonna never be displayed!"); +} else { + println("but this always will be displayed"); +} +``` + +Remember that only one condition statement branch (`if` or `else`) +can be executed in single statement. There is no possibility +to execute both clauses: +``` +x = 1; + +if (x > 0) { + println(true); +} else { + println(false); +} + +if (x < 0) { + println(false); +} else { + println(true); +} +``` +Above example will print twice `true`. First statement's condition is met, +so `if` clause did execute, but second statement's condition is resolved +to false so `else` clause did execute. + +#### `if-else-if-else-if-...` statement +Above constructions are sufficient +to create `if-else-if-...` pseudo-construction. +Thanks to it you are able to put additional execution branches +to existing condition statement. +*Pseudo* in this case means that there is no special parsing rules +nor evaluators related to this construction. It is just a combination +of `if` and `if-else` statements presented above. + +Example: +``` +x = 3; +if (x == 0) { + println("x is zero"); +} else if (x == 1) { + println("x is one"); +} else if (x == 2) { + println("x is two"); +} else if (x == 3) { + println("x is three"); +} else { + println("x is neither zero, one, two nor three"); +} + +# technically this construction is: + +if (x == 0) { + println("x is zero"); +} else { + if (x == 1) { + println("x is one"); + } else { + if (x == 2) { + println("x is two"); + } else { + if (x == 3) { + println("x is three"); + } else { + println("x is neither zero, one, two nor three"); + } + } + } +} +``` +Thanks to its tree-like construction control flow falls through all conditions. +A branch first matched branch is executed +and following conditions are not checked anymore: +``` +x = 0 +if (x == 1) { + println(); # Condition is resolved to false +} else if (x == 0) { + println(); # Condition is met +} else if (x == 0) { + println(); # Condition is also met, +} # but it hasn't been even checked because above + # branch has been already matched and executed +``` + +### Loop statement / expression +SMNP language provides a loop statement which also acts as expression. +The statement has been totally designed from scratch and is not similar +to any known loop statements from other languages. +Loop allows you to repeat some instructions with counter set programmatically. +All loops are created through loop operator (`^`). + +#### Simple loop +The simplest loop consists only of counter, loop operator and instruction or block +of instructions. + +Example: +``` +# the simplest loop +3 ^ print("Money "); # Money Money Money + +# the same using block of code + +3 ^ { + print("Money "); +} + +# Output: Money Money Money +``` + +Of course you can use any expression as counter, like variable or function call: +``` +function provideCounter(integer x) { + return x * 2; +} + +provideCounter(5) ^ print("a"); # aaaaaaaaaa +``` + +You can define a counter variable whose value is increased with each iteration +using `as` clause (note, that values start from 0): +``` +3 as i ^ print(i, " "); # 0 1 2 +``` + +#### *while* loop +You can create a loop which is controlled by logic condition. +It can be done just putting a `bool` value as a counter. + +Example: +``` +end = false; +i = 0; + +not end ^ { + if (i == 3) { + end = true; + } + + i = i + 1; +} + +println(end); # true +println(i); # 4 +``` + +Note that condition-controlled loop doesn't support `as` clause. + +#### *For-each* loop +This kind of loop works both with lists and maps. +##### List +You can put a list as a counter. In this case statement will be executed +for each element of list. You can obtain an access to element through `as` clause +like in previous examples. + +Example: +``` +x = [@c, @d, @e]; +x ^ print("Money "); # Money Money Money + +x as n ^ print(n, " "); # C D E +``` + +You can put additional parameter into `as` clause. In this case the first parameter +will be just iterator that is increased after each loop iteration. The second parameter +will be value of passed list. If you are using more than one parameter in `as` clause, +you have to enclose them between parentheses. + +Example: +``` +[@c, @d, @e] as (i, n) ^ println(i, ". ", n); + +# Output: +# 0. C +# 1. D +# 2. E +``` + +##### Map +Map is also supported to act as an counter of loop. +In this case statement will be executed for each value of passed map. + +Example: +``` +x = { + a -> @c, + b -> @d, + c -> @e +}; + +x ^ print("Money "); # Money Money Money +``` + +You are also able to access keys and values of iterated map using parameters in `as` clause. +If you pass only one parameter, it will contain a value of map. +If you pass two parameters, the first one will contain a key and the second one respectively a value. +If you pass three parameters, the first one will be an iterator increased by one with all +iteration, the second one will be key and the third one will be value. + +Example: +``` +myMap = { + first -> true, + second -> [@c, @d, @e], + third -> 14 +}; + +myMap as value ^ println(value); +# Output: +# true +# [C, D, E] +# 14 + +myMap as (key, value) ^ println(key, ": ", value); +# Output: +# first: true, +# second: [C, D, E] +# third: 14 + +myMap as (i, key, value) ^ println(i, ". ", key, ": ", value); +# Output: +# 0. first: true, +# 1. second: [C, D, E] +# 2. third: 14 +``` + +#### Filters +Loops in SMNP language also support filtering. +The filtering can be done with `%` clause, which contains a condition +that must be met to execute iteration. `%` clause is placed at the very last +of loop statement. + +Example: +``` +10 as i ^ println(i) % mod(i, 2) == 0; +# Output: +# 0 +# 2 +# 4 +# 6 +# 8 +``` +Filtering can be useful with simple counting loops like above, but it is really powerful with +*for-each* loops. You can pass further only elements of list/map that met +defined conditions. +It is even more useful with special feature of loops that is covered in next section. + +#### Loop expression +Really strength of designed loops relies on the fact, that all kinds of them actually can work as *expressions*. +It means that you can use any kind of loop as e.g. function argument, because they can produce a value. +They work similar to Python's list comprehension and actually produce a new list. +One important condition you have to met in order to use loop as expression is to put +expression as a loop's statement. Because of that in loop expression you can't use +block of code. The statement must be a single instruction. + +Example: +``` +x = [1, 2, 3, 4]; + +# 'y' holds numbers multipied by 2 +y = x as i ^ i * 2; # y = [2, 4, 6, 8] + +# 'z' holds squares of even numbers +z = x as i ^ i ** 2 % mod(i, 2) == 0; # y = [4.0, 16.0] +``` + +Note, that loop statement is right associative. +That means nested loop statements are being accumulated in right hand branch. +Take look at example: +``` +2 ^ 4 ^ 6 ^ print(); +# is exactly the same as +2 ^ (4 ^ (6 ^ print())); +``` +is parsed to following abstract syntax tree: +``` + ^ + / \ +2 ^ + / \ + 4 ^ + / \ + 6 print("a") +``` +It could be a flaw if you would like to create a mapping chain using loop statements +(like streams in Java 8 or LINQ in .NET), because it requires from operator +to be left associative, so that first instruction would produce value and pass it further. +It can be of course achieved with parentheses: +``` +data = ["lorem", "ipsum", "dolor", "sit", "amet"]; +output = (((((data as d + ^ d + % d.length > 3) as d + ^ d.length) as d + ^ d * 2) as d + ^ d + 1) as d + ^ d + % d == 11); + +println(output); # [11, 11, 11] +``` +As you can see it actually works but is not convenient and readable. + +### Throwing errors +Even though SMNP language doesn't have any sophisticated error handling mechanism, +you are able to raise errors using `throw` statement. +The `throw` construction is inspired by Java language, however instead of +throwing exceptions SMNP language allows you only to throw string values. +When control flow meet `throw` statement, program execution is immediately interrupted +and Execution Error is raised with message passed to `throw` statement. + +Example: +``` +function divide(integer a, integer b) { + if (b == 0) { + throw "You are trying to divide by 0!"; + } + + return a / b; +} +``` +If you try to invoke e.g. `divide(2, 0)` the Execution Error will be thrown +with message, that you are trying to divide by 0. + +SMNP language does not support any equivalent of `try-catch` statements known from +other languages, so you are actually unable to implement more advanced multi-layer +error handling system, because the language is not aimed on building complex systems +and there is simply no need to implement sophisticated error handling system +in simple music . + + +### Standard library +SMNP is provided with builtin standard library which consists of +some fundamental functions and methods that increase language's functionality. +SMNP don't require you to do anything in order to use anything from its standard library. +Just put function/method name with proper arguments and that's it. +Technically SMNP's standard library (*stdlib*) consists of 2 layers: native library and language library. + +#### Native library +Native library contains functions and methods that +are implemented **directly in Python**. +Generally this part of *stdlib* introduces fundamental functions +and methods that: +* enables communication between +SMNP and operating system, like: +`print`, `read`, `exit` etc. +* are related to audio module, like: +`wave`, `synth`, `wait`, `play` etc. +* are strictly bound to runtime environment, like: +`type`, `debug` etc. +* make use of Python libraries, like: +`rand`, `fft`, `plot` etc. +* are constructor-like functions, like: +`Integer`, `Map`, `Note` etc. + +Native functions and methods are located in `smnp/module` module. + +#### Language library +Language library contains functions and methods that are +implemented **in SMNP language itself**. +Some of SMNP assumptions points to move as many code as possible +from native library to language library, even at cost of efficiency. +This library contains such functions and methods as: `flat`, `transpose`, +`mod`, `tuplet`, `sample`, `random`, `join` and so on. + +Language library is a single file named `main.mus` and located +in `smnp/library/code` module. + +#### *stdlib* documentation +Documentation of both native and language library is included in +the `STDLIB.md` file of present repository. + +## SMNP language interpreter +SMNP language interpreter consists of three parts composed +to pipeline: +* *tokenizer* (or *lexer*) +* *parser* +* *evaluator* + +All of these components participate in processing and executing +passed code, producing output that can be consumed by next component. + +### Tokenizer +Tokenizer is the first component in code processing pipeline. +Input code is directly passed to tokenizer which splits +it to several pieces called *tokens*. Each token contains of +main properties, such as *value* and related *token type* (or *tag), +for example: +* literal: `"Hello, world!"` is token with value `Hello, world!` and +token type `STRING` +* literal: `abc123` is token with value `abc123` and token type `IDENTIFIER +* etc. +Apart from mentioned data, each token also includes some metadata, like +location of value in code (both line and column). + +You can check what tokens are produced for arbitrary input code +using `--tokens` flag, for example: +``` +$ smnp --tokens --dry-run -c "[1, 2, 3] as i ^ println(\"Current: \" + i.toString());" +[Current(0): {OPEN_SQUARE, '[', (0, 0)} +{OPEN_SQUARE, '[', (0, 0)}, {INTEGER, '1', (0, 1)}, {COMMA, ',', (0, 2)}, {INTEGER, '2', (0, 4)}, {COMMA, ',', (0, 5)}, {INTEGER, '3', (0, 7)}, {CLOSE_SQUARE, ']', (0, 8)}, {AS, 'as', (0, 10)}, {IDENTIFIER, 'i', (0, 13)}, {CARET, '^', (0, 15)}, {IDENTIFIER, 'println', (0, 17)}, {OPEN_PAREN, '(', (0, 24)}, {STRING, 'Current: ', (0, 25)}, {PLUS, '+', (0, 37)}, {IDENTIFIER, 'i', (0, 39)}, {DOT, '.', (0, 40)}, {IDENTIFIER, 'toString', (0, 41)}, {OPEN_PAREN, '(', (0, 49)}, {CLOSE_PAREN, ')', (0, 50)}, {CLOSE_PAREN, ')', (0, 51)}, {SEMICOLON, ';', (0, 52)}] +``` + +Tokenizer tries to match input with all available patterns, sticking +with rule *first-match*. That means if there is more than one patterns +that match input, only first will be applied. +This is why you can't for example name your variables or functions/methods +with keywords. Take a look at the output of following command: +``` +$ smnp --tokens --dry-run -c "function = 14;" +[Current(0): {FUNCTION, 'function', (0, 0)} +{FUNCTION, 'function', (0, 0)}, {ASSIGN, '=', (0, 9)}, {INTEGER, '14', (0, 11)}, {SEMICOLON, ';', (0, 13)}] +Syntax Error + [line 1, col 10] + +Expected function/method name, found '=' +``` +The first token has type of `FUNCTION`, not `IDENTIFIER` which is expected +for assignment operation. + +All tokenizer-related code is located in `smnp/token` module. + +### Parser LL(1) +Parser is the next stage of code processing pipeline. +It takes input from tokenizer and tries to compose a tree (called +AST, which stands for **a**bstract **s**yntax **t**ree) basing on known rules, which are called *productions*. +As long as tokenizer defines language's *alphabet*, i.e. a set +of available terminals, parser defines *grammar* of that language. +It means that tokenizer can for example detect unknown character +or sequence of characters meanwhile parser is able to detect unknown +constructions built with known tokens. +A good example is last snippet from above section related to tokenizer: +``` +$ smnp --tokens --dry-run -c "function = 14;" +[Current(0): {FUNCTION, 'function', (0, 0)} +{FUNCTION, 'function', (0, 0)}, {ASSIGN, '=', (0, 9)}, {INTEGER, '14', (0, 11)}, {SEMICOLON, ';', (0, 13)}] +Syntax Error + [line 1, col 10] + +Expected function/method name, found '=' +``` +You can see, that tokenizer has successfully done his job, +but parser throw a syntax error saying that it does not know +any production that could (directly or indirectly) match +`FUNCTION ASSIGN INTEGER SEMICOLON` sequence. + +You can check AST produced for arbitrary input code +using `--ast` flag, for example: +``` +$ smnp --ast --dry-run -c "[1, 2, 3] as i ^ println(\"Current: \" + i.toString());" +Program (line 0, col 0) + └─Loop (line 1, col 1) + ├─List (line 1, col 1) + │ ├─IntegerLiteral (line 1, col 2) + │ │ └'1' + │ ├─IntegerLiteral (line 1, col 5) + │ │ └'2' + │ └─IntegerLiteral (line 1, col 8) + │ └'3' + ├─Operator (line 1, col 16) + │ └'^' + ├─FunctionCall (line 1, col 18) + │ ├─Identifier (line 1, col 18) + │ │ └'println' + │ └─ArgumentsList (line 1, col 25) + │ └─Sum (line 1, col 38) + │ ├─StringLiteral (line 1, col 26) + │ │ └'Current: ' + │ ├─Operator (line 1, col 38) + │ │ └'+' + │ └─Access (line 1, col 41) + │ ├─Identifier (line 1, col 40) + │ │ └'i' + │ ├─Operator (line 1, col 41) + │ │ └'.' + │ └─FunctionCall (line 1, col 42) + │ ├─Identifier (line 1, col 42) + │ │ └'toString' + │ └─ArgumentsList (line 1, col 50) + ├─LoopParameters (line 1, col 14) + │ └─Identifier (line 1, col 14) + │ └'i' + └─NoneNode (line 0, col 0) +``` + +Technically SMNP does have **LL(1)** parser implemented. +The acronym means: +* input is read from **L**eft to right +* parser produces a **L**eft-to-right derivation +* parser uses one lookahead token. +Even though this kind of parsers is treated as the least sophisticated, in most cases +they do the job and are enough even for more advanced use cases. + +SMNP language parser has some fundamental helper function that provides +something like construction blocks that are used in right production +rules implementations. SMNP language parser actually is a combination +of sub-parsers that are able to parse subset of language. + +For example `smnp/ast/atom.py` file contains parsers related to parsing +atomic values, like literals and so on (note also that *expression* with parentheses +on both sides is treated like atom). +The file defines `AtomParser` which is actually a function that takes list +of tokens and produces AST. In this case `AtomParser` is able to parse only token +sequences mentioned before, like string literals, integer literals etc. + +But `AtomParser` is used by `UnitParser` located in `smnp/ast/unit.py`. +`UnitParser` introduces another production rules, like unary minus (`-`) operator +and dot (`.`) operator (which is used to accessing fields and methods). +In the first case, production is implemented using `Parser.allOf()` helper function +which implements *and*-operation (conjunction) performed between symbols. +```python +# smnp/ast/unit.py +minusOperator = Parser.allOf( + Parser.terminal(TokenType.MINUS, createNode=Operator.withValue), + Parser.doAssert(AtomParser, "atom"), + createNode=MinusOperator.withValues, + name="minus" +) + +# this implements following production rule: +# minusOperator ::= "-" atom +``` + +Next production rule of `UnitParser` produces `atom2` symbol using `Parser.oneOf()` helper function which +implements *or*-operation (alternative) performed between symbols: +```python +atom2 = Parser.oneOf( + minusOperator, + AtomParser, + name="atom2" +) + +# which implements following production rule: +# atom2 ::= minusOperator | atom +``` + +The last production rule of `UnitParser` is related to `unit` symbol and implements dot operator: +```python +Parser.leftAssociativeOperatorParser( + atom2, + [TokenType.DOT], + Parser.doAssert(atom2, "atom"), + createNode=lambda left, op, right: Access.withValues(left, op, right), + name="unit" +) + +# which implements following production rule: +# unit ::= atom2 | atom2 "." atom2 +# (leftAssociativeOperator() is function that implements production rule of left-associative operator) +``` + +`UnitParser` is then used by `FactorParser` (`smnp/ast/factor.py`) which defines power operator (`**`) +and logic negation operator (`not`). `FactorParser` is used by `TermParser` (`smnp/ast/term.py`) and `TermParser` is used in turn +by `ExpressionParser` (`smnp/ast/expression.py`). +It is also worth paying attention to the impact of cascade of production rules on operators precedence. + +As you can see, almost each parser is cascadingly composed from another parsers with consistently increased +supported subset of SMNP language. Thanks to the design, parsers are quite easy to debug and test. + +All parser-related code is located in `smnp/ast` module. + +### Evaluator +Evaluator is the last stage of SMNP language processing pipeline and also is the heart +of all SMNP tool, which takes AST as input and performs programmed operations. +Similar to implemented parser, evaluator works *recursively* because of processing tree-like +structure. +Evaluator's architecture is similar to parser's one. Evaluator consists of +smaller evaluators which are able to evaluate small part of AST's node types. + +Because evaluator introduces as *runtime* term, it also works on special object +called *environment*. The *environment* object contains some runtime information, +like available functions (both coming from *stdlib* and user-defined), scopes +(stack of defined variables), call stack and some meta information, like +source of code. This object is passed through all evaluators along with AST and its +subtrees. + +Example of `and` and `or` operators evaluators: +```python +# smnp/runtime/evaluators/logic.py +class AndEvaluator(Evaluator): + + @classmethod + def evaluator(cls, node, environment): + left = expressionEvaluator(doAssert=True)(node.left, environment).value + right = expressionEvaluator(doAssert=True)(node.right, environment).value + return Type.bool(left.value and right.value) + + +class OrEvaluator(Evaluator): + + @classmethod + def evaluator(cls, node, environment): + left = expressionEvaluator(doAssert=True)(node.left, environment).value + right = expressionEvaluator(doAssert=True)(node.right, environment).value + return Type.bool(left.value or right.value) +``` + +Above evaluators are bound to `And` and respectively `Or` AST nodes in `smnp/runtime/evaluators/expression.py` file, +which defines `ExpressionEvaluator`: +```python +result = Evaluator.oneOf( + # (...) + Evaluator.forNodes(AndEvaluator.evaluate, And), + Evaluator.forNodes(OrEvaluator.evaluate, Or), + AtomEvaluator.evaluate +)(node, environment) +``` +The `ExpressionEvaluator` uses `Evaluator.oneOf()` helper function which +tries to evaluate given node using one of defined evaluators. +`Evaluator.forNodes()` helper function in turn wraps evaluator to condition +which checks if node is one of given node types that are accepted by evaluator. + +`ExpressionEvaluator` among with some other evaluators is used in main evaluator +in `smnp/runtime/evaluator.py` file. + +Evaluator can be disabled using `--dry-run` flag. +All evaluator-related code is located in `smnp/runtime` module. + +### Interpreter +`Interpreter` actually isn't an another language processing +stage, rather it is a facade that composes each stage in one pipeline, +accepting a raw SMNP code as input. It also imports +each function and method from SMNP modules (`smnp/module` module, which is actually +a *SMNP native library*) and includes +it to passed `environment` object. As long as environment containing functions and methods +that come from *SMNP language library* is created in `smnp/main.py` file, +it is passed as argument to `_interpret()` function as `baseEnvironment` and extends +newly created `environment` object with its functions and methods. From now +`environment` object contains both *SMNP native library*'s and *SMNP language library*'s +functions and methods. + +Snippet below contains `_interpret()` function that composes pipeline of stages +mentioned before: +```python +# smnp/program/interpreter.py +# (...) +from smnp.module import functions, methods +# (...) + +class Interpreter: + # (...) + + @staticmethod + def _interpret(lines, source, printTokens=False, printAst=False, execute=True, baseEnvironment=None): + environment = Environment([{}], functions, methods, source=source) + + if baseEnvironment is not None: + environment.extend(baseEnvironment) + + try: + tokens = tokenize(lines) + if printTokens: + print(tokens) + + ast = parse(tokens) + if printAst: + ast.print() + + if execute: + evaluate(ast, environment) + + return environment + except RuntimeException as e: + e.environment = environment + e.file = environment.source + raise e +``` +That's the main flow of SMNP code execution. + +## Audio module +Audio module consists so far of 3 elements including 2 output modules (which +produces a song) and 1 input module (which work is based on microphone input). + +### `sound` type +`sound` type was totally covered in previous chapter, so there is nothing to say more. +You are able to load music and play it via `play()` method. +Technically this type uses *SoundFile* library which is responsible for loading +and playing music, so `sound` type actually is a wrapper for Python class provided +by *SoundFile* framework. + +### Synthesising module +In oppose to `sound` type, synthesising module is **designed and implemented from scratch**. +The only 3rd party code is provided with two frameworks: +* *SoundDevice* library, which actually +is an abstract layer above hardware and allows SMNP to pass custom data to sound card in order +to play music through speakers +* *Numpy* framework, which provides a special kind of list (*Numpy*'s array) that +is the only container type accepted by *SoundDevice*. + +#### Synthesising process +Synthesising process consists of two stages: compiling notes and playing wave. +Depending on your needs the stages can be performed separately in different functions +or merged to just one call. + +First of all you need to compile notes in order to have wave (which is of `list` type) using `wave` function. +Then already compiled wave can be finally synthesised with `synth` function. +``` +notes = [@c, @d, @e, @f]; +compiledWave = wave(notes); +synth(compiledWave); + +``` +Above code is really simple, so you can get rid of `wave` function invocation +and pass notes directly to `synth` function: +``` +synth([@c, @d, @e, @f]); +``` +And that's it! Both above codes will play following notes: +![cdef](img/notes/cdef.png) + +##### Rests +Rest is nothing but `integer` placed among the notes. +Value of number determines rest's length and works similar to duration part of note literal. +For example: +* `1` - whole rest +* `2` - half rest +* `4` - quarter rest +* `8` - eighth rest +* `16` - sixteenth rest +* `32` - thirty-second rest +* `64` - sixty-fourth rest +* etc. + +As long as rests are represented as integers, they don't supports dots which make them +longer with half of value, however you dot can be specified as another rest with +half value of previous one, for example: dotted half pause = `[2, 4]`. + +Example of using rests: +``` +notes = [@c, @d, 4, @f, @g, @a, 4, @c5]; +synth(notes); +``` +This code will play following notes: +![cd4fga8c](img/notes/cd4fga8c.png) + +##### Polyphony +You can have as many note staffs as you want and you are still able to merge them to one +wave, that can be played later. + +Both `wave` function and `synth` function accepts multiple lists of notes +as arguments. If you pass more than one list, all of them will be merged +and normalised to achieve polyphonic effect. + +Let's say you have following voices: +* ![twinkle1](img/notes/twinkle1.png) +* ![twinkle2](img/notes/twinkle2.png) + +Notes above can be merged to one wave, as follows: +``` +twinkle1 = [@c, @c, @g, @g, @a, @a, @g:2]; +twinkle2 = [@c, @c, @e, @e, @f, @f, @e:2]; +twinkle = wave(twinkle1, twinkle2); +synth(twinkle); + +# or just +synth(twinkle1, twinkle2); +``` +The result is: +![twinkle3](img/notes/twinkle3.png) + +**Remember**, that in order to achieve the effect you have to pass `list` arguments, +even if they will consist just of one element. + +Example: +``` +synth([@c], [@e], [@g], [@c5]); +synth(@c, @e, @g, @c5); +``` +The first line represents following notes: +![poly1](img/notes/poly1.png) + +but the second line is not polyphonic anymore: +![poly2](img/notes/poly2.png) + +##### Tuplets +Tuplets can be achieved with `tuplet` function, that comes from *stdlib*. +First argument of function determines how many notes +you can put, the second one determines how many notes will be +replaced and the last argument is vararg containing desired notes. + +Example: +``` +notes = [@g:2, @d5:2] + tuplet(3, 2, @c5:8, @h:8, @a:8) + [@g5:2, @d5] +synth(notes) +``` +the code will play following notes: +![starwars](img/notes/starwars.png) +Instead of 2 eighth notes (the second argument of `tuplet` function) +we are going to have 3 eighth notes (the first argument of `tuplet` function). + +Because `tuplet` function produces a list of notes, and both `synth` and +`wave` functions expect flat lists (i.e. lists that don't contain another lists) +there is need to concatenate 3 lists in order to have just one. +Another option is to include `tuplet` function to list in order to have +nested lists and then use `flat` function, as follows: +``` +notes = [@g:2, @d5:2, tuplet(3, 2, @c5:8, @h:8, @a:8), @g5:2, @d5] +synth(flat(notes)) +``` + +#### Synthesising configuration +Both `wave` and `synth` function have overloaded version that accepts +a config as first argument. + +The config value is actually of `map<>` type and +can include following keys: +* `bpm: integer` +* `tuning: integer` +* `overtones: list` +* `attack: float` +* `decay: float` + +Parameters above will be covered in this section. All keys included +to config map that don't match presented parameters +will be ignored. + +Remember, that the config argument is just additional argument +and are rules presented in previous section are applicable here as well. + +Example: +``` +w = wave({ bpm -> 270 }, @c, @d, @e, @f); + +w2 = wave({ bpm -> 60, tuning -> 432 }, + [@e, @f, @e, @d, @e:2, 2], + transpose(-12, [@g, @a, @g, @g, @g:2, 2]), + transpose(-12, [@c, @c, @c:4d, @h3:8, @c:2, 2]) +); +``` + +##### Tempo +Tempo can be adjusted with `bpm` property. +BPM means beats per minute and determines (in this case) how many quarter notes +will be played during single minute. +For example: +* 60 bpm - 60 quarter notes per minute - one quarter note per second +* 120 bpm - 120 quarter notes per minute - two quarter notes per second +* and so on. + +``` +notes = noteRange(@c, @c5, "diatonic"); # C Major scale +synth({ bpm -> 60 }, notes); # notes will be being played during 8s +synth({ bpm -> 120 }, notes); # notes will be being played during 4s +synth({ bpm -> 240 }, notes); # notes will be being played drugin 2s +``` + +##### Tuning +`tuning` is the property that determines frequency of `@a` sound. +Typically it is set to 440Hz or 432Hz, but it can be any positive value. +Using this property you are able to adjust tuning of all available notes. + +Example: +``` +a4 = @a:128; +plot(wave({ tuning -> 440, overtones -> [1.0] }, a4)); +plot(wave({ tuning -> 127, overtones -> [1.0] }, a4)); +``` +![a_440](img/plots/a_440.png) +![a_127](img/plots/a_127.png) +Plots above presents fundamental tone's sine wave of the same note: `@a` +in different tuning systems. + +You can hear the difference executing following code: +``` +notes = noteRange(@c, @c5, "diatonic"); # C Major scale +synth({ tuning -> 440 }, notes); +synth({ tuning -> 512 }, notes); +``` + +##### Overtones +Overtones allows you to specify the sound of single note being compiled. +In fact each note can be composed from sine waves with greater and greater +frequencies. +`overtones` parameter is a list of float values, that represents +a quotient of influence each overtone to output wave's amplitude. +First value at the list is related to fundamental tone. Second one +is related to first overtone (second harmonic), the third one +is related to second overtone (third harmonic) and so on. +You can read more about overtones at [Wikipedia](https://en.wikipedia.org/wiki/Overtone). + +Sum of list of overtones' values must be less or equal to 1. +To disable overtone simply put `0.0` at proper place of list. +For example sound with `overtones = [0.7, 0.0, 0.3]` consists +only of fundamental tone with amplitude equal to 0.7 of total amplitude and +third harmonic with amplitude equal to 0.3 of total amplitude. No other overtones +are enabled. + +Examples (with related plots): + +1st example (1 overtone - ideal pitchfork sound) +``` +# v 1st +w = wave({ overtones -> [1.0] }, @a:64) # a = 440 Hz +plot(w) +plot(fft(w)) +``` +![a_1overtone](img/plots/a_1overtone.png) +![a_1overtone_fft](img/plots/a_1overtone_fft.png) + + +2nd example (2 overtones) +``` +# v 1st v 3rd +w = wave({ overtones -> [0.7, 0.0, 0.3] }, @a:64) +plot(w) +plot(fft(w)) +``` +![a_2overtones](img/plots/a_2overtones.png) +![a_2overtones_fft](img/plots/a_2overtones_fft.png) + +3rd example (3 overtones) +``` +# v 1st v 3rd v 14th +w = wave({ overtones -> [0.5, 0.0, 0.3] + (10 ^ 0.0) + [0.2] }, @a:64) +plot(w) +plot(fft(w)) +``` +![a_3overtones](img/plots/a_3overtones.png) +![a_3overtones_fft](img/plots/a_3overtones_fft.png) + +##### Decay +`decay` parameter defines the tail of sound. Value of the parameter +determines how fast the sound is going to be "blanked". +The greater value of `decay` parameter, the sound will be blanked faster. +If `decay` parameter is set to 0, sound will not change its magnitude anymore. + +Example (with proper plots): +``` +a4 = @a:16; +plot(wave({ decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 0.5, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 1, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 5, overtones -> [1.0] }, a4)); +plot(wave({ decay -> 10, overtones -> [1.0] }, a4)); +``` +![decay0](img/plots/decay0.png) +![decay05](img/plots/decay05.png) +![decay1](img/plots/decay1.png) +![decay5](img/plots/decay5.png) +![decay10](img/plots/decay10.png) + +##### Attack +`attack` parameter is the last one supported by config map +and in oppose to `decay` determines how fast the sound is going to reach +full magnitude. +The greater value of `attack`, the sound will reach full magnitude faster. +If `attack` parameter is set to 0, the effect will be disabled and full +magnitude will be reached immediately. + +Example (with proper plots): +``` +a4 = @a:16; +plot(wave({ attack -> 0, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 1, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 5, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 10, decay -> 0, overtones -> [1.0] }, a4)); +plot(wave({ attack -> 100, decay -> 0, overtones -> [1.0] }, a4)); +``` +![attack0](img/plots/attack0.png) +![attack1](img/plots/attack1.png) +![attack5](img/plots/attack5.png) +![attack10](img/plots/attack10.png) +![attack100](img/plots/attack100.png) + +##### Mixing parameters +You are able to mix parameters and achieve really interesting results +to emulate for example piano, guitar, string ensemble etc. + +For example, you can achieve this sound's shape: +![example_config](img/plots/example_config.png) +using following code: +``` +config = { + attack -> 2, + decay -> 7, + overtones -> [0.5, 0.0, 0.0, 0.0, 0.3, 0.0, 0.2] +}; + +plot(wave(config, @a:64)); + +synth(config, [@c:1, @d:1, @e:1, @f:1], [2, @c5:1, @h:2, @c:1, @a:1]); +``` +It sounds really magically, isn't it? +Feel free to mix parameters in order to achieve desired sound +and enjoy it! + +##### Default values +If you don't pass any of supported parameters or even use +the simplest signature of `wave` or `synth` function, following default +values will be assigned to each parameter: +* `bpm = 120` +* `tuning = 440` +* `overtones = [0.4, 0.3, 0.1, 0.1, 0.1]` +* `decay = 4` +* `attack = 100` + +which causes following sound's shape: +``` +plot(wave(@a:64)) +``` +![default_config](img/plots/default.png) + +### Listening module +Listening module is the only implemented module that processes input +coming from microphone. +Moreover the module contains only one function which is `wait`. +The function accepts 2 integer arguments: `soundLevel` and `silenceLevel` and is useful to ear training. +The goal of `wait` function is to hang program execution and wait +for incoming sound. Only when the sound sounds and then stops, +the program's execution will be resumed. + +Function can be in three states. Let's call them: +* `WAIT_FOR_SOUND` +* `WAIT_FOR_SILENCE` +* `RESUME`. + +Now it is possible to show how it works using finite states machine: +![wait_fsm](img/schemas/wait_fsm.png) + +In other words, `wait` function waits for sound, which means +it hangs program execution and waits for exceeding `soundLevel` parameter by input level, +which is the first argument of `wait` function. +After that it waits for sound's stop (i.e. input level must be less than `silenceLevel`, which is +the second argument of `wait` function). +Only then `wait` function ends its work and resumes program's execution. + +If you are not sure about what `soundLevel` or `silenceLevel` values you need to use, +you can run SMNP program with `-m` flag in order to test in real time what kind of noise produces what level. + +Instead of calling function with two arguments, you can invoke it without any argument. +In that case: +``` +wait() +``` +is equivalent of +``` +wait(300, 10) +``` + +## Command-line interface +SMNP is provided as command-line tool and so far doesn't support +any graphic interface. +The simplest way to use SMNP is to type `smnp `, where `` +is a file containing SMNP language code. +You can put more files than one, just remember to split them with +spaces. +You are also able to execute SMNP language inline, which can be useful +in some kinds of batch scripts. +It can be done using `-c` option, like on following example: +``` +$ smnp -c "println(\"Hello, world!\")" +Hello, world! +$ +``` +Remember to escape all quotes if you are typing string literals +in this mode. + +Apart from this options mentioned so far, SMNP provides you another ones, +which you can list using `smnp -h` command. +All of them are also covered below: +* `-c ` - allows you to execute code passed as argument instead +of reading it from file +* `-m` - allows you to test input level coming from microphone +* `-h` - displays simple manual of SMNP usage with all options and +flags described +* `-v` - displays version of SMNP + +Options planned to be implemented: +* `-C ` (not implemented yet) - allows you to override +default file that contains common configuration of SMNP tool +* `-P =` (not implemented yet) - allows you to pass +properties that you will be able to obtain from code + +Because SMNP acts also as educational tool on field of parsing formal +languages, there are also implemented some flags allowing you +to see what is going on at each language processing stage: +* `--tokens` - prints tokenizer's output for passed code +* `--ast` - *pretty-prints* abstract syntax tree as parser's output for passed code +* `--dry-run` - runs language-processing tools without involving evaluator + diff --git a/STDLIB.md b/STDLIB.md new file mode 100644 index 0000000..9c0bc36 --- /dev/null +++ b/STDLIB.md @@ -0,0 +1,1084 @@ +# Standard library documentation +## SMNP native library +### `Float(integer number)` +### `Float(string number)` +### `Float(float number)` +Constructor-like function that produces a `float` type from either +integer, string or even float types. +###### Arguments +* `number` - integer/string/float representation of float type +###### Output +* input argument converted to `float` type +##### Example +``` +a = 14; +b = "3.14"; +A = Float(a); +B = Float(b); + +println(A == 14.0); # true +println(B == 3.14); # true +``` + +*** +### `Integer(float number)` +### `Integer(string number)` +### `Integer(integer number)` +Constructor-like function that produces a `integer` type from +either float, string or even integer types. + +**Note:** float-to-integer conversion is always done +with floor rounding first (see example). +###### Arguments +* `number` - float/string/integer representation of integer type +###### Output +* input argument converted to `integer` type +##### Example +``` +a = 3.14; +b = 3.74; +c = "14"; + +A = Integer(a); +B = Integer(B); +C = Integer(C); + +println(A == 3); # true +println(B == 3); # true +println(C == 14); # true +``` + +*** +### `Map(keyValuePairs...)` +### `Map(list keyValuePairs)` +Constructor-like function that produces a `map` type using passed +lists, that first element is a key and the second one is a value. +The second element can be anything, whereas the first one +can be only of types: `integer`, `string`, `bool`, `note` and `type`. +Both versions of the function works similar. The only difference is +that the first version accepts two-element lists as vararg, whereas +the second one accepts a single list of mentioned two-element lists (see example). +###### Arguments +* `keyValuePairs` - list/vararg of two-element lists that represents key-value pairs. +The first element of the list is key related to second element of it which acts as value. +###### Output +* a `map` composed from given lists +##### Example +``` +x = Map(["c", @c], ["d", @d], ["e", @e]); +y = Map([["c", @c], ["d", @d], ["e", @e]]); +z = { c -> @c, d -> @d, e -> @e }; + +println(x == y and y == z); # true +``` + +*** +### `Note(string pitch, integer octave, integer duration, bool dot)` +Constructor-like function that produces a `note` type +with given parameters. +###### Arguments +* `pitch` - a pitch of note (case insensitive) +* `octave` - a octave of note +* `duration` - a duration of note +* `dot` - produced note is dotted if true +###### Output +* a `note` with given parameters +##### Example +``` +x = Note("C#", 3, 2, true); + +println(x == @c#3:2d); # true +``` + +*** +### `Sound(string path)` +Constructor-like function that produces a `sound` object +associated with a sound file of given path. +Note, that this is the only way to obtain a `sound` object. +###### Arguments +* `path` - a relative or absolute path to music file +###### Output +* a `sound` value with sound of given path + +*** +### `wait()` +### `wait(integer soundLevel, integer silenceLevel)` +Hangs program execution and waits for sound. +If sound sounds it will wait for its stop. +Only then function will resume program execution. + +Read *README/Audio module/Listening module* to learn more about that function. + +###### Arguments +* `soundLevel` - a value that must be exceeded for the function to +recognize microphone input as a sound; default value: `300` +* `silenceLevel` - a value that must be lower than input level for the +function to recognize its as a end of sound; default value: `10` +##### Example +``` +availableNotes = noteRange(@c, @c5); + +# Perform the training scenario 10 times +10 ^ { + # Get random notes from 'availableNotes' list + x = sample(availableNotes); + y = sample(availableNotes); + + # Print and synthesize them + println(x, " -> ", y); + synth(x, y); + + # Wait for user to sing them + wait(50, 150); + + # Give another 2 sec for user + # to get ready for next turn + sleep(2); +} +``` + +*** +### `wave( notes...)` +### `wave(list notes...)` +### `wave(map<> config, notes...)` +### `wave(map<> config, list notes...)` +Compiles a given notes to wave, which is represented by list of floats. +You can pass additional `config` object which is a map overriding +default configuration parameters. + +Read *README/Audio module/Synthesising module* to learn more +about that function. + +###### Arguments +* `notes` - vararg/list of notes and integers which is going to be +compiled to wave +* `config` (optional) - a map whose parameters override default function +parameters +###### Output +* `list wave` - a list of floats that represents already compiled +wave that can be synthesised or plotted +##### Example +``` +notes = [@c, @c, @g, @g, @a, @a, @g:2, @f, @f, @e, @e, @d, @d, @c:2]; +music = wave({ bpm -> 170, overtones -> [0.7, 0.0, 0.0, 0.2, 0.0, 0.0, 0.1] }, notes); +synth(music); +``` + +*** +### `synth(list waves...)` +Synthesises and plays waves (one after another) already compiled by `wave` function. +###### Arguments +* `waves` - lists of floats that represent a wave capable to be synthesised + +##### Example +``` +wv = wave(noteRange(@c, @c5, "diatonic")); +synth(wv); +``` + +### `synth( notes...)` +### `synth(list notes...)` +### `synth(map<> config, notes...)` +### `synth(map<> config, list notes...)` +Works in the same way as `wave` function. The only difference is that +the `wave` function produces a compiled wave as list of floats, whereas +the `synth` function immediately synthesises and plays wave without +returning anything. +###### Arguments +* `notes` - vararg/list of notes and integers which is going to be +compiled to wave +* `config` (optional) - a map whose parameters override default function +parameters + +##### Example +``` +notes = [@c:8, @h3:8, @c:8, @d:8, @e:8, @d:8, @e:8, @f:8, @g, @a, @g, 4]; +2 ^ synth({ bpm -> 170, decay -> 3, overtones -> [0.5, 0.0, 0.3, 0.0, 0.15, 0.0, 0.05] }, notes); +``` + +*** +### `pause(integer value)` **deprecated** +Hangs a program execution for a time corresponding +to duration of note with given value. +The function also takes a `bpm` variable from the context. +###### Arguments +* `value` a value of corresponding note duration + +##### Example +``` +bpm = 60 # 60 beats per min = 1 beat per sec +pause(4) # the same as sleep(1) +pause(2) # the same as sleep(2) +pause(1) # the same as sleep(4) + +bpm = 120 # 120 beats per min = 2 beats per sec +pause(2) # the same as sleep(1) +pause(1) # the same as sleep(2) +``` + +##### Deprecation note +In the very first version of SMNP the `bpm` value wasn't passed +directly to `synth` or `wave` method, instead it was set +as regular variable assignment in the code. That's why +the `pause` function looks for `bpm` variable in the context. + +Current version of `pause` function can be implemented as follows: +``` +function newPause(integer value, integer bpm = 120) { + synth({ bpm -> bpm }, value); +} +``` + +*** +### `plot(list fun)` +Treats passed list of floats as mathematical function of indices +and draws its 2D plot (it uses *Matplotlib* plotting framework). +It is really useful to plot compiled waves (see example). +###### Arguments +* `fun` - list of floats to be drawn + +##### Example +``` +cNote = @c:256; +cNoteWave = wave({ attack -> 0, decay -> 0, overtones -> [1.0] }, c); +plot(cNoteWave); +``` +![c](img/plots/c.png) + +*** +### `fft(list fun)` +Transposes input list of floats being treated as function to another +list of floats using fast algorithm computing discrete Fourier transform (provided by *Numpy* framework). +It can be useful to plot wave spectrum or overtones of note sound. +###### Arguments +* `fun` list of floats which is going to be processed with FFT algorithm +###### Output +* a list of floats being a result of transforming input list with FFT +algorithm + +##### Example +``` +# The sound consists of 5 overtones: 1st, 3rd, 6th, 12th and 13th: +overtones = [0.5, 0.0, 0.3, 0.0, 0.0, 0.15] + (5 ^ 0.0) + [0.03, 0.02]; +signal = wave({ overtones -> overtones }, @c); +spectrum = fft(signal); +plot(spectrum); +``` +![spectrum](img/plots/spectrum.png) + +*** +### `rand(integer min, integer max)` +Returns a random `integer` from range *min—max* inclusive. +###### Arguments +* `min` - lower range limit (also included to the range) +* `max` - upper range limit (also included to the range) +###### Output +* a number of `integer` type randomly picked from given range + +Example: +``` +println(5 ^ rand(0, 3)); +# Output: +# 1 +# 0 +# 1 +# 3 +# 2 +``` + +*** +### `print(values...)` +Prints a string representation of each passed object to standard output. +The function implicitly invokes `toString()` method of each passed object, +concatenates it and prints to standard output. +###### Arguments +* `values` - arbitrary objects, whose string representations are about +to be printed + +##### Example +``` +print(1, 2, 3, "hello"); +print(" world!"); +# Output: 123hello world! +``` + +*** +### `println(values...)` +Works at the same way as `print` function, but appends +a new line character at the end. +###### Arguments +* `values` - arbitrary objects, whose string representations are about +to be printed + +##### Example +``` +println(1, 2, 3, "hello"); +println(" world!"); +# Output: +# 123hello +# world! +``` + +*** +### `read()` +### `read(string prompt)` +### `read(type expectedType)` +### `read(string prompt, type expectedType` +Reads user provided input from standard input and returns it. +It can also display given prompt message and convert input +to expected type. + +###### Arguments +* `prompt` - a message instructing user to provide data +* `expectedType` - a type to which data should be converted. +In case of conversion troubles an error is raised. +**Note**: only `integer`, `bool`, `note` and string of course are +supported so far. + +###### Output +* user's input as `string` if `expectedType` is not provided, otherwise +user's input as desired type (just if it is supported) + +##### Example +``` +$ echo "14" | smnp -c "println(typeOf(read(integer)))" +integer +``` + +*** +### `typeOf(object)` +Returns a object's type. + +###### Arguments +* `object` - arbitrary object which type is to be checked +###### Output +* a type of provided object, as `type` value + +##### Example +``` +println(typeOf(14)); # integer +println(typeOf(@A#)); # note +println(typeOf([1, 2, 3])); # list +println(typeOf([@c, @d, 4])); # list +println(typeOf({ c -> @c, d -> @d })); # map +``` + +*** +### `sleep(integer value)` +Hangs program execution for given time (in seconds). + +###### Arguments +* `value` number of seconds of program execution hang + +##### Example +``` +println("Hello"); +sleep(4); +println("World!"); +# Output: +# Hello +# World! (after 4s) +``` + +*** +### `exit(integer code)` +Immediately interrupts program execution and returns provided +given code to operating system. + +###### Arguments +* `code` - an exit code. If function `exit` is never called, SMNP returns +a `0` value as default. + +##### Example +``` +$ smnp -c "exit(38);" +$ echo $? +38 +``` + +*** +### `debug(string parameter)` +Allows to get some information about environment at different +program execution stages. +###### Arguments +* `parameter` - name of parameter which is to be displayed. +Available values: + * `environment` - prints whole environment object + * `variables` - prints only variables with their scopes + * `functions` - prints declared functions including those coming from *stdlib* + * `methods` - prints declared methods including those coming from *stdlib* + * `callstack` - prints a call stack + +##### Example +``` +debug("variables"); +x = 1; +debug("variables"); + +# Output: +# Scopes: +# [0]: {} +# Scopes: +# [0]: {'x': INTEGER(1)} +``` + +*** +### `.get(integer index)` +Returns list element of given index. +###### Arguments +* `index` - index of desired element. **Note**, that counting starts from **0**. +###### Output +* element of list with given index. Throws an error if index is out of bounds. + +##### Example +``` +myList = [1, 2, 3, 4]; +lastElement = myList.get(3); +println(lastElement); # 4 +``` + +*** +### `.get( key)` +Returns map element associated with given key. +###### Arguments +* `key` - key of value that is about to be returned +###### Output +* element associated with given key. Throws an error if key doesn't exist. + +##### Example +``` +myMap = { + true -> false, + integer -> 14, + hello -> "world" +}; + +element = myMap.get("hello"); +println(element); # world + +``` + +*** +### `.play()` +Plays a music file loaded into `sound` type. + +##### Example +``` +music = Sound("music/buxtehude_passacaglia.ogg"); +music.play(); +``` + +*** +### `.toString()` +Returns a `string` representation of any available object. + +##### Example +``` +println(typeOf(14.toString())); # string +println(typeOf("hello, world".toString())); # string +println(typeOf(true.toString())); # string +println(typeOf(@Gb3:16d.toString())); # string +println(typeOf({ first -> @c, second -> [1, 2, 3] }.toString())); # string +println(typeOf(void.toString())); # string +``` + +## SMNP language library +All *SMNP language library* is defined in single file: `smnp/library/code/main.mus`. +Note that some of functions start with underscore (`_`). Even though +SMNP language doesn't have access modifiers known from other languages (like +Java's `private`, `public` etc.), you shouldn't use these method and they aren't +covered in this documentation. + +### `flat(list lists...)` +Converts nested lists to single one. You can think, that the function +removes nested square brackets and leaves the outer ones (see examples). +###### Arguments +* `lists` - nested lists supposed to be flat and concatenated +###### Output +* a single list composed of given lists that have been flatten + +##### Example +``` +list1 = [1, 2, [3]]; +list2 = [4, 5, [6, [7]]]; +list3 = [[8, 9], [10], [[11, 12], 13], 14]; + +a = flat(list1, list2, list3); +b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + +c = flat([@c, [@d, @e], [@f, [@g, @a], @h, @c5, [@d5]]]); +d = [@c, @d, @e, @f, @g, @a, @h, @c5, @d5]; + +overtones = flat([0.5, 10^0.0, 0.3, 5^0.0, 0.1, 3^0.0, 0.1]); + +println(a == b); +println(c == d); +println(overtones); + +# Output: +# true +# true +# [0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.1] +``` + +*** +### `mod(integer a, integer b)` +Computes and returns reminder after division of `a` by `b`. +###### Arguments +* `a` - the first operand of *modulo* operation +* `b` - the first operand of *modulo* operation +###### Output +* `a mod b` value computed as `a - b * floor(a/b)` + +##### Example +``` +12 as i ^ println(i, " mod ", 3, " = ", mod(i, 3)); +# Output: +# 0 mod 3 = 0 +# 1 mod 3 = 1 +# 2 mod 3 = 2 +# 3 mod 3 = 0 +# 4 mod 3 = 1 +# 5 mod 3 = 2 +# 6 mod 3 = 0 +# 7 mod 3 = 1 +# 8 mod 3 = 2 +# 9 mod 3 = 0 +# 10 mod 3 = 1 +# 11 mod 3 = 2 +``` + +*** +### `sample(items...)` +### `sample(list items)` +Returns randomly picked item from provided arguments or list of items. +###### Arguments +* `items` - list of items from which one of them is to be randomly picked +###### Output +* one, randomly picked element of provided list + +##### Example +``` +5 ^ println(sample(noteRange(@c, @f))); +# Output: +# CIS +# F +# C +# D +# C + +println(sample(1, 2, 3, 4, 5)); # 4 +``` + +*** +### `random(map<> items...)` +Returns randomly picked value from given items according +to provided probability and using uniform distribution. +Function randomly picks a one of provided maps and returns +a value that is assigned to `value` key of picked map. +###### Arguments +* `items` - a maps consisted of two key-value pairs: + * arbitrary value assigned to `value` key which is + an object to be returned + * `integer` value assigned to `percent` key which + is a percent value of probability of picking + the map. Sum of each map's `percent` value must be + equal to 100. + +###### Output +* an object assigned to `value` of randomly picked map provided as argument + +##### Example +``` +10 ^ { + x = random( + { percent -> 75, value -> "hello" }, + { percent -> 20, value -> "world" }, + { percent -> 5, value -> "hey" } + ); + + println(x); +} + +# Output: +# hello +# world +# hey +# hello +# hello +# hello +# hello +# world +# hello +# hello +``` + +*** +### `range( a, b, step = 1)` +Returns a list containing all values with given step from range *a—b* inclusive. + +###### Arguments +* `a` - lower range limit (also included to the range) +* `b` - upper range limit (also included to the range) +* `step` (optional) - difference between each pair of adjacent elements + +###### Output +* list of integers or float numbers (depending on type of provided arguments) + +##### Example +``` +println(range(1, 5) == [1, 2, 3, 4, 5]); # true +println(range(1, 10, 2) == [1, 3, 5, 7, 9]); # true +println(range(5, 30, 5) == [5, 10, 15, 20, 25, 30]); # true + +# Using minus operator ('-') you are able to reverse lists: +println(-range(2, 12, 2) == [12, 10, 8, 6, 4, 2]); # true +``` + +*** +### `noteRange(note a, note b, string filter = "all")` +Returns a list containing all notes from range *a—b* inclusive which meets +provided filter. +###### Arguments +* `a` - lower range limit (also included to the range) +* `b` - upper range limit (also included to the range) +* `filter` (optional) - allows to select what kind of notes should be +included to the list. +Available filters: + * `all` - returns all notes from given range + * `diatonic` - returns only diatonic notes (C, D, E, F, G, A and H) from given range + * `chromatic` - returns only chromatic notes (C#/Db, D#/Eb, F#/Gb, G#/Ab, A#/B) from given range +###### Output +* a list of notes from given range with provided filter applied + +##### Example +``` +println(noteRange(@g3, @g) == [@g3, @g#3, @a3, @b3, @h3, @c, @c#, @d, @d#, @e, @f, @f#, @g]); # true +println(noteRange(@c, @c5, "diatonic") == [@c, @d, @e, @f, @g, @a, @h, @c5]); # true +println(noteRange(@d5, @a5, "chromatic") == [@d#5, @f#5, @g#5]); # true +``` + +*** +### `transpose(integer value, notes...)` +### `transpose(integer value, list notes)` +Transposes provided notes by given value which is a number of semitones. +Note, that integers acting as rests are ignored. +###### Arguments +* `value` - a number of semitones. If the value is positive, notes will be up-transposed, +otherwise it'll be down-transposed. +* `notes` - a list of notes and integers. All notes of the list will be transposed +by given number of semitones. +###### Output +* list of transposed notes with integers remaining at their original places + +##### Example +``` +cMajor = [@c, @d, @e, @e, @f, @f, @e:2, 2]; +dMajor = transpose(2, cMajor); +gMajor = transpose(-7, dMajor); + +println(dMajor == [@d, @e, @f#, @f#, @g, @g, @f#:2, 2]); +println(gMajor == [@g3, @a3, @h3, @h3, @c, @c, @h3:2, 2]); +``` + +*** +### `transposeTo(note target, notes...)` +### `transposeTo(note target, list notes)` +Transposes provided notes to given target, so that the first of transposed +notes will be equal to `target`. Integers acting as rests are ignored. +###### Arguments +* `target` - determines a pitch and octave of first of transposed notes +* `notes` - a list of notes and integers supposed to be transposed +###### Output +* list of transposed notes with integers remaining at their original places + +##### Example +``` +cMajorScale = noteRange(@c, @c5, "diatonic"); +aMajorScale = transposeTo(@a, cMajorScale); +d5MajorScale = transposeTo(@d5, aMajorScale); + +println(aMajorScale == [@a, @h, @c#5, @d5, @e5, @f#5, @g#5, @a5]); # true +println(d5MajorScale == [@d5, @e5, @f#5, @g5, @a5, @h5, @c#6, @d6]); # true +``` + +*** +### `semitones( notes...)` +### `semitones(list notes)` +Returns a list of numbers of semitones between provided notes. +###### Arguments +* `notes` a list of notes and integers acting as music rests. +The list is filtering to get rid of integers so they are just ignored as they were +not there. +###### Output +* typically list of integers which represent a number of semitones +between each pair of adjacent notes. +If you invoke the function with only 2 note arguments, the return value will be +just an integer not wrapped in any lists (see examples). + +##### Example +``` +a = semitones(@c, @g); +b = semitones(@c, @d, @e, @f); +c = semitones([@c, @g]); +d = semitones([@c, @d, @e, @f]); +e = semitones([@c, 2, 4, @g]); +f = semitones([@c, @d, @e, @f], [@g, @a, @h, @c5]); + +println(a); # 7 +println(b); # [2, 2, 1] +println(c); # [7] +println(d); # [[2, 2, 1]] +println(e); # [7] +println(f); # [[2, 2, 1], [2, 2, 1]] +``` + +*** +### `stringInterval(integer semitones)` +Returns a pretty interval name as string for given number of semitones. +Works only for pitch values, so the max accepted value of `semitones` is 11. +###### Arguments +* `semitones` - a number of semitones to be converted to string +###### Output +* interval name for given semitones number + +##### Example +``` +12 as i ^ println(stringInterval(i)); +# Output: +# 1 +# 2m +# 2M +# 3m +# 3M +# 4 +# 5d/4A +# 5 +# 6m +# 6M +# 7m +# 7M +``` + +*** +### `interval( notes...)` +### `interval(list notes)` +Composes mentioned `semitones` and `stringInterval` functions together. +Works at the same way as `semitones` function. The difference is that +produces list of strings (containing a pretty name of intervals) +instead of list of numbers (acting as numbers of semitones). +###### Arguments +* `notes` a list of notes and integers acting as music rests (see `semitones` function documentation) +###### Output +* typically list of strings which represents a pretty name of intervals +(see also `semitones` function documentation) + +##### Example +``` +a = interval(@c, @g); +b = interval(@c, @d, @e, @f); +c = interval([@c, @g]); +d = interval([@c, @d, @e, @f]); +e = interval([@c, 2, 4, @g]); +f = interval([@c, @d, @e, @f], [@g, @a, @h, @c5]); + +println(a); # 5 +println(b); # [2M, 2M, 2m] +println(c); # [5] +println(d); # [[2M, 2M, 2m]] +println(e); # [5] +println(f); # [[2M, 2M, 2m], [2M, 2M, 2m]] +``` + +*** +### `tuplet(integer n, integer m, note notes...)` +Returns given tuplet of provided notes as list. +###### Arguments +* `n` - how many notes does tuplet contain: + * `n = 3` for triplet + * `n = 5` for quintuplet + * etc. +* `m` - how many notes is replaced by tuplet: + * `m = 2` for triplet + * `m = 4` for quintuplet + * etc. +* `notes` - notes supposed to be included in tuplet. **Note**, that +the number of notes must be equal to `n` argument. +###### Output +* created tuplet as list of notes + +##### Example +``` +triplet = tuplet(3, 2, @c, @d, @e); +quintuplet = tuplet(5, 4, @c, @d, @e, @f, @g); + +synth(triplet); +synth(quintuplet); +``` + +*** +### `metronome(integer bpm = 120, integer beats = 4, countMeasures = false)` +Starts a metronome with synthesised beats and accents. + +###### Arguments +* `bpm` (optional) - determines metronome's tempo +* `beats` (optional) - determines a time signature per quarters: + * `4` = 4/4 + * `3` = 3/4 + * `2` = 2/4 + * etc. +* `countMeasures` (optional) - prints a number of measure for each cycle + +##### Example +``` +$ smnp -c "metronome(120, 4, true)" +1 +2 +3 +4 +``` + +*** +### `alert( cycles = true, string melody = "beep", list overtones = [0.5, 0.0, 0.0, 0.5])` +Synthesises an alert melody. +###### Arguments +* `cycles` - number of alert cycles. Infinite if `true`. +* `melody` - a name of synthesised melody. Available values: + * `beep` + * `s1` + * `s2` + * `s3` + * `semitones` +* `overtones` - allows to compose custom sound of alert + +##### Example +``` +$ ./some_exhausting_task && echo "Done!" && smnp -c "alert()" +/after a long time/ +Done! +``` + +*** +### `noteFromIntRepr(integer intRepr, integer duration, bool dot)` +Creates a `note` with given duration (including or not the dot) basing on +passed *intRepr* value. +The *intRepr* is a number representation of note's octave and pitch. +It is computed using following algorithm: +1. let's renumber available pitches: + * C ⇒ 0 + * C# ⇒ 1 + * D ⇒ 2 + * D# ⇒ 3 + * E ⇒ 4 + * F ⇒ 5 + * F# ⇒ 6 + * G ⇒ 7 + * G# ⇒ 8 + * A ⇒ 9 + * A# ⇒ 10 + * H ⇒ 11 +2. convert a note's pitch value using mappings above +3. multiple note's octave by 12 and add it to the result of the second point + +For example: +``` +# For @f#5 note: +# 1. f# => 6 +# 2. 5th octave => 5 * 12 = 60 +# 3. 60 + 6 = 66 +# 66 is the intRepr of @f#5 note +``` + +**Note**, that the *intRepr* depends only on pitch and octave. +Note duration doesn't have any effect on the *intRepr* value. +That's why the function accepts also duration-related arguments. +###### Arguments +* `intRepr` - a `integer` determining a pitch and octave of new note +* `duration` - a duration of new note +* `dot` - true if note should be dotted +###### Output +* note created from given arguments + +##### Example +``` +x = noteFromIntRepr(66, 8, true); +println(x == @f#5:8d); # true +``` + +*** +### `.toIntRepr()` +Returns a *intRepr* of note. +###### Output +* an *intRepr* as `integer` + +##### Example +``` +x = @f#5.toIntRepr(); +println(x); # 66 +``` + +*** +### `.transpose(integer value)` +Copies note and transposes it by given semitones. +###### Arguments +* `value` - a number of semitones +###### Output +* new transposed note with copied duration-related attributes + +##### Example +``` +x = @Fb; +y = x.transpose(2); + +println(x == @Fb); # true +println(y == @Gb); # true +``` + +*** +### `.withOctave(integer octave)` +Copies note and returns it with new value of `octave` parameter. +###### Arguments +* `octave` - desired octave +###### Output +* copy of note with changed value of `octave` parameter + +##### Example +``` +x = @F5:8d; +y = x.withOctave(6); + +println(x == @F5:8d); # true +println(y == @F6:8d); # true +``` + +*** +### `.withDuration(integer duration)` +Copies note and returns it with new value of `duration` parameter. +###### Arguments +* `duration` - desired duration +###### Output +* copy of note with changed value of `duration` parameter + +##### Example +``` +x = @F5:8d; +y = x.withDuration(2); + +println(x == @F5:8d); # true +println(y == @F5:2d); # true +``` + + +*** +### `.withDot(bool dot)` +Copies note and returns it with new value of `dot` parameter. +###### Arguments +* `dot` - determines if new note is supposed to be dotted +###### Output +* copy of note with changed value of `dot` parameter + +##### Example +``` +x = @F5:8d; +y = x.withDot(false); + +println(x == @F5:8d); # true +println(y == @F5:8); # true +``` + +*** +### `.contains(expectedValue)` +Checks if list does contain given value. +###### Arguments +* `expectedValue` - a searched element +###### Output +* `true` if provided value is contained in list, `false` otherwise + +##### Example +``` +myList = [1, 2, @c, true, bool, "hello"]; +println(myList.contains("hello")); # true +println(myList.contains(integer)); # false +``` + +*** +### `.containsKey(expectedKey)` +Checks if map does containing a key-value pair with given key. +###### Arguments +* `expectedKey` - a searched key +* `true` if map does contain a key-value pair with given key, `false` otherwise + +##### Example +``` +myMap = { + hello -> 14, + world -> @Db:16, + true -> false, + integer -> [3.14] +}; + +println(myMap.containsKey("hello")); # true +println(myMap.containsKey(false)); # false + +``` + + +*** +### `.containsValue(expectedValue)` +Checks if map does containing a key-value pair with given value. +###### Arguments +* `expectedValue` - a searched value +* `true` if map does contain a key-value pair with given value, `false` otherwise + +##### Example +``` +myMap = { + hello -> 14, + world -> @Db:16, + true -> false, + integer -> [3.14] +}; + +println(myMap.containsValue([3.14])); # true +println(myMap.containsValue({})); # false +``` + + +*** +### `.contains(key, value)` +Checks if map does containing given key-value pair. +###### Arguments +* `key` - a key of searched key-value pair +* `value` - a value of searched key-value pair +* `true` if map does contain given key-value pair, `false` otherwise + +##### Example +``` +myMap = { + hello -> 14, + world -> @Db:16, + true -> false, + integer -> [3.14] +}; + +println(myMap.contains(integer, [3.14])); # true +println(myMap.contains(integer, 3.14)); # false +println(myMap.contains(type, [3.14])); # false +``` + +*** +### `.join(list l)` +Treats the string as delimiter and uses it to concatenate each +element of passed list of strings. +###### Arguments +* `l` - list of strings to be concatenated +###### Output +* a single, concatenated string + +##### Example +``` +myList = [@c, @d, @e, @f]; +output = " :: ".join(myList as e ^ e.toString()); +println(output); # C :: D :: E :: F +``` + diff --git a/examples/adeste.mus b/examples/adeste.mus new file mode 100644 index 0000000..63f631e --- /dev/null +++ b/examples/adeste.mus @@ -0,0 +1,36 @@ +println("Adeste Fideles"); +println("John Francis Wade"); + +s1 = [@g, @g:2, @d, @g, @a:2, @d:2, @h, @a, @h, @c5, @h:2, @a, @g]; +s2 = [@g:2, @f#, @e, @f#, @g, @a, @h, @f#:2, @e:4d, @d:8, @d:2d, 4]; +s3 = [@d5:2, @c5, @h, @c5:2, @h:2, @a, @h, @g, @a, @f#:4d, @e:8, @d]; +s4 = [@g, @g, @f#, @g, @a, @g:2, @d, @h, @h, @a, @h, @c5, @h:2, @a]; +s5 = [@h, @c5, @h, @a, @g, @f#:2, @g, @c5, @h:2, @a:4d, @g:8, @g:1]; + +S = s1 + s2 + s3 + s4 + s5; + +a1 = [@d, @d:2, @d, @d, @e:2, @d:2, @d, @d, @d, @e, @d:2, @d, @h3]; +a2 = [@h3, @c#, @d, @c#, @d, @d, @d, @d, @d:2, @c#:4d, @d:8, @d:2d, 4]; +a3 = [@d:2, @e:8, @f#:8, @g, @g, @f#, @g:2, @d, @d, @e, @e, @d:2, @d]; +a4 = [@d, @d:1, @d:2d, @d, @d:1, @d:2d]; +a5 = [@g, @f#, @g, @d, @d:8, @c#:8, @d:2, @d, @e, @d:2, @d:4d, @h3:8, @h3:1]; + +A = a1 + a2 + a3 + a4 + a5; + +t1 = [@h3, @h3:2, @h3, @h3, @c:2, @a3:2, @g3, @a3, @g3, @g3, @g3:2, @f#3, @g3]; +t2 = [@g3:2, @a3, @a3, @a3, @g3, @f#3, @d3, @a3:2, @g3:4d, @f#3:8, @f#3:2d, 4]; +t3 = [@h3:2, @c, @d, @c:2, @d:2, @d, @g3, @h3, @c, @a3:2, @f#3]; +t4 = [@h3, @h3, @a3, @h3, @c, @h3:2d, @g3, @g3, @f#3, @g3, @a3, @g3:2, @f#3]; +t5 = [@d, @d, @d, @a3, @a3, @a3:2, @g3:2, @g3:2, @f#3:4d, @g3:8, @g3:1]; + +T = t1 + t2 + t3 + t4 + t5; + +b1 = [@g3, @g3:2, @h3, @g3, @g3:2, @f#3:2, @g3, @f#3, @g3, @c3, @d3:2, @d3, @e3]; +b2 = [@e3:2, @d3, @a2, @d3, @h2, @f#2, @g2, @a2:2, @a2:4d, @d3:8, @d2:2d, 4]; +b3 = [@h3:2, @a3, @g3, @a3:2, @g3:2, @f#3, @g3, @e3, @c3, @d3:2, @d3]; +b4 = [4, 1, 1, 1, 2, 4]; +b5 = [@g3, @a3, @g3, @f#3, @e3, @d3, @c3, @h2, @c3, @d3:2, @d3:4d, @g2:8, @g2:1]; + +B = b1 + b2 + b3 + b4 + b5; + +synth({ tuning -> 431, attack -> 100, decay -> 1.2, overtones -> [0.7, 0, 0.2, 0.1] }, S, A, T, B); \ No newline at end of file diff --git a/examples/bohemian.mus b/examples/bohemian.mus new file mode 100644 index 0000000..c94a985 --- /dev/null +++ b/examples/bohemian.mus @@ -0,0 +1,42 @@ +println("Bohemian Rhapsody :: part"); +println("by Queen"); + +m1 = [@d:8, @d:8, @d:2, 8, @b3:8, @c:8, @d:16, @d:16, @d, 4, 8, @d:16, @d:16]; +m2 = [@eb:8, @f:8, @eb:8, @d:8, @c, @c:8, @d:8, @eb:16, @f:8, @eb:8d, @d:8, @c, 4, @d:8, @d:8, @d:2, @d:8, @f:8]; +m3 = [@a:8d, @g:16, @g:2, 8, @g:8, @b:8, @b:8, @b:8, @b:8, @b:8d, @g:16, @d:8d, @c:16, @c:2, 2]; +M1 = [1, 1] + m1 + m2 + m3; + +p_B = [@d:8, @f3:8, @b3:8, @d:8, @g:8, @f3:8, @f:8, @f:8]; +p_g = [@d:8, @g3:8, @b3:8, @d:8, @a:8, @g3:8, @g:8, @b3:8]; +p_c = [@g:8, @c:8, @eb:8, @g:8, @d5:8, @c:8, @c5:8, @c:8]; +p_cF = [@b:8, @c:8, @eb:8, @g:8, @a:8, @eb:8, @f:8, @c:8]; +p1 = [@eb:8, @c:8, @eb:8, @g:8, @eb:8, @g:8, @eb:8, @g:8, @eb:8, @a3:8, @eb:8, @g:8, @eb:8, @g:8, @eb:8, @g:8]; + +P1 = p_B + p_B + p_B + p_g + p_c + p_cF + p_B + p_g + p1; + +b1 = [@b3:1, @g3:1, @c3:1, @c3:2, @f3:2]; +b2 = [@b3:2d, @a3, @g3:1, @c3:2, @h2, @b2, @a2:2, @ab2, @g2]; +B1 = [@b3:1, @b3:1] + b1 + b2; + +# Eb +m1 = [@g:16, @g:16, @g:2, 8, @f:8, @g:16, @ab:16, @g:2, 4, 8, @g:16, @g:16, @ab:8d, @g:16, @g:8, @f:16, @f:2, @b3:16]; +m2 = [@b3:8, @f:8, @f:8, @g:16, @g:8d, @ab:8, @ab:8, @b:16, @ab:16, @ab:8, @g:8, 8, @f:16, @g:16, @b:4d, @f:16, @g:16]; +m3 = [@eb:4d, @b3:16, @b3:16, @h3:8, @db:8, @h3:16, @db:16, @h3:8, @b3:2, 2]; +M2 = m1 + m2 + m3; + +p_Eb = [@b:8, @eb:8, @eb5:8, @eb:8, @b:8, @eb:8, @f:8, @b:8]; +p_f = [@ab:8, @f:8, @ab:8, @ab:8, @ab:8, @eb:8, @ab:8, @d:8]; +p_B = [@d, @f, @g, @ab]; +p_EbB = [@b:8, @eb:8, @eb5:8, @eb:8, @b:8, @d:8, @b:8, @d:8]; +p_cf = [@g:8, @c:8, @g:8, @c:8, @h3:2]; +P2 = p_Eb + p_c + p_f + p_B + p_EbB + p_cf; + +b1 = [@eb3:2d, @d3, @c3:1, @f3:4d, @e3:8, @eb3, @d3, @b2, @b2, @b2, @b2, @eb3:2, @d3:2]; +b2 = [@c2:2, @f2:2, @eb2:2, 2]; +B2 = b1 + b2; + +M = M1 + M2; +P = P1 + P2; +B = B1 + B2; + +synth({ tuning -> 432, bpm -> 72, overtones -> [0.5, 0.3, 0.15, 0.05], decay -> 0.8 }, transpose(12, M), P, B); diff --git a/examples/cantina.mus b/examples/cantina.mus new file mode 100644 index 0000000..23d8e9e --- /dev/null +++ b/examples/cantina.mus @@ -0,0 +1,120 @@ +println("Star Wars :: Cantina Band"); +println("by John Williams"); + +cb1 = [@a, @d5, @a, @d5] + [@a:8, @d5, @a:8, 8, @g#:8, @a]; +cb2 = [@a:8, @g#:8, @a:8, @g:8, 8, @f#:8, @g:8, @gb:8]; +cb3 = [@f:4d, @d:2, 8]; +cb4 = [@g:8, 8, @g:4d, @f#:8, @g]; +cb5 = [@c5:8, @b, @a, @g:4d]; +cb6 = [@c5:8, 8, @c5:4d, @a:8, @g]; +cb7 = [@f:4d, @d:2, 8]; +cb8 = [@d:2, @f:2, @a:2, @c5:2]; +cb9 = [@eb5, @d5, @g#:8, @a, @f:8]; + +CB1 = cb1 + cb2 + cb3 + cb1 + cb4 + cb5 + cb1 + cb6 + cb7 + cb8 + cb9 + [1]; + +cb11 = 2 ^ [8, @a5, @f5:8, @a5:8, 8, 4]; +cb12 = [8, @a5, @f5:8, @g#5:8, @a5, @f5:8]; +cb13 = [@f5:4d, @d5:2, 8]; +cb14 = [8, @a5, @f5:8, @g#5:8, @a5, @g5:8]; +cb15 = [@g5:2, @c5:2]; +cb16 = [@b:8, @d5:8, @f5, @h:8, @d5:8, @f5]; +cb17 = [@g#:8, @a5, @d5:2, 8]; +cb18 = [@d:8, @f:8, @h:8, @d5:8, @g#:8, @a, @f:8]; +cb19 = [@f:2d, 4]; + +CB2 = cb11 + cb12 + cb13 + cb11 + cb14 + cb15 + cb11 + cb12 + cb13 + cb16 + cb17 + cb18 + cb19; + +cb20 = [@f, 8, @ab, @f:8, @g]; +cb21 = [8, @f:8, @ab:8, @f:8, @g:8, @f:8, @ab:8, @d:8]; +cb22 = [@f, 8, @ab, @f:8, @g]; +cb23 = [@f:8, @f:8, @ab:8, @f:8, @g:8, @f:8, @ab:8, @f:8]; +cb24 = [@g:8, @f:8, @ab:8, @f:8, @g:8, @f:8, @ab:8, @d:8]; +cb25 = [@f:8, @f:8, @ab:8, @f:8, @ab:8, @f, @f:8, @f:2d, 4]; + +CB3 = cb20 + cb21 + cb22 + cb21 + cb22 + cb21 + cb23 + cb24 + cb22 + cb21 + cb22 + cb21 + cb22 + cb21 + cb25; + +cb26 = [@c5, 8, @e5:8, 4, @g5]; +cb27 = [@g5:8, @g5, @g5:8, @g5, @e5:8, @c5:8]; +cb28 = [@f5, @f5:8, @f5:8, @f5:8, @g5, @a5:8, @a5:1]; +cb29 = [@e5, @e5, @g5:8, @g5, @c5:8]; +cb30 = [@c5:8, @e5, @c5:8, @e5, @g5]; +cb31 = [@f5, @f5:8, @f5, @g5, @a5:8, @a5:1]; +cb32 = [@b5, @b5, @d6, @b5:8, @db6:8]; +cb33 = [@db6:8, @db6:8, @b5, @db6, @b5]; +cb34 = [@f5, @f5, @f5:8, @ab5, @d6:8, @d6:1]; +cb35 = [8, @b, @d5:8, @g5, @g5]; +cb36 = [8, @e5, 8, @e5, @e5]; +cb37 = [8, @f5, @f5:8, @f5, @f5:8, @f5:8]; +cb38 = [@f5:8, @f5, @f5:8, @f5:8, @f5:8, @f5]; + +CB4 = cb26 + cb27 + cb28 + cb29 + cb30 + cb31 + cb32 + cb33 + cb34 + cb35 + cb36 + cb37 + cb38; + +cb39 = [@ab:8, @gb:8, @ab:8, @gb:8, @ab:8, @gb:8, @ab:8, @gb:8]; +cb40 = [@ab:8, @db5, @ab:8, @db5, @ab:8, @db5:8]; +cb41 = [8, @ab:8, @b:8, @h:8, @c5:8, @h:8, @b:8, 8]; +cb42 = [@ab:8, @db5, @ab:8, @db5, @ab:8, @a:8]; +cb43 = [@b:8, @a:8, @b:4d, @a:8, @b:8, @a:8]; +cb44 = [@b:8, @a:8, @b:8, @h:8, @c5:8, 8, @gb:8, @g:8]; +cb45 = [@b:8, @a:8, @b:4d, @a:8, @b:8, @h:8]; +cb46 = [@c5:8, @g:8, @e:8, @c:8, 8, @c:8, @db:8, @c:8]; +cb47 = [@db:8, @b3, @db:8, @fb:8, @db, @fb:8]; +cb48 = [@f:8, @e:8, @f:8, @gb:8, @g:8, @c, @g:8]; +cb49 = [@ab:8, @g:8, @ab:8, @a:8, @b:8, @a:8, @b:8, @h:8]; +cb50 = [@c5:8, @c5:8, 4, @c5:8, @c5:8, 4]; +cb51 = [@c5, @e5:2, @f5:8, @db5:8]; +cb52 = [@ab:8, @f:8, @fb:8, @eb:8, @d:8, 4, @gb:8]; +cb53 = [@c5:8, @c5, @gb:8, @c5:8, @c5, @h:8]; +cb54 = [@c5:8, @c5, @h:8, @c5:8, @c5, @c5:8]; +cb55 = [@db5:8, @fb:8, @db5:4d, @fb:8, @db5:8, @fb:8]; +cb56 = [@db5:8, @fb:8, @db5:8, @fb:8, @eb:8, @c5, @c5:8]; +cb57 = [@db5:8, @fb:8, @db5:8, @fb:8, @eb:8, @c5, @h3:8]; +cb58 = [@b3:8, @db:8, @gb:8, @b:8, @c:8, @e:8, @g:8, @c5:8]; +cb59 = [@db5:8, @db, @c:8, @db, 4]; +cb60 = [4, @a3, @b3, @a3, @b3, @a3, @gb3:8, @f3:8, @d3:8, 8]; +cb61 = [8, @a3, @a3, @a3, @a3, @a3, @a3:8, @a3:8, @a3]; + +CB5 = cb39 + cb40 + cb41 + cb39 + cb42 + cb43 + cb44 + cb39 + cb42 + cb45 + cb46 + cb47 + cb48 + cb49 + cb50 + cb50 + cb51 + cb52 + cb53 + cb54 + cb55 + cb56 + cb55 + cb56 + cb55 + cb57 + cb58 + cb59 + cb60 + cb60 + cb60; + +CB = CB1 + CB2 + CB3 + (2 ^ CB4) + CB5; + +b1 = (3 ^ [@d3, 4, @a2, 4]) + [@f3, 4, @c3, 4, @d3, 4, @a2, 4, @d3, 4, @a2, 4, @g2, 4, @g2, 4]; +b2 = [@c3, @d3, @d#3, @e3, @d3, 4, @a2, 4, @d3, 4, @a2, 4, @c3, 4, @c3, 4]; +b3 = [@f3, 4, @c3, 4, @b2, 4, @b2, 4, @f3, 4, @d3, 4, @g3, 4, @c3, 4, @f3, @e3, @d3, @c3]; + +B1 = b1 + b2 + b3; + +b4 = (3 ^ [@d3, 4, @a2, 4]) + [@d3:4d, @g#2:4d, @a2]; +b5 = (3 ^ [@d3, 4, @a2, 4]) + [@eb3:2, @c3, @c3]; +b6 = (4 ^ [@d3, 4, @a2, 4]); +b7 = [@b2:8, @b2:8, 4, @h2:8, @h2:8, 4, @c3:4d, @d3:8, 8, @d3:8, @d3, @g2, 4, @c3:4d, @f3:4d, @e3, @d3, @c3]; + +B2 = b4 + b5 + b6 + b7; + +b8 = 3 ^ [@d3, @c3, @b2, @a2, @g2, @f2, @e2, @d2]; +b9 = [@d3, 4, 2, 2, 4, @a2]; +b10 = [@d3, @b2, @a2, @a2, @d3:8, 8, 4, 2]; + +B3 = b8 + b9 + b8 + b10; + +b11 = [@c3, 4, @g2, 4, @c3, 8, @b2:4d, @g2, @f2, 4, @c3, 4, @f3, 8, @e3:4d, @d3]; +b12 = [@c3, 4, @g3, 4, @c3, 8, @b2:4d, @g2, @f2, 4, @c3, 4, @d3, 8, @f#2:4d, @a2]; +b13 = [@b2, 4, @b2, 4, @h2, 4, @h2, 4, @c3, 4, @c3, 4, @d3, 8, @d3:4d, @f#2]; +b14 = [@g2, 4, @d3, 4, @c3, 8, @c3:4d, @g3, @f3, 4, @db3, 4, @f3, @f3, @e3, @d3]; + +B4 = b11 + b12 + b13 + b14; + +b15 = [@db3, @c3, @b2, @ab2, @gb2, @f2, @eb2, @db2, @db3, @b2, @a2, @ab2]; +b16 = [@db3, @c3, @b2, @ab2, @gb2, @f2, @eb2, @db2, @eb2, @e2, @f2, @gb2]; +b17 = [@e2, @d2, @h2, @ab2, @h2, @ab2, @f2, @eb2, @db2, @c2, @b1, @ab1]; +# ^ or B? +b18 = [@gb2, @gb2, @gb2, @db3, @c3, @c3, @e3, @g3, @gb3, @gb3, @g3, @g3]; +b19 = [@ab3, @db3, @c3, @c3, @f3, @c3, @gb3, @gb3, @c3, 4, @c3, 4]; +b20 = [@f3, 4, @f3, 4, @c3, @c3, 4, @db3:8, @f3:8, @ab3:8, @a3:8, @b3:8, @h3:8, @c:8, 8, 8, 16, @g3:16]; +b21 = [@c3, 4, @c3, 4, @f3, 4, @f3, 4, @gb2, 4, @ab2, 4, @a2, 4, @db3, 4, @gb2, 4, @ab2, 4, @a2, 4, @db3, 4, @d, 4, @eb, 4]; +b22 = [@e3, 4, @f3, 4, @gb3, 4, @g3, 4, @db3, 4, @db3, 4]; +B5 = b15 + b16 + b17 + b18 + b19 + b20 + b21 + b22; + +B = B1 + B2 + B3 + (2 ^ B4) + B5; + +synth({ tuning -> 432, bpm -> 270 }, flat(CB), flat(B)); diff --git a/examples/les_anges.mus b/examples/les_anges.mus new file mode 100644 index 0000000..8e6d996 --- /dev/null +++ b/examples/les_anges.mus @@ -0,0 +1,31 @@ +println("Les Anges dans nos campagnes"); + +s = [@a, @a, @a, @c5, @c5:4d, @b:8, @a:2, @a, @g, @a, @c5, @a:4d, @g:8, @f:2]; +sc = [@c5:2, @d5:8, @c5:8, @b:8, @a:8, @b:2, @c5:8, @b:8, @a:8, @g:8, @a:2, @b:8, @a:8, @g:8, @f:8, @g:4d, @c:8, @c:2, @f, @g, @a, @b]; +sca = [@a:2, @g, 4]; +scb = [@a:2, @g:2, @f:2d, 4]; + +S = flat(2^s, sc, sca, sc, scb); + +a = [@f, @f, @e, @e, @g, @e, @f:2, @f, @e, @f, @f, @f, @e, @f:2]; +ac = [@f, @a:8, @g:8, @f:2d, @g:8, @f:8, @e:2d, @f:8, @e:8, @d:2, @c:4d, @c:8, @c:2, @c, @e, @f, @g]; +aca = [@f:2, @e, 4]; +acb = [@f:2, @e:2, @c:2d, 4]; + +A = flat(2^a, ac, aca, ac, acb); + +t = [@c, @c, @c, @c, @d, @c, @c:2, @c, @c, @c, @c, @c:4d, @b3:8, @a3:2]; +tc = [@a3:2, @d:1, @c:1, @b3:2, @g3, @f3, @e3:2, @f3, @c, @c, @d]; +tca = [@c:2, @c, 4]; +tcb = [@c:2d, @b3, @a3:2d, 4]; + +T = flat(2^t, tc, tca, tc, tcb); + +b = [@f3, @f3, @a3, @a3, @g3, @c3, @f3:2, @f3, @c3, @f3, @a3, @c, @c3, @f3:2]; +bc = [@f3:2, @d3, @f3, @g3:2, @c3, @e3, @f3:2, @b2, @d3, @e3, @d3, @c3, @b2, @a2, @c3, @f3, @b2]; +bca = [@c3:2, @c3, 4]; +bcb = [@c3:1, @f3:2d, 4]; + +B = flat(2^b, bc, bca, bc, bcb); + +synth({ tuning -> 432, overtones -> [0.6, 0.3, 0.07, 0.03], attack -> 200, decay -> 0.6 }, S, A, T, B); diff --git a/examples/per_crucem.mus b/examples/per_crucem.mus new file mode 100644 index 0000000..6a00e05 --- /dev/null +++ b/examples/per_crucem.mus @@ -0,0 +1,18 @@ +println("Per Crucem - canon"); + +d1 = [@d:2, @d:2, @c:2, @f:4d, @f:8, @f, @f, @e, @d, @e:2, @d:2]; +d2 = [@d:8, @f:8, @a:8, @d5:8, @b:8, @a:8, @g, @c:8, @e:8, @g:8, @c5:8, @a:8, @g:8, @f, @b3:8, @d:8, @f:8, @b:8, @g:8, @f:8, @e, @a, @e, @f:2]; +d3 = [@a:2, @b:2, @c5:2, @a:4d, @a:8, @b, @f, @g, @g, @a, @e, @f:2]; +d4 = [@d5:8, @c5:8, @b:8, @a:8, @g:8, @a:8, @b, @c5:8, @b:8, @a:8, @g:8, @f:8, @g:8, @a, @b:8, @a:8, @g:8, @f:8, @e:8, @f:8, @g, @a, @e, @f:2]; +d5 = [@d5:2, @d5:2, @c5:2, @c5:2, @d5:8, @c5:8, @b:8, @a:8, @b, @b, @a:2, @a:2]; +d6 = [@d:8, @d:8, @d:8, @d:8, @g:8, @g:8, @g, @c:8, @c:8, @c:8, @c:8, @f:8, @f:8, @f, @b3:8, @b3:8, @b3:8, @b3:8, @e:8, @e:8, @e, @a3, @a3, @d:2]; + +p = [1, 1, 1, 1]; + +S = d1 + d2 + d3 + d4 + d5 + d6 + d1 + d2 + d3; +A = p + d1 + d2 + d3 + d4 + d5 + d6 + d1 + d2; +T = p + p + d1 + d2 + d3 + d4 + d5 + d6 + d1; +B = p + p + p + d1 + d2 + d3 + d4 + d5 + d6; + +wv = wave(S, A, transpose(-12, T), transpose(-12, B)); +synth(wv); \ No newline at end of file diff --git a/examples/quem_pastores.mus b/examples/quem_pastores.mus new file mode 100644 index 0000000..dba085b --- /dev/null +++ b/examples/quem_pastores.mus @@ -0,0 +1,31 @@ +println("Narodził się nam Zbawiciel / Quem pastores laudavere"); +println("mel.: XIV w."); +println("sł.: XV - XVI w."); +println("harm.: Bartłomiej Pluta"); + +s1 = [@f:2, @a, @c5:2, @a, @c5:2, @d5, @c5, @g:2]; +s2 = [@a:2, @c5, @b:2, @g, @f:2, @d, @e, @c:2]; +s3 = [@a:2, @b, @c5:2, @d5, @c5:2, @g, @a, @f:2]; +s4 = [@b:2, @b, @a, @g, @a, @f, @d, @e, @f:2d]; + +a1 = [@c:2, @f, @g:2, @f, @f, @g, @f, @f, @f, @e]; +a2 = [@f, @e, @eb, @d:2, @c, @d:2, @d, @c, @c, @b3]; +a3 = [@c:2, @c, @f, @d, @g, @g:2, @e, @e, @d:2]; +a4 = [@f:2, @f, @f, @e, @f, @c:2, @c, @c:2d]; + +t1 = [@a, @b, @c5, @c5, @b, @c5, @a:2, @b, @a, @c5:2]; +t2 = [@c5:2, @a, @b:2, @c5, @d5, @c, @b, @g, @g:2]; +t3 = [@f, @a, @g, @a:2, @b, @b:2, @g, @g, @a:2]; +t4 = [@d5, @db5, @db5, @c5, @b, @a, @g:2, @b, @a:2d]; + +b1 = [@f:2, @f, @e:2, @f, @f:2, @f, @c, @c:2]; +b2 = [@f:2, @f#, @g, @f, @e, @d, @f, @g, @c, @e:2]; +b3 = [@f:2, @e, @f, @f#, @g, @e, @d, @c, @c#, @d, @c]; +b4 = [@b3:2, @b, @c:2, @c, @c:2, @c, @f:2d]; + +S = s1 + s2 + s3 + s4; +A = a1 + a2 + a3 + a4; +T = transpose(-12, t1 + t2 + t3 + t4); +B = transpose(-12, b1 + b2 + b3 + b4); + +synth({ overtones -> [0.6, 0.25, 0.1] + (2 ^ 0.0) + [0.05], attack -> 50, decay -> 1 }, S, A, T, B); \ No newline at end of file diff --git a/img/notes/cd4fga8c.png b/img/notes/cd4fga8c.png new file mode 100644 index 0000000..fc76f96 Binary files /dev/null and b/img/notes/cd4fga8c.png differ diff --git a/img/notes/cdef.png b/img/notes/cdef.png new file mode 100644 index 0000000..59c5d0c Binary files /dev/null and b/img/notes/cdef.png differ diff --git a/img/notes/poly1.png b/img/notes/poly1.png new file mode 100644 index 0000000..417e945 Binary files /dev/null and b/img/notes/poly1.png differ diff --git a/img/notes/poly2.png b/img/notes/poly2.png new file mode 100644 index 0000000..045fef2 Binary files /dev/null and b/img/notes/poly2.png differ diff --git a/img/notes/starwars.png b/img/notes/starwars.png new file mode 100644 index 0000000..ecb7e88 Binary files /dev/null and b/img/notes/starwars.png differ diff --git a/img/notes/twinkle1.png b/img/notes/twinkle1.png new file mode 100644 index 0000000..ed4e7d7 Binary files /dev/null and b/img/notes/twinkle1.png differ diff --git a/img/notes/twinkle2.png b/img/notes/twinkle2.png new file mode 100644 index 0000000..19da26a Binary files /dev/null and b/img/notes/twinkle2.png differ diff --git a/img/notes/twinkle3.png b/img/notes/twinkle3.png new file mode 100644 index 0000000..fd4a361 Binary files /dev/null and b/img/notes/twinkle3.png differ diff --git a/img/plots/a_127.png b/img/plots/a_127.png new file mode 100644 index 0000000..79013c1 Binary files /dev/null and b/img/plots/a_127.png differ diff --git a/img/plots/a_1overtone.png b/img/plots/a_1overtone.png new file mode 100644 index 0000000..106aa4e Binary files /dev/null and b/img/plots/a_1overtone.png differ diff --git a/img/plots/a_1overtone_fft.png b/img/plots/a_1overtone_fft.png new file mode 100644 index 0000000..2cf2e6e Binary files /dev/null and b/img/plots/a_1overtone_fft.png differ diff --git a/img/plots/a_2overtones.png b/img/plots/a_2overtones.png new file mode 100644 index 0000000..8c2f4bb Binary files /dev/null and b/img/plots/a_2overtones.png differ diff --git a/img/plots/a_2overtones_fft.png b/img/plots/a_2overtones_fft.png new file mode 100644 index 0000000..fd320ca Binary files /dev/null and b/img/plots/a_2overtones_fft.png differ diff --git a/img/plots/a_3overtones.png b/img/plots/a_3overtones.png new file mode 100644 index 0000000..bd098e6 Binary files /dev/null and b/img/plots/a_3overtones.png differ diff --git a/img/plots/a_3overtones_fft.png b/img/plots/a_3overtones_fft.png new file mode 100644 index 0000000..f48a7fc Binary files /dev/null and b/img/plots/a_3overtones_fft.png differ diff --git a/img/plots/a_440.png b/img/plots/a_440.png new file mode 100644 index 0000000..b21eb34 Binary files /dev/null and b/img/plots/a_440.png differ diff --git a/img/plots/attack0.png b/img/plots/attack0.png new file mode 100644 index 0000000..5a62732 Binary files /dev/null and b/img/plots/attack0.png differ diff --git a/img/plots/attack1.png b/img/plots/attack1.png new file mode 100644 index 0000000..c5534a4 Binary files /dev/null and b/img/plots/attack1.png differ diff --git a/img/plots/attack10.png b/img/plots/attack10.png new file mode 100644 index 0000000..00daf12 Binary files /dev/null and b/img/plots/attack10.png differ diff --git a/img/plots/attack100.png b/img/plots/attack100.png new file mode 100644 index 0000000..a845d21 Binary files /dev/null and b/img/plots/attack100.png differ diff --git a/img/plots/attack5.png b/img/plots/attack5.png new file mode 100644 index 0000000..fb88b5b Binary files /dev/null and b/img/plots/attack5.png differ diff --git a/img/plots/c.png b/img/plots/c.png new file mode 100644 index 0000000..8aa190a Binary files /dev/null and b/img/plots/c.png differ diff --git a/img/plots/decay0.png b/img/plots/decay0.png new file mode 100644 index 0000000..a845d21 Binary files /dev/null and b/img/plots/decay0.png differ diff --git a/img/plots/decay05.png b/img/plots/decay05.png new file mode 100644 index 0000000..e1eecb6 Binary files /dev/null and b/img/plots/decay05.png differ diff --git a/img/plots/decay1.png b/img/plots/decay1.png new file mode 100644 index 0000000..af454b5 Binary files /dev/null and b/img/plots/decay1.png differ diff --git a/img/plots/decay10.png b/img/plots/decay10.png new file mode 100644 index 0000000..5b2dd98 Binary files /dev/null and b/img/plots/decay10.png differ diff --git a/img/plots/decay5.png b/img/plots/decay5.png new file mode 100644 index 0000000..ddf18ab Binary files /dev/null and b/img/plots/decay5.png differ diff --git a/img/plots/default.png b/img/plots/default.png new file mode 100644 index 0000000..4397a57 Binary files /dev/null and b/img/plots/default.png differ diff --git a/img/plots/example_config.png b/img/plots/example_config.png new file mode 100644 index 0000000..939b0c1 Binary files /dev/null and b/img/plots/example_config.png differ diff --git a/img/plots/spectrum.png b/img/plots/spectrum.png new file mode 100644 index 0000000..fc3bb2b Binary files /dev/null and b/img/plots/spectrum.png differ diff --git a/img/schemas/wait_fsm.png b/img/schemas/wait_fsm.png new file mode 100644 index 0000000..c569f07 Binary files /dev/null and b/img/schemas/wait_fsm.png differ diff --git a/smnp/ast/node/expression.py b/smnp/ast/node/expression.py index 950ccc8..344e4c7 100644 --- a/smnp/ast/node/expression.py +++ b/smnp/ast/node/expression.py @@ -113,11 +113,11 @@ def LoopParser(input): return Parser.allOf( ExpressionWithoutLoopParser, Parser.optional(loopParameters), - Parser.terminal(TokenType.DASH, createNode=Operator.withValue), + Parser.terminal(TokenType.CARET, createNode=Operator.withValue), StatementParser, Parser.optional(loopFilter), createNode=Loop.loop, - name="dash-loop" + name="caret-loop" )(input) diff --git a/smnp/library/code/main.mus b/smnp/library/code/main.mus new file mode 100644 index 0000000..9614f1c --- /dev/null +++ b/smnp/library/code/main.mus @@ -0,0 +1,492 @@ +function flat(list lists...) { + return _flat(lists as l ^ _flat(l, []), []); +} + +function _flat(list l, list output) { + l as elem ^ { + if (typeOf(elem) == list) { + output = _flat(elem, output); + } else { + output = output + [elem]; + } + } + + return output; +} + +extend note as n { + function withOctave(integer octave) { + return Note(n.pitch, octave, n.duration, n.dot); + } + + function withDuration(integer duration) { + return Note(n.pitch, n.octave, duration, n.dot); + } + + function withDot(bool dot) { + return Note(n.pitch, n.octave, n.duration, dot); + } + + function toIntRepr() { + return n.octave * 12 + _pitchToNumber(n.pitch); + } + + function transpose(integer value) { + return noteFromIntRepr(n.toIntRepr() + value, n.duration, n.dot); + } +} + +function noteFromIntRepr(integer intRepr, integer duration, bool dot) { + pitch = _numberToPitch(mod(intRepr, 12)); + octave = Integer(intRepr / 12); + return Note(pitch, octave, duration, dot); +} + +function mod(integer a, integer b) { + return a - b * Integer(a/b); +} + +function _pitchToNumber(string pitch) { + return _keysToIntMapper( + "C", + "CIS", + "D", + "DIS", + "E", + "F", + "FIS", + "G", + "GIS", + "A", + "AIS", + "H" + ).get(pitch); +} + +function _keysToIntMapper(keys...) { + return Map(keys as (i, key) ^ [key, i]); +} + +function _numberToPitch(integer number) { + return ["C", "CIS", "D", "DIS", "E", "F", "FIS", "G", "GIS", "A", "AIS", "H"].get(number); +} + +function transpose(integer value, > notes...) { + if (notes.size == 1) { + first = notes.get(0); + if (typeOf(first) == integer) { + return first; + } else if (typeOf(first) == note) { + return first.transpose(value); + } else if (typeOf(first) == list) { + return _transpose(value, first); + } + } + + noteOrInteger = false; + lists = false; + + notes as n ^ { + if (typeOf(n) == note or typeOf(n) == integer) { + noteOrInteger = true; + if (lists) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } else if (typeOf(n) == list) { + lists = true; + if (noteOrInteger) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } + } + + output = []; + notes as n ^ { + if (typeOf(n) == integer) { + output = output + [n]; + } else if (typeOf(n) == note) { + output = output + [n.transpose(value)]; + } else if (typeOf(n) == list) { + output = output + [_transpose(value, n)]; + } + } + + return output; +} + +function _transpose(integer value, list notes) { + output = []; + notes as n ^ { + if (typeOf(n) == integer) { + output = output + [n]; + } else if (typeOf(n) == note) { + output = output + [n.transpose(value)]; + } + } + return output; +} + +function transposeTo(note target, > notes...) { + if (notes.size == 1) { + first = notes.get(0); + if (typeOf(first) == integer) { + return first; + } else if (typeOf(first) == note) { + return _transposeTo(target, notes).get(0); + } else if (typeOf(first) == list) { + return _transposeTo(target, first); + } + } + + noteOrInteger = false; + lists = false; + + notes as n ^ { + if (typeOf(n) == note or typeOf(n) == integer) { + noteOrInteger = true; + if (lists) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } else if (typeOf(n) == list) { + lists = true; + if (noteOrInteger) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } + } + + if (noteOrInteger) { + return _transposeTo(target, notes); + } + + if (lists) { + return notes as n ^ _transposeTo(target, n); + } +} + +function _transposeTo(note target, list notes) { + if (notes.size == 0) { + throw "Provide list with one note at least"; + } + + firstNote = notes.get(0); + semitones = semitones(firstNote, target); + return transpose(semitones, notes); +} + +function tuplet(integer n, integer m, note notes...) { + if (n != notes.size) { + throw "Expected " + n.toString() + " notes exactly, whereas " + notes.size.toString() + " was passed"; + } + + return notes as x ^ x.withDuration(x.duration * n / m); +} + +extend list as l with function contains(expectedValue) { + return (l as value ^ value % value == expectedValue).size > 0; +} + +extend map as m { + function containsKey(expectedKey) { + return m.keys.contains(expectedKey); + } + + function containsValue(expectedValue) { + return m.values.contains(expectedValue); + } + + function contains(key, value) { + if (m.keys.contains(key)) { + return m.get(key) == value; + } + + return false; + } +} + +function sample(items...) { + if (items.size == 0) { + throw "Provide one item at least"; + } + + if (items.size == 1 and typeOf(items) == list) { + return items.get(0).get(rand(0, items.get(0).size-1)); + } + + return items.get(rand(0, items.size-1)); +} + +extend string as s with function join(list l) { + output = ""; + l as (index, item) ^ { + output = output + item; + if (index < l.size - 1) { + output = output + s; + } + } + + return output; +} + +function random(map<> items...) { + accumulator = 0; + items as (index, item) ^ { + if (item.size != 2) { + throw "Expected lists with two items: percent and value"; + } + + if (not item.containsKey("percent")) { + throw "Item " + (index+1).toString() + " does not have 'percent' key"; + } + + if (not item.containsKey("value")) { + throw "Item " + (index+1).toString() + " does not have 'value' key"; + } + + accumulator = accumulator + item.get("percent"); + } + + if (accumulator != 100) { + throw "Sum of first element of each item must be equal to 100"; + } + + accumulator = 0; + random = rand(0, 99); + items as item ^ { + accumulator = accumulator + item.get("percent"); + if (random < accumulator) { + return item.get("value"); + } + } +} + +function semitones(> notes...) { + noteOrInteger = false; + lists = false; + + notes as n ^ { + if (typeOf(n) == note or typeOf(n) == integer) { + noteOrInteger = true; + if (lists) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } else if (typeOf(n) == list) { + lists = true; + if (noteOrInteger) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } + } + + if (noteOrInteger) { + return _semitones(notes); + } + + if (lists) { + output = []; + notes as n ^ { + output = output + [_semitones(n)]; + } + + #if (output.size == 1) { + # return output.get(0) + #} + + return output; + } + + return []; +} + +function _semitones(list notes) { + onlyNotes = notes as n ^ n % typeOf(n) == note; + + if (onlyNotes.size == 2 and typeOf(onlyNotes.get(0)) == note and typeOf(onlyNotes.get(1)) == note) { + first = onlyNotes.get(0); + second = onlyNotes.get(1); + return second.toIntRepr() - first.toIntRepr(); + } + + if (onlyNotes.size < 2) { + throw "Provide 2 notes at least to evaluate semitones between them"; + } + + output = []; + range(1, onlyNotes.size-1) as i ^ { + output = output + [onlyNotes.get(i).toIntRepr() - onlyNotes.get(i-1).toIntRepr()]; + } + + return output; +} + +function stringInterval(integer semitones) { + return [ + "1", + "2m", + "2M", + "3m", + "3M", + "4", + "5d/4A", + "5", + "6m", + "6M", + "7m", + "7M" + ].get(semitones); +} + +function interval(> notes...) { + noteOrInteger = false; + lists = false; + + notes as n ^ { + if (typeOf(n) == note or typeOf(n) == integer) { + noteOrInteger = true; + if (lists) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } else if (typeOf(n) == list) { + lists = true; + if (noteOrInteger) { + throw "Mixing notes and integers with lists of them is not supported"; + } + } + } + + if (noteOrInteger) { + semitones = _semitones(notes); + if (typeOf(semitones) == list) { + return semitones as n ^ stringInterval(n); + } else { + return stringInterval(semitones); + } + } + + if (lists) { + output = []; + notes as n ^ { + semitones = _semitones(n); + if (typeOf(semitones) == list) { + output = output + [_semitones(n) as semitone ^ stringInterval(semitone)]; + } else { + output = output + [stringInterval(semitones)]; + } + } + + #if (output.size == 1) { + # return output.get(0); + #} + + return output; + } + + return []; +} + +function noteRange(note a, note b, string filter = "all") { + filters = { + "all" -> [ "C", "CIS", "D", "DIS", "E", "F", "FIS", "G", "GIS", "A", "AIS", "H" ], + "diatonic" -> [ "C", "D", "E", "F", "G", "A", "H" ], + "chromatic" -> [ "CIS", "DIS", "FIS", "GIS", "AIS" ] + }; + + if (not filters.containsKey(filter)) { + throw "Unknown filter: '" + filter + "'"; + } + + notes = range(a.toIntRepr(), b.toIntRepr()) as intRepr ^ noteFromIntRepr(intRepr, a.duration, a.dot); + return notes as n ^ n % filters.get(filter).contains(n.pitch); + +} + +function range( a, b, step = 1) { + if (not (step > 0)) { + throw "Step should be greater than 0"; + } + + if (a > b) { + throw "Upper range value should be greater than lower or equal to"; + } + + output = []; + + i = a; + i <= b ^ { + output = output + [i]; + i = i + step; + } + + return output; +} + +function alert( cycles = true, string melody = "beep", list overtones = [0.5, 0.0, 0.0, 0.5]) { + if (not [integer, bool].contains(typeOf(cycles))) { + throw "Provide 'true' or number of cycles as first argument"; + } + + if (typeOf(cycles) == integer) { + if (cycles < 1) { + throw "Number of cycles cannot be less than 1"; + } + } + + if (typeOf(cycles) == bool) { + if (not cycles) { + throw "Provide 'true' or number of cycles as first argument"; + } + } + + notes = { + "beep" -> [@c5:16, 32, @c5:16, 3], + "s1" -> noteRange(@c5:32, @g5:32), + "s2" -> _upDown(noteRange(@c5:32, @g5:32)), + "s3" -> [@a5:16, @d5:16], + "semitone" -> [@c5:16, @db5:16] + }; + + if (not notes.containsKey(melody)) { + throw "Unknown melody '" + melody + "'. Available: 'beep', 's1', 's2', 's3' and 'semitone'"; + } + + config = { + bpm -> 120, + decay -> 0.5, + attack -> 200, + overtones -> overtones + }; + + wave = wave(config, notes.get(melody)); + + cycles ^ synth(wave); +} + +function _upDown(list l) { + return l + -l; +} + +function metronome(integer bpm = 120, integer beats = 4, countMeasures = false) { + accent = wave({ + overtones -> flat([0.5, 0.1, 10^0, 0.1, 10^0, 0.1, 20^0, 0.1, 25^0, 0.05, 25^0, 0.05]), + attack -> 0, + decay -> 5, + bpm -> bpm + }, @c); + + beat = wave({ + overtones -> flat([0.5, 10^0, 0.3, 10^0, 0.2]), + attack -> 0, + decay -> 100, + bpm -> bpm + }, @c); + + measure = 1; + true ^ { + if (countMeasures) { + println(measure); + measure = measure + 1; + } + synth(accent); + beats - 1 ^ synth(beat); + } +} diff --git a/smnp/module/system/function/read.py b/smnp/module/system/function/read.py index 07d67f9..099960a 100644 --- a/smnp/module/system/function/read.py +++ b/smnp/module/system/function/read.py @@ -47,7 +47,7 @@ def getValueAccordingToType(value, type): raise ValueError() - raise RuntimeException(f"Type {type.value.name.lower()} is not suuported", None) + raise RuntimeException(f"Type {type.value.name.lower()} is not supported", None) except ValueError: raise RuntimeException(f"Invalid value '{value}' for type {type.value.name.lower()}", None) diff --git a/smnp/runtime/evaluators/function.py b/smnp/runtime/evaluators/function.py index 6b59ca1..a1a7875 100644 --- a/smnp/runtime/evaluators/function.py +++ b/smnp/runtime/evaluators/function.py @@ -51,7 +51,7 @@ class ReturnEvaluator(Evaluator): # Disclaimer # Exception system usage to control program execution flow is really bad idea. # However because of lack of 'goto' instruction equivalent in Python - # there is to need to use some mechanism to break function execution on 'return' statement + # there is a need to use some mechanism to break function execution on 'return' statement # and immediately go to Environment's method 'invokeFunction()' or 'invokeMethod()', # which can handle value that came with exception and return it to code being executed. else: diff --git a/smnp/runtime/evaluators/power.py b/smnp/runtime/evaluators/power.py index 5d9abb9..2239f15 100644 --- a/smnp/runtime/evaluators/power.py +++ b/smnp/runtime/evaluators/power.py @@ -18,4 +18,4 @@ class PowerEvaluator(Evaluator): if not right.type in supportedTypes: raise RuntimeException(f"Operator '{node.operator.value}' is supported only by {[t.name.lower() for t in supportedTypes]} type", node.right.pos) - return Type.integer(int(left.value ** right.value)) \ No newline at end of file + return Type.float(float(left.value ** right.value)) \ No newline at end of file diff --git a/smnp/token/tokenizer.py b/smnp/token/tokenizer.py index e00b772..075acca 100644 --- a/smnp/token/tokenizer.py +++ b/smnp/token/tokenizer.py @@ -36,7 +36,7 @@ tokenizers = ( defaultTokenizer(TokenType.SLASH), defaultTokenizer(TokenType.MINUS), defaultTokenizer(TokenType.PLUS), - defaultTokenizer(TokenType.DASH), + defaultTokenizer(TokenType.CARET), defaultTokenizer(TokenType.DOTS), defaultTokenizer(TokenType.AMP), defaultTokenizer(TokenType.DOT), diff --git a/smnp/token/type.py b/smnp/token/type.py index 1812993..636b62e 100644 --- a/smnp/token/type.py +++ b/smnp/token/type.py @@ -21,7 +21,7 @@ class TokenType(Enum): SLASH = '/' MINUS = '-' PLUS = '+' - DASH = '^' + CARET = '^' DOTS = '...' AMP = '&' DOT = '.'