Improve support for functions #1

This commit is contained in:
Bartłomiej Pluta
2019-07-04 02:09:24 +02:00
parent c8ff5ce38f
commit 6390ac20de
31 changed files with 514 additions and 328 deletions

View File

@@ -1,10 +0,0 @@
import pysine
from Note import Note, NotePitch
import time
BREAK = 0
#TODO: duration powinno byc w prawdziwych nutach: 4 = cwierc, 2 = pol etc.

View File

@@ -1,5 +1,3 @@
from Tokenizer import tokenize, TokenType
from Parser import parse
from AST import * from AST import *
from Note import Note from Note import Note
from Error import RuntimeException from Error import RuntimeException
@@ -25,20 +23,7 @@ def evaluateString(string, environment):
value = value.replace('{' + k + '}', objectString(v)) #TODO: poprawic value = value.replace('{' + k + '}', objectString(v)) #TODO: poprawic
return value return value
def objectString(obj):
if isinstance(obj, str):
return obj
if isinstance(obj, int):
return str(obj)
if isinstance(obj, Note):
return obj.note.name
if isinstance(obj, list):
return "(" + ", ".join([objectString(v) for v in obj]) + ")"
if isinstance(obj, float):
return f"{int(obj*100)}%"
if obj is None:
raise RuntimeException(None, f"Trying to interpret void")
raise RuntimeException(None, f"Don't know how to interpret {str(obj)}")
def evaluateNote(note, environment): def evaluateNote(note, environment):
return note.value return note.value

View File

@@ -1,43 +0,0 @@
import numpy as np
import sounddevice as sd
from Note import Note
import os
import sys
import time
FS = 44100
def play(args, env):
if len(args) > 0 and isinstance(args[0], list):
playList(args[0], env)
return
playList(args, env)
def playList(notes, env):
bpm = env.findVariable("bpm", int)
if all(isinstance(x, Note) or isinstance(x, int) for x in notes):
for x in notes:
if isinstance(x, Note):
playNote(x, bpm)
if isinstance(x, int):
doPause(x, bpm)
def playNote(note, bpm):
frequency = note.toFrequency()
duration = 60 * 4 / note.duration / bpm
duration *= 1.5 if note.dot else 1
sine(frequency, duration)
def sine(frequency, duration):
samples = (np.sin(2*np.pi*np.arange(FS*duration)*frequency/FS)).astype(np.float32)
sd.play(samples, FS)
time.sleep(duration)
def doPause(value, bpm):
time.sleep(60 * 4 / value / bpm)
def pause(args, env):
bpm = env.findVariable("bpm")
value = args[0]
doPause(value, bpm)

View File

@@ -1,251 +0,0 @@
import sys
from parser.Environment import objectString
from Note import *
import random
import Synth
import time
from Error import RuntimeException
from NoiseDetector import waitForSound
from Parser import parseNote
from Tokenizer import Token, TokenType, tokenizeNote
from functools import reduce
types = {
int: 'integer',
str: 'string',
list: 'list',
float: 'percent',
Note: 'note',
type(None): 'void'
}
class Environment():
def __init__(self, scopes, functions, methods):
self.scopes = scopes
self.functions = functions
self.methods = methods
self.customFunctions = {}
self.callStack = []
def findVariable(self, name, type=None):
for scope in reversed(self.scopes):
if name in scope:
value = scope[name]
if type is not None:
if isinstance(value, type):
return value
else:
return value
raise RuntimeException(None, f"Variable '{name}' is not declared" + ("" if type is None else f" (expected type: {types[type]})"))
def findVariableScope(self, name, type=None):
for scope in reversed(self.scopes):
if name in scope:
if type is not None:
if isinstance(scope[name], type):
return scope
else:
return scope
def doPrint(args, env):
print("".join([objectString(arg) for arg in args]))
def semitonesList(list):
for x in list:
if not isinstance(x, Note) and not isistance(x, int):
pass # invalid arguments
withoutPauses = tuple(filter(lambda x: isinstance(x, Note), list))
r = [Note.checkInterval(withoutPauses[i-1], withoutPauses[i]) for i, _ in enumerate(withoutPauses) if i != 0]
return r
def returnElementOrList(list):
return list[0] if len(list) == 1 else list
def semitones(args, env):
if len(args) > 0 and isinstance(args[0], list):
return returnElementOrList(semitonesList(args[0]))
return returnElementOrList(semitonesList(args))
def intervalList(list):
r = [intervalToString(x) for x in semitonesList(list)]
return returnElementOrList(r)
def interval(args, env):
if len(args) > 0 and isinstance(args[0], list):
return intervalList(args[0])
return intervalList(args)
def transposeTo(args, env):
if len(args) > 1 and isinstance(args[0], Note) and all(isinstance(x, list) for i, x in enumerate(args) if i != 0):
target = args[0]
result = []
for i, notes in enumerate(args):
if i == 0:
continue
if len(notes) > 0:
first = notes[0]
semitones = semitonesList([first, target])[0]
result.append([note.transpose(semitones) for note in notes if isinstance(note, Note)])
else:
result.append([])
return returnElementOrList(result)
else:
pass # not valid signature
def transpose(args, env):
if len(args) > 1 and isinstance(args[0], int) and all(isinstance(arg, list) for i, arg in enumerate(args) if i != 0):
value = args[0]
transposed = []
for i, arg in enumerate(args):
if i == 0:
continue
if not isinstance(arg, list):
return # is not list
transposed.append([note.transpose(value) for note in arg if isinstance(note, Note)])
return returnElementOrList(transposed)
if len(args) > 1 and all(isinstance(arg, Note) for i, arg in enumerate(args) if i != 0):
value = args[0]
transposed = [note.transpose(value) for i, note in enumerate(args) if i != 0]
return returnElementOrList(transposed)
else:
return # not valid signature
def objectType(args, env):
if len(args) == 1:
return types[type(args[0])]
else:
pass # not valid signature
def exit(args, env):
if len(args) == 1 and isinstance(args[0], int):
sys.exit(args[0])
else:
pass # not valid signature
def sleep(args, env):
if len(args) == 1 and isinstance(args[0], int):
time.sleep(args[0])
else:
pass # not valid signature
def rand(args, env):
if not all(isinstance(x, list) and len(x) == 2 and isinstance(x[0], float) for x in args):
return # not valid signature
if sum([x[0] for x in args]) != 1.0:
return # not sums to 100%
choice = random.random()
acc = 0
for e in args:
acc += e[0]
if choice <= acc:
return e[1]
def read(args, env):
if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str):
print(args[0], end="")
value = input()
if args[1] == "integer":
try:
return int(value)
except ValueError as v:
pass # not int
elif args[1] == "string":
return value
elif args[1] == "note":
chars, token = tokenizeNote(value, 0, 0)
if chars == 0:
return # not note
return parseNote([token], None).value
else:
pass # invalid type
elif len(args) == 1 and isinstance(args[0], str):
print(args[0], end="")
return input()
elif len(args) == 0:
return input()
else:
pass # not valid signature
def changeDuration(args, env):
if len(args) == 2 and isinstance(args[0], Note) and isinstance(args[1], int):
return args[0].withDuration(args[1])
return # invalid signature
def changeOctave(args, env):
if len(args) == 2 and isinstance(args[0], Note) and isinstance(args[1], int):
return args[0].withOctave(args[1])
return # invalid signature
def tupletList(n, m, list):
return [note.withDuration(note.duration * n / m) for note in list]
def tuplet(args, env):
if len(args) > 2 and type(args[0]) == int and type(args[1]) == int and all(type(x) == Note for x in args[2:]):
n = args[0] # how many notes
m = args[1] # instead of number of notes (3-tuplet: 3 instead 2; 5-tuplet: 5 instead 4 etc.)
return returnElementOrList(tupletList(n, m, args[2:]))
elif len(args) == 3 and type(args[0]) == int and type(args[1]) == int and type(args[2]) == list and all(type(x) == Note for x in args[2]):
n = args[0]
m = args[1]
l = args[2]
return returnElementOrList(tupletList(n, m, l))
else:
pass # not valid signature
def combine(args, env):
if all(type(x) == list for x in args):
return reduce((lambda x, y: x + y), args)
def flat(args, env):
return _flat(args, [])
def _flat(input, output = []):
for item in input:
if type(item) == list:
_flat(item, output)
else:
output.append(item)
return output
def createEnvironment():
functions = {
'print': doPrint,
'synth': Synth.play,
'pause': Synth.pause,
'type': objectType,
'sample': sample,
'semitones': semitones,
'interval': interval,
'transpose': transpose,
'transposeTo': transposeTo,
'sleep': sleep,
'random': rand,
'changeDuration': changeDuration,
'changeOctave': changeOctave,
'wait': waitForSound,
'read': read,
'debug': lambda args, env: print(args),
'tuplet': tuplet,
'combine': combine,
'flat': flat,
'exit': exit
}
methods = {
str: {},
list: {},
float: {},
Note: {
'synth': Synth.play
},
type(None): {},
}
variables = {
"bpm": 120
}
return Environment([ variables ], functions, methods)

View File

View File

@@ -0,0 +1,31 @@
from smnp.error.runtime import RuntimeException
class Environment():
def __init__(self, scopes, functions, methods):
self.scopes = scopes
self.functions = functions
self.methods = methods
self.customFunctions = {}
self.callStack = [] #TODO remove
def findVariable(self, name, type=None, pos=None):
for scope in reversed(self.scopes):
if name in scope:
value = scope[name]
if type is not None:
if isinstance(value, type):
return value
else:
return value
raise RuntimeException(pos, f"Variable '{name}' is not declared" + (
"" if type is None else f" (expected type: {type})"))
def findVariableScope(self, name, type=None):
for scope in reversed(self.scopes):
if name in scope:
if type is not None:
if isinstance(scope[name], type):
return scope
else:
return scope

View File

@@ -0,0 +1,48 @@
import smnp.environment.function.list as l
from smnp.environment.environment import Environment
from smnp.environment.function import synth, base, interval, note, transposer, rand, mic
from smnp.environment.function.model import Function, ONLY_FUNCTION
from smnp.note.model import Note
def createEnvironment():
functions = {
'exit': Function(base.exit, ONLY_FUNCTION),
'print': Function(base.display, ONLY_FUNCTION),
'read': Function(base.read, ONLY_FUNCTION),
'type': Function(base.objectType, ONLY_FUNCTION),
'sleep': Function(base.sleep, ONLY_FUNCTION),
'synth': Function(synth.synth, ONLY_FUNCTION),
'pause': Function(synth.pause, ONLY_FUNCTION),
'changeDuration': Function(note.changeDuration, ONLY_FUNCTION),
'changeOctave': Function(note.changeOctave, ONLY_FUNCTION),
'semitones': Function(interval.semitones, ONLY_FUNCTION),
'interval': Function(interval.interval, ONLY_FUNCTION),
'transpose': Function(transposer.transpose, ONLY_FUNCTION),
'transposeTo': Function(transposer.transposeTo, ONLY_FUNCTION),
'random': Function(rand.random, ONLY_FUNCTION),
# 'sample': sample,
'wait': Function(mic.wait, ONLY_FUNCTION),
'tuplet': Function(note.tuplet, ONLY_FUNCTION),
'combine': Function(l.combine, ONLY_FUNCTION),
'flat': Function(l.flat, ONLY_FUNCTION),
'debug': Function(lambda args, env: print(args), ONLY_FUNCTION),
}
methods = {
str: {},
list: {},
float: {},
Note: {
'synth': synth.synth
},
type(None): {},
}
variables = {
"bpm": 120
}
return Environment([ variables ], functions, methods)

View File

View File

@@ -0,0 +1,55 @@
import sys
import time
def display(args, env):
print("".join([arg.stringify() for arg in args]))
def objectType(args, env):
if len(args) == 1:
return args[0].stringify()
else:
pass # not valid signature
def exit(args, env):
if len(args) == 1 and isinstance(args[0], int):
sys.exit(args[0])
else:
pass # not valid signature
def sleep(args, env):
if len(args) == 1 and isinstance(args[0], int):
time.sleep(args[0])
else:
pass # not valid signature
def read(args, env):
if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str):
print(args[0], end="")
value = input()
if args[1] == "integer":
try:
return int(value)
except ValueError as v:
pass # not int
elif args[1] == "string":
return value
# TODO: note - wydzielić parsowanie nut do osobnej funkcji w pakiecie smnp.note
# elif args[1] == "note":
# chars, token = tokenizeNote(value, 0, 0)
# if chars == 0:
# return # not note
# return parseNote([token], None).value
else:
pass # invalid type
elif len(args) == 1 and isinstance(args[0], str):
print(args[0], end="")
return input()
elif len(args) == 0:
return input()
else:
pass # not valid signature

View File

@@ -0,0 +1,27 @@
from smnp.environment.function.tools import returnElementOrList
from smnp.note.interval import intervalToString
from smnp.note.model import Note
def interval(args, env):
if len(args) > 0 and isinstance(args[0], list):
return intervalList(args[0])
return intervalList(args)
def intervalList(list):
r = [intervalToString(x) for x in semitonesList(list)]
return returnElementOrList(r)
def semitonesList(list):
for x in list:
if not isinstance(x, Note) and not isinstance(x, int):
pass # invalid arguments
withoutPauses = tuple(filter(lambda x: isinstance(x, Note), list))
r = [Note.checkInterval(withoutPauses[i-1], withoutPauses[i]) for i, _ in enumerate(withoutPauses) if i != 0]
return r
def semitones(args, env):
if len(args) > 0 and isinstance(args[0], list):
return returnElementOrList(semitonesList(args[0]))
return returnElementOrList(semitonesList(args))

View File

@@ -0,0 +1,19 @@
from functools import reduce
def combine(args, env):
if all(type(x) == list for x in args):
return reduce((lambda x, y: x + y), args)
def flat(args, env):
return _flat(args, [])
def _flat(input, output = []):
for item in input:
if type(item) == list:
_flat(item, output)
else:
output.append(item)
return output

View File

@@ -0,0 +1,3 @@
def wait(args, env):
pass

View File

@@ -0,0 +1,18 @@
from enum import Enum, auto
class FunctionType(Enum):
FUNCTION = auto()
METHOD = auto()
class Function:
def __init__(self, signature, function):
self.signature = signature
self.function = function
def call(self, env, args):
result = self.signature(args)
if result[0]:
return self.function(env, *result[1:])
# todo: raise illegal signature exception or something

View File

@@ -0,0 +1,31 @@
from smnp.environment.function.tools import returnElementOrList
def changeDuration(args, env):
if len(args) == 2 and isinstance(args[0], Note) and isinstance(args[1], int):
return args[0].withDuration(args[1])
return # invalid signature
def changeOctave(args, env):
if len(args) == 2 and isinstance(args[0], Note) and isinstance(args[1], int):
return args[0].withOctave(args[1])
return # invalid signature
def tupletList(n, m, list):
return [note.withDuration(note.duration * n / m) for note in list]
def tuplet(args, env):
if len(args) > 2 and type(args[0]) == int and type(args[1]) == int and all(type(x) == Note for x in args[2:]):
n = args[0] # how many notes
m = args[1] # instead of number of notes (3-tuplet: 3 instead 2; 5-tuplet: 5 instead 4 etc.)
return returnElementOrList(tupletList(n, m, args[2:]))
elif len(args) == 3 and type(args[0]) == int and type(args[1]) == int and type(args[2]) == list and all(type(x) == Note for x in args[2]):
n = args[0]
m = args[1]
l = args[2]
return returnElementOrList(tupletList(n, m, l))
else:
pass # not valid signature

View File

@@ -0,0 +1,17 @@
import random as r
def random(args, env):
if not all(isinstance(x, list) and len(x) == 2 and isinstance(x[0], float) for x in args):
return # not valid signature
if sum([x[0] for x in args]) != 1.0:
return # not sums to 100%
choice = r.random()
acc = 0
for e in args:
acc += e[0]
if choice <= acc:
return e[1]
#TODO: sample

View File

@@ -0,0 +1,93 @@
# from smnp.type.model import Type
#
from smnp.type.model import Type
class Matcher:
def __init__(self, objectType, matcher):
self.type = objectType
self.matcher = matcher
def match(self, value):
if self.type is not None and self.type != value.type:
return False
return self.matcher(value)
def andWith(self, matcher):
if self.type != matcher.type:
raise RuntimeError("Support types of matches are not the same")
return Matcher(self.type, lambda x: self.matcher(x) and matcher.matcher(x))
def varargSignature(varargMatcher, *basicSignature):
def check(args):
if len(basicSignature) > len(args):
return doesNotMatchVararg(basicSignature)
for i in range(len(basicSignature)):
if not basicSignature[i].match(args[i]):
return doesNotMatchVararg(basicSignature)
for i in range(len(basicSignature), len(args)):
if not varargMatcher.match(args[i]):
return doesNotMatchVararg(basicSignature)
return True, (*args[:len(basicSignature)]), args[len(basicSignature):]
return check
def doesNotMatchVararg(basicSignature):
return (False, *[None for n in basicSignature], None)
#
def signature(*signature):
def check(args):
if len(signature) != len(args):
return doesNotMatch(signature)
for s, a in zip(signature, args):
if not s.match(a):
return doesNotMatch(signature)
return (True, *args)
return check
def doesNotMatch(sign):
return (False, *[None for n in sign])
def ofTypes(*types):
def check(value):
return value.type in types
return Matcher(None, check)
def listOf(*types):
def check(value):
return len([item for item in value.value if not item.type in types]) == 0
return Matcher(Type.LIST, check)
def listMatches(*pattern):
def check(value):
return signature(pattern)(value.value)[0]
return Matcher(Type.LIST, check)
def recursiveListMatcher(matcher):
if matcher.type == Type.LIST:
raise RuntimeError(f"Passed matcher will be handling non-list types, so it cannot have type set to {Type.LIST}")
def check(value):
if value.type != Type.LIST:
return matcher.match(value)
for item in value.value:
return check(item)
return Matcher(Type.LIST, check)

View File

@@ -0,0 +1,6 @@
def synth(args, env):
pass
def pause(args, env):
pass

View File

@@ -0,0 +1,38 @@
from smnp.environment.function.model import Function
def returnElementOrList(list):
return list[0] if len(list) == 1 else list
def combineFunctions(*functions):
if len(functions) == 0:
raise RuntimeError("Must be passed one function at least")
def signature(args):
ret = None
for fun in functions:
ret = fun.signature(args)
if ret[0] == True:
return ret
return ret
def function(env, *args):
originalArgs = removeFirstLevelNesting(args)
for fun in functions:
if fun.signature(originalArgs)[0]:
return fun.function(env, *args)
return None
return Function(signature, function)
def removeFirstLevelNesting(l):
flat = []
for item in l:
if type(item) == list:
for i in item:
flat.append(i)
else:
flat.append(item)
return flat

View File

@@ -0,0 +1,41 @@
from smnp.environment.function.interval import semitonesList
from smnp.environment.function.tools import returnElementOrList
from smnp.note.model import Note
def transposeTo(args, env):
if len(args) > 1 and isinstance(args[0], Note) and all(isinstance(x, list) for i, x in enumerate(args) if i != 0):
target = args[0]
result = []
for i, notes in enumerate(args):
if i == 0:
continue
if len(notes) > 0:
first = notes[0]
semitones = semitonesList([first, target])[0]
result.append([note.transpose(semitones) for note in notes if isinstance(note, Note)])
else:
result.append([])
return returnElementOrList(result)
else:
pass # not valid signature
def transpose(args, env):
if len(args) > 1 and isinstance(args[0], int) and all(
isinstance(arg, list) for i, arg in enumerate(args) if i != 0):
value = args[0]
transposed = []
for i, arg in enumerate(args):
if i == 0:
continue
if not isinstance(arg, list):
return # is not list
transposed.append([note.transpose(value) for note in arg if isinstance(note, Note)])
return returnElementOrList(transposed)
if len(args) > 1 and all(isinstance(arg, Note) for i, arg in enumerate(args) if i != 0):
value = args[0]
transposed = [note.transpose(value) for i, note in enumerate(args) if i != 0]
return returnElementOrList(transposed)
else:
return # not valid signature

View File

@@ -1,4 +1,4 @@
class RuntimeException(Exception): class RuntimeException(Exception):
def __init__(self, pos, msg): def __init__(self, pos, msg):
posStr = "" if pos is None else f"[line {pos[0]+1}, col {pos[1]+1}]" posStr = "" if pos is None else f" [line {pos[0]+1}, col {pos[1]+1}]"
self.msg = f"Syntax error {posStr}:\n{msg}" self.msg = f"Runtime error{posStr}:\n{msg}"

View File

@@ -1,4 +1,4 @@
class SyntaxException(Exception): class SyntaxException(Exception):
def __init__(self, pos, msg): def __init__(self, pos, msg):
posStr = "" if pos is None else f"[line {pos[0]+1}, col {pos[1]+1}]" posStr = "" if pos is None else f" [line {pos[0]+1}, col {pos[1]+1}]"
self.msg = f"Syntax error {posStr}:\n{msg}" self.msg = f"Syntax error{posStr}:\n{msg}"

0
smnp/mic/__init__.py Normal file
View File

View File

View File

@@ -1,9 +1,10 @@
import numpy as np
import sounddevice as sd
import os
from collections import deque from collections import deque
from statistics import mean from statistics import mean
import numpy as np
import sounddevice as sd
class NoiseDetector: class NoiseDetector:
def __init__(self, noiseTreshold=300, silenceTreshold=10, bufferSize=20): def __init__(self, noiseTreshold=300, silenceTreshold=10, bufferSize=20):
self.reachedNoise = False self.reachedNoise = False
@@ -43,6 +44,7 @@ class NoiseDetector:
sd.sleep(200) sd.sleep(200)
#TODO: do environment
def waitForSound(args, env): def waitForSound(args, env):
noiseTreshold = 300 noiseTreshold = 300
silenceTreshold = 10 silenceTreshold = 10

0
smnp/runtime/__init__.py Normal file
View File

0
smnp/synth/__init__.py Normal file
View File

24
smnp/synth/player.py Normal file
View File

@@ -0,0 +1,24 @@
import time
from smnp.note.model import Note
from smnp.synth.wave import sine
def playNotes(notes, bpm):
for note in notes:
{
Note: play,
int: pause
}[type(note)](note, bpm)
def play(note, bpm):
frequency = note.toFrequency()
duration = 60 * 4 / note.duration / bpm
duration *= 1.5 if note.dot else 1
sine(frequency, duration)
def pause(value, bpm):
time.sleep(60 * 4 / value / bpm)

12
smnp/synth/wave.py Normal file
View File

@@ -0,0 +1,12 @@
import time
import numpy as np
import sounddevice as sd
FS = 44100
def sine(frequency, duration):
samples = (np.sin(2*np.pi*np.arange(FS*duration)*frequency/FS)).astype(np.float32)
sd.play(samples, FS)
time.sleep(duration)

0
smnp/type/__init__.py Normal file
View File

23
smnp/type/model.py Normal file
View File

@@ -0,0 +1,23 @@
from enum import Enum
from smnp.error.runtime import RuntimeException
from smnp.note.model import Note
class Type(Enum):
INTEGER = (int, lambda x: str(x))
STRING = (str, lambda x: x)
LIST = (list, lambda x: f"({', '.join([e.stringify() for e in x])})")
PERCENT = (float, lambda x: f"{int(x * 100)}%")
NOTE = (Note, lambda x: x.note.name)
VOID = (None, lambda x: _failStringify(x))
def stringify(self, element):
return self.value[1](element)
def _failStringify(obj):
raise RuntimeException(None, f"Not able to interpret '{obj.type.name()}'")

17
smnp/type/value.py Normal file
View File

@@ -0,0 +1,17 @@
class Value:
def __init__(self, objectType, value):
self.value = value
if type(value) == objectType.value[0]:
self.type = objectType
else:
raise RuntimeError(f"Invalid type '{objectType.name}' for value '{value}'")
def stringify(self):
return self.type.stringify(self.value)
def __str__(self):
return f"{self.type.name}({self.stringify()})"
def __repr__(self):
return self.__str__()