From 0dcf5287e15c9dc25adef1aea490977f316aef3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Sun, 28 Jul 2019 19:48:39 +0200 Subject: [PATCH 1/9] Add polyphony AND add overtones do synthesed tones --- smnp/module/synth/function/pause.py | 4 +-- smnp/module/synth/function/synth.py | 29 +++++++++++----- smnp/module/synth/lib/player.py | 24 ------------- smnp/module/synth/lib/wave.py | 52 +++++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 38 deletions(-) delete mode 100644 smnp/module/synth/lib/player.py 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/synth.py b/smnp/module/synth/function/synth.py index 0c6c834..7002310 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -1,13 +1,24 @@ -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.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 -_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): + rawNotes = [note.value for note in notes] + play(rawNotes, env.findVariable("bpm").value, env.findVariable("overtones").value) -function = Function(_signature, _function, 'synthNote') \ No newline at end of file +_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) +def _function2(env, notes): + play([ notes ], env.findVariable("bpm").value, env.findVariable("overtones").value) + + +function = CombinedFunction( + 'synth', + Function(_signature1, _function1), + Function(_signature2, _function2) +) \ 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..8259423 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -3,10 +3,56 @@ import time 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 play(notes, bpm, overtones): + compiled = compilePolyphony(notes, bpm, overtones) + sd.play(compiled) + time.sleep(len(compiled) / FS) + + +def compilePolyphony(notes, bpm, overtones): + compiledLines = [1 / len(notes) * compileNotes(line, bpm, overtones) 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, bpm, overtones): + dispatcher = { + Type.NOTE: lambda note, overtones: sineForNote(note.value, bpm, overtones), + Type.INTEGER: lambda note, overtones: silenceForPause(note.value, bpm) + } + + return np.concatenate([dispatcher[note.type](note, overtones) for note in notes]) + + +def sineForNote(note, bpm, overtones): + frequency = note.toFrequency() + duration = 60 * 4 / note.duration / bpm + duration *= 1.5 if note.dot else 1 + return sound(frequency, duration, overtones) + + +def sound(frequency, duration, overtones): + return sum(a.value * sine((i+1) * frequency, duration) for i, a in enumerate(overtones)) + + 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, bpm): + duration = 60 * 4 / value / bpm + return np.zeros(int(FS * duration)) From a5875425fc821ebbffdf498afb745fa3f448fa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Sun, 28 Jul 2019 21:20:11 +0200 Subject: [PATCH 2/9] Enable passing config map to synth function --- smnp/module/synth/function/synth.py | 59 +++++++++++++++++++++++++---- smnp/module/synth/lib/wave.py | 2 +- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 7002310..0ae66a4 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -1,20 +1,65 @@ +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 play from smnp.type.model import Type from smnp.type.signature.matcher.list import listOf -from smnp.type.signature.matcher.type import ofTypes +from smnp.type.signature.matcher.type import ofTypes, ofType -_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) -def _function1(env, notes): + +DEFAULT_BPM = 120 +DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1] + + +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 == Type.FLOAT 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 + + +_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function1(env, config, notes): rawNotes = [note.value for note in notes] - play(rawNotes, env.findVariable("bpm").value, env.findVariable("overtones").value) + bpm = getBpm(config) + overtones = getOvertones(config) + + play(rawNotes, bpm, overtones) -_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) -def _function2(env, notes): - play([ notes ], env.findVariable("bpm").value, env.findVariable("overtones").value) +_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function2(env, config, notes): + bpm = getBpm(config) + overtones = getOvertones(config) + play([ notes ], bpm, overtones) function = CombinedFunction( diff --git a/smnp/module/synth/lib/wave.py b/smnp/module/synth/lib/wave.py index 8259423..5df57be 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -46,7 +46,7 @@ def sineForNote(note, bpm, overtones): def sound(frequency, duration, overtones): - return sum(a.value * sine((i+1) * frequency, duration) for i, a in enumerate(overtones)) + return sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(overtones)) def sine(frequency, duration): From 73ea88d8d9a7c81d79e4ab4e7d3cbd8723bca19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Mon, 29 Jul 2019 17:19:41 +0200 Subject: [PATCH 3/9] Overload synth function to accept notes without config object --- smnp/module/synth/function/synth.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 0ae66a4..9a0b69d 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -46,8 +46,20 @@ def getOvertones(config): return DEFAULT_OVERTONES -_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) -def _function1(env, config, notes): + +_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) +def _function1(env, notes): + rawNotes = [note.value for note in notes] + play(rawNotes, DEFAULT_BPM, DEFAULT_OVERTONES) + + +_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) +def _function2(env, notes): + play([ notes ], DEFAULT_BPM, DEFAULT_OVERTONES) + + +_signature3 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function3(env, config, notes): rawNotes = [note.value for note in notes] bpm = getBpm(config) overtones = getOvertones(config) @@ -55,8 +67,8 @@ def _function1(env, config, notes): play(rawNotes, bpm, overtones) -_signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) -def _function2(env, config, notes): +_signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) +def _function4(env, config, notes): bpm = getBpm(config) overtones = getOvertones(config) play([ notes ], bpm, overtones) @@ -65,5 +77,7 @@ def _function2(env, config, notes): function = CombinedFunction( 'synth', Function(_signature1, _function1), - Function(_signature2, _function2) + Function(_signature2, _function2), + Function(_signature3, _function3), + Function(_signature4, _function4), ) \ No newline at end of file From 13b069dc7da0ecc911ed964c9a7df85d553dbffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Mon, 29 Jul 2019 17:53:41 +0200 Subject: [PATCH 4/9] Add support for non-quoted (identifier) map keys being used as string --- smnp/ast/node/map.py | 8 ++++++-- smnp/runtime/evaluators/atom.py | 11 +++++++---- smnp/runtime/evaluators/map.py | 19 ------------------- 3 files changed, 13 insertions(+), 25 deletions(-) delete mode 100644 smnp/runtime/evaluators/map.py 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/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 From 07f08b05571a0c3f759d29328da55e4b107d2a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Mon, 29 Jul 2019 17:59:38 +0200 Subject: [PATCH 5/9] Add support for integers in passing overtones to synth --- smnp/module/synth/function/synth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 9a0b69d..482faf7 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -29,7 +29,7 @@ def getOvertones(config): 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 == Type.FLOAT 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: From 8abae7c2fff472c171959e90db8bfc9a36630843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Mon, 29 Jul 2019 21:43:20 +0200 Subject: [PATCH 6/9] Add decay to synthetiser --- smnp/module/synth/function/synth.py | 34 +++++++++++++++++++++++---- smnp/module/synth/lib/wave.py | 36 +++++++++++++++++------------ 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 482faf7..913cbeb 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -10,6 +10,7 @@ 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 def getBpm(config): @@ -46,16 +47,38 @@ def getOvertones(config): 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 + + +class Config: + def __init__(self, bpm, overtones, decay): + self.bpm = bpm + self.overtones = overtones + self.decay = decay + + @staticmethod + def default(): + return Config(DEFAULT_BPM, DEFAULT_OVERTONES, DEFAULT_DECAY) + _signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) def _function1(env, notes): rawNotes = [note.value for note in notes] - play(rawNotes, DEFAULT_BPM, DEFAULT_OVERTONES) + play(rawNotes, Config.default()) _signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) def _function2(env, notes): - play([ notes ], DEFAULT_BPM, DEFAULT_OVERTONES) + play([ notes ], Config.default()) _signature3 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) @@ -63,15 +86,18 @@ def _function3(env, config, notes): rawNotes = [note.value for note in notes] bpm = getBpm(config) overtones = getOvertones(config) + decay = getDecay(config) - play(rawNotes, bpm, overtones) + play(rawNotes, Config(bpm, overtones, decay)) _signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) def _function4(env, config, notes): bpm = getBpm(config) overtones = getOvertones(config) - play([ notes ], bpm, overtones) + decay = getDecay(config) + + play([ notes ], Config(bpm, overtones, decay)) function = CombinedFunction( diff --git a/smnp/module/synth/lib/wave.py b/smnp/module/synth/lib/wave.py index 5df57be..5c39985 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -12,14 +12,14 @@ def pause(value, bpm): time.sleep(60 * 4 / value / bpm) -def play(notes, bpm, overtones): - compiled = compilePolyphony(notes, bpm, overtones) +def play(notes, config): + compiled = compilePolyphony(notes, config) sd.play(compiled) time.sleep(len(compiled) / FS) -def compilePolyphony(notes, bpm, overtones): - compiledLines = [1 / len(notes) * compileNotes(line, bpm, overtones) for line in notes] +def compilePolyphony(notes, config): + compiledLines = [1 / len(notes) * compileNotes(line, config) for line in notes] return sum(adjustSize(compiledLines)) @@ -29,30 +29,36 @@ def adjustSize(compiledLines): return [np.concatenate([line, np.zeros(maxSize - len(line))]) for line in compiledLines] -def compileNotes(notes, bpm, overtones): +def compileNotes(notes, config): dispatcher = { - Type.NOTE: lambda note, overtones: sineForNote(note.value, bpm, overtones), - Type.INTEGER: lambda note, overtones: silenceForPause(note.value, bpm) + 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, overtones) for note in notes]) + return np.concatenate([dispatcher[note.type](note, config) for note in notes]) -def sineForNote(note, bpm, overtones): +def sineForNote(note, config): frequency = note.toFrequency() - duration = 60 * 4 / note.duration / bpm + duration = 60 * 4 / note.duration / config.bpm duration *= 1.5 if note.dot else 1 - return sound(frequency, duration, overtones) + return sound(frequency, duration, config) -def sound(frequency, duration, overtones): - return sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(overtones)) +def sound(frequency, duration, config): + return decay(sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(config.overtones)), config) + + +def decay(wave, config): + magnitude = np.exp(-config.decay/len(wave) * np.arange(len(wave))) + + return magnitude * wave def sine(frequency, duration): return (np.sin(2 * np.pi * np.arange(FS * duration) * frequency / FS)).astype(np.float32) -def silenceForPause(value, bpm): - duration = 60 * 4 / value / bpm +def silenceForPause(value, config): + duration = 60 * 4 / value / config.bpm return np.zeros(int(FS * duration)) From 7e55fe4c1a7ba083ca925173ebc4729cbc046306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Tue, 30 Jul 2019 13:59:18 +0200 Subject: [PATCH 7/9] Add 'plot' function and remove deprecated 'percent' type --- smnp/module/synth/__init__.py | 4 +- smnp/module/synth/function/plot.py | 106 ++++++++++++++++++++++++++++ smnp/module/synth/function/synth.py | 24 +++++-- smnp/module/synth/lib/wave.py | 18 ++++- smnp/type/model.py | 1 - 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 smnp/module/synth/function/plot.py diff --git a/smnp/module/synth/__init__.py b/smnp/module/synth/__init__.py index 6d9a8ce..fc2169d 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 -functions = [ synth.function, pause.function ] +functions = [ synth.function, pause.function, plot.function ] methods = [] \ 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..20a3ac6 --- /dev/null +++ b/smnp/module/synth/function/plot.py @@ -0,0 +1,106 @@ +from smnp.error.runtime import RuntimeException +from smnp.function.model import CombinedFunction, 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.type import ofType + +DEFAULT_BPM = 120 +DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1] +DEFAULT_DECAY = 4 +DEFAULT_ATTACK = 100 + +# TODO: this code is shared with synth.py module, remove repetition +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 = signature(ofType(Type.NOTE)) +def _function1(env, note): + config = Config.default() + + plot(note.value, config) + + +_signature2 = signature(ofType(Type.MAP), ofType(Type.NOTE)) +def _function2(env, config, note): + bpm = getBpm(config) + overtones = getOvertones(config) + decay = getDecay(config) + attack = getAttack(config) + + plot(note.value, Config(bpm, overtones, decay, attack)) + + +function = CombinedFunction( + 'plot', + Function(_signature1, _function1), + Function(_signature2, _function2) +) + diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index 913cbeb..f29bf8f 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -11,6 +11,7 @@ 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): @@ -59,15 +60,28 @@ def getDecay(config): 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): + 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) + return Config(DEFAULT_BPM, DEFAULT_OVERTONES, DEFAULT_DECAY, DEFAULT_ATTACK) _signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) @@ -87,8 +101,9 @@ def _function3(env, config, notes): bpm = getBpm(config) overtones = getOvertones(config) decay = getDecay(config) + attack = getAttack(config) - play(rawNotes, Config(bpm, overtones, decay)) + play(rawNotes, Config(bpm, overtones, decay, attack)) _signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) @@ -96,8 +111,9 @@ def _function4(env, config, notes): bpm = getBpm(config) overtones = getOvertones(config) decay = getDecay(config) + attack = getAttack(config) - play([ notes ], Config(bpm, overtones, decay)) + play([ notes ], Config(bpm, overtones, decay, attack)) function = CombinedFunction( diff --git a/smnp/module/synth/lib/wave.py b/smnp/module/synth/lib/wave.py index 5c39985..c99a0aa 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -1,5 +1,6 @@ import time +import matplotlib.pyplot as plt import numpy as np import sounddevice as sd @@ -12,6 +13,13 @@ def pause(value, bpm): time.sleep(60 * 4 / value / bpm) +def plot(note, config): + Y = sineForNote(note, config) + X = np.arange(len(Y)) + plt.plot(X, Y) + plt.show() + + def play(notes, config): compiled = compilePolyphony(notes, config) sd.play(compiled) @@ -46,7 +54,7 @@ def sineForNote(note, config): def sound(frequency, duration, config): - return decay(sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(config.overtones)), config) + return attack(decay(sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(config.overtones)), config), config) def decay(wave, config): @@ -55,6 +63,14 @@ def decay(wave, config): 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): return (np.sin(2 * np.pi * np.arange(FS * duration) * frequency / FS)).astype(np.float32) 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) From a7de7f027966b0c6fb1a460e9d9bcfca42a7992c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Tue, 30 Jul 2019 15:12:32 +0200 Subject: [PATCH 8/9] Optimise time of generating overtones --- smnp/module/synth/lib/wave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smnp/module/synth/lib/wave.py b/smnp/module/synth/lib/wave.py index c99a0aa..05df133 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -54,7 +54,7 @@ def sineForNote(note, config): def sound(frequency, duration, config): - return attack(decay(sum(a * sine((i+1) * frequency, duration) for i, a in enumerate(config.overtones)), config), 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): From aca6e6bb55bb86a3eae82e0a77a7c8c6675a5e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Pluta?= Date: Tue, 30 Jul 2019 16:41:49 +0200 Subject: [PATCH 9/9] Create tools for compiling waves --- smnp/module/synth/__init__.py | 4 +- smnp/module/synth/function/compile.py | 141 ++++++++++++++++++++++++++ smnp/module/synth/function/plot.py | 107 ++----------------- smnp/module/synth/function/synth.py | 110 +++----------------- smnp/module/synth/lib/wave.py | 14 ++- 5 files changed, 172 insertions(+), 204 deletions(-) create mode 100644 smnp/module/synth/function/compile.py diff --git a/smnp/module/synth/__init__.py b/smnp/module/synth/__init__.py index fc2169d..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, plot +from smnp.module.synth.function import synth, pause, plot, compile -functions = [ synth.function, pause.function, plot.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/plot.py b/smnp/module/synth/function/plot.py index 20a3ac6..4d415f9 100644 --- a/smnp/module/synth/function/plot.py +++ b/smnp/module/synth/function/plot.py @@ -1,106 +1,13 @@ -from smnp.error.runtime import RuntimeException -from smnp.function.model import CombinedFunction, Function +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.type import ofType +from smnp.type.signature.matcher.list import listOf -DEFAULT_BPM = 120 -DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1] -DEFAULT_DECAY = 4 -DEFAULT_ATTACK = 100 - -# TODO: this code is shared with synth.py module, remove repetition -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 +_signature = signature(listOf(Type.FLOAT)) +def _function(env, wave): + rawWave = [ m.value for m in wave.value ] + plot(rawWave) -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 = signature(ofType(Type.NOTE)) -def _function1(env, note): - config = Config.default() - - plot(note.value, config) - - -_signature2 = signature(ofType(Type.MAP), ofType(Type.NOTE)) -def _function2(env, config, note): - bpm = getBpm(config) - overtones = getOvertones(config) - decay = getDecay(config) - attack = getAttack(config) - - plot(note.value, Config(bpm, overtones, decay, attack)) - - -function = CombinedFunction( - 'plot', - Function(_signature1, _function1), - Function(_signature2, _function2) -) - +function = Function(_signature, _function, 'plotWave') diff --git a/smnp/module/synth/function/synth.py b/smnp/module/synth/function/synth.py index f29bf8f..04816be 100644 --- a/smnp/module/synth/function/synth.py +++ b/smnp/module/synth/function/synth.py @@ -1,119 +1,40 @@ -from smnp.error.runtime import RuntimeException 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.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): - rawNotes = [note.value for note in notes] - play(rawNotes, Config.default()) + wave = compile.__function1(notes) + play(wave) _signature2 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER)) def _function2(env, notes): - play([ notes ], Config.default()) + wave = compile.__function2(notes) + play(wave) _signature3 = varargSignature(listOf(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) def _function3(env, config, notes): - rawNotes = [note.value for note in notes] - bpm = getBpm(config) - overtones = getOvertones(config) - decay = getDecay(config) - attack = getAttack(config) - - play(rawNotes, Config(bpm, overtones, decay, attack)) + wave = compile.__function3(config, notes) + play(wave) _signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) def _function4(env, config, notes): - bpm = getBpm(config) - overtones = getOvertones(config) - decay = getDecay(config) - attack = getAttack(config) + wave = compile.__function4(config, notes) + play(wave) - play([ notes ], Config(bpm, overtones, decay, attack)) + +_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( @@ -122,4 +43,5 @@ function = CombinedFunction( 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/wave.py b/smnp/module/synth/lib/wave.py index 05df133..2f106b6 100644 --- a/smnp/module/synth/lib/wave.py +++ b/smnp/module/synth/lib/wave.py @@ -13,17 +13,15 @@ def pause(value, bpm): time.sleep(60 * 4 / value / bpm) -def plot(note, config): - Y = sineForNote(note, config) - X = np.arange(len(Y)) - plt.plot(X, Y) +def plot(wave): + X = np.arange(len(wave)) + plt.plot(X, wave) plt.show() -def play(notes, config): - compiled = compilePolyphony(notes, config) - sd.play(compiled) - time.sleep(len(compiled) / FS) +def play(wave): + sd.play(wave) + time.sleep(len(wave) / FS) def compilePolyphony(notes, config):