5 Commits

Author SHA1 Message Date
Bartłomiej Pluta
24bcb25bee Enable tuning 2019-09-06 22:59:09 +02:00
Bartłomiej Pluta
83ef3f6888 Create CLI scaffolding 2019-09-06 18:26:02 +02:00
Bartłomiej Pluta
3061dae723 Merge branch 'create-virtualenv' 2019-09-06 14:46:51 +02:00
Bartłomiej Pluta
aca227ac5e Create virtual environment with pipenv 2019-09-06 13:48:28 +02:00
Bartłomiej Pluta
7ec967a014 Merge branch 'add-polyphony' 2019-09-06 13:46:02 +02:00
11 changed files with 300 additions and 31 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
__pycache__/ __pycache__/
*.mus *.mus
.idea/* .idea/*
venv/

15
Pipfile Normal file
View File

@@ -0,0 +1,15 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
sounddevice = "*"
soundfile = "*"
numpy = "*"
matplotlib = "*"
[requires]
python_version = "3.7"

180
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,180 @@
{
"_meta": {
"hash": {
"sha256": "92b0c636f6ac9be9b32aa25b1cdc96dcbe85602d5c68a2084dc39d25392540b6"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"cffi": {
"hashes": [
"sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
"sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
"sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
"sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
"sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
"sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
"sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
"sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
"sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
"sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
"sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
"sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
"sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
"sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
"sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
"sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
"sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
"sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
"sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
"sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
"sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
"sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
"sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
"sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
"sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
"sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
"sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
"sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
],
"version": "==1.12.3"
},
"cycler": {
"hashes": [
"sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
"sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
],
"version": "==0.10.0"
},
"kiwisolver": {
"hashes": [
"sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f",
"sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7",
"sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe",
"sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c",
"sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5",
"sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75",
"sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187",
"sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641",
"sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883",
"sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5",
"sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2",
"sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3",
"sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389",
"sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897",
"sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a",
"sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c",
"sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326",
"sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0",
"sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e",
"sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544",
"sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995",
"sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f",
"sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee",
"sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004",
"sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2",
"sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9",
"sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a",
"sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f"
],
"version": "==1.1.0"
},
"matplotlib": {
"hashes": [
"sha256:1febd22afe1489b13c6749ea059d392c03261b2950d1d45c17e3aed812080c93",
"sha256:31a30d03f39528c79f3a592857be62a08595dec4ac034978ecd0f814fa0eec2d",
"sha256:4442ce720907f67a79d45de9ada47be81ce17e6c2f448b3c64765af93f6829c9",
"sha256:796edbd1182cbffa7e1e7a97f1e141f875a8501ba8dd834269ae3cd45a8c976f",
"sha256:934e6243df7165aad097572abf5b6003c77c9b6c480c3c4de6f2ef1b5fdd4ec0",
"sha256:bab9d848dbf1517bc58d1f486772e99919b19efef5dd8596d4b26f9f5ee08b6b",
"sha256:c1fe1e6cdaa53f11f088b7470c2056c0df7d80ee4858dadf6cbe433fcba4323b",
"sha256:e5b8aeca9276a3a988caebe9f08366ed519fff98f77c6df5b64d7603d0e42e36",
"sha256:ec6bd0a6a58df3628ff269978f4a4b924a0d371ad8ce1f8e2b635b99e482877a"
],
"index": "pypi",
"version": "==3.1.1"
},
"numpy": {
"hashes": [
"sha256:03f2ebcbffcce2dec8860633b89a93e80c6a239d21a77ae8b241450dc21e8c35",
"sha256:078c8025da5ab9e8657edc9c2a1e9642e06e953bc7baa2e65c1aa9d9dfb7e98b",
"sha256:0fbfa98c5d5c3c6489cc1e852ec94395d51f35d9ebe70c6850e47f465038cdf4",
"sha256:1c841033f4fe6801648180c3033c45b3235a8bbd09bc7249010f99ea27bb6790",
"sha256:2c0984a01ddd0aeec89f0ce46ef21d64761048cd76c0074d0658c91f9131f154",
"sha256:4c166dcb0fff7cb3c0bbc682dfb5061852a2547efb6222e043a7932828c08fb5",
"sha256:8c2d98d0623bd63fb883b65256c00454d5f53127a5a7bcdaa8bdc582814e8cb4",
"sha256:8cb4b6ae45aad6d26712a1ce0a3f2556c5e1484867f9649e03496e45d6a5eba4",
"sha256:93050e73c446c82065b7410221b07682e475ac51887cd9368227a5d944afae80",
"sha256:a3f6b3024f8826d8b1490e6e2a9b99e841cd2c375791b1df62991bd8f4c00b89",
"sha256:bede70fd8699695363f39e86c1e869b2c8b74fb5ef135a67b9e1eeebff50322a",
"sha256:c304b2221f33489cd15a915237a84cdfe9420d7e4d4828c78a0820f9d990395c",
"sha256:f11331530f0eff69a758d62c2461cd98cdc2eae0147279d8fc86e0464eb7e8ca",
"sha256:fa5f2a8ef1e07ba258dc07d4dd246de23ef4ab920ae0f3fa2a1cc5e90f0f1888",
"sha256:fb6178b0488b0ce6a54bc4accbdf5225e937383586555604155d64773f6beb2b",
"sha256:fd5e830d4dc31658d61a6452cd3e842213594d8c15578cdae6829e36ad9c0930"
],
"index": "pypi",
"version": "==1.17.1"
},
"pycparser": {
"hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
],
"version": "==2.19"
},
"pyparsing": {
"hashes": [
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
],
"version": "==2.4.2"
},
"python-dateutil": {
"hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
],
"version": "==2.8.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"sounddevice": {
"hashes": [
"sha256:8d0571349f9a438a97f2c69da760f195cf5ddf2351072199cc1dfede4785a207",
"sha256:a0dda9dea9a5038ec0dafee4cdebd6d9a09086405d4833a7e3f6b18c109f89f3",
"sha256:a71cd3daabc8cb13e0ec9a29e7d4ba9b707386f3c81b3501b20f64209bcaece7",
"sha256:b038315e3497e01b1cb22630bc9b5025fcfad40e5ef7c125a9d565befa7220e0",
"sha256:c340d0a68945439571a13b287bd11b2d2884b61cd47cc5de8356d88221edf580"
],
"index": "pypi",
"version": "==0.3.13"
},
"soundfile": {
"hashes": [
"sha256:181393f8daac38bda997c4284b760bcd726dc0e847f620020d7bf2d3d538a985",
"sha256:3f90971bb1884575ada9a0731e878c91cc1ffde14a778f0857deaa2fd04849ac",
"sha256:637f6218c867b8cae80f6989634a0813b416b3e6132480d056e6e5a89a921571",
"sha256:7fe04272cbfb94ab648acb0db31ace0800f5a4cf46e9d45b4d71ddb15abce1a0",
"sha256:80a3d2ed37199ffa7066c097d7b82b8413922e22870b62b54dc264bf0169f4c2"
],
"index": "pypi",
"version": "==0.10.2"
}
},
"develop": {}
}

2
os_requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
You have to have installed `PortAudio` in order to use app.

0
smnp/cli/__init__.py Normal file
View File

23
smnp/cli/parser.py Normal file
View File

@@ -0,0 +1,23 @@
import argparse
VERSION = "0.1"
DESCRIPTION = """
Simple Music Notation Processor is a command line tool enabling you to do some music stuff using custom domain-specific language.
"""
class CliParser(object):
def __init__(self):
self.parser = argparse.ArgumentParser(description=DESCRIPTION)
self.parser.add_argument('file', nargs='*', help='a file containing SMNP code')
self.parser.add_argument('-c', '--code', action='append', default=[], type=str, help='a string with SMNP code')
self.parser.add_argument('-m', '--mic', action='store_true', help='test microphone level')
self.parser.add_argument('-C', '--config', type=argparse.FileType('-r'), help='a file containing settings (not implemented yet)')
self.parser.add_argument('-p', '--params', action='append', help='pass arguments to program (not implemented yet)')
self.parser.add_argument('-v', '--version', action='version')
self.parser.add_argument('--tokens', action='store_true', help='print tokens of parsed code')
self.parser.add_argument('--ast', action='store_true', help='print abstract syntax tree of parsed code')
self.parser.add_argument('--dry-run', action='store_true', help='don\'t execute passed code')
self.parser.version = VERSION
def parse(self):
return self.parser.parse_args()

View File

@@ -1,17 +1,38 @@
import sys from smnp.cli.parser import CliParser
from smnp.error.base import SmnpException from smnp.error.base import SmnpException
from smnp.library.loader import loadStandardLibrary from smnp.library.loader import loadStandardLibrary
from smnp.module.mic.lib.detector.noise import NoiseDetector
from smnp.program.interpreter import Interpreter from smnp.program.interpreter import Interpreter
def interpretFile(args, file):
stdLibraryEnv = loadStandardLibrary() if not args.dry_run else None
Interpreter.interpretFile(file, printTokens=args.tokens, printAst=args.ast, execute=not args.dry_run, baseEnvironment=stdLibraryEnv)
def interpretString(args, string):
stdLibraryEnv = loadStandardLibrary() if not args.dry_run else None
Interpreter.interpretString(string, printTokens=args.tokens, printAst=args.ast, execute=not args.dry_run, baseEnvironment=stdLibraryEnv, source='<cli>')
def main(): def main():
try: try:
stdLibraryEnv = loadStandardLibrary() parser = CliParser()
Interpreter.interpretFile(sys.argv[1], printTokens=False, printAst=False, execute=True, baseEnvironment=stdLibraryEnv) args = parser.parse()
if args.mic:
nd = NoiseDetector()
nd.test()
for code in args.code:
interpretString(args, code)
for file in args.file:
interpretFile(args, file)
except SmnpException as e: except SmnpException as e:
print(e.message()) print(e.message())
except KeyboardInterrupt: except KeyboardInterrupt:
print("Program interrupted") print("Program interrupted")

View File

@@ -12,6 +12,7 @@ DEFAULT_BPM = 120
DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1] DEFAULT_OVERTONES = [0.4, 0.3, 0.1, 0.1, 0.1]
DEFAULT_DECAY = 4 DEFAULT_DECAY = 4
DEFAULT_ATTACK = 100 DEFAULT_ATTACK = 100
DEFAULT_TUNING = 440
def getBpm(config): def getBpm(config):
@@ -72,16 +73,29 @@ def getAttack(config):
return DEFAULT_ATTACK return DEFAULT_ATTACK
def getTuning(config):
key = Type.string("tuning")
if key in config.value:
tuning = config.value[key]
if not tuning.type in [Type.INTEGER, Type.FLOAT] or tuning.value < 0:
raise RuntimeException("The 'tuning' property must be non-negative integer or float", None)
return tuning.value
return DEFAULT_TUNING
class Config: class Config:
def __init__(self, bpm, overtones, decay, attack): def __init__(self, bpm, overtones, decay, attack, tuning):
self.bpm = bpm self.bpm = bpm
self.overtones = overtones self.overtones = overtones
self.decay = decay self.decay = decay
self.attack = attack self.attack = attack
self.tuning = tuning
@staticmethod @staticmethod
def default(): def default():
return Config(DEFAULT_BPM, DEFAULT_OVERTONES, DEFAULT_DECAY, DEFAULT_ATTACK) return Config(DEFAULT_BPM, DEFAULT_OVERTONES, DEFAULT_DECAY, DEFAULT_ATTACK, DEFAULT_TUNING)
_signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER)) _signature1 = varargSignature(listOf(Type.NOTE, Type.INTEGER))
@@ -114,8 +128,9 @@ def __function3(config, notes):
overtones = getOvertones(config) overtones = getOvertones(config)
decay = getDecay(config) decay = getDecay(config)
attack = getAttack(config) attack = getAttack(config)
tuning = getTuning(config)
return compilePolyphony(rawNotes, Config(bpm, overtones, decay, attack)) return compilePolyphony(rawNotes, Config(bpm, overtones, decay, attack, tuning))
_signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP)) _signature4 = varargSignature(ofTypes(Type.NOTE, Type.INTEGER), ofType(Type.MAP))
@@ -128,8 +143,9 @@ def __function4(config, notes):
overtones = getOvertones(config) overtones = getOvertones(config)
decay = getDecay(config) decay = getDecay(config)
attack = getAttack(config) attack = getAttack(config)
tuning = getTuning(config)
return compilePolyphony([ notes ], Config(bpm, overtones, decay, attack)) return compilePolyphony([ notes ], Config(bpm, overtones, decay, attack, tuning))
function = CombinedFunction( function = CombinedFunction(

View File

@@ -4,6 +4,7 @@ import matplotlib.pyplot as plt
import numpy as np import numpy as np
import sounddevice as sd import sounddevice as sd
from smnp.note.pitch import Tuning
from smnp.type.model import Type from smnp.type.model import Type
FS = 44100 FS = 44100
@@ -25,7 +26,8 @@ def play(wave):
def compilePolyphony(notes, config): def compilePolyphony(notes, config):
compiledLines = [1 / len(notes) * compileNotes(line, config) for line in notes] tuning = Tuning(config.tuning)
compiledLines = [1 / len(notes) * compileNotes(line, config, tuning) for line in notes]
return sum(adjustSize(compiledLines)) return sum(adjustSize(compiledLines))
@@ -35,17 +37,17 @@ def adjustSize(compiledLines):
return [np.concatenate([line, np.zeros(maxSize - len(line))]) for line in compiledLines] return [np.concatenate([line, np.zeros(maxSize - len(line))]) for line in compiledLines]
def compileNotes(notes, config): def compileNotes(notes, config, tuning):
dispatcher = { dispatcher = {
Type.NOTE: lambda note, overtones: sineForNote(note.value, config), Type.NOTE: lambda note, overtones: sineForNote(note.value, config, tuning),
Type.INTEGER: lambda note, overtones: silenceForPause(note.value, config) Type.INTEGER: lambda note, overtones: silenceForPause(note.value, config)
} }
return np.concatenate([dispatcher[note.type](note, config) for note in notes]) return np.concatenate([dispatcher[note.type](note, config) for note in notes])
def sineForNote(note, config): def sineForNote(note, config, tuning):
frequency = note.toFrequency() frequency = note.toFrequency(tuning)
duration = 60 * 4 / note.duration / config.bpm duration = 60 * 4 / note.duration / config.bpm
duration *= 1.5 if note.dot else 1 duration *= 1.5 if note.dot else 1
return sound(frequency, duration, config) return sound(frequency, duration, config)

View File

@@ -16,8 +16,8 @@ class Note:
self.duration = duration self.duration = duration
self.dot = dot self.dot = dot
def toFrequency(self): def toFrequency(self, tuning):
return self.note.toFrequency() * 2 ** self.octave return tuning[self.note] * 2 ** self.octave
def transpose(self, interval): def transpose(self, interval):
origIntRepr = self._intRepr() origIntRepr = self._intRepr()

View File

@@ -2,6 +2,7 @@ from enum import Enum
from smnp.error.note import NoteException from smnp.error.note import NoteException
_semitone = 2**(1/12)
class NotePitch(Enum): class NotePitch(Enum):
C = 0 C = 0
@@ -17,21 +18,21 @@ class NotePitch(Enum):
AIS = 10 AIS = 10
H = 11 H = 11
def toFrequency(self): # def toFrequency(self):
return { # return {
NotePitch.C: 16.35, # NotePitch.C: 16.35,
NotePitch.CIS: 17.32, # NotePitch.CIS: 17.32,
NotePitch.D: 18.35, # NotePitch.D: 18.35,
NotePitch.DIS: 19.45, # NotePitch.DIS: 19.45,
NotePitch.E: 20.60, # NotePitch.E: 20.60,
NotePitch.F: 21.83, # NotePitch.F: 21.83,
NotePitch.FIS: 23.12, # NotePitch.FIS: 23.12,
NotePitch.G: 24.50, # NotePitch.G: 24.50,
NotePitch.GIS: 25.96, # NotePitch.GIS: 25.96,
NotePitch.A: 27.50, # NotePitch.A: 27.50,
NotePitch.AIS: 29.17, # NotePitch.AIS: 29.17,
NotePitch.H: 30.87 # NotePitch.H: 30.87
}[self] # }[self]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -50,6 +51,14 @@ class NotePitch(Enum):
raise NoteException(f"Note '{string}' does not exist") raise NoteException(f"Note '{string}' does not exist")
class Tuning(object):
def __init__(self, a4=440):
self.tuning = { value: a4/_semitone**(57-i) for i, value in enumerate(NotePitch.__members__) }
def __getitem__(self, item):
return self.tuning[str(item)]
stringToPitch = { stringToPitch = {
'cb': NotePitch.H, 'cb': NotePitch.H,
'c': NotePitch.C, 'c': NotePitch.C,