diff --git a/smnp/ast/node/map.py b/smnp/ast/node/map.py index 47784dd..e6afbef 100644 --- a/smnp/ast/node/map.py +++ b/smnp/ast/node/map.py @@ -1,4 +1,5 @@ from smnp.ast.node.atom import LiteralParser +from smnp.ast.node.identifier import IdentifierLiteralParser from smnp.ast.node.iterable import abstractIterableParser from smnp.ast.node.model import Node from smnp.ast.node.operator import BinaryOperator, Operator @@ -31,12 +32,15 @@ class Map(Node): def MapParser(input): from smnp.ast.node.expression import ExpressionParser - keyParser = LiteralParser + keyParser = Parser.oneOf( + LiteralParser, + IdentifierLiteralParser + ) valueParser = ExpressionParser mapEntryParser = Parser.allOf( keyParser, - Parser.terminal(TokenType.ARROW, createNode=Operator.withValue, doAssert=True), + Parser.terminal(TokenType.ARROW, createNode=Operator.withValue), Parser.doAssert(valueParser, "expression"), createNode=MapEntry.withValues ) diff --git a/smnp/module/synth/__init__.py b/smnp/module/synth/__init__.py index 6d9a8ce..a8b743a 100644 --- a/smnp/module/synth/__init__.py +++ b/smnp/module/synth/__init__.py @@ -1,4 +1,4 @@ -from smnp.module.synth.function import synth, pause +from smnp.module.synth.function import synth, pause, plot, compile -functions = [ synth.function, pause.function ] +functions = [ synth.function, pause.function, plot.function, compile.function ] methods = [] \ No newline at end of file diff --git a/smnp/module/synth/function/compile.py b/smnp/module/synth/function/compile.py new file mode 100644 index 0000000..8a03845 --- /dev/null +++ b/smnp/module/synth/function/compile.py @@ -0,0 +1,141 @@ +from smnp.error.runtime import RuntimeException +from smnp.function.model import Function, CombinedFunction +from smnp.function.signature import varargSignature +from smnp.module.synth.lib.wave import compilePolyphony + +from smnp.type.model import Type +from smnp.type.signature.matcher.list import listOf +from smnp.type.signature.matcher.type import ofTypes, ofType + + +DEFAULT_BPM = 120 +DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1] +DEFAULT_DECAY = 4 +DEFAULT_ATTACK = 100 + + +def getBpm(config): + key = Type.string("bpm") + if key in config.value: + bpm = config.value[key] + if bpm.type != Type.INTEGER or bpm.value <= 0: + raise RuntimeException("The 'bpm' property must be positive integer", None) + + return bpm.value + + return DEFAULT_BPM + + +def getOvertones(config): + key = Type.string("overtones") + if key in config.value: + overtones = config.value[key] + rawOvertones = [overtone.value for overtone in overtones.value] + if overtones.type != Type.LIST or not all(overtone.type in [Type.FLOAT, Type.INTEGER] for overtone in overtones.value): + raise RuntimeException("The 'overtones' property must be list of floats", None) + + if len(rawOvertones) < 1: + raise RuntimeException("The 'overtones' property must contain one overtone at least", None) + + if any(overtone < 0 for overtone in rawOvertones): + raise RuntimeException("The 'overtones' property mustn't contain negative values", None) + + if sum(rawOvertones) > 1.0: + raise RuntimeException("The 'overtones' property must contain overtones which sum is not greater than 1.0", None) + + return rawOvertones + + return DEFAULT_OVERTONES + + +def getDecay(config): + key = Type.string("decay") + if key in config.value: + decay = config.value[key] + if not decay.type in [Type.INTEGER, Type.FLOAT] or decay.value < 0: + raise RuntimeException("The 'decay' property must be non-negative integer or float", None) + + return decay.value + + return DEFAULT_DECAY + + +def getAttack(config): + key = Type.string("attack") + if key in config.value: + attack = config.value[key] + if not attack.type in [Type.INTEGER, Type.FLOAT] or attack.value < 0: + raise RuntimeException("The 'attack' property must be non-negative integer or float", None) + + return attack.value + + return DEFAULT_ATTACK + + +class Config: + def __init__(self, bpm, overtones, decay, attack): + self.bpm = bpm + self.overtones = overtones + self.decay = decay + self.attack = attack + + @staticmethod + def default(): + return Config(DEFAULT_BPM, DEFAULT_OVERTONES, DEFAULT_DECAY, DEFAULT_ATTACK) + + +_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) +def _function1(env, notes): + return Type.list([Type.float(float(m)) for m in __function1(notes)]) + + +def __function1(notes): + return compilePolyphony([note.value for note in notes], Config.default()) + + +_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) +def _function2(env, notes): + return Type.list([Type.float(float(m)) for m in __function2(notes)]) + + +def __function2(notes): + return compilePolyphony([ notes ], Config.default()) + + +_signature3 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function3(env, config, notes): + return Type.list([ Type.float(float(m)) for m in __function3(config, notes) ]) + + +def __function3(config, notes): + rawNotes = [note.value for note in notes] + + bpm = getBpm(config) + overtones = getOvertones(config) + decay = getDecay(config) + attack = getAttack(config) + + return compilePolyphony(rawNotes, Config(bpm, overtones, decay, attack)) + + +_signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function4(env, config, notes): + return Type.list([ Type.float(float(m)) for m in __function4(config, notes) ]) + + +def __function4(config, notes): + bpm = getBpm(config) + overtones = getOvertones(config) + decay = getDecay(config) + attack = getAttack(config) + + return compilePolyphony([ notes ], Config(bpm, overtones, decay, attack)) + + +function = CombinedFunction( + 'wave', + Function(_signature1, _function1), + Function(_signature2, _function2), + Function(_signature3, _function3), + Function(_signature4, _function4), +) \ No newline at end of file diff --git a/smnp/module/synth/function/pause.py b/smnp/module/synth/function/pause.py index e1869f8..4af81df 100644 --- a/smnp/module/synth/function/pause.py +++ b/smnp/module/synth/function/pause.py @@ -1,13 +1,13 @@ from smnp.function.model import Function from smnp.function.signature import signature -from smnp.module.synth.lib import player +from smnp.module.synth.lib.wave import pause from smnp.type.model import Type from smnp.type.signature.matcher.type import ofTypes _signature = signature(ofTypes(Type.INTEGER)) def _function(env, value): bpm = env.findVariable('bpm') - player.pause(value.value, bpm.value) + pause(value.value, bpm.value) function = Function(_signature, _function, 'pause') \ No newline at end of file diff --git a/smnp/module/synth/function/plot.py b/smnp/module/synth/function/plot.py new file mode 100644 index 0000000..4d415f9 --- /dev/null +++ b/smnp/module/synth/function/plot.py @@ -0,0 +1,13 @@ +from smnp.function.model import Function +from smnp.function.signature import signature +from smnp.module.synth.lib.wave import plot +from smnp.type.model import Type +from smnp.type.signature.matcher.list import listOf + +_signature = signature(listOf(Type.FLOAT)) +def _function(env, wave): + rawWave = [ m.value for m in wave.value ] + plot(rawWave) + + +function = Function(_signature, _function, 'plotWave') diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 0c6c834..04816be 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -1,13 +1,47 @@ -from smnp.function.model import Function -from smnp.function.signature import signature -from smnp.module.synth.lib.player import play +from smnp.function.model import Function, CombinedFunction +from smnp.function.signature import varargSignature +from smnp.module.synth.function import compile +from smnp.module.synth.lib.wave import play from smnp.type.model import Type -from smnp.type.signature.matcher.type import ofType +from smnp.type.signature.matcher.list import listOf +from smnp.type.signature.matcher.type import ofTypes, ofType -_signature = signature(ofType(Type.NOTE)) -def _function(env, note): - bpm = env.findVariable('bpm') - play(note.value, bpm.value) +_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) +def _function1(env, notes): + wave = compile.__function1(notes) + play(wave) -function = Function(_signature, _function, 'synthNote') \ No newline at end of file +_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) +def _function2(env, notes): + wave = compile.__function2(notes) + play(wave) + + +_signature3 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function3(env, config, notes): + wave = compile.__function3(config, notes) + play(wave) + + +_signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function4(env, config, notes): + wave = compile.__function4(config, notes) + play(wave) + + +_signature5 = varargSignature(listOf(Type.FLOAT)) +def _function5(env, waves): + for wave in waves: + rawWave = [m.value for m in wave.value] + play(rawWave) + + +function = CombinedFunction( + 'synth', + Function(_signature1, _function1), + Function(_signature2, _function2), + Function(_signature3, _function3), + Function(_signature4, _function4), + Function(_signature5, _function5) +) \ No newline at end of file diff --git a/smnp/module/synth/lib/player.py b/smnp/module/synth/lib/player.py deleted file mode 100644 index ad1e56b..0000000 --- a/smnp/module/synth/lib/player.py +++ /dev/null @@ -1,24 +0,0 @@ -import time - -from smnp.module.synth.lib.wave import sine -from smnp.note.model import Note - - -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) - diff --git a/smnp/module/synth/lib/wave.py b/smnp/module/synth/lib/wave.py index 3c2e1d7..2f106b6 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -1,12 +1,78 @@ import time +import matplotlib.pyplot as plt import numpy as np import sounddevice as sd +from smnp.type.model import Type + FS = 44100 +def pause(value, bpm): + time.sleep(60 * 4 / value / bpm) + + +def plot(wave): + X = np.arange(len(wave)) + plt.plot(X, wave) + plt.show() + + +def play(wave): + sd.play(wave) + time.sleep(len(wave) / FS) + + +def compilePolyphony(notes, config): + compiledLines = [1 / len(notes) * compileNotes(line, config) for line in notes] + return sum(adjustSize(compiledLines)) + + +def adjustSize(compiledLines): + maxSize = max(len(line) for line in compiledLines) + + return [np.concatenate([line, np.zeros(maxSize - len(line))]) for line in compiledLines] + + +def compileNotes(notes, config): + dispatcher = { + Type.NOTE: lambda note, overtones: sineForNote(note.value, config), + Type.INTEGER: lambda note, overtones: silenceForPause(note.value, config) + } + + return np.concatenate([dispatcher[note.type](note, config) for note in notes]) + + +def sineForNote(note, config): + frequency = note.toFrequency() + duration = 60 * 4 / note.duration / config.bpm + duration *= 1.5 if note.dot else 1 + return sound(frequency, duration, config) + + +def sound(frequency, duration, config): + return attack(decay(sum(overtone * sine((i+1) * frequency, duration) for i, overtone in enumerate(config.overtones) if overtone > 0), config), config) + + +def decay(wave, config): + magnitude = np.exp(-config.decay/len(wave) * np.arange(len(wave))) + + return magnitude * wave + + +def attack(wave, config): + magnitude = -np.exp(-config.attack / len(wave) * np.arange(len(wave)))+1 \ + if config.attack > 0 \ + else np.ones(len(wave)) + + return magnitude * wave + + 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) \ No newline at end of file + return (np.sin(2 * np.pi * np.arange(FS * duration) * frequency / FS)).astype(np.float32) + + +def silenceForPause(value, config): + duration = 60 * 4 / value / config.bpm + return np.zeros(int(FS * duration)) diff --git a/smnp/runtime/evaluators/atom.py b/smnp/runtime/evaluators/atom.py index 7918900..d38df62 100644 --- a/smnp/runtime/evaluators/atom.py +++ b/smnp/runtime/evaluators/atom.py @@ -3,7 +3,7 @@ from smnp.ast.node.identifier import Identifier from smnp.ast.node.list import List from smnp.ast.node.map import Map from smnp.error.runtime import RuntimeException -from smnp.runtime.evaluator import Evaluator +from smnp.runtime.evaluator import Evaluator, EvaluationResult from smnp.runtime.evaluators.expression import expressionEvaluator from smnp.runtime.evaluators.float import FloatEvaluator from smnp.runtime.evaluators.iterable import abstractIterableEvaluator @@ -59,12 +59,15 @@ class MapEvaluator(Evaluator): @classmethod def evaluator(cls, node, environment): map = {} - exprEvaluator = expressionEvaluator(doAssert=True) + keyEvaluator = Evaluator.oneOf( + Evaluator.forNodes(lambda node, environment: EvaluationResult.OK(Type.string(node.value)), Identifier), + expressionEvaluator(doAssert=True) + ) for entry in node.children: - key = exprEvaluator(entry.key, environment).value + key = keyEvaluator(entry.key, environment).value if key in map: raise RuntimeException(f"Duplicated key '{key.stringify()}' found in map", entry.pos) - map[key] = exprEvaluator(entry.value, environment).value + map[key] = expressionEvaluator(doAssert=True)(entry.value, environment).value return Type.map(map) diff --git a/smnp/runtime/evaluators/map.py b/smnp/runtime/evaluators/map.py deleted file mode 100644 index f1596f8..0000000 --- a/smnp/runtime/evaluators/map.py +++ /dev/null @@ -1,19 +0,0 @@ -from smnp.error.runtime import RuntimeException -from smnp.runtime.evaluator import Evaluator -from smnp.runtime.evaluators.expression import expressionEvaluator -from smnp.type.model import Type - - -class MapEvaluator(Evaluator): - - @classmethod - def evaluator(cls, node, environment): - map = {} - exprEvaluator = expressionEvaluator(doAssert=True) - for entry in node.children: - key = exprEvaluator(entry.key, environment).value - if key in map: - raise RuntimeException(f"Duplicated key '{key.stringify()}' found in map", entry.pos) - map[key] = exprEvaluator(entry.value, environment).value - - return Type.map(map) \ No newline at end of file diff --git a/smnp/type/model.py b/smnp/type/model.py index 926a8e4..ae99964 100644 --- a/smnp/type/model.py +++ b/smnp/type/model.py @@ -12,7 +12,6 @@ class Type(Enum): STRING = (str, lambda x: x) LIST = (list, lambda x: f"[{', '.join([e.stringify() for e in x])}]") MAP = (dict, lambda x: '{' + ', '.join(f"'{k.stringify()}' -> '{v.stringify()}'" for k, v in x.items()) + '}') - PERCENT = (float, lambda x: f"{int(x * 100)}%") NOTE = (Note, lambda x: x.note.name) BOOL = (bool, lambda x: str(x).lower()) SOUND = (Sound, lambda x: x.file)