Add 'plot' function and remove deprecated 'percent' type

This commit is contained in:
Bartłomiej Pluta
2019-07-30 13:59:18 +02:00
parent 8abae7c2ff
commit 7e55fe4c1a
5 changed files with 145 additions and 8 deletions

View File

@@ -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 = []

View File

@@ -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)
)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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)