diff --git a/Audio.py b/Audio.py index 9c0f800..8c47fe8 100644 --- a/Audio.py +++ b/Audio.py @@ -4,10 +4,7 @@ import time BREAK = 0 -def playNote(note, bpm): - frequency = note.toFrequency() - duration = 60 * 4 / note.duration / bpm - pysine.sine(frequency, duration) + #TODO: duration powinno byc w prawdziwych nutach: 4 = cwierc, 2 = pol etc. diff --git a/Environment.py b/Environment.py new file mode 100644 index 0000000..4605ec6 --- /dev/null +++ b/Environment.py @@ -0,0 +1,205 @@ +import sys +from Evaluator import RuntimeException, objectString +from Note import * +import random +import Synth +import time + +types = { + int: 'integer', + str: 'string', + list: 'list', + float: 'percent', + Note: 'note', + type(None): 'void' +} + +class Environment(): + def __init__(self, scopes, functions): + self.scopes = scopes + self.functions = functions + + 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(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 sample(args, env): + if len(args) == 1 and isinstance(args[0], list): + return _sample(args[0]) + elif len(args) == 0: + return _sample(Note.range(Note(NotePitch.C), Note(NotePitch.H))) + elif all(isinstance(x, Note) for x in args): + return _sample(args) + else: + pass # not valid signature + +def _sample(list): + return list[int(random.uniform(0, len(list)))] + +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 returnElementOrList(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 semitonesList(args[0]) + return semitonesList(args) + +def intervalList(list): + r = [intervalToString(x) for x in 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([target, first]) + 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): + 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 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 upper(args, env): + value = [] + for arg in args: + if isinstance(arg, Note): + value.append(arg.getUpperNeighbour()) + elif isinstance(arg, list): + value.append(upperList(arg)) + else: + pass # invalid argument + return returnElementOrList(value) + +def upperList(list): + if all(isinstance(x, Note) for x in list): + value = [note.getUpperNeighbour() for note in list] + return returnElementOrList(value) + else: + pass #not valid signature + +def lower(args, env): + value = [] + for arg in args: + if isinstance(arg, Note): + value.append(arg.getLowerNeighbour()) + elif isinstance(arg, list): + value.append(lowerList(arg)) + else: + pass # invalid argument + return returnElementOrList(value) + +def lowerList(list): + if all(isinstance(x, Note) for x in list): + value = [note.getLowerNeighbour() for note in list] + return returnElementOrList(value) + 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 createEnvironment(): + functions = { + 'print': doPrint, + 'synth': Synth.play, + 'pause': Synth.pause, + 'type': objectType, + 'sample': sample, + 'semitones': semitones, + 'interval': interval, + 'transpose': transpose, + 'transposeTo': transposeTo, + 'upper': upper, + 'lower': lower, + 'sleep': sleep, + 'random': rand, + 'exit': exit + + } + + variables = { + "bpm": 120 + } + + return Environment([ variables ], functions) + diff --git a/Evaulator.py b/Evaluator.py similarity index 64% rename from Evaulator.py rename to Evaluator.py index 357b6bd..fffcaa6 100644 --- a/Evaulator.py +++ b/Evaluator.py @@ -1,20 +1,11 @@ -import os from Tokenizer import tokenize, TokenType from Parser import parse from AST import * -import sys -from Note import * -import random -import Midi +from Note import Note class RuntimeException(Exception): pass -class Environment(): - def __init__(self, scopes, functions): - self.scopes = scopes - self.functions = functions - def evaluateProgram(program, environment): for node in program.children: evaluate(node, environment) @@ -23,18 +14,12 @@ def evaluateInteger(integer, environment): return integer.value def evaluatePercent(percent, environment): - pass + return percent.value.value * 0.01 def evaluateIdentifier(identifier, environment): - value = findVariable(identifier.identifier, environment) + value = environment.findVariable(identifier.identifier) return value -def findVariable(name, environment): - for scope in reversed(environment.scopes): - if name in scope: - return scope[name] - raise RuntimeException(f"Variable '{name}' is not declared") - def evaluateString(string, environment): value = string.value for scope in reversed(environment.scopes): @@ -42,6 +27,21 @@ def evaluateString(string, environment): value = value.replace('{' + k + '}', objectString(v)) #TODO: poprawic 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(f"Trying to interpret void") + raise RuntimeException(f"Don't know how to interpret {str(obj)}") + def evaluateNote(note, environment): return note.value @@ -69,7 +69,11 @@ def evaluateList(list, environment): def evaluateAssignment(assignment, environment): target = assignment.target.identifier value = evaluate(assignment.value, environment) - environment.scopes[-1][target] = value + scopeOfExistingVariable = environment.findVariableScope(target) + if scopeOfExistingVariable is not None: + scopeOfExistingVariable[target] = value + else: + environment.scopes[-1][target] = value def evaluateAsterisk(asterisk, environment): count = evaluate(asterisk.iterator, environment) @@ -130,77 +134,3 @@ def evaluate(input, environment): return evaluateColon(input, environment) if isinstance(input, IdentifierNode): return evaluateIdentifier(input, environment) - -def rand(args, env): - if len(args) == 1 and isinstance(args[0], list): - return args[0][int(random.uniform(0, len(args[0])))] - -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]) + ")" - raise RuntimeException(f"Don't know how to interpret {str(obj)}") - -def prt(args, env): - print("".join([objectString(arg) for arg in args])) - -def semitonesList(list): - 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[0] if len(r) == 1 else r - -def semitones(args, env): - if len(args) > 0 and isinstance(args[0], list): - return semitonesList(args[0]) - return semitonesList(args) - -def intervalList(list): - r = [intervalToString(x) for x in list] - return r[0] if len(r) == 1 else r - -def interval(args, env): - if len(args) > 0 and isinstance(args[0], list): - return intervalList(args[0]) - return intervalList(args) - -def transpose(args, env): - if len(args) > 1 and isinstance(args[0], int): - 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 transposed - else: - return # not valid signature - -if __name__ == "__main__": - functions = { - 'print': prt, - 'midi': Midi.play, - 'pause': Midi.pause, - 'type': lambda args, env: print(type(args[0])), - 'random': rand, - 'semitones': semitones, - 'interval': interval, - 'transpose': transpose - - } - - with open(sys.argv[1], 'r') as source: - lines = [line.rstrip('\n') for line in source.readlines()] - - tokens = [token for token in tokenize(lines) if token.type != TokenType.COMMENT] - - ast = parse(tokens) - environment = Environment([{ "bpm": 120 }], functions) - evaluate(ast, environment) - diff --git a/Midi.py b/Midi.py deleted file mode 100644 index 1dbbce6..0000000 --- a/Midi.py +++ /dev/null @@ -1,44 +0,0 @@ -from Note import * -import Audio -import os -import sys -import time - -def parseNotes(notes): - map = { NotePitch.C: 'c', NotePitch.CIS: 'c#', NotePitch.D: 'd', NotePitch.DIS: 'd#', - NotePitch.E: 'e', NotePitch.F: 'f', NotePitch.FIS: 'f#', NotePitch.G: 'g', - NotePitch.GIS: 'g#', NotePitch.A: 'a', NotePitch.AIS: 'a#', NotePitch.H: 'b' } - parsed = [(f"{map[note.note]}{note.octave}", note.duration) for note in notes] - print(parsed) - -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 = findVariable("bpm", env) - if all(isinstance(x, Note) or isinstance(x, int) for x in notes): - for x in notes: - if isinstance(x, Note): - Audio.playNote(x, bpm) - if isinstance(x, int): - doPause(x, bpm) - #sys.stdout = open(os.devnull, 'w') - #sys.stderr = open(os.devnull, 'w') - #sys.stdout = sys.__stdout__ - #sys.stderr = sys.__stderr__ - -def findVariable(name, environment): - for scope in reversed(environment.scopes): - if name in scope: - return scope[name] - -def pause(args, env): - bpm = findVariable("bpm", env) - value = args[0] - doPause(value, bpm) - -def doPause(value, bpm): - time.sleep(60 * 4 / value / bpm) diff --git a/Note.py b/Note.py index 65633cf..301c7ad 100644 --- a/Note.py +++ b/Note.py @@ -3,18 +3,18 @@ from Error import ParseError import math class NotePitch(Enum): - C = 1 - CIS = 2 - D = 3 - DIS = 4 - E = 5 - F = 6 - FIS = 7 - G = 8 - GIS = 9 - A = 10 - AIS = 11 - H = 12 + C = 0 + CIS = 1 + D = 2 + DIS = 3 + E = 4 + F = 5 + FIS = 6 + G = 7 + GIS = 8 + A = 9 + AIS = 10 + H = 11 def __str__(self): return self.name @@ -53,13 +53,6 @@ class NotePitch(Enum): return map[string.lower()] except KeyError as e: raise ParseError(f"Note '{string}' does not exist") - - @staticmethod - def range(a, b): - aValue = a.value - bValue = b.value - - return [note[1] for note in NotePitch.__members__.items() if note[1].value >= aValue and note[1].value <= bValue] class Note: def __init__(self, note, octave = 4, duration = 4): @@ -79,10 +72,7 @@ class Note: def transpose(self, interval): origIntRepr = self._intRepr() transposedIntRepr = origIntRepr + interval - pitch = transposedIntRepr % len(NotePitch) - note = NotePitch(pitch if pitch != 0 else self.note.value) - octave = int(transposedIntRepr / len(NotePitch)) - return Note(note, octave, self.duration) + return Note._fromIntRepr(transposedIntRepr, self.duration) def _intRepr(self): return self.octave * len(NotePitch) + self.note.value @@ -93,13 +83,27 @@ class Note: def __repr__(self): return self.__str__() + def getLowerNeighbour(self): + return Note._fromIntRepr(max((self._intRepr()-1), 0)) + + def getUpperNeighbour(self): + maxIntRepr = Note(NotePitch.H, 9)._intRepr() # max value for one-digit octave number + return Note._fromIntRepr(min((self._intRepr()+1), maxIntRepr)) + + @staticmethod + def _fromIntRepr(intRepr, duration=4): + note = NotePitch(intRepr % len(NotePitch)) + octave = int(intRepr / len(NotePitch)) + return Note(note, octave, duration) + + @staticmethod def checkInterval(a, b): return a._intRepr() - b._intRepr() @staticmethod def range(a, b): - return [Note(note, 1, 1) for note in NotePitch.range(a.note, b.note)] + return [Note._fromIntRepr(x, 4) for x in range(a._intRepr(), b._intRepr()+1)] def intervalToString(interval): octaveInterval = int(abs(interval) / len(NotePitch)) diff --git a/Synth.py b/Synth.py new file mode 100644 index 0000000..13fca9f --- /dev/null +++ b/Synth.py @@ -0,0 +1,42 @@ +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 + 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 = findVariable("bpm", env) + value = args[0] + doPause(value, bpm) diff --git a/Tokenizer.py b/Tokenizer.py index 6037bc0..7015a3a 100644 --- a/Tokenizer.py +++ b/Tokenizer.py @@ -166,7 +166,7 @@ tokenizers = ( tokenizeWhitespaces ) -def tokenize(lines): +def doTokenize(lines): tokens = [] for lineNumber, line in enumerate(lines): current = 0 @@ -185,15 +185,6 @@ def tokenize(lines): return [token for token in tokens if token.type is not None] -if __name__ == "__main__": - try: - with open(sys.argv[1], 'r') as source: - lines = [line.rstrip('\n') for line in source.readlines()] - - tokens = tokenize(lines) - - for token in tokens: - print(token) - except TokenizerError as e: - print(str(e)) - +def tokenize(lines): + tokens = doTokenize(lines) + return list(filter(lambda x: x.type != TokenType.COMMENT, tokens)) diff --git a/main.py b/main.py new file mode 100644 index 0000000..c96519a --- /dev/null +++ b/main.py @@ -0,0 +1,17 @@ +from Tokenizer import tokenize +from Parser import parse +from Evaluator import evaluate +from Environment import createEnvironment +import sys + +if __name__ == "__main__": + with open(sys.argv[1], 'r') as source: + lines = [line.rstrip('\n') for line in source.readlines()] + + env = createEnvironment() + + tokens = tokenize(lines) + + ast = parse(tokens) + + evaluate(ast, env)