diff --git a/Audio.py b/Audio.py new file mode 100644 index 0000000..9c0f800 --- /dev/null +++ b/Audio.py @@ -0,0 +1,13 @@ +import pysine +from Note import Note, NotePitch +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/Evaulator.py b/Evaulator.py index 63b4f87..357b6bd 100644 --- a/Evaulator.py +++ b/Evaulator.py @@ -1,9 +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 class RuntimeException(Exception): pass @@ -48,7 +50,7 @@ def evaluateFunctionCall(functionCall, environment): arguments = evaluateList(functionCall.arguments, environment) for name, definition in environment.functions.items(): if name == function: - return definition(arguments) + return definition(arguments, environment) raise RuntimeException(f"Function '{function}' does not exist") @@ -129,7 +131,7 @@ def evaluate(input, environment): if isinstance(input, IdentifierNode): return evaluateIdentifier(input, environment) -def rand(args): +def rand(args, env): if len(args) == 1 and isinstance(args[0], list): return args[0][int(random.uniform(0, len(args[0])))] @@ -144,16 +146,52 @@ def objectString(obj): return "(" + ", ".join([objectString(v) for v in obj]) + ")" raise RuntimeException(f"Don't know how to interpret {str(obj)}") -def prt(args): +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) -if __name__ == "__main__": - +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': lambda args: print(":".join([str(type(arg)) for arg in args])), - 'type': lambda args: print(type(args[0])), - 'random': rand + 'midi': Midi.play, + 'pause': Midi.pause, + 'type': lambda args, env: print(type(args[0])), + 'random': rand, + 'semitones': semitones, + 'interval': interval, + 'transpose': transpose } @@ -163,6 +201,6 @@ if __name__ == "__main__": tokens = [token for token in tokenize(lines) if token.type != TokenType.COMMENT] ast = parse(tokens) - environment = Environment([{}], functions) + environment = Environment([{ "bpm": 120 }], functions) evaluate(ast, environment) diff --git a/Midi.py b/Midi.py new file mode 100644 index 0000000..1dbbce6 --- /dev/null +++ b/Midi.py @@ -0,0 +1,44 @@ +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 d05b06a..65633cf 100644 --- a/Note.py +++ b/Note.py @@ -1,5 +1,6 @@ from enum import Enum from Error import ParseError +import math class NotePitch(Enum): C = 1 @@ -21,6 +22,26 @@ class NotePitch(Enum): def __repr__(self): return self.__str__() + def toFrequency(self): + return { + NotePitch.C: 16.35, + NotePitch.CIS: 17.32, + NotePitch.D: 18.35, + NotePitch.DIS: 19.45, + NotePitch.E: 20.60, + NotePitch.F: 21.83, + NotePitch.FIS: 23.12, + NotePitch.G: 24.50, + NotePitch.GIS: 25.96, + NotePitch.A: 27.50, + NotePitch.AIS: 29.17, + NotePitch.H: 30.87 + }[self] + + @staticmethod + def checkInterval(a, b): + return a.value - b.value + @staticmethod def toPitch(string): try: @@ -41,7 +62,7 @@ class NotePitch(Enum): 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, duration): + def __init__(self, note, octave = 4, duration = 4): if type(note) == str: self.note = NotePitch.toPitch(note) else: @@ -49,6 +70,52 @@ class Note: self.octave = octave self.duration = duration + def hash(self): + return f"{self.note.value}{self.octave}{self.duration}" + + def toFrequency(self): + return self.note.toFrequency() * 2 ** self.octave + + 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) + + def _intRepr(self): + return self.octave * len(NotePitch) + self.note.value + + def __str__(self): + return f"{self.note}({self.octave}')[{self.duration}]" + + def __repr__(self): + return self.__str__() + + @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)] + +def intervalToString(interval): + octaveInterval = int(abs(interval) / len(NotePitch)) + pitchInterval = abs(interval) % len(NotePitch) + pitchIntervalName = { + 0: "1", + 1: "1>/2<", + 2: "2", + 3: "3<", + 4: "3>", + 5: "4", + 6: "4>/5<", + 7: "5", + 8: "6<", + 9: "6>", + 10: "7<", + 11: "7>" + } + return (str(pitchIntervalName[pitchInterval]) + (f"(+{octaveInterval}')" if octaveInterval > 0 else "")) diff --git a/Parser.py b/Parser.py index 096918d..d2c027b 100644 --- a/Parser.py +++ b/Parser.py @@ -17,8 +17,8 @@ def parseNote(input, parent): consumedChars = 1 notePitch = value[consumedChars] consumedChars += 1 - octave = 1 - duration = 1 + octave = 4 + duration = 4 if consumedChars < len(value) and value[consumedChars] in ('b', '#'): notePitch += value[consumedChars] consumedChars += 1