Merge branch 'add-polyphony'
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = []
|
||||
141
smnp/module/synth/function/compile.py
Normal file
141
smnp/module/synth/function/compile.py
Normal file
@@ -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),
|
||||
)
|
||||
@@ -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')
|
||||
13
smnp/module/synth/function/plot.py
Normal file
13
smnp/module/synth/function/plot.py
Normal file
@@ -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')
|
||||
@@ -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')
|
||||
_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)
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user