Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cfc92db3d | |||
| 9bd4ec0ff0 | |||
| 7d6ba407bd | |||
| fe65c87ed2 | |||
| c8a45a3f8b | |||
| 8f5d8adf48 | |||
| cc3497fd29 | |||
| d3edfb5b42 | |||
| c9b6a01ef3 | |||
| 830cc94744 | |||
| c88f02419d | |||
| f0b9de0118 | |||
| 5d122a90e0 | |||
| 882a9a7b4b | |||
| 48426fdb0e | |||
| 8929940328 | |||
| 95b69d1d41 | |||
| c30ff41f1f | |||
| c8c590d7ed | |||
| bd2a1587d7 | |||
| 545ae1f92a | |||
| 7e015a635d | |||
| 745632ce59 | |||
| 323b41b36e | |||
| ea0e9ae0cd | |||
| 004578e56e | |||
| 7bf6669cd1 | |||
| 13f68f7ee7 | |||
| 5fd03fac6b | |||
| 115da4c96c | |||
| bc779b0c81 | |||
| 6167c4be33 | |||
| f60e21c341 | |||
| 3d543601e6 | |||
| 7d35ce0118 | |||
| f996e57acf | |||
| 9e37bd366f | |||
| da020cc3a2 | |||
| 78295da323 | |||
| 443d4e6639 | |||
| ef27557c76 | |||
| 1168449dfa | |||
| f0e1aca247 | |||
| 7a2f0b5a0a | |||
| 864b110931 | |||
| 0d285a63eb | |||
| f4f819291a | |||
| 595fc346ae | |||
| bcc86f4cfe | |||
| 3e32c9ecf2 | |||
| 6dd8333007 | |||
| 63dab6fbdf | |||
| 805d7af216 | |||
| 4dd0f12aca | |||
| 5e78ea9c4c | |||
| 77cf10c95c | |||
| acbfacee59 | |||
| 973d3eab2b | |||
| e9c0731d7e | |||
| a9e2936a29 | |||
| 65257217c5 | |||
| ffbff359fa | |||
| 9a4c970b95 | |||
| cca08b250f | |||
| bae01efa58 | |||
| c7a905f5a0 | |||
| f417f0282e | |||
| 5e9bf94d42 | |||
| a7da9795a5 | |||
| 0816ac65db | |||
| 1ff7e3b6d2 | |||
| f6c6497319 | |||
| 10f66d0027 | |||
| 726dc42902 | |||
| 90d7b0bdff | |||
| e240c45693 | |||
| 7cc89d3333 | |||
| 4f019bfda4 | |||
| 70b56dbc52 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -165,6 +165,8 @@ cython_debug/
|
|||||||
# VSCode
|
# VSCode
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# Debugging file
|
# Debugging files
|
||||||
debug.py
|
debug/
|
||||||
|
debug*.py
|
||||||
|
test.py
|
||||||
|
|
||||||
|
|||||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
include ziffers/*
|
||||||
|
include ziffers/classes/*
|
||||||
|
include ziffers/spec/*
|
||||||
|
global-include *.lark
|
||||||
57
README.md
57
README.md
@ -1,4 +1,59 @@
|
|||||||
# Ziffers for Python 3.10+
|
# Ziffers for Python 3.10+
|
||||||
|
|
||||||
This repo is hosting an attempt at porting the [Ziffers](https://github.com/amiika/ziffers) numbered musical notation to Python 3.10+. This library is using [parsimonious](https://github.com/erikrose/parsimonious) for PEG parsing.
|
This repo is hosting experimental parser for the [Ziffers](https://github.com/amiika/ziffers) numbered musical notation to Python 3.10+. This library is using lark lalr-1 parser and ebnf PEG for parsing the notation.
|
||||||
|
|
||||||
|
## Supported environments
|
||||||
|
|
||||||
|
Ziffers python supports following live coding and computer-aided composition environments:
|
||||||
|
|
||||||
|
* [Sardine](https://github.com/Bubobubobubobubo/sardine)
|
||||||
|
* [Music21](https://github.com/cuthbertLab/music21)
|
||||||
|
* [Csound](http://www.csounds.com/manual/html/ScoreTop.html)
|
||||||
|
* [Sonicsynth](https://github.com/Frikallo/SonicSynth)
|
||||||
|
|
||||||
|
# Status:
|
||||||
|
|
||||||
|
**Supported:**
|
||||||
|
```
|
||||||
|
Pitches: -2 -1 0 1 2
|
||||||
|
Chords: 0 024 2 246
|
||||||
|
Note lengths: w 0 h 1 q 2 e 3 s 4
|
||||||
|
Subdivision: [1 2 [3 4]]
|
||||||
|
Decimal durations: 0.25 0 1 <0.333>2 3
|
||||||
|
Octaves: ^ 0 ^ 1 _ 2 _ 3
|
||||||
|
Escaped octave: <2> 1 <1>1<-2>3
|
||||||
|
Named chords: i i i i
|
||||||
|
Randoms: % ? % ? % ?
|
||||||
|
Random between: (-3,6)
|
||||||
|
Repeat: [: 1 (2,6) 3 :4]
|
||||||
|
Repeat cycles: [: <q e> (1,4) <(2 3) (3 (1,7))> :]
|
||||||
|
Lists: h 1 q(0 1 2 3) 2
|
||||||
|
List cycles: (: <q e> (1,4) <(2 3) (3 (1,7))> :)
|
||||||
|
Loop cycles: <0 <1 <2 <3 <4 5>>>>>
|
||||||
|
Basic operations: (1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 10)*(2 9 3))%7
|
||||||
|
Product operations: (0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2
|
||||||
|
Euclid cycles: (q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)
|
||||||
|
Transformations: (0 1 2)<r> (0 1 2)<i>(-2 1)
|
||||||
|
List assignation: A=(0 (1,6) 3) B=(3 ? 2) B A B B A
|
||||||
|
Random repeat: (: 1 (2,6) 3 :4)
|
||||||
|
```
|
||||||
|
|
||||||
|
**New features:**
|
||||||
|
```
|
||||||
|
Shorthand for random repeat: (2 5):3 [2 5 1]:4 (1,6):6
|
||||||
|
```
|
||||||
|
|
||||||
|
**Partial support:**
|
||||||
|
```
|
||||||
|
Escape/eval: {10 11} {3+1*2} // {1.2 2.43} NOT SUPPORTED YET.
|
||||||
|
Roman chords: i ii iii i^maj i^7
|
||||||
|
```
|
||||||
|
|
||||||
|
**TBD:**
|
||||||
|
```
|
||||||
|
Random selections: [q 1 2, q 3 e 4 6]
|
||||||
|
Conditionals: 1 {%<0.5?3} 3 4 (: 1 2 {%<0.2?3:2} :3)
|
||||||
|
Functions: (0 1 2 3){x%3==0?x-2:x+2}
|
||||||
|
Polynomials: (-10..10){(x**3)*(x+1)%12}
|
||||||
|
Modal interchange (a-g): iiia ig ivf^7
|
||||||
|
```
|
||||||
101
example.musicxml
Normal file
101
example.musicxml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
||||||
|
<score-partwise version="4.0">
|
||||||
|
<movement-title>Music21 Fragment</movement-title>
|
||||||
|
<identification>
|
||||||
|
<creator type="composer">Music21</creator>
|
||||||
|
<encoding>
|
||||||
|
<encoding-date>2023-06-27</encoding-date>
|
||||||
|
<software>music21 v.9.1.0</software>
|
||||||
|
</encoding>
|
||||||
|
</identification>
|
||||||
|
<defaults>
|
||||||
|
<scaling>
|
||||||
|
<millimeters>7</millimeters>
|
||||||
|
<tenths>40</tenths>
|
||||||
|
</scaling>
|
||||||
|
</defaults>
|
||||||
|
<part-list>
|
||||||
|
<score-part id="P0c4c819b93d5335473e23baff277bd9f">
|
||||||
|
<part-name />
|
||||||
|
</score-part>
|
||||||
|
</part-list>
|
||||||
|
<!--=========================== Part 1 ===========================-->
|
||||||
|
<part id="P0c4c819b93d5335473e23baff277bd9f">
|
||||||
|
<!--========================= Measure 1 ==========================-->
|
||||||
|
<measure implicit="no" number="1">
|
||||||
|
<attributes>
|
||||||
|
<divisions>10080</divisions>
|
||||||
|
<time>
|
||||||
|
<beats>4</beats>
|
||||||
|
<beat-type>4</beat-type>
|
||||||
|
</time>
|
||||||
|
<clef>
|
||||||
|
<sign>G</sign>
|
||||||
|
<line>2</line>
|
||||||
|
</clef>
|
||||||
|
</attributes>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>D</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>E</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<rest />
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>D</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>E</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>G</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<note print-object="no" print-spacing="yes">
|
||||||
|
<rest />
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<barline location="right">
|
||||||
|
<bar-style>light-heavy</bar-style>
|
||||||
|
</barline>
|
||||||
|
</measure>
|
||||||
|
</part>
|
||||||
|
</score-partwise>
|
||||||
42
examples/csound/play_with_csound.py
Normal file
42
examples/csound/play_with_csound.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Example of using ziffers with Csound score."""
|
||||||
|
try:
|
||||||
|
import ctcsound
|
||||||
|
except (ImportError,TypeError):
|
||||||
|
csound_imported = false
|
||||||
|
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
# Csound numeric score is very versatile so it is hard to do generic tranformer
|
||||||
|
# See http://www.csounds.com/manual/html/ScoreTop.html and http://www.csounds.com/manual/html/ScoreStatements.html
|
||||||
|
|
||||||
|
# There is a simple converter implemented that uses format:
|
||||||
|
# i {instrument} {start time} {duration} {amplitude} {frequency}
|
||||||
|
# See ziffers_to_csound_score in ziffers/converters.py
|
||||||
|
|
||||||
|
if(csound_imported):
|
||||||
|
|
||||||
|
# Parse ziffers notation, scale in SCALA format
|
||||||
|
parsed = zparse("w 0 024 q 0 1 2 346 r e (5 3 9 2 -605)+(0 -3 6) q 0h24e67s9^s1^e3^5^7", key="D4", scale="100. 200. 250. 400. 560.")
|
||||||
|
|
||||||
|
score = ziffers_to_csound_score(parsed, 180, 1500, "FooBar") # 180 bpm, 1500 amplitude, FooBar instrument
|
||||||
|
|
||||||
|
print("Generated score:")
|
||||||
|
print(score)
|
||||||
|
|
||||||
|
# Define FooBar Csound instrument
|
||||||
|
orc = """
|
||||||
|
instr FooBar
|
||||||
|
out(linen(oscili(p4,p5),0.1,p3,0.1))
|
||||||
|
endin
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Run score with Csound
|
||||||
|
c = ctcsound.Csound()
|
||||||
|
c.setOption("-odac")
|
||||||
|
c.compileOrc(orc)
|
||||||
|
c.readScore(score)
|
||||||
|
c.start()
|
||||||
|
c.perform()
|
||||||
|
c.stop()
|
||||||
|
else:
|
||||||
|
print("Csound not found! First download from https://csound.com/ and add to PATH or PYENV (Windows path: C:\Program Files\Csound6_x64\bin). Then install ctcsound with 'pip install ctcsound'.")
|
||||||
82
examples/jupyter/test_jupylet.ipynb
Normal file
82
examples/jupyter/test_jupylet.ipynb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Playing note: 60\n",
|
||||||
|
"Playing note: 67\n",
|
||||||
|
"Playing note: 64\n",
|
||||||
|
"Playing note: 69\n",
|
||||||
|
"Playing note: 65\n",
|
||||||
|
"Playing note: 76\n",
|
||||||
|
"Playing note: 67\n",
|
||||||
|
"Playing note: 64\n",
|
||||||
|
"Playing note: 69\n",
|
||||||
|
"Playing note: 65\n",
|
||||||
|
"Playing note: 62\n",
|
||||||
|
"Playing note: 71\n",
|
||||||
|
"Playing note: 74\n",
|
||||||
|
"Playing note: 65\n",
|
||||||
|
"Playing note: 67\n",
|
||||||
|
"Playing note: 69\n",
|
||||||
|
"Playing note: 64\n",
|
||||||
|
"Playing note: 62\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stderr",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Stream callback called with status: <sounddevice.CallbackFlags: output underflow>.\n",
|
||||||
|
"Stream callback called with status: <sounddevice.CallbackFlags: output underflow>.\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"from jupylet.audio.bundle import *\n",
|
||||||
|
"from clockblocks import *\n",
|
||||||
|
"from ziffers import *\n",
|
||||||
|
"\n",
|
||||||
|
"melody = zparse(\"q 0 4 2 5 e 3 9 4 2 s 5 3 1 6 8 3 4 5 2 1\")\n",
|
||||||
|
"bpm = 180\n",
|
||||||
|
"\n",
|
||||||
|
"clock = Clock(initial_tempo=bpm)\n",
|
||||||
|
"\n",
|
||||||
|
"for n in melody.evaluated_values:\n",
|
||||||
|
" if(isinstance(n, Pitch)):\n",
|
||||||
|
" print(\"Playing note: \" + str(n.get_note()))\n",
|
||||||
|
" tb303.play(n.get_note(), n.get_beat())\n",
|
||||||
|
" clock.wait(n.get_beat())\n",
|
||||||
|
"\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.11.0b4"
|
||||||
|
},
|
||||||
|
"orig_nbformat": 4
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 2
|
||||||
|
}
|
||||||
130
examples/jupyter/test_music21.ipynb
Normal file
130
examples/jupyter/test_music21.ipynb
Normal file
File diff suppressed because one or more lines are too long
26
examples/music21/create_midi_file.py
Normal file
26
examples/music21/create_midi_file.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from music21 import *
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
# Create melody string
|
||||||
|
melody = "q 0 2 4 r e 1 4 3 2 s 0 1 2 6 e 2 8 2 1 h 0"
|
||||||
|
bass_line = "(q 0 2 4 6 e 1 4 3 2 s 0 1 2 6 e 2 8 2 1 h 0)-(7)"
|
||||||
|
|
||||||
|
# Parse ziffers notation to melody from string
|
||||||
|
parsed = zparse(melody)
|
||||||
|
parsed_bass = zparse(bass_line)
|
||||||
|
|
||||||
|
# Convert to music21 objects
|
||||||
|
part1 = to_music21(parsed, time="4/4")
|
||||||
|
part2 = to_music21(parsed_bass, time="4/4")
|
||||||
|
|
||||||
|
# Add instruments
|
||||||
|
part1.insert(instrument.Piano())
|
||||||
|
part2.insert(instrument.Soprano())
|
||||||
|
|
||||||
|
# Create score
|
||||||
|
song = stream.Score()
|
||||||
|
song.insert(0,part1)
|
||||||
|
song.insert(0,part2)
|
||||||
|
|
||||||
|
# Write to midi file under examples/music21/midi folder
|
||||||
|
song.write('midi', fp='examples/music21/output/ziffers_example.mid')
|
||||||
9
examples/music21/create_musicxml.py
Normal file
9
examples/music21/create_musicxml.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from music21 import *
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
# Parse ziiffers object to music21 object
|
||||||
|
parsed = zparse("q 1 024 5 235 h 02345678{12} 0", key='C', scale='Zyditonic')
|
||||||
|
s2 = to_music21(parsed,time="4/4")
|
||||||
|
|
||||||
|
# Save to MusicXML file
|
||||||
|
s2.write('musicxml', fp='examples/music21/output/ziffers_example.xml')
|
||||||
9
examples/music21/create_png.py
Normal file
9
examples/music21/create_png.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from music21 import *
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
# Parse ziiffers object to music21 object
|
||||||
|
parsed = zparse('1 2 qr e 124')
|
||||||
|
s2 = to_music21(parsed,time="4/4")
|
||||||
|
|
||||||
|
# Save object as png file
|
||||||
|
s2.write("musicxml.png", fp="examples/music21/output/example.png")
|
||||||
BIN
examples/music21/output/example-1.png
Normal file
BIN
examples/music21/output/example-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
99
examples/music21/output/example.musicxml
Normal file
99
examples/music21/output/example.musicxml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
||||||
|
<score-partwise version="4.0">
|
||||||
|
<identification>
|
||||||
|
<encoding>
|
||||||
|
<encoding-date>2023-06-27</encoding-date>
|
||||||
|
<software>music21 v.9.1.0</software>
|
||||||
|
</encoding>
|
||||||
|
</identification>
|
||||||
|
<defaults>
|
||||||
|
<scaling>
|
||||||
|
<millimeters>7</millimeters>
|
||||||
|
<tenths>40</tenths>
|
||||||
|
</scaling>
|
||||||
|
</defaults>
|
||||||
|
<part-list>
|
||||||
|
<score-part id="P2a5e2bdd0bc8bab63f324063ff62ac90">
|
||||||
|
<part-name />
|
||||||
|
</score-part>
|
||||||
|
</part-list>
|
||||||
|
<!--=========================== Part 1 ===========================-->
|
||||||
|
<part id="P2a5e2bdd0bc8bab63f324063ff62ac90">
|
||||||
|
<!--========================= Measure 1 ==========================-->
|
||||||
|
<measure implicit="no" number="1">
|
||||||
|
<attributes>
|
||||||
|
<divisions>10080</divisions>
|
||||||
|
<time>
|
||||||
|
<beats>4</beats>
|
||||||
|
<beat-type>4</beat-type>
|
||||||
|
</time>
|
||||||
|
<clef>
|
||||||
|
<sign>G</sign>
|
||||||
|
<line>2</line>
|
||||||
|
</clef>
|
||||||
|
</attributes>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>D</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>E</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<rest />
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>D</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>E</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>G</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<note print-object="no" print-spacing="yes">
|
||||||
|
<rest />
|
||||||
|
<duration>5040</duration>
|
||||||
|
<type>eighth</type>
|
||||||
|
</note>
|
||||||
|
<barline location="right">
|
||||||
|
<bar-style>light-heavy</bar-style>
|
||||||
|
</barline>
|
||||||
|
</measure>
|
||||||
|
</part>
|
||||||
|
</score-partwise>
|
||||||
BIN
examples/music21/output/ziffers_example.mid
Normal file
BIN
examples/music21/output/ziffers_example.mid
Normal file
Binary file not shown.
228
examples/music21/output/ziffers_example.xml
Normal file
228
examples/music21/output/ziffers_example.xml
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
|
||||||
|
<score-partwise version="4.0">
|
||||||
|
<movement-title>Music21 Fragment</movement-title>
|
||||||
|
<identification>
|
||||||
|
<creator type="composer">Music21</creator>
|
||||||
|
<encoding>
|
||||||
|
<encoding-date>2023-06-27</encoding-date>
|
||||||
|
<software>music21 v.9.1.0</software>
|
||||||
|
</encoding>
|
||||||
|
</identification>
|
||||||
|
<defaults>
|
||||||
|
<scaling>
|
||||||
|
<millimeters>7</millimeters>
|
||||||
|
<tenths>40</tenths>
|
||||||
|
</scaling>
|
||||||
|
</defaults>
|
||||||
|
<part-list>
|
||||||
|
<score-part id="P381d8734c7dfe8b1fbfca186c059f010">
|
||||||
|
<part-name />
|
||||||
|
</score-part>
|
||||||
|
</part-list>
|
||||||
|
<!--=========================== Part 1 ===========================-->
|
||||||
|
<part id="P381d8734c7dfe8b1fbfca186c059f010">
|
||||||
|
<!--========================= Measure 1 ==========================-->
|
||||||
|
<measure implicit="no" number="1">
|
||||||
|
<attributes>
|
||||||
|
<divisions>10080</divisions>
|
||||||
|
<time>
|
||||||
|
<beats>4</beats>
|
||||||
|
<beat-type>4</beat-type>
|
||||||
|
</time>
|
||||||
|
<clef>
|
||||||
|
<sign>G</sign>
|
||||||
|
<line>2</line>
|
||||||
|
</clef>
|
||||||
|
</attributes>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>1</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
<accidental>sharp</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>F</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>A</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>F</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>G</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>10080</duration>
|
||||||
|
<type>quarter</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
</measure>
|
||||||
|
<!--========================= Measure 2 ==========================-->
|
||||||
|
<measure implicit="no" number="2">
|
||||||
|
<note>
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>F</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>G</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>A</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>4</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>C</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>D</step>
|
||||||
|
<alter>-1</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>flat</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>F</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>G</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>5</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note>
|
||||||
|
<chord />
|
||||||
|
<pitch>
|
||||||
|
<step>F</step>
|
||||||
|
<alter>0</alter>
|
||||||
|
<octave>6</octave>
|
||||||
|
</pitch>
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
<accidental>natural</accidental>
|
||||||
|
</note>
|
||||||
|
<note print-object="no" print-spacing="yes">
|
||||||
|
<rest />
|
||||||
|
<duration>20160</duration>
|
||||||
|
<type>half</type>
|
||||||
|
</note>
|
||||||
|
<barline location="right">
|
||||||
|
<bar-style>light-heavy</bar-style>
|
||||||
|
</barline>
|
||||||
|
</measure>
|
||||||
|
</part>
|
||||||
|
</score-partwise>
|
||||||
9
examples/music21/show_in_musescore.py
Normal file
9
examples/music21/show_in_musescore.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from music21 import *
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
# Parse Ziffers string to music21 object
|
||||||
|
s = to_music21('(i v vi vii^dim)@(q0 e 2 1 q 012)', key="d3", scale="Minor", time="4/4", bpm=190)
|
||||||
|
|
||||||
|
# See https://web.mit.edu/music21/doc/installing/installAdditional.html
|
||||||
|
# Attempt to open / show the midi in MuseScore
|
||||||
|
s.show()
|
||||||
17
examples/sonicsynth/play_with_sonicsynth.py
Normal file
17
examples/sonicsynth/play_with_sonicsynth.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'''Simple example of using SonicSynth to play Ziffers melody.'''
|
||||||
|
from sonicsynth import *
|
||||||
|
from ziffers import *
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
melody = zparse("(q 0 4 2 5 e 3 9 4 2 s 5 3 1 6 8 3 4 5 2 1 q 0)+(0 3 -2 4 2)", key="E3", scale="Aerathitonic")
|
||||||
|
|
||||||
|
def build_waveform(melody, bpm):
|
||||||
|
for item in melody.evaluated_values:
|
||||||
|
if isinstance(item, Pitch):
|
||||||
|
time_in_seconds = item.duration * 4 * 60 / bpm
|
||||||
|
yield generate_square_wave(frequency=item.freq, amplitude=0.25, duration=time_in_seconds)
|
||||||
|
|
||||||
|
waveform = np.concatenate(list(build_waveform(melody,130)))
|
||||||
|
|
||||||
|
player = Playback(44100)
|
||||||
|
player.play(waveform)
|
||||||
23
examples/sonicsynth/test_arpeggio_effect.py
Normal file
23
examples/sonicsynth/test_arpeggio_effect.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'''Testing arpeggio effect with sonicsynth and ziffers'''
|
||||||
|
from sonicsynth import *
|
||||||
|
from ziffers import *
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
melody = zparse("(q 0 024 2 246 e 4 2 6 2 q 5 0)+(0 3 1 2)", key="D4", scale="Minor")
|
||||||
|
|
||||||
|
def build_waveform(melody, bpm):
|
||||||
|
for item in melody.evaluated_values:
|
||||||
|
if isinstance(item, Pitch):
|
||||||
|
time_in_seconds = item.duration * 4 * 60 / bpm
|
||||||
|
yield generate_sine_wave(frequency=item.freq, amplitude=0.5, duration=time_in_seconds)
|
||||||
|
elif isinstance(item, Chord):
|
||||||
|
time_in_seconds = item.durations[0] * 4 * 60 / bpm
|
||||||
|
for pitch in item.pitch_classes:
|
||||||
|
# Create "NES arpeggio effect"
|
||||||
|
for i in range(1,len(item.durations)):
|
||||||
|
yield generate_sine_wave(frequency=pitch.freq, amplitude=0.5, duration=time_in_seconds/(len(item.durations)*3))
|
||||||
|
|
||||||
|
waveform = np.concatenate(list(build_waveform(melody,180)))
|
||||||
|
|
||||||
|
player = Playback(44100)
|
||||||
|
player.play(waveform)
|
||||||
7
main.py
7
main.py
@ -23,7 +23,7 @@ if __name__ == "__main__":
|
|||||||
"Repeat cycles": "[: <q e> (1,4) <(2 3) (3 (1,7))> :]",
|
"Repeat cycles": "[: <q e> (1,4) <(2 3) (3 (1,7))> :]",
|
||||||
"Lists": "h 1 q(0 1 2 3) 2",
|
"Lists": "h 1 q(0 1 2 3) 2",
|
||||||
"List cycles": "(: <q e> (1,4) <(2 3) (3 (1,7))> :)",
|
"List cycles": "(: <q e> (1,4) <(2 3) (3 (1,7))> :)",
|
||||||
"Loop cycles (for zloop or z0-z9)": "<0 <1 <2 <3 <4 5>>>>>",
|
"Loop cycles": "<0 <1 <2 <3 <4 5>>>>>",
|
||||||
"Basic operations": "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7",
|
"Basic operations": "(1 2 (3 4)+2)*2 ((1 2 3)+(0 9 13))-2 ((3 4 {10})*(2 9 3))%7",
|
||||||
"Product operations": "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2",
|
"Product operations": "(0 1 2 3)+(1 4 2 3) (0 1 2)-(0 2 1)+2",
|
||||||
"Euclid cycles": "(q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)",
|
"Euclid cycles": "(q1)<6,7>(q4 (e3 e4) q2) (q1)<6,7>(q4 q3 q2)",
|
||||||
@ -36,7 +36,6 @@ if __name__ == "__main__":
|
|||||||
}
|
}
|
||||||
for ex in expressions:
|
for ex in expressions:
|
||||||
try:
|
try:
|
||||||
print("Parsed: " + parse_expression(expressions[ex]).text)
|
print(f"{ex}: " + parse_expression(expressions[ex]).text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[red]Failed on {ex}[/red]")
|
print(f"[red]Failed on {ex}[/red]: "+expressions[ex])
|
||||||
# print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...")
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "python-ziffers"
|
name = "ziffers"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
description = "Port of the Ziffers numerical notation for Python"
|
description = "Port of the Ziffers numerical notation for Python"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Raphaël Forment", email="raphael.forment@gmail.com"},
|
{name = "Raphaël Forment", email="raphael.forment@gmail.com"},
|
||||||
@ -13,7 +13,10 @@ requires-python = ">=3.10"
|
|||||||
keywords = ["mininotation", "algorithmic music", "parser"]
|
keywords = ["mininotation", "algorithmic music", "parser"]
|
||||||
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Topic :: Software Development"
|
"Topic :: Artistic Software",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -22,12 +25,34 @@ dependencies = [
|
|||||||
"pytest>=7.2.1",
|
"pytest>=7.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://github.com/Bubobubobubobubo/ziffers-python"
|
homepage = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||||
documentation = "https://github.com/Bubobubobubobubo/ziffers-python"
|
documentation = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||||
repository = "https://github.com/Bubobubobubobubo/ziffers-python"
|
repository = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ['ziffers','ziffers.classes','ziffers.spec']
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
target_version = ['py311']
|
||||||
target_version = ['py310']
|
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
(
|
||||||
|
/(
|
||||||
|
\.eggs # exclude a few common directories in the
|
||||||
|
| \.git # root of the project
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|||||||
4
test.py
4
test.py
@ -1,4 +0,0 @@
|
|||||||
from ziffers import *
|
|
||||||
# a = z1("q e 1 ^ ^ 2 _ 3 4 <3> 3")
|
|
||||||
a = z1("1 2")
|
|
||||||
print(a.take(3))
|
|
||||||
14
tests/test_multi_03.py
Normal file
14
tests/test_multi_03.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
""" Test cases for the parser """
|
||||||
|
import pytest
|
||||||
|
from ziffers import zparse
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("1 2 3", [[1, 2, 3], [0.25,0.25,0.25]]),
|
||||||
|
("q2 eq3 e.4", [[2, 3, 4], [0.25,0.375,0.1875]]),
|
||||||
|
("{q9 e10 23}", [[9,10,23],[0.25,0.125,0.25]])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_multi_var(pattern: str, expected: list):
|
||||||
|
assert zparse(pattern).collect(6, keys=["pitch_class", "duration"]) == [item*2 for item in expected]
|
||||||
@ -1,54 +0,0 @@
|
|||||||
""" Test cases for the parser """
|
|
||||||
import pytest
|
|
||||||
from ziffers import parse_expression
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_parse():
|
|
||||||
expressions = [
|
|
||||||
"[1 [2 3]]",
|
|
||||||
"(1 (1,3) 1..3)",
|
|
||||||
"_^ q _qe^3 qww_4 _123 <1 2>",
|
|
||||||
"q _2 _ 3 ^ 343",
|
|
||||||
"2 qe2 e4",
|
|
||||||
"q 2 <3 343>",
|
|
||||||
"q (2 <3 343 (3 4)>)",
|
|
||||||
]
|
|
||||||
results = []
|
|
||||||
for expression in expressions:
|
|
||||||
try:
|
|
||||||
print(f"Parsing expression: {expression}")
|
|
||||||
result = parse_expression(expression)
|
|
||||||
results.append(True)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
results.append(False)
|
|
||||||
|
|
||||||
# Return true if all the results are true (succesfully parsed)
|
|
||||||
print(results)
|
|
||||||
assert all(results)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"pattern",
|
|
||||||
[
|
|
||||||
"1 2 3",
|
|
||||||
"q3 e4 s5",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_parsing_text(pattern: str):
|
|
||||||
assert parse_expression(pattern).text == pattern
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"pattern,expected",
|
|
||||||
[
|
|
||||||
("1 2 3", [1, 2, 3]),
|
|
||||||
("q2 eq3", [2, 3]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_pitch_classes(pattern: str, expected: list):
|
|
||||||
assert parse_expression(pattern).pitch_classes() == expected
|
|
||||||
|
|
||||||
# TODO: Add tests for octaves
|
|
||||||
# ("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]),
|
|
||||||
# ("_ 1 _2 <3>3 ^^4", [-1,-2,3,-1]),
|
|
||||||
@ -2,8 +2,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from ziffers import scale
|
from ziffers import scale
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name,expected",
|
"name,expected",
|
||||||
[
|
[
|
||||||
@ -49,5 +47,5 @@ def test_notenames(name: str, expected: int):
|
|||||||
)
|
)
|
||||||
def test_note_to_midi(pitch_classes: str, expected: int):
|
def test_note_to_midi(pitch_classes: str, expected: int):
|
||||||
assert [
|
assert [
|
||||||
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian") for val in pitch_classes
|
scale.note_from_pc(root=60, pitch_class=val, intervals="Ionian")[0] for val in pitch_classes
|
||||||
] == expected
|
] == expected
|
||||||
210
tests/test_singular_02.py
Normal file
210
tests/test_singular_02.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
""" Test cases for the parser """
|
||||||
|
import pytest
|
||||||
|
from ziffers import zparse, get_items
|
||||||
|
|
||||||
|
# pylint: disable=missing-function-docstring, line-too-long, invalid-name
|
||||||
|
|
||||||
|
def test_can_parse():
|
||||||
|
expressions = [
|
||||||
|
"0 1 2 3 4 5 6 7 8 9 T E",
|
||||||
|
"023 i iv iv^min",
|
||||||
|
"[1 [2 3]]",
|
||||||
|
"(1 (1,3) 1..3)",
|
||||||
|
"_^ q _qe^3 qww_4 _123 <1 2>",
|
||||||
|
"q _2 _ 3 ^ 343",
|
||||||
|
"2 qe2 e4",
|
||||||
|
"q 2 <3 343>",
|
||||||
|
"q (2 <3 343 (3 4)>)",
|
||||||
|
"? 1 2",
|
||||||
|
"(? 2 ? 4)+(1,4)",
|
||||||
|
"(1 2 <2 3>)+(0 1 2)"
|
||||||
|
]
|
||||||
|
results = []
|
||||||
|
for expression in expressions:
|
||||||
|
try:
|
||||||
|
print(f"Parsing expression: {expression}")
|
||||||
|
result = zparse(expression)
|
||||||
|
results.append(True)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
results.append(False)
|
||||||
|
|
||||||
|
# Return true if all the results are true (succesfully parsed)
|
||||||
|
print(results)
|
||||||
|
assert all(results)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern",
|
||||||
|
[
|
||||||
|
"1 2 3",
|
||||||
|
"q3 e4 s5",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parsing_text(pattern: str):
|
||||||
|
assert zparse(pattern).text == pattern
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("1 2 3", [1, 2, 3]),
|
||||||
|
("q2 eq3", [2, 3]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_pitch_classes(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"pitch_class") == expected*2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("__6 _0 _1 _2 _3 _4 _5 _6 0 1 2 3 4 5 6 ^0 ^1 ^2 ^3 ^4 ^5 ^6 ^^0", [-2, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2]),
|
||||||
|
("_ 1 _2 <3>3 ^^4", [-1, -2, 3, 1]),
|
||||||
|
("^ 1 ^1 3 _2 ^^1 _2 <-1> 2 ^4", [1, 2, 1, 0, 3, 0, -1, 0]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_pitch_octaves(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"octave") == expected*2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("w [1 [2 3]]", [0.5, 0.25, 0.25]),
|
||||||
|
("1.0 [1 [2 3]] 4 [3 [4 5]]", [0.5, 0.25, 0.25, 1.0, 0.5, 0.25, 0.25]),
|
||||||
|
("0.5 (0 0.25 3)+1", [0.5, 0.25]),
|
||||||
|
("[0 2 <2 8>:2 4] 0", [0.05, 0.05, 0.05, 0.05, 0.05, 0.25])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_subdivisions(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"duration") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("[: 1 [: 2 :] 3 :]", [62, 64, 64, 65, 62, 64, 64, 65]),
|
||||||
|
("(: 1 (: 2 :) 3 :)", [62, 64, 64, 65, 62, 64, 64, 65]),
|
||||||
|
("(1 2:2 3):2", [62, 64, 64, 65, 62, 64, 64, 65]),
|
||||||
|
("1:4",[62,62,62,62])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_repeats(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("[: 3 [2 4] :]", [0.25, 0.125, 0.125, 0.25, 0.125, 0.125]),
|
||||||
|
("[: 1 [2 [5 6]] 3 [2 4] :]", [0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125, 0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125]),
|
||||||
|
("(: 3 [2 4] :)", [0.25, 0.125, 0.125, 0.25, 0.125, 0.125]),
|
||||||
|
("(: 1 [2 [5 6]] 3 [2 4] :)", [0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125, 0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125]),
|
||||||
|
("(3 [2 4]):2", [0.25, 0.125, 0.125, 0.25, 0.125, 0.125]),
|
||||||
|
("(1 [2 [5 6]] 3 [2 4]):2", [0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125, 0.25, 0.125, 0.0625, 0.0625, 0.25, 0.125, 0.125]),
|
||||||
|
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_repeat_durations(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"duration") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("0 [0 2] 0", [0.25, 0.125, 0.125, 0.25, 0.25, 0.125, 0.125, 0.25, 0.25, 0.125, 0.125, 0.25]),
|
||||||
|
("w 0 [0 [1 [2 3]] 0] 9", [1.0, 0.3333333333333333, 0.16666666666666666, 0.08333333333333333, 0.08333333333333333, 0.3333333333333333, 1.0, 1.0, 0.3333333333333333, 0.16666666666666666, 0.08333333333333333, 0.08333333333333333]),
|
||||||
|
("1.0 0 [[2 3] [3 5]] 4", [1.0, 0.25, 0.25, 0.25, 0.25, 1.0, 1.0, 0.25, 0.25, 0.25, 0.25, 1.0]),
|
||||||
|
("2.0 0 [1[2[3[4 5]6]7]8] 9", [2.0, 0.6666666666666666, 0.2222222222222222, 0.07407407407407407, 0.037037037037037035, 0.037037037037037035, 0.07407407407407407, 0.2222222222222222, 0.6666666666666666, 2.0, 2.0, 0.6666666666666666])
|
||||||
|
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_looping_durations(pattern: str, expected: list):
|
||||||
|
parsed = zparse(pattern)
|
||||||
|
durations = []
|
||||||
|
for i in range(12):
|
||||||
|
durations.append(parsed[i].duration)
|
||||||
|
assert durations == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("e 1 | 3 | h 3 | e3 | 4", [0.125,0.25,0.5,0.125,0.25])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_measure_durations(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"duration") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("^ 1 | _ 3 | ^3 | 3 | _4", [1,-1,1,0,-1])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_measure_octaves(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"octave") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("e r qr r q r", [0.125, 0.25, 0.125, 0.25])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_rest(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"duration") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("-3..9 1..9", [55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 62, 64, 65, 67, 69, 71, 72, 74, 76])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_ranges(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("0 1 2 3", [None, 60, 62, 64])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_degree_notation(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern, degrees=True),len(expected)*2,"note") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("i ii iii iv v vi vii", [[60, 64, 67], [62, 65, 69], [64, 67, 71], [65, 69, 72], [67, 71, 74], [69, 72, 76], [71, 74, 77]])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_romans(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"note") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("[: i vi v :]", [[0, 2, 4], [5, 0, 2], [4, 6, 1], [0, 2, 4], [5, 0, 2], [4, 6, 1]]),
|
||||||
|
("i ii iii iv v vi vii", [[0, 2, 4], [1, 3, 5], [2, 4, 6], [3, 5, 0], [4, 6, 1], [5, 0, 2], [6, 1, 3]]),
|
||||||
|
("i^7 i^min iv^6", [[0, 2, 4, 6], [0, 2, 4], [3, 5, 0, 1]])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_romans_pcs(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"pitches") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("(0 1 2 3 r)+(1)", [1,2,3,4,None]),
|
||||||
|
("(0 1 2 3 r)-(0 1 4)", [0, -1, -2, -3, None, 1, 0, -1, -2, None, 4, 3, 2, 1, None])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_operations(pattern: str, expected: list):
|
||||||
|
assert get_items(zparse(pattern),len(expected)*2,"pitch_class") == expected*2
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pattern,expected",
|
||||||
|
[
|
||||||
|
("(i)+<0 1 2>", [[60, 64, 67], [62, 65, 69], [64, 67, 71], [60, 64, 67]]),
|
||||||
|
("(i)+<0 <1 2>>",[[60, 64, 67], [62, 65, 69], [60, 64, 67], [64, 67, 71]])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_cycles(pattern: str, expected: list):
|
||||||
|
zparse.cache_clear() # Clear cache for cycles
|
||||||
|
assert get_items(zparse(pattern),4,"note") == expected
|
||||||
@ -1,6 +1,9 @@
|
|||||||
from .parser import *
|
from .parser import *
|
||||||
from .mapper import *
|
from .mapper import *
|
||||||
from .classes import *
|
|
||||||
from .common import *
|
from .common import *
|
||||||
from .defaults import *
|
from .defaults import *
|
||||||
from .scale import *
|
from .scale import *
|
||||||
|
from .converters import *
|
||||||
|
from .spec import *
|
||||||
|
from .classes import *
|
||||||
|
from .generators import *
|
||||||
|
|||||||
@ -1,676 +0,0 @@
|
|||||||
""" Ziffers classes for the parsed notation """
|
|
||||||
from dataclasses import dataclass, field, replace
|
|
||||||
import itertools
|
|
||||||
import operator
|
|
||||||
import random
|
|
||||||
from .defaults import DEFAULT_OPTIONS
|
|
||||||
from .scale import note_from_pc, midi_to_pitch_class
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Meta:
|
|
||||||
"""Abstract class for all Ziffers items"""
|
|
||||||
|
|
||||||
kwargs: dict = field(default=None, repr=False)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.kwargs:
|
|
||||||
for key, val in self.kwargs.items():
|
|
||||||
setattr(self, key, val)
|
|
||||||
|
|
||||||
def update(self, new_values):
|
|
||||||
"""Update attributes from dict"""
|
|
||||||
for key, value in new_values.items():
|
|
||||||
if hasattr(self, key):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def update_new(self, new_values):
|
|
||||||
"""Updates new attributes from dict"""
|
|
||||||
for key, value in new_values.items():
|
|
||||||
if hasattr(self, key):
|
|
||||||
if getattr(self, key) is None:
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Item(Meta):
|
|
||||||
"""Class for all Ziffers text based items"""
|
|
||||||
|
|
||||||
text: str = field(default=None)
|
|
||||||
|
|
||||||
def get_item(self):
|
|
||||||
"""Return the item"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Whitespace:
|
|
||||||
"""Class for whitespace"""
|
|
||||||
|
|
||||||
text: str
|
|
||||||
item_type: str = field(default=None, repr=False, init=False)
|
|
||||||
|
|
||||||
def get_item(self):
|
|
||||||
"""Returns None. Used in filtering"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class DurationChange(Item):
|
|
||||||
"""Class for changing duration"""
|
|
||||||
|
|
||||||
value: float
|
|
||||||
key: str = field(default="duration", repr=False, init=False)
|
|
||||||
item_type: str = field(default="change", repr=False, init=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OctaveChange(Item):
|
|
||||||
"""Class for changing octave"""
|
|
||||||
|
|
||||||
value: int
|
|
||||||
key: str = field(default="octave", repr=False, init=False)
|
|
||||||
item_type: str = field(default="change", repr=False, init=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class OctaveAdd(Item):
|
|
||||||
"""Class for modifying octave"""
|
|
||||||
|
|
||||||
value: int
|
|
||||||
key: str = field(default="octave", repr=False, init=False)
|
|
||||||
item_type: str = field(default="add", repr=False, init=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Event(Item):
|
|
||||||
"""Abstract class for events with duration"""
|
|
||||||
|
|
||||||
duration: float = field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Pitch(Event):
|
|
||||||
"""Class for pitch in time"""
|
|
||||||
|
|
||||||
pitch_class: int
|
|
||||||
octave: int = field(default=None)
|
|
||||||
modifier: int = field(default=0)
|
|
||||||
note: int = field(default=None)
|
|
||||||
key: str = field(default=None)
|
|
||||||
scale: str | list = field(default=None)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
if self.text is None:
|
|
||||||
self.text = str(self.pitch_class)
|
|
||||||
self.update_note()
|
|
||||||
|
|
||||||
def update_note(self):
|
|
||||||
"""Update note if Key, Scale and Pitch-class is present"""
|
|
||||||
if (
|
|
||||||
(self.key is not None)
|
|
||||||
and (self.scale is not None)
|
|
||||||
and (self.pitch_class is not None)
|
|
||||||
and (self.note is None)
|
|
||||||
):
|
|
||||||
note = note_from_pc(
|
|
||||||
root=self.key,
|
|
||||||
pitch_class=self.pitch_class,
|
|
||||||
intervals=self.scale,
|
|
||||||
modifier=self.modifier if self.modifier is not None else 0,
|
|
||||||
octave=self.octave if self.octave is not None else 0,
|
|
||||||
)
|
|
||||||
self.set_note(note)
|
|
||||||
|
|
||||||
def set_note(self, note: int) -> int:
|
|
||||||
"""Sets a note for the pitch and returns the note.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
note (int): Midi note
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Returns the saved note
|
|
||||||
"""
|
|
||||||
self.note = note
|
|
||||||
return note
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument
|
|
||||||
def get_value(self) -> int:
|
|
||||||
"""Returns the pitch class
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Integer value for the pitch
|
|
||||||
"""
|
|
||||||
return self.pitch_class
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RandomPitch(Event):
|
|
||||||
"""Class for random pitch"""
|
|
||||||
|
|
||||||
pitch_class: int = field(default=None)
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument
|
|
||||||
def get_value(self) -> int:
|
|
||||||
"""Return random value
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Returns random pitch
|
|
||||||
"""
|
|
||||||
return self.pitch_class
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RandomPercent(Item):
|
|
||||||
"""Class for random percent"""
|
|
||||||
|
|
||||||
percent: float = field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Chord(Event):
|
|
||||||
"""Class for chords"""
|
|
||||||
|
|
||||||
pitch_classes: list[Pitch] = field(default=None)
|
|
||||||
notes: list[int] = field(default=None)
|
|
||||||
|
|
||||||
def set_notes(self, notes: list[int]):
|
|
||||||
"""Set notes to the class"""
|
|
||||||
self.notes = notes
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RomanNumeral(Event):
|
|
||||||
"""Class for roman numbers"""
|
|
||||||
|
|
||||||
value: str = field(default=None)
|
|
||||||
chord_type: str = field(default=None)
|
|
||||||
notes: list[int] = field(default_factory=[])
|
|
||||||
pitch_classes: list = None
|
|
||||||
|
|
||||||
def set_notes(self, chord_notes: list[int]):
|
|
||||||
"""Set notes to roman numeral
|
|
||||||
|
|
||||||
Args:
|
|
||||||
chord_notes (list[int]): List of notes in midi to be added
|
|
||||||
"""
|
|
||||||
self.notes = chord_notes
|
|
||||||
|
|
||||||
def set_pitch_classes(self, pitches: list[tuple]):
|
|
||||||
"""Set pitch classes to roman numeral
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pitches (list[tuple]): Pitch classes to be added
|
|
||||||
"""
|
|
||||||
if self.pitch_classes is None:
|
|
||||||
self.pitch_classes = []
|
|
||||||
for pitch in pitches:
|
|
||||||
self.pitch_classes.append(Pitch(**pitch))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Function(Event):
|
|
||||||
"""Class for functions"""
|
|
||||||
|
|
||||||
run: str = field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Sequence(Meta):
|
|
||||||
"""Class for sequences of items"""
|
|
||||||
|
|
||||||
values: list
|
|
||||||
text: str = field(default=None)
|
|
||||||
wrap_start: str = field(default=None, repr=False)
|
|
||||||
wrap_end: str = field(default=None, repr=False)
|
|
||||||
local_index: int = field(default=0, init=False)
|
|
||||||
evaluation: bool = field(default=False, init=False)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
self.text = self.__collect_text()
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
|
||||||
return self.values[index]
|
|
||||||
|
|
||||||
def update_values(self, new_values):
|
|
||||||
"""Update value attributes from dict"""
|
|
||||||
for key, value in new_values.items():
|
|
||||||
for obj in self.values:
|
|
||||||
if key != "text" and hasattr(obj, key):
|
|
||||||
setattr(obj, key, value)
|
|
||||||
|
|
||||||
def __collect_text(self) -> str:
|
|
||||||
"""Collect text value from values"""
|
|
||||||
text = "".join([val.text for val in self.values])
|
|
||||||
if self.wrap_start is not None:
|
|
||||||
text = self.wrap_start + text
|
|
||||||
if self.wrap_end is not None:
|
|
||||||
text = text + self.wrap_end
|
|
||||||
return text
|
|
||||||
|
|
||||||
def evaluate_tree(self, options=None):
|
|
||||||
"""Evaluates and flattens the Ziffers object tree"""
|
|
||||||
for item in self.values:
|
|
||||||
if isinstance(item, Sequence):
|
|
||||||
if item.evaluation:
|
|
||||||
yield from item.evaluate(options)
|
|
||||||
else:
|
|
||||||
yield from item.evaluate_tree(options)
|
|
||||||
else:
|
|
||||||
# Get value / generated value from the item
|
|
||||||
current = item.get_item()
|
|
||||||
# Ignore items that returns None
|
|
||||||
if current is not None:
|
|
||||||
if isinstance(current, (DurationChange, OctaveChange, OctaveAdd)):
|
|
||||||
options = self.__update_options(current, options)
|
|
||||||
else:
|
|
||||||
if set(("key", "scale")) <= options.keys():
|
|
||||||
if isinstance(current, Cyclic):
|
|
||||||
current = current.get_value()
|
|
||||||
if isinstance(current, (Pitch, RandomPitch, RandomInteger)):
|
|
||||||
current = self.__update_pitch(current, options)
|
|
||||||
elif isinstance(current, Chord):
|
|
||||||
current = self.__update_chord(current, options)
|
|
||||||
elif isinstance(current, RomanNumeral):
|
|
||||||
current = self.__create_chord_from_roman(
|
|
||||||
current, options
|
|
||||||
)
|
|
||||||
current.update_new(options)
|
|
||||||
yield current
|
|
||||||
|
|
||||||
def filter(self, keep: tuple):
|
|
||||||
"""Filter out items from sequence.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keep (tuple): Tuple describing classes to keep
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Sequence: Copy of the sequence with filtered values.
|
|
||||||
"""
|
|
||||||
return replace(
|
|
||||||
self, values=[item for item in self.values if isinstance(item, keep)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def __update_options(self, current: Item, options: dict) -> dict:
|
|
||||||
"""Update options based on current item
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (Item): Current item like Duration change, Octave change etc.
|
|
||||||
options (dict): Current options
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Updated options
|
|
||||||
"""
|
|
||||||
if current.item_type == "change": # Change options
|
|
||||||
options[current.key] = current.value
|
|
||||||
elif current.item_type == "add":
|
|
||||||
if current.key in options: # Add to existing value
|
|
||||||
options[current.key] += current.value
|
|
||||||
else: # Create value if not existing
|
|
||||||
options[current.key] = current.value
|
|
||||||
return options
|
|
||||||
|
|
||||||
def __update_pitch(self, current: Item, options: dict) -> dict:
|
|
||||||
"""Update pich based on optons
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (Item): _description_
|
|
||||||
options (dict): _description_
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: _description_
|
|
||||||
"""
|
|
||||||
if hasattr(current, "modifier"):
|
|
||||||
c_modifier = 0
|
|
||||||
elif "modifier" in options:
|
|
||||||
c_modifier = options["modifier"]
|
|
||||||
else:
|
|
||||||
c_modifier = 0
|
|
||||||
|
|
||||||
if hasattr(current, "octave"):
|
|
||||||
c_octave = 0
|
|
||||||
elif "octave" in options:
|
|
||||||
c_octave = options["octave"]
|
|
||||||
else:
|
|
||||||
c_octave = 0
|
|
||||||
|
|
||||||
note = note_from_pc(
|
|
||||||
root=options["key"],
|
|
||||||
pitch_class=current.get_value(),
|
|
||||||
intervals=options["scale"],
|
|
||||||
modifier=c_modifier,
|
|
||||||
octave=c_octave,
|
|
||||||
)
|
|
||||||
new_pitch = Pitch(
|
|
||||||
pitch_class=current.get_value(),
|
|
||||||
text=str(current.get_value()),
|
|
||||||
note=note,
|
|
||||||
octave=c_octave,
|
|
||||||
modifier=c_modifier,
|
|
||||||
kwargs=options,
|
|
||||||
)
|
|
||||||
return new_pitch
|
|
||||||
|
|
||||||
def __update_chord(self, current: Chord, options: dict) -> Chord:
|
|
||||||
"""Update chord based on options
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (Chord): Current chord object
|
|
||||||
options (dict): Options
|
|
||||||
re (bool, optional): Re-evaluation flag. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Chord: Returns updated chord
|
|
||||||
"""
|
|
||||||
pcs = current.pitch_classes
|
|
||||||
notes = [
|
|
||||||
pc.set_note(note_from_pc(options["key"], pc.pitch_class, options["scale"]))
|
|
||||||
for pc in pcs
|
|
||||||
]
|
|
||||||
current.set_notes(notes)
|
|
||||||
return current
|
|
||||||
|
|
||||||
def __create_chord_from_roman(self, current: RomanNumeral, options: dict) -> Chord:
|
|
||||||
"""Create chord fom roman numeral
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (RomanNumeral): Current roman numeral
|
|
||||||
options (dict): Options
|
|
||||||
re (bool, optional): Re-evaluation flag. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Chord: New chord created from Roman numeral
|
|
||||||
"""
|
|
||||||
key = options["key"]
|
|
||||||
scale = options["scale"]
|
|
||||||
pitches = [midi_to_pitch_class(note, key, scale) for note in current.notes]
|
|
||||||
chord_notes = [
|
|
||||||
note_from_pc(
|
|
||||||
root=key,
|
|
||||||
pitch_class=pitch,
|
|
||||||
intervals=scale,
|
|
||||||
modifier=current.modifier if hasattr(current, "modifier") else 0,
|
|
||||||
)
|
|
||||||
for pitch in pitches
|
|
||||||
]
|
|
||||||
chord = Chord(text="".join(pitches), pitch_classes=pitches, notes=chord_notes)
|
|
||||||
return chord
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Ziffers(Sequence):
|
|
||||||
"""Main class for holding options and the current state"""
|
|
||||||
|
|
||||||
options: dict = field(default_factory=DEFAULT_OPTIONS)
|
|
||||||
loop_i: int = 0
|
|
||||||
iterator = None
|
|
||||||
current: Item = field(default=None)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
self.current = next(self.iterator)
|
|
||||||
self.loop_i += 1
|
|
||||||
return self.current
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, dangerous-default-value
|
|
||||||
def init_opts(self, options=None):
|
|
||||||
"""Evaluate the Ziffers tree using the options"""
|
|
||||||
self.options.update(DEFAULT_OPTIONS)
|
|
||||||
if options:
|
|
||||||
self.options.update(options)
|
|
||||||
else:
|
|
||||||
self.options = DEFAULT_OPTIONS
|
|
||||||
|
|
||||||
self.iterator = iter(self.evaluate_tree(self.options))
|
|
||||||
|
|
||||||
def re_eval(self, options=None):
|
|
||||||
"""Re-evaluate the iterator"""
|
|
||||||
self.options.update(DEFAULT_OPTIONS)
|
|
||||||
if options:
|
|
||||||
self.options.update(options)
|
|
||||||
self.iterator = iter(self.evaluate_tree(self.options))
|
|
||||||
|
|
||||||
def get_list(self):
|
|
||||||
"""Return list"""
|
|
||||||
return list(self)
|
|
||||||
|
|
||||||
def take(self, num: int) -> list[Pitch]:
|
|
||||||
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
num (int): Number of pitch classes to take from the sequence
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of pitch class items
|
|
||||||
"""
|
|
||||||
return list(itertools.islice(itertools.cycle(self), num))
|
|
||||||
|
|
||||||
def loop(self) -> iter:
|
|
||||||
"""Return cyclic loop"""
|
|
||||||
return itertools.cycle(iter(self))
|
|
||||||
|
|
||||||
def set_defaults(self, options: dict):
|
|
||||||
"""Sets options for the parser
|
|
||||||
|
|
||||||
Args:
|
|
||||||
options (dict): Options as a dict
|
|
||||||
"""
|
|
||||||
self.options = DEFAULT_OPTIONS | options
|
|
||||||
|
|
||||||
# TODO: Refactor these
|
|
||||||
def pitch_classes(self) -> list[int]:
|
|
||||||
"""Return list of pitch classes as ints"""
|
|
||||||
return [val.pitch_class for val in self.values if isinstance(val, Pitch)]
|
|
||||||
|
|
||||||
def durations(self) -> list[float]:
|
|
||||||
"""Return list of pitch durations as floats"""
|
|
||||||
return [val.dur for val in self.values if isinstance(val, Pitch)]
|
|
||||||
|
|
||||||
def pairs(self) -> list[tuple]:
|
|
||||||
"""Return list of pitches and durations"""
|
|
||||||
return [
|
|
||||||
(val.pitch_class, val.dur) for val in self.values if isinstance(val, Pitch)
|
|
||||||
]
|
|
||||||
|
|
||||||
def octaves(self) -> list[int]:
|
|
||||||
"""Return list of octaves"""
|
|
||||||
return [val.octave for val in self.values if isinstance(val, Pitch)]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class ListSequence(Sequence):
|
|
||||||
"""Class for Ziffers list sequences"""
|
|
||||||
|
|
||||||
wrap_start: str = field(default="(", repr=False)
|
|
||||||
wrap_end: str = field(default=")", repr=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Integer(Item):
|
|
||||||
"""Class for integers"""
|
|
||||||
|
|
||||||
value: int
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument
|
|
||||||
def get_value(self):
|
|
||||||
"""Return value of the integer"""
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RandomInteger(Item):
|
|
||||||
"""Class for random integer"""
|
|
||||||
|
|
||||||
min: int
|
|
||||||
max: int
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
if self.min > self.max:
|
|
||||||
new_max = self.min
|
|
||||||
self.min = self.max
|
|
||||||
self.max = new_max
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument
|
|
||||||
def get_value(self):
|
|
||||||
"""Evaluate the random value for the generator"""
|
|
||||||
return random.randint(self.min, self.max)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RepeatedListSequence(Sequence):
|
|
||||||
"""Class for Ziffers list sequences"""
|
|
||||||
|
|
||||||
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
|
||||||
wrap_start: str = field(default="(:", repr=False)
|
|
||||||
wrap_end: str = field(default=":)", repr=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Subdivision(Item):
|
|
||||||
"""Class for subdivisions"""
|
|
||||||
|
|
||||||
values: list[Event]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Cyclic(Item):
|
|
||||||
"""Class for cyclic sequences"""
|
|
||||||
|
|
||||||
values: list
|
|
||||||
cycle: int = 0
|
|
||||||
wrap_start: str = field(default="<", repr=False)
|
|
||||||
wrap_end: str = field(default=">", repr=False)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
self.text = self.__collect_text()
|
|
||||||
self.values = [val for val in self.values if not isinstance(val, Whitespace)]
|
|
||||||
|
|
||||||
def __collect_text(self) -> str:
|
|
||||||
"""Collect text value from values"""
|
|
||||||
text = "".join([val.text for val in self.values])
|
|
||||||
if self.wrap_start is not None:
|
|
||||||
text = self.wrap_start + text
|
|
||||||
if self.wrap_end is not None:
|
|
||||||
text = text + self.wrap_end
|
|
||||||
return text
|
|
||||||
|
|
||||||
def get_value(self):
|
|
||||||
"""Get the value for the current cycle"""
|
|
||||||
value = self.values[self.cycle % len(self.values)]
|
|
||||||
self.cycle += 1
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Range(Item):
|
|
||||||
"""Class for range"""
|
|
||||||
|
|
||||||
start: int = field(default=None)
|
|
||||||
end: int = field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Operator(Item):
|
|
||||||
"""Class for math operators"""
|
|
||||||
|
|
||||||
value: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class ListOperation(Sequence):
|
|
||||||
"""Class for list operations"""
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
self.evaluation = True
|
|
||||||
|
|
||||||
def filter_operation(self, values):
|
|
||||||
"""Filtering for the operation elements"""
|
|
||||||
keep = (Sequence, Event, RandomInteger, Integer, Cyclic)
|
|
||||||
for item in values:
|
|
||||||
if isinstance(item, Sequence):
|
|
||||||
yield item.filter(keep)
|
|
||||||
elif isinstance(item, keep):
|
|
||||||
yield item
|
|
||||||
|
|
||||||
def evaluate(self, options: dict):
|
|
||||||
"""Evaluates the operation"""
|
|
||||||
operators = self.values[1::2] # Fetch every second operator element
|
|
||||||
values = self.values[::2] # Fetch every second list element
|
|
||||||
values = list(self.filter_operation(values)) # Filter out crap
|
|
||||||
result = values[0] # Start results with the first array
|
|
||||||
for i, operand in enumerate(operators):
|
|
||||||
operation = operand.value
|
|
||||||
right_value = values[i + 1]
|
|
||||||
if isinstance(right_value, Sequence):
|
|
||||||
result = [
|
|
||||||
Pitch(
|
|
||||||
pitch_class=operation(x.get_value(), y.get_value()),
|
|
||||||
kwargs=options,
|
|
||||||
)
|
|
||||||
for x in result
|
|
||||||
for y in right_value
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
result = [
|
|
||||||
Pitch(
|
|
||||||
pitch_class=operation(x.get_value(), right_value.get_value()),
|
|
||||||
kwargs=options,
|
|
||||||
)
|
|
||||||
for x in result
|
|
||||||
]
|
|
||||||
return Sequence(values=result)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Operation(Item):
|
|
||||||
"""Class for lisp-like operations: (+ 1 2 3) etc."""
|
|
||||||
|
|
||||||
values: list
|
|
||||||
operator: operator
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Eval(Sequence):
|
|
||||||
"""Class for evaluation notation"""
|
|
||||||
|
|
||||||
result: ... = None
|
|
||||||
wrap_start: str = field(default="{", repr=False)
|
|
||||||
wrap_end: str = field(default="}", repr=False)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
super().__post_init__()
|
|
||||||
self.result = eval(self.text)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Atom(Item):
|
|
||||||
"""Class for evaluable atoms"""
|
|
||||||
|
|
||||||
value: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Euclid(Item):
|
|
||||||
"""Class for euclidean cycles"""
|
|
||||||
|
|
||||||
pulses: int
|
|
||||||
length: int
|
|
||||||
onset: list
|
|
||||||
offset: list = field(default=None)
|
|
||||||
rotate: int = field(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class RepeatedSequence(Sequence):
|
|
||||||
"""Class for repeats"""
|
|
||||||
|
|
||||||
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
|
||||||
wrap_start: str = field(default="[:", repr=False)
|
|
||||||
wrap_end: str = field(default=":]", repr=False)
|
|
||||||
3
ziffers/classes/__init__.py
Normal file
3
ziffers/classes/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .items import *
|
||||||
|
from .root import *
|
||||||
|
from .sequences import *
|
||||||
645
ziffers/classes/items.py
Normal file
645
ziffers/classes/items.py
Normal file
@ -0,0 +1,645 @@
|
|||||||
|
""" Ziffers item classes """
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from math import floor
|
||||||
|
import random
|
||||||
|
from ..scale import (
|
||||||
|
note_from_pc,
|
||||||
|
midi_to_pitch_class,
|
||||||
|
midi_to_freq,
|
||||||
|
get_scale_length,
|
||||||
|
chord_from_degree,
|
||||||
|
)
|
||||||
|
from ..common import repeat_text
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Meta:
|
||||||
|
"""Abstract class for all Ziffers items"""
|
||||||
|
|
||||||
|
kwargs: dict = field(default=None, repr=False)
|
||||||
|
local_options: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.kwargs:
|
||||||
|
self.update_options(self.kwargs)
|
||||||
|
|
||||||
|
def replace_options(self, new_values):
|
||||||
|
"""Replaces attribute values from dict"""
|
||||||
|
for key, value in new_values.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def update_options(self, options):
|
||||||
|
"""Updates attribute values only if value is None"""
|
||||||
|
merged_options = self.local_options | options
|
||||||
|
for key, value in merged_options.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
if key == "octave":
|
||||||
|
local_value = self.local_options.get("octave", False)
|
||||||
|
oct_change = self.local_options.get("octave_change", False)
|
||||||
|
if oct_change:
|
||||||
|
setattr(self, key, oct_change)
|
||||||
|
elif local_value:
|
||||||
|
setattr(self, key, value + local_value)
|
||||||
|
elif getattr(self, key) is None:
|
||||||
|
setattr(self, key, value)
|
||||||
|
elif getattr(self, key) is None:
|
||||||
|
local_value = self.local_options.get(key, False)
|
||||||
|
if local_value:
|
||||||
|
value = local_value
|
||||||
|
setattr(self, key, value)
|
||||||
|
if key == "duration":
|
||||||
|
setattr(self, "beat", value * 4)
|
||||||
|
|
||||||
|
def dict(self):
|
||||||
|
"""Returns safe dict from the dataclass"""
|
||||||
|
return {k: str(v) for k, v in asdict(self).items()}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Item(Meta):
|
||||||
|
"""Class for all Ziffers text based items"""
|
||||||
|
|
||||||
|
text: str = field(default=None)
|
||||||
|
measure: int = field(default=0, init=False)
|
||||||
|
|
||||||
|
def get_updated_item(self, options: dict):
|
||||||
|
"""Get updated item with replaced options
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options (dict): Options as a dict
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Item: Returns updated item
|
||||||
|
"""
|
||||||
|
self.replace_options(options)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_options(self) -> dict:
|
||||||
|
"""Return local options from item
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Options as a dict
|
||||||
|
"""
|
||||||
|
keys = ["octave", "modifier", "key", "scale", "duration"]
|
||||||
|
return {key: getattr(self, key) for key in keys if hasattr(self, key)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Whitespace:
|
||||||
|
"""Class for whitespace"""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Modification(Item):
|
||||||
|
"""Superclass for pitch modifications"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: ...
|
||||||
|
|
||||||
|
def as_options(self):
|
||||||
|
"""Return modification as a dict"""
|
||||||
|
return {self.key: self.value}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class DurationChange(Modification):
|
||||||
|
"""Class for changing duration"""
|
||||||
|
|
||||||
|
value: float
|
||||||
|
key: str = field(default="duration", repr=False, init=False)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OctaveChange(Modification):
|
||||||
|
"""Class for changing octave"""
|
||||||
|
|
||||||
|
value: int
|
||||||
|
key: str = field(default="octave", repr=False, init=False)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class OctaveAdd(Modification):
|
||||||
|
"""Class for modifying octave"""
|
||||||
|
|
||||||
|
value: int
|
||||||
|
key: str = field(default="octave", repr=False, init=False)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Event(Item):
|
||||||
|
"""Abstract class for events with duration"""
|
||||||
|
|
||||||
|
duration: float = field(default=None)
|
||||||
|
beat: float = field(default=None)
|
||||||
|
|
||||||
|
def get_duration(self):
|
||||||
|
"""Getter for duration"""
|
||||||
|
return self.duration
|
||||||
|
|
||||||
|
def get_beat(self):
|
||||||
|
"""Getter for beat"""
|
||||||
|
return self.beat
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Rest(Event):
|
||||||
|
"""Class for rests"""
|
||||||
|
|
||||||
|
def get_note(self):
|
||||||
|
"""Getter for note"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_freq(self):
|
||||||
|
"""Getter for freq"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_octave(self):
|
||||||
|
"""Getter for octave"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_pitch_class(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Measure(Item):
|
||||||
|
"""Class for measures/bars. Used to reset default options."""
|
||||||
|
|
||||||
|
text: str = field(default="|", init=False)
|
||||||
|
|
||||||
|
def reset_options(self, options: dict):
|
||||||
|
"""Reset options when measure changes"""
|
||||||
|
next_measure = options.get("measure", 0) + 1
|
||||||
|
start_options = options["start_options"].copy()
|
||||||
|
options.clear()
|
||||||
|
options.update(start_options)
|
||||||
|
options["measure"] = next_measure
|
||||||
|
options["start_options"] = start_options.copy()
|
||||||
|
self.measure = next_measure
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Pitch(Event):
|
||||||
|
"""Class for pitch in time"""
|
||||||
|
|
||||||
|
pitch_class: int
|
||||||
|
pitch_bend: int = field(default=None)
|
||||||
|
octave: int = field(default=None)
|
||||||
|
modifier: int = field(default=None)
|
||||||
|
note: int = field(default=None)
|
||||||
|
key: str = field(default=None)
|
||||||
|
scale: str | list = field(default=None)
|
||||||
|
freq: float = field(default=None)
|
||||||
|
degrees: bool = field(default=None)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
if self.text is None:
|
||||||
|
self.text = str(self.pitch_class)
|
||||||
|
self.update_note()
|
||||||
|
# self._update_text()
|
||||||
|
|
||||||
|
def _update_text(self):
|
||||||
|
if self.octave is not None:
|
||||||
|
self.text = repeat_text("^", "_", self.octave) + self.text
|
||||||
|
if self.modifier is not None:
|
||||||
|
self.text = repeat_text("#", "b", self.modifier) + self.text
|
||||||
|
|
||||||
|
def get_note(self):
|
||||||
|
"""Getter for note"""
|
||||||
|
return self.note
|
||||||
|
|
||||||
|
def get_freq(self):
|
||||||
|
"""Getter for freq"""
|
||||||
|
return self.freq
|
||||||
|
|
||||||
|
def get_octave(self):
|
||||||
|
"""Getter for octave"""
|
||||||
|
return self.octave
|
||||||
|
|
||||||
|
def get_beat(self):
|
||||||
|
"""Getter for beat"""
|
||||||
|
return self.beat
|
||||||
|
|
||||||
|
def get_pitch_class(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return self.pitch_class
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return self.pitch_bend
|
||||||
|
|
||||||
|
def update_note(self, force: bool = False):
|
||||||
|
"""Update note if Key, Scale and Pitch-class are present"""
|
||||||
|
if (
|
||||||
|
(self.key is not None)
|
||||||
|
and (self.scale is not None)
|
||||||
|
and (self.pitch_class is not None)
|
||||||
|
and (self.note is None or force)
|
||||||
|
):
|
||||||
|
note, pitch_bend = note_from_pc(
|
||||||
|
root=self.key,
|
||||||
|
pitch_class=self.pitch_class,
|
||||||
|
intervals=self.scale,
|
||||||
|
modifier=self.modifier if self.modifier is not None else 0,
|
||||||
|
octave=self.octave if self.octave is not None else 0,
|
||||||
|
degrees=self.degrees
|
||||||
|
)
|
||||||
|
self.pitch_bend = pitch_bend
|
||||||
|
self.freq = midi_to_freq(note)
|
||||||
|
self.note = floor(note)
|
||||||
|
if self.duration is not None:
|
||||||
|
self.beat = self.duration * 4
|
||||||
|
|
||||||
|
def check_note(self, options: dict):
|
||||||
|
"""Check for note modification"""
|
||||||
|
if "key" in options and self.key is not options["key"]:
|
||||||
|
self.key = options["key"]
|
||||||
|
edit = True
|
||||||
|
if "scale" in options and self.scale is not options["scale"]:
|
||||||
|
self.scale = options["scale"]
|
||||||
|
edit = True
|
||||||
|
if edit:
|
||||||
|
self.update_note(True)
|
||||||
|
|
||||||
|
def set_note(self, note: int) -> int:
|
||||||
|
"""Sets a note for the pitch and returns the note.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
note (int): Midi note
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Returns the saved note
|
||||||
|
"""
|
||||||
|
self.note = note
|
||||||
|
return note
|
||||||
|
|
||||||
|
def set_freq(self, freq: float):
|
||||||
|
"""Set frequency for the pitch object"""
|
||||||
|
self.freq = freq
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, unused-argument
|
||||||
|
def get_value(self, options) -> int:
|
||||||
|
"""Returns the pitch class
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Integer value for the pitch
|
||||||
|
"""
|
||||||
|
return self.pitch_class
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RandomPitch(Event):
|
||||||
|
"""Class for random pitch"""
|
||||||
|
|
||||||
|
pitch_class: int = field(default=None)
|
||||||
|
|
||||||
|
def get_value(self, options: dict) -> int:
|
||||||
|
"""Return random value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Returns random pitch
|
||||||
|
"""
|
||||||
|
if options:
|
||||||
|
scale = options["scale"]
|
||||||
|
if isinstance(scale, str):
|
||||||
|
scale_length = get_scale_length(options.get("scale", "Major"))
|
||||||
|
else:
|
||||||
|
scale_length = len(scale)
|
||||||
|
else:
|
||||||
|
scale_length = 9
|
||||||
|
|
||||||
|
return random.randint(0, scale_length)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RandomPercent(Item):
|
||||||
|
"""Class for random percent"""
|
||||||
|
|
||||||
|
percent: float = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Chord(Event):
|
||||||
|
"""Class for chords"""
|
||||||
|
|
||||||
|
pitch_classes: list[Pitch] = field(default=None)
|
||||||
|
notes: list[int] = field(default=None)
|
||||||
|
inversions: int = field(default=None)
|
||||||
|
pitches: list[int] = field(default=None, init=False)
|
||||||
|
pitch_bends: list[int] = field(default=None, init=False)
|
||||||
|
freqs: list[float] = field(default=None, init=False)
|
||||||
|
octaves: list[int] = field(default=None, init=False)
|
||||||
|
durations: list[float] = field(default=None, init=False)
|
||||||
|
beats: list[float] = field(default=None, init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.inversions is not None:
|
||||||
|
self.invert(self.inversions)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def note(self):
|
||||||
|
"""Synonym for notes"""
|
||||||
|
return self.notes
|
||||||
|
|
||||||
|
def set_notes(self, notes: list[int]):
|
||||||
|
"""Set notes to the class"""
|
||||||
|
self.notes = notes
|
||||||
|
|
||||||
|
def get_note(self):
|
||||||
|
"""Getter for notes"""
|
||||||
|
return self.notes
|
||||||
|
|
||||||
|
def get_freq(self):
|
||||||
|
"""Getter for freqs"""
|
||||||
|
return self.freqs
|
||||||
|
|
||||||
|
def get_octave(self):
|
||||||
|
"""Getter for octaves"""
|
||||||
|
return self.octaves
|
||||||
|
|
||||||
|
def get_beat(self):
|
||||||
|
"""Getter for beats"""
|
||||||
|
return self.beats
|
||||||
|
|
||||||
|
def get_pitch_class(self):
|
||||||
|
"""Getter for pitches"""
|
||||||
|
return self.pitches
|
||||||
|
|
||||||
|
def get_pitch_bend(self):
|
||||||
|
"""Getter for pitche"""
|
||||||
|
return self.pitch_bends
|
||||||
|
|
||||||
|
def get_duration(self):
|
||||||
|
"""Getter for durations"""
|
||||||
|
return self.durations
|
||||||
|
|
||||||
|
def invert(self, value: int):
|
||||||
|
"""Chord inversion"""
|
||||||
|
new_pitches = (
|
||||||
|
list(reversed(self.pitch_classes)) if value < 0 else self.pitch_classes
|
||||||
|
)
|
||||||
|
for _ in range(abs(value)):
|
||||||
|
new_pitch = new_pitches[_ % len(new_pitches)]
|
||||||
|
if not new_pitch.local_options.get("octave"):
|
||||||
|
new_pitch.local_options["octave"] = 0
|
||||||
|
new_pitch.local_options["octave"] += -1 if value <= 0 else 1
|
||||||
|
|
||||||
|
self.pitch_classes = new_pitches
|
||||||
|
|
||||||
|
def update_notes(self, options=None):
|
||||||
|
"""Update notes"""
|
||||||
|
pitches, pitch_bends, notes, freqs, octaves, durations, beats = ([] for _ in range(7))
|
||||||
|
|
||||||
|
# Update notes
|
||||||
|
for pitch in self.pitch_classes:
|
||||||
|
if options is not None:
|
||||||
|
pitch.update_options(options)
|
||||||
|
pitch.update_note(True)
|
||||||
|
|
||||||
|
# Sort by generated notes
|
||||||
|
self.pitch_classes = sorted(self.pitch_classes, key=lambda x: x.note)
|
||||||
|
|
||||||
|
# Create helper lists
|
||||||
|
for pitch in self.pitch_classes:
|
||||||
|
pitches.append(pitch.pitch_class)
|
||||||
|
pitch_bends.append(pitch.pitch_bend)
|
||||||
|
notes.append(pitch.note)
|
||||||
|
freqs.append(pitch.freq)
|
||||||
|
octaves.append(pitch.octave)
|
||||||
|
durations.append(pitch.duration)
|
||||||
|
beats.append(pitch.beat)
|
||||||
|
|
||||||
|
self.pitches = pitches
|
||||||
|
self.pitch_bends = pitch_bends
|
||||||
|
self.notes = notes
|
||||||
|
self.freqs = freqs
|
||||||
|
self.octaves = octaves
|
||||||
|
self.durations = durations
|
||||||
|
self.duration = durations[0]
|
||||||
|
self.beats = beats
|
||||||
|
self.text = "".join([val.text for val in self.pitch_classes])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RomanNumeral(Event):
|
||||||
|
"""Class for roman numbers"""
|
||||||
|
|
||||||
|
value: str = field(default=None)
|
||||||
|
chord_type: str = field(default=None)
|
||||||
|
notes: list[int] = field(default=None, init=False)
|
||||||
|
pitch_classes: list = field(default=None, init=False)
|
||||||
|
inversions: int = field(default=None)
|
||||||
|
evaluated_chord: Chord = None
|
||||||
|
|
||||||
|
def set_notes(self, chord_notes: list[int]):
|
||||||
|
"""Set notes to roman numeral
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chord_notes (list[int]): List of notes in midi to be added
|
||||||
|
"""
|
||||||
|
self.notes = chord_notes
|
||||||
|
|
||||||
|
def set_pitch_classes(self, pitches: list[tuple]):
|
||||||
|
"""Set pitch classes to roman numeral
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pitches (list[tuple]): Pitch classes to be added
|
||||||
|
"""
|
||||||
|
if self.pitch_classes is None:
|
||||||
|
self.pitch_classes = []
|
||||||
|
for pitch in pitches:
|
||||||
|
self.pitch_classes.append(Pitch(**pitch))
|
||||||
|
|
||||||
|
def evaluate_chord(self, options: dict) -> Chord:
|
||||||
|
"""Create chord fom roman numeral"""
|
||||||
|
key = options["key"]
|
||||||
|
scale = options["scale"]
|
||||||
|
pitch_text = ""
|
||||||
|
pitch_classes = []
|
||||||
|
self.notes = chord_from_degree(
|
||||||
|
self.value, self.chord_type, options["scale"], options["key"]
|
||||||
|
)
|
||||||
|
for note in self.notes:
|
||||||
|
pitch_dict = midi_to_pitch_class(note, key, scale)
|
||||||
|
pitch_classes.append(
|
||||||
|
Pitch(
|
||||||
|
pitch_class=pitch_dict["pitch_class"],
|
||||||
|
note=note,
|
||||||
|
freq=midi_to_freq(note),
|
||||||
|
kwargs=(options | pitch_dict),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pitch_text += pitch_dict["text"]
|
||||||
|
|
||||||
|
chord = Chord(
|
||||||
|
text=pitch_text,
|
||||||
|
pitch_classes=pitch_classes,
|
||||||
|
duration=options["duration"],
|
||||||
|
notes=self.notes,
|
||||||
|
kwargs=options,
|
||||||
|
inversions=self.inversions,
|
||||||
|
)
|
||||||
|
|
||||||
|
chord.update_notes(options)
|
||||||
|
|
||||||
|
self.evaluated_chord = chord
|
||||||
|
|
||||||
|
return chord
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Function(Event):
|
||||||
|
"""Class for functions"""
|
||||||
|
|
||||||
|
run: ... = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class FunctionList(Event):
|
||||||
|
"""Class for functions"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class VariableAssignment(Item):
|
||||||
|
"""Class for defining variables"""
|
||||||
|
|
||||||
|
variable: str
|
||||||
|
value: Item
|
||||||
|
pre_eval: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Variable(Event):
|
||||||
|
"""Class for using variables"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Sample(Event):
|
||||||
|
"""Class for samples"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class SampleList(Event):
|
||||||
|
"""Class for using multiple samples"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class VariableList(Item):
|
||||||
|
"""Class for using variables"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Integer(Item):
|
||||||
|
"""Class for integers"""
|
||||||
|
|
||||||
|
value: int
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, unused-argument
|
||||||
|
def get_value(self, options):
|
||||||
|
"""Return value of the integer"""
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RandomInteger(Item):
|
||||||
|
"""Class for random integer"""
|
||||||
|
|
||||||
|
min: int
|
||||||
|
max: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
if self.min > self.max:
|
||||||
|
new_max = self.min
|
||||||
|
self.min = self.max
|
||||||
|
self.max = new_max
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, unused-argument
|
||||||
|
def get_value(self, options: dict = None):
|
||||||
|
"""Evaluate the random value for the generator"""
|
||||||
|
return random.randint(self.min, self.max)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Cyclic(Item):
|
||||||
|
"""Class for cyclic sequences"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
cycle: int = 0
|
||||||
|
wrap_start: str = field(default="<", repr=False)
|
||||||
|
wrap_end: str = field(default=">", repr=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.text = self.__collect_text()
|
||||||
|
self.values = [val for val in self.values if not isinstance(val, Whitespace)]
|
||||||
|
|
||||||
|
def __collect_text(self) -> str:
|
||||||
|
"""Collect text value from values"""
|
||||||
|
text = "".join([val.text for val in self.values])
|
||||||
|
if self.wrap_start is not None:
|
||||||
|
text = self.wrap_start + text
|
||||||
|
if self.wrap_end is not None:
|
||||||
|
text = text + self.wrap_end
|
||||||
|
return text
|
||||||
|
|
||||||
|
def get_value(self, options=None):
|
||||||
|
"""Get the value for the current cycle"""
|
||||||
|
value = self.values[self.cycle % len(self.values)]
|
||||||
|
self.cycle += 1
|
||||||
|
if options:
|
||||||
|
value.update_options(options)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Range(Item):
|
||||||
|
"""Class for range"""
|
||||||
|
|
||||||
|
start: int = field(default=None)
|
||||||
|
end: int = field(default=None)
|
||||||
|
|
||||||
|
def evaluate(self, options):
|
||||||
|
"""Evaluates range and generates a generator of Pitches"""
|
||||||
|
merged_options = options | self.local_options
|
||||||
|
if options["octave"]:
|
||||||
|
merged_options["octave"] += options["octave"]
|
||||||
|
if self.start < self.end:
|
||||||
|
for i in range(self.start, self.end + 1):
|
||||||
|
yield Pitch(pitch_class=i, kwargs=merged_options)
|
||||||
|
elif self.start > self.end:
|
||||||
|
for i in reversed(range(self.end, self.start + 1)):
|
||||||
|
yield Pitch(pitch_class=i, kwargs=merged_options)
|
||||||
|
else:
|
||||||
|
yield Pitch(pitch_class=self.start, kwargs=merged_options)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Operator(Item):
|
||||||
|
"""Class for math operators"""
|
||||||
|
|
||||||
|
value: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Atom(Item):
|
||||||
|
"""Class for evaluable atoms"""
|
||||||
|
|
||||||
|
value: ...
|
||||||
205
ziffers/classes/root.py
Normal file
205
ziffers/classes/root.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""Root class for Ziffers object"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from itertools import islice, cycle
|
||||||
|
from ..defaults import DEFAULT_OPTIONS
|
||||||
|
from .items import Item, Pitch, Chord, Event, Rest
|
||||||
|
from .sequences import Sequence, Subdivision
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Ziffers(Sequence):
|
||||||
|
"""Main class for holding options and the current state"""
|
||||||
|
|
||||||
|
options: dict = field(default_factory=DEFAULT_OPTIONS.copy())
|
||||||
|
start_options: dict = None
|
||||||
|
loop_i: int = field(default=0, init=False)
|
||||||
|
cycle_i: int = field(default=0, init=False)
|
||||||
|
iterator = None
|
||||||
|
current: Item = field(default=None)
|
||||||
|
cycle_length: int = field(default=0, init=False)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
self.loop_i = index % self.cycle_length
|
||||||
|
new_cycle = index // self.cycle_length
|
||||||
|
# Re-evaluate if the prior loop has ended
|
||||||
|
if new_cycle > self.cycle_i or new_cycle < self.cycle_i:
|
||||||
|
self.re_eval()
|
||||||
|
self.cycle_i = new_cycle
|
||||||
|
self.cycle_length = len(self.evaluated_values)
|
||||||
|
self.loop_i = index % self.cycle_length
|
||||||
|
return self.evaluated_values[self.loop_i]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.evaluated_values)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
self.current = next(self.iterator)
|
||||||
|
self.loop_i += 1
|
||||||
|
return self.current
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, dangerous-default-value
|
||||||
|
def init_opts(self, options=None):
|
||||||
|
"""Evaluate the Ziffers tree using the options"""
|
||||||
|
self.options.update(DEFAULT_OPTIONS.copy())
|
||||||
|
if options:
|
||||||
|
self.options.update(options)
|
||||||
|
else:
|
||||||
|
self.options = DEFAULT_OPTIONS.copy()
|
||||||
|
|
||||||
|
self.start_options = self.options.copy()
|
||||||
|
self.options["start_options"] = self.start_options
|
||||||
|
self.init_tree(self.options)
|
||||||
|
|
||||||
|
def re_eval(self):
|
||||||
|
"""Re-evaluate the iterator"""
|
||||||
|
self.options = self.start_options.copy()
|
||||||
|
self.options["start_options"] = self.start_options
|
||||||
|
self.init_tree(self.options)
|
||||||
|
|
||||||
|
def init_tree(self, options):
|
||||||
|
"""Initialize evaluated values and perform post-evaluation"""
|
||||||
|
self.evaluated_values = list(self.evaluate_tree(options))
|
||||||
|
self.evaluated_values = list(self.post_evaluation())
|
||||||
|
self.iterator = iter(self.evaluated_values)
|
||||||
|
self.cycle_length = len(self.evaluated_values)
|
||||||
|
|
||||||
|
def post_evaluation(self):
|
||||||
|
"""Post-evaluation performs evaluation that can only be done after initial evaluation"""
|
||||||
|
for item in self.evaluated_values:
|
||||||
|
if isinstance(item, Subdivision):
|
||||||
|
yield from item.evaluate_durations()
|
||||||
|
else:
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def get_list(self):
|
||||||
|
"""Return list"""
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
def take(self, num: int) -> list[Pitch]:
|
||||||
|
"""Take number of pitch classes from the parsed sequence. Cycles from the beginning.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num (int): Number of pitch classes to take from the sequence
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of pitch class items
|
||||||
|
"""
|
||||||
|
return list(islice(cycle(self), num))
|
||||||
|
|
||||||
|
def loop(self) -> iter:
|
||||||
|
"""Return cyclic loop"""
|
||||||
|
return cycle(iter(self))
|
||||||
|
|
||||||
|
def set_defaults(self, options: dict):
|
||||||
|
"""Sets options for the parser
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options (dict): Options as a dict
|
||||||
|
"""
|
||||||
|
self.options = DEFAULT_OPTIONS.copy() | options
|
||||||
|
|
||||||
|
def pitch_classes(self) -> list[int]:
|
||||||
|
"""Return list of pitch classes as ints"""
|
||||||
|
return [
|
||||||
|
val.get_pitch_class()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
|
]
|
||||||
|
|
||||||
|
def pitch_bends(self) -> list[int]:
|
||||||
|
"""Return list of pitch bend values"""
|
||||||
|
return [
|
||||||
|
val.get_pitch_bend()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
|
]
|
||||||
|
|
||||||
|
def notes(self) -> list[int]:
|
||||||
|
"""Return list of midi notes"""
|
||||||
|
return [
|
||||||
|
val.get_note()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
|
]
|
||||||
|
|
||||||
|
def durations(self) -> list[float]:
|
||||||
|
"""Return list of pitch durations as floats"""
|
||||||
|
return [
|
||||||
|
val.get_duration()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, Event)
|
||||||
|
]
|
||||||
|
|
||||||
|
def total_duration(self) -> float:
|
||||||
|
"""Return total duration"""
|
||||||
|
return sum(
|
||||||
|
[val.duration for val in self.evaluated_values if isinstance(val, Event)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def total_beats(self) -> float:
|
||||||
|
"""Return total beats"""
|
||||||
|
return sum(self.beats())
|
||||||
|
|
||||||
|
def beats(self) -> list[float]:
|
||||||
|
"""Return list of pitch durations as floats"""
|
||||||
|
return [
|
||||||
|
val.get_beat() for val in self.evaluated_values if isinstance(val, Event)
|
||||||
|
]
|
||||||
|
|
||||||
|
def pairs(self) -> list[tuple]:
|
||||||
|
"""Return list of pitches and durations"""
|
||||||
|
return [
|
||||||
|
[val.get_pitch_class(), val.get_duration()]
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, Pitch) or isinstance(val, Chord) or isinstance(val, Rest)
|
||||||
|
]
|
||||||
|
|
||||||
|
def freq_pairs(self) -> list[tuple]:
|
||||||
|
"""Return list of pitches in freq and durations"""
|
||||||
|
return [
|
||||||
|
[val.get_freq(), val.get_duration()]
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, Pitch) or isinstance(val, Chord) or isinstance(val, Rest)
|
||||||
|
]
|
||||||
|
|
||||||
|
def octaves(self) -> list[int]:
|
||||||
|
"""Return list of octaves"""
|
||||||
|
return [
|
||||||
|
val.get_octave()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
|
]
|
||||||
|
|
||||||
|
def freqs(self) -> list[int]:
|
||||||
|
"""Return list of octaves"""
|
||||||
|
return [
|
||||||
|
val.get_freq()
|
||||||
|
for val in self.evaluated_values
|
||||||
|
if isinstance(val, (Pitch, Chord, Rest))
|
||||||
|
]
|
||||||
|
|
||||||
|
def collect(self, num: int = None, keys: str | list = None) -> list:
|
||||||
|
"""Collect n items from parsed Ziffers"""
|
||||||
|
if num is None:
|
||||||
|
num = len(self.evaluated_values)
|
||||||
|
if keys is None or isinstance(keys, str):
|
||||||
|
keys = [keys]
|
||||||
|
all_items = []
|
||||||
|
values = []
|
||||||
|
for key in keys:
|
||||||
|
for i in range(num):
|
||||||
|
if key is not None:
|
||||||
|
values.append(getattr(self[i], key, None))
|
||||||
|
else:
|
||||||
|
values.append(self[i])
|
||||||
|
all_items.append(values)
|
||||||
|
values = []
|
||||||
|
if len(all_items) > 1:
|
||||||
|
return all_items
|
||||||
|
if len(all_items) == 1:
|
||||||
|
return all_items[0]
|
||||||
|
return None
|
||||||
739
ziffers/classes/sequences.py
Normal file
739
ziffers/classes/sequences.py
Normal file
@ -0,0 +1,739 @@
|
|||||||
|
""" Sequence classes for Ziffers """
|
||||||
|
from dataclasses import dataclass, field, replace
|
||||||
|
from itertools import product
|
||||||
|
from math import floor
|
||||||
|
import random
|
||||||
|
from types import LambdaType
|
||||||
|
from copy import deepcopy
|
||||||
|
import operator
|
||||||
|
from ..defaults import DEFAULT_OPTIONS
|
||||||
|
from ..common import cyclic_zip, euclidian_rhythm, flatten
|
||||||
|
from ..scale import note_from_pc, midi_to_freq
|
||||||
|
from .items import (
|
||||||
|
Meta,
|
||||||
|
Item,
|
||||||
|
Event,
|
||||||
|
DurationChange,
|
||||||
|
OctaveChange,
|
||||||
|
OctaveAdd,
|
||||||
|
Pitch,
|
||||||
|
Rest,
|
||||||
|
RandomPitch,
|
||||||
|
Chord,
|
||||||
|
RomanNumeral,
|
||||||
|
Cyclic,
|
||||||
|
RandomInteger,
|
||||||
|
Range,
|
||||||
|
Integer,
|
||||||
|
VariableAssignment,
|
||||||
|
Variable,
|
||||||
|
VariableList,
|
||||||
|
Measure,
|
||||||
|
Function,
|
||||||
|
Modification,
|
||||||
|
Whitespace,
|
||||||
|
Sample,
|
||||||
|
SampleList,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Could be refactored to each class?
|
||||||
|
def resolve_item(item: Meta, options: dict):
|
||||||
|
"""Resolve cyclic value"""
|
||||||
|
if isinstance(item, Sequence):
|
||||||
|
if isinstance(item, ListOperation):
|
||||||
|
yield from item.evaluate(options)
|
||||||
|
elif isinstance(item, (RepeatedSequence, RepeatedListSequence)):
|
||||||
|
yield from item.resolve_repeat(options)
|
||||||
|
elif isinstance(item, Subdivision):
|
||||||
|
item.evaluate_values(options)
|
||||||
|
yield item
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
yield from item.evaluate_values(options)
|
||||||
|
else:
|
||||||
|
yield from item.evaluate_tree(options)
|
||||||
|
elif isinstance(item, VariableAssignment):
|
||||||
|
if item.pre_eval:
|
||||||
|
pre_options = options.copy()
|
||||||
|
pre_options["pre_eval"] = True
|
||||||
|
options[item.variable.name] = Sequence(
|
||||||
|
values=list(resolve_item(item.value, pre_options))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
options[item.variable.name] = item.value
|
||||||
|
elif isinstance(item, Variable):
|
||||||
|
if options[item.name]:
|
||||||
|
if item.name in options:
|
||||||
|
opt_item = options[item.name]
|
||||||
|
if isinstance(opt_item, LambdaType):
|
||||||
|
yield Function(
|
||||||
|
run=opt_item,
|
||||||
|
text=item.text,
|
||||||
|
kwargs=(options | item.local_options),
|
||||||
|
local_options=item.local_options,
|
||||||
|
)
|
||||||
|
elif isinstance(opt_item, str):
|
||||||
|
yield Sample(
|
||||||
|
name=opt_item,
|
||||||
|
text=item.text,
|
||||||
|
kwargs=(options | item.local_options),
|
||||||
|
local_options=item.local_options,
|
||||||
|
)
|
||||||
|
variable = deepcopy(opt_item)
|
||||||
|
yield from resolve_item(variable, options)
|
||||||
|
elif isinstance(item, VariableList):
|
||||||
|
seqlist = []
|
||||||
|
sample_list = True
|
||||||
|
for var in item.values:
|
||||||
|
if var.name in options:
|
||||||
|
opt_item = options[var.name]
|
||||||
|
if isinstance(opt_item, LambdaType):
|
||||||
|
sample_list = False
|
||||||
|
seqlist.append(
|
||||||
|
Function(
|
||||||
|
run=opt_item,
|
||||||
|
text=var.text,
|
||||||
|
kwargs=(options | var.local_options),
|
||||||
|
local_options=var.local_options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(opt_item, str):
|
||||||
|
seqlist.append(
|
||||||
|
Sample(
|
||||||
|
name=opt_item,
|
||||||
|
text=var.text,
|
||||||
|
kwargs=(options | var.local_options),
|
||||||
|
local_options=var.local_options,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(opt_item, Sequence):
|
||||||
|
sample_list = False
|
||||||
|
seqlist.append(opt_item)
|
||||||
|
if len(seqlist) > 0:
|
||||||
|
if sample_list:
|
||||||
|
yield SampleList(values=seqlist, kwargs=options)
|
||||||
|
else:
|
||||||
|
yield PolyphonicSequence(values=seqlist)
|
||||||
|
elif isinstance(item, Range):
|
||||||
|
yield from item.evaluate(options)
|
||||||
|
elif isinstance(item, Cyclic):
|
||||||
|
yield from resolve_item(item.get_value(), options)
|
||||||
|
elif isinstance(item, Euclid):
|
||||||
|
yield from euclidean_items(item, options)
|
||||||
|
elif isinstance(item, Modification):
|
||||||
|
update_modifications(item, options)
|
||||||
|
elif isinstance(item, Measure):
|
||||||
|
item.reset_options(options)
|
||||||
|
elif options["degrees"] is True and isinstance(item, Pitch) and item.pitch_class == 0:
|
||||||
|
yield Rest(text="r", kwargs=options)
|
||||||
|
elif isinstance(item, Meta): # Filters whitespace
|
||||||
|
yield update_item(item, options)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_integer_value(item, options):
|
||||||
|
"""Helper for resolving integer value of different types"""
|
||||||
|
while isinstance(item, Cyclic):
|
||||||
|
item = item.get_value(options)
|
||||||
|
if isinstance(item, Pitch):
|
||||||
|
return item.get_value(options)
|
||||||
|
if isinstance(item, Integer):
|
||||||
|
return item.get_value(options)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def update_item(item, options):
|
||||||
|
"""Update or create new pitch"""
|
||||||
|
if set(("key", "scale")) <= options.keys():
|
||||||
|
if isinstance(item, Pitch):
|
||||||
|
item.update_options(options)
|
||||||
|
item.update_note()
|
||||||
|
if options.get("pre_eval", False):
|
||||||
|
item.duration = options["duration"]
|
||||||
|
if isinstance(item, Rest):
|
||||||
|
item.update_options(options)
|
||||||
|
elif isinstance(item, (RandomPitch, RandomInteger)):
|
||||||
|
item = create_pitch(item, options)
|
||||||
|
elif isinstance(item, Chord):
|
||||||
|
item.update_options(options)
|
||||||
|
item.update_notes(options)
|
||||||
|
elif isinstance(item, RomanNumeral):
|
||||||
|
item = item.evaluate_chord(options)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def euclidean_items(euclid: Item, options: dict):
|
||||||
|
"""Loops values from generated euclidean sequence"""
|
||||||
|
euclid.evaluate(options)
|
||||||
|
for item in euclid.evaluated_values:
|
||||||
|
yield from resolve_item(item, options)
|
||||||
|
|
||||||
|
|
||||||
|
def update_modifications(current: Item, options: dict) -> dict:
|
||||||
|
"""Update options based on current item"""
|
||||||
|
if isinstance(current, (OctaveChange, DurationChange)):
|
||||||
|
options[current.key] = current.value
|
||||||
|
elif isinstance(current, OctaveAdd):
|
||||||
|
if current.key in options: # Add to existing value
|
||||||
|
options[current.key] += current.value
|
||||||
|
else: # Create value if not existing
|
||||||
|
options[current.key] = current.value
|
||||||
|
|
||||||
|
|
||||||
|
def create_pitch(current: Item, options: dict) -> Pitch:
|
||||||
|
"""Create pitch based on values and options"""
|
||||||
|
|
||||||
|
merged_options = options | current.local_options
|
||||||
|
|
||||||
|
if "modifier" in merged_options:
|
||||||
|
c_modifier = merged_options["modifier"]
|
||||||
|
else:
|
||||||
|
c_modifier = 0
|
||||||
|
|
||||||
|
if hasattr(current, "modifier") and current.modifier is not None:
|
||||||
|
c_modifier += current.modifier
|
||||||
|
|
||||||
|
if "octave" in merged_options:
|
||||||
|
c_octave = merged_options["octave"]
|
||||||
|
if "octave" in options:
|
||||||
|
c_octave = options["octave"] + c_octave
|
||||||
|
else:
|
||||||
|
c_octave = 0
|
||||||
|
|
||||||
|
if hasattr(current, "octave") and current.octave is not None:
|
||||||
|
c_octave += current.octave
|
||||||
|
|
||||||
|
current_value = current.get_value(merged_options)
|
||||||
|
|
||||||
|
note, pitch_bend = note_from_pc(
|
||||||
|
root=merged_options["key"],
|
||||||
|
pitch_class=current_value,
|
||||||
|
intervals=merged_options["scale"],
|
||||||
|
modifier=c_modifier,
|
||||||
|
octave=c_octave,
|
||||||
|
degrees=merged_options["degrees"]
|
||||||
|
)
|
||||||
|
new_pitch = Pitch(
|
||||||
|
pitch_class=current_value,
|
||||||
|
text=str(current_value),
|
||||||
|
freq=midi_to_freq(note),
|
||||||
|
note=floor(note),
|
||||||
|
pitch_bend=pitch_bend,
|
||||||
|
octave=c_octave,
|
||||||
|
modifier=c_modifier,
|
||||||
|
kwargs=merged_options,
|
||||||
|
)
|
||||||
|
return new_pitch
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Sequence(Meta):
|
||||||
|
"""Class for sequences of items"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
text: str = field(default=None)
|
||||||
|
wrap_start: str = field(default=None, repr=False)
|
||||||
|
wrap_end: str = field(default=None, repr=False)
|
||||||
|
local_index: int = field(default=0, init=False)
|
||||||
|
evaluated_values: list = field(default=None)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.text = self.__collect_text()
|
||||||
|
self.update_local_options()
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.values[index]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.values)
|
||||||
|
|
||||||
|
def update_local_options(self):
|
||||||
|
"""Update value attributes from dict"""
|
||||||
|
if self.local_options:
|
||||||
|
for obj in self.values:
|
||||||
|
if isinstance(obj, Event):
|
||||||
|
if obj.local_options:
|
||||||
|
obj.local_options = (
|
||||||
|
obj.local_options | self.local_options.copy()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
obj.local_options = self.local_options.copy()
|
||||||
|
|
||||||
|
def __collect_text(self) -> str:
|
||||||
|
"""Collect text value from values"""
|
||||||
|
text = "".join([val.text for val in self.values])
|
||||||
|
if self.wrap_start is not None:
|
||||||
|
text = self.wrap_start + text
|
||||||
|
if self.wrap_end is not None:
|
||||||
|
text = text + self.wrap_end
|
||||||
|
return text
|
||||||
|
|
||||||
|
def evaluate_tree(self, options: dict = None, eval_tree: bool = False):
|
||||||
|
"""Evaluate the tree and return array of resolved pitches
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options (dict, optional): Options for the pitches. Defaults to None.
|
||||||
|
eval_tree (bool, optional): Flag for using the evaluated subtree. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Start of the main function: Evaluate and flatten the Ziffers object tree
|
||||||
|
values = self.evaluated_values if eval_tree else self.values
|
||||||
|
for item in values:
|
||||||
|
yield from resolve_item(item, options)
|
||||||
|
|
||||||
|
def filter(self, keep: tuple):
|
||||||
|
"""Filter out items from sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keep (tuple): Tuple describing classes to keep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sequence: Copy of the sequence with filtered values.
|
||||||
|
"""
|
||||||
|
return replace(
|
||||||
|
self, values=[item for item in self.values if isinstance(item, keep)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class PolyphonicSequence:
|
||||||
|
"""Class for polyphonic sequence"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class ListSequence(Sequence):
|
||||||
|
"""Class for Ziffers list sequences"""
|
||||||
|
|
||||||
|
wrap_start: str = field(default="(", repr=False)
|
||||||
|
wrap_end: str = field(default=")", repr=False)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RepeatedListSequence(Sequence):
|
||||||
|
"""Class for Ziffers list sequences"""
|
||||||
|
|
||||||
|
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
||||||
|
wrap_start: str = field(default="(:", repr=False)
|
||||||
|
wrap_end: str = field(default=":)", repr=False)
|
||||||
|
|
||||||
|
def resolve_repeat(self, options: dict):
|
||||||
|
"""Repeats items and generates new random values"""
|
||||||
|
repeats = self.repeats.get_value(options)
|
||||||
|
if not isinstance(repeats, int):
|
||||||
|
repeats = resolve_integer_value(repeats, options)
|
||||||
|
for _ in range(repeats):
|
||||||
|
for item in self.evaluate_tree(options):
|
||||||
|
yield from resolve_item(item, options)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Subdivision(Sequence):
|
||||||
|
"""Class for subdivisions"""
|
||||||
|
|
||||||
|
full_duration: float = field(default=None, init=False)
|
||||||
|
|
||||||
|
def evaluate_values(self, options):
|
||||||
|
"""Evaluate values and store to evaluated_values"""
|
||||||
|
self.full_duration = options["duration"]
|
||||||
|
self.evaluated_values = list(self.evaluate_tree(options))
|
||||||
|
|
||||||
|
def evaluate_durations(self, duration=None):
|
||||||
|
"""Calculate new durations by dividing with the number of items in the sequence"""
|
||||||
|
if duration is None:
|
||||||
|
duration = self.full_duration
|
||||||
|
new_d = duration / len(self.evaluated_values)
|
||||||
|
for item in self.evaluated_values:
|
||||||
|
if isinstance(item, Subdivision):
|
||||||
|
yield from item.evaluate_durations(new_d)
|
||||||
|
if isinstance(item, Event):
|
||||||
|
if duration is not None:
|
||||||
|
item.duration = new_d
|
||||||
|
item.beat = new_d * 4
|
||||||
|
yield item
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class ListOperation(Sequence):
|
||||||
|
"""Class for list operations"""
|
||||||
|
|
||||||
|
evaluated_values: list = None
|
||||||
|
|
||||||
|
def evaluate(self, options=DEFAULT_OPTIONS.copy()):
|
||||||
|
"""Evaluates the operation"""
|
||||||
|
|
||||||
|
def _filter_whitespace(input_list):
|
||||||
|
for item in input_list:
|
||||||
|
if isinstance(item, Meta):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def _filter_operation(input_list, options):
|
||||||
|
"""Filter and evaluate values"""
|
||||||
|
flattened_list = []
|
||||||
|
for item in input_list:
|
||||||
|
if isinstance(item, (list, Sequence)):
|
||||||
|
if isinstance(item, ListOperation):
|
||||||
|
flattened_list.extend(item.evaluated_values)
|
||||||
|
elif isinstance(item, Subdivision):
|
||||||
|
item.evaluate_values(options)
|
||||||
|
flattened_list.extend(list(item.evaluate_durations()))
|
||||||
|
elif isinstance(item, RepeatedListSequence):
|
||||||
|
flattened_list.extend(list(item.resolve_repeat(options)))
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
flattened_list.extend(item.evaluate_values(options))
|
||||||
|
else:
|
||||||
|
flattened_list.append(_filter_operation(item, options))
|
||||||
|
elif isinstance(item, Cyclic):
|
||||||
|
value = item.get_value(options)
|
||||||
|
if isinstance(value, Sequence):
|
||||||
|
flattened_list.extend(_filter_operation(value, options))
|
||||||
|
elif isinstance(value, (Event, RandomInteger, Integer)):
|
||||||
|
flattened_list.append(value)
|
||||||
|
elif isinstance(item, Modification):
|
||||||
|
options = options | item.as_options()
|
||||||
|
elif isinstance(item, RomanNumeral):
|
||||||
|
item = item.evaluate_chord(options)
|
||||||
|
flattened_list.append(item)
|
||||||
|
elif isinstance(item, Range):
|
||||||
|
flattened_list.extend(list(item.evaluate(options)))
|
||||||
|
elif isinstance(item, (Event, RandomInteger, Integer)):
|
||||||
|
item.update_options(options)
|
||||||
|
item = update_item(item, options)
|
||||||
|
flattened_list.append(item)
|
||||||
|
|
||||||
|
if isinstance(input_list, Sequence):
|
||||||
|
return replace(input_list, values=flattened_list)
|
||||||
|
|
||||||
|
return flattened_list
|
||||||
|
|
||||||
|
def _pick_from_list(left, right, options):
|
||||||
|
"""Pick random numbers from a list"""
|
||||||
|
if isinstance(left, Sequence):
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
|
||||||
|
if isinstance(right, Sequence):
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
|
||||||
|
if not isinstance(right, (list, Sequence)):
|
||||||
|
right = Sequence(values=[right])
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for num in right.values:
|
||||||
|
for _ in range(num.get_value(options)):
|
||||||
|
result.append(random.choice(left.values))
|
||||||
|
return flatten(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _select_from_list(left, right, options):
|
||||||
|
"""Select number of items from shuffled list"""
|
||||||
|
if isinstance(left, Sequence):
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
|
||||||
|
if isinstance(right, Sequence):
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
|
||||||
|
if not isinstance(right, (list, Sequence)):
|
||||||
|
right = Sequence(values=[right])
|
||||||
|
|
||||||
|
result = []
|
||||||
|
left = left.values
|
||||||
|
|
||||||
|
for num in right.values:
|
||||||
|
random.shuffle(left)
|
||||||
|
num = num.get_value(options)
|
||||||
|
new_list = [left[i % len(left)] for i in range(num)]
|
||||||
|
result += new_list
|
||||||
|
|
||||||
|
return flatten(result)
|
||||||
|
|
||||||
|
def _vertical_arpeggio(left, right, options):
|
||||||
|
"""Vertical arpeggio operation, eg. (135)@(q 1 2 021)"""
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
if not isinstance(left, list):
|
||||||
|
left = list(left.evaluate_tree(options))
|
||||||
|
if not isinstance(right, list):
|
||||||
|
right = list(right.evaluate_tree(options))
|
||||||
|
arp_items = []
|
||||||
|
|
||||||
|
for item in left:
|
||||||
|
for index in right:
|
||||||
|
pcs = item.pitch_classes
|
||||||
|
if isinstance(index, Pitch):
|
||||||
|
new_pitch = deepcopy(pcs[index.get_value(options) % len(pcs)])
|
||||||
|
new_pitch.duration = index.duration
|
||||||
|
arp_items.append(new_pitch)
|
||||||
|
else: # Should be a chord
|
||||||
|
new_pitches = []
|
||||||
|
for pitch in index.pitch_classes:
|
||||||
|
new_pitch = deepcopy(
|
||||||
|
pcs[pitch.get_value(options) % len(pcs)]
|
||||||
|
)
|
||||||
|
new_pitch.duration = pitch.duration
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
new_chord.text = "".join(
|
||||||
|
[val.text for val in new_chord.pitch_classes]
|
||||||
|
)
|
||||||
|
arp_items.append(new_chord)
|
||||||
|
|
||||||
|
return Sequence(values=arp_items)
|
||||||
|
|
||||||
|
def _horizontal_arpeggio(left, right, options):
|
||||||
|
"""Horizontal arpeggio operation, eg. (1 2 3 4)#(0 3 2 1)"""
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
left = list(left.evaluate_tree(options))
|
||||||
|
right = list(right.evaluate_tree(options))
|
||||||
|
arp_items = []
|
||||||
|
for index in right:
|
||||||
|
new_item = deepcopy(left[index.get_value(options) % len(left)])
|
||||||
|
arp_items.append(new_item)
|
||||||
|
return Sequence(values=arp_items)
|
||||||
|
|
||||||
|
def _cyclic_zip(left, right, options):
|
||||||
|
"""Cyclic zip operaiton, eg. (q e)<>(1 2 3)"""
|
||||||
|
left = list(_filter_whitespace(left))
|
||||||
|
right = list(_filter_whitespace(right))
|
||||||
|
result = Sequence(values=cyclic_zip(left, right))
|
||||||
|
return _filter_operation(result, options)
|
||||||
|
|
||||||
|
def _python_operations(left, right, options):
|
||||||
|
"""Python math operations"""
|
||||||
|
|
||||||
|
def __chord_operation(chord, pitch_y, yass, options):
|
||||||
|
"""Operation for single chords"""
|
||||||
|
new_pitches = []
|
||||||
|
pitch_y = pitch_y.get_value(options)
|
||||||
|
for pitch_x in chord.pitch_classes:
|
||||||
|
pitch_x = pitch_x.pitch_class
|
||||||
|
pitch_y = resolve_integer_value(pitch_y, options)
|
||||||
|
new_pitch = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
pitch_y if yass else pitch_x, pitch_x if yass else pitch_y
|
||||||
|
),
|
||||||
|
kwargs=options,
|
||||||
|
)
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
return new_chord
|
||||||
|
|
||||||
|
# _python_operation starts. Filter & evaluate items.
|
||||||
|
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
if isinstance(right, Sequence):
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
elif isinstance(right, Cyclic):
|
||||||
|
right = right.get_value(options)
|
||||||
|
|
||||||
|
# Create product of items.
|
||||||
|
pairs = product(
|
||||||
|
(right.values if isinstance(right, Sequence) else [right]), left
|
||||||
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for first, second in pairs:
|
||||||
|
if isinstance(first, Chord) and isinstance(second, Chord):
|
||||||
|
new_pitches = []
|
||||||
|
for pitch_x in first.pitch_classes:
|
||||||
|
for pitch_y in second.pitch_classes:
|
||||||
|
new_pitch = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
pitch_x.pitch_class, pitch_y.pitch_class
|
||||||
|
),
|
||||||
|
kwargs=options,
|
||||||
|
)
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
outcome = new_chord
|
||||||
|
elif isinstance(first, Chord):
|
||||||
|
outcome = __chord_operation(first, second, False, options)
|
||||||
|
elif isinstance(second, Chord):
|
||||||
|
outcome = __chord_operation(second, first, True, options)
|
||||||
|
elif isinstance(first, Rest) or isinstance(second, Rest):
|
||||||
|
outcome = Rest(duration=first.get_duration())
|
||||||
|
else:
|
||||||
|
outcome = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
first.get_value(options), second.get_value(options)
|
||||||
|
),
|
||||||
|
kwargs=second.get_options(),
|
||||||
|
)
|
||||||
|
results.append(outcome)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Start of the evaluate() function
|
||||||
|
|
||||||
|
operators = self.values[1::2] # Fetch every second operator element
|
||||||
|
values = self.values[::2] # Fetch every second list element
|
||||||
|
# values = _filter_operation(values, options) # Filter out
|
||||||
|
if len(values) == 1:
|
||||||
|
return values[0] # If right hand doesnt contain anything sensible
|
||||||
|
left = values[0] # Start results with the first array
|
||||||
|
|
||||||
|
for i, operand in enumerate(operators):
|
||||||
|
operation = operand.value
|
||||||
|
right = values[i + 1]
|
||||||
|
if isinstance(operation, str):
|
||||||
|
if operation == "vertical":
|
||||||
|
left = _vertical_arpeggio(left, right, options)
|
||||||
|
elif operation == "horizontal":
|
||||||
|
left = _horizontal_arpeggio(left, right, options)
|
||||||
|
elif operation == "zip":
|
||||||
|
left = _cyclic_zip(left, right, options)
|
||||||
|
elif operation == "pick":
|
||||||
|
left = _pick_from_list(left, right, options)
|
||||||
|
elif operation == "select":
|
||||||
|
left = _select_from_list(left, right, options)
|
||||||
|
|
||||||
|
else:
|
||||||
|
left = _python_operations(left, right, options)
|
||||||
|
return left
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Eval(Sequence):
|
||||||
|
"""Class for evaluation notation"""
|
||||||
|
|
||||||
|
wrap_start: str = field(default="{", repr=False)
|
||||||
|
wrap_end: str = field(default="}", repr=False)
|
||||||
|
|
||||||
|
# def __post_init__(self):
|
||||||
|
# self.text = "".join([val.text for val in flatten(self.values)])
|
||||||
|
# super().__post_init__()
|
||||||
|
|
||||||
|
def evaluate_values(self, options):
|
||||||
|
operations = [val for val in self.values if isinstance(val, (Operation, Rest))]
|
||||||
|
eval_values = []
|
||||||
|
for val in operations:
|
||||||
|
if isinstance(val,Operation):
|
||||||
|
eval_values.append(Pitch(pitch_class=val.evaluate(), kwargs=options | val.local_options))
|
||||||
|
else:
|
||||||
|
eval_values.append(val)
|
||||||
|
|
||||||
|
self.evaluated_values = eval_values
|
||||||
|
|
||||||
|
return self.evaluated_values
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class LispOperation(Sequence):
|
||||||
|
"""Class for lisp-like operations: (+ 1 2 3) etc."""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
operator: operator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Operation(Sequence):
|
||||||
|
"""Class for sequential operations"""
|
||||||
|
|
||||||
|
values: list
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
return eval(self.text)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class RepeatedSequence(Sequence):
|
||||||
|
"""Class for repeats"""
|
||||||
|
|
||||||
|
repeats: RandomInteger | Integer = field(default_factory=Integer(value=1, text="1"))
|
||||||
|
wrap_start: str = field(default="[:", repr=False)
|
||||||
|
wrap_end: str = field(default=":]", repr=False)
|
||||||
|
|
||||||
|
evaluated_values: list = None
|
||||||
|
|
||||||
|
def resolve_repeat(self, options):
|
||||||
|
"""Resolves all items"""
|
||||||
|
self.evaluate_values(options)
|
||||||
|
repeats = self.repeats.get_value(options)
|
||||||
|
if not isinstance(repeats, int):
|
||||||
|
repeats = resolve_integer_value(repeats, options)
|
||||||
|
for _ in range(repeats):
|
||||||
|
for item in self.evaluated_values:
|
||||||
|
yield from resolve_item(item, options)
|
||||||
|
|
||||||
|
def evaluate_values(self, options):
|
||||||
|
"""Evaluate values and store to evaluated_values"""
|
||||||
|
self.evaluated_values = list(self.evaluate(options))
|
||||||
|
|
||||||
|
def evaluate(self, options: dict):
|
||||||
|
"""Evaluate repeated sequence partially. Leaves Cycles intact."""
|
||||||
|
self.local_options = options.copy()
|
||||||
|
for item in self.values:
|
||||||
|
if isinstance(item, Sequence):
|
||||||
|
if isinstance(item, ListOperation):
|
||||||
|
yield from item.evaluate_tree(self.local_options, True)
|
||||||
|
elif isinstance(item, RepeatedSequence):
|
||||||
|
yield item
|
||||||
|
elif isinstance(item, Subdivision):
|
||||||
|
item.evaluate_values(options)
|
||||||
|
yield item
|
||||||
|
elif isinstance(item, Eval):
|
||||||
|
yield from item.evaluate_values(options)
|
||||||
|
else:
|
||||||
|
yield from item
|
||||||
|
elif isinstance(item, Cyclic):
|
||||||
|
yield item # Return the cycle instead of values
|
||||||
|
elif isinstance(item, Modification):
|
||||||
|
self.local_options = self.local_options | item.as_options()
|
||||||
|
elif isinstance(item, Rest):
|
||||||
|
yield item.get_updated_item(self.local_options)
|
||||||
|
elif isinstance(item, Range):
|
||||||
|
yield from item.evaluate(self.local_options)
|
||||||
|
elif isinstance(item, (Pitch, Chord, RomanNumeral)):
|
||||||
|
yield item
|
||||||
|
elif isinstance(item, (Event, RandomInteger)):
|
||||||
|
yield Pitch(
|
||||||
|
pitch_class=item.get_value(self.local_options),
|
||||||
|
kwargs=self.local_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Euclid(Item):
|
||||||
|
"""Class for euclidean cycles"""
|
||||||
|
|
||||||
|
pulses: int
|
||||||
|
length: int
|
||||||
|
onset: ListSequence
|
||||||
|
offset: ListSequence = field(default=None)
|
||||||
|
rotate: int = field(default=0)
|
||||||
|
evaluated_values: list = field(default=None)
|
||||||
|
|
||||||
|
def evaluate(self, options):
|
||||||
|
"""Evaluate values using euclidean spread"""
|
||||||
|
onset_values = [
|
||||||
|
val for val in self.onset.values if not isinstance(val, Whitespace)
|
||||||
|
]
|
||||||
|
onset_length = len(onset_values)
|
||||||
|
booleans = euclidian_rhythm(self.pulses, self.length, self.rotate)
|
||||||
|
self.evaluated_values = []
|
||||||
|
|
||||||
|
if self.offset is not None:
|
||||||
|
offset_values = [
|
||||||
|
val for val in self.offset.values if not isinstance(val, Whitespace)
|
||||||
|
]
|
||||||
|
offset_length = len(offset_values)
|
||||||
|
|
||||||
|
on_i = 0
|
||||||
|
off_i = 0
|
||||||
|
|
||||||
|
for i in range(self.length):
|
||||||
|
if booleans[i]:
|
||||||
|
value = onset_values[on_i % onset_length]
|
||||||
|
on_i += 1
|
||||||
|
else:
|
||||||
|
if self.offset is None:
|
||||||
|
value = Rest(duration=options["duration"])
|
||||||
|
else:
|
||||||
|
value = offset_values[off_i % offset_length]
|
||||||
|
off_i += 1
|
||||||
|
|
||||||
|
self.evaluated_values.append(value)
|
||||||
@ -1,12 +1,39 @@
|
|||||||
""" Common methods used in parsing """
|
""" Common methods used in parsing """
|
||||||
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
def flatten(arr: list) -> list:
|
def flatten(arr: list) -> list:
|
||||||
"""Flattens array"""
|
"""Flattens array"""
|
||||||
return (
|
return (
|
||||||
flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else [])
|
flatten(arr[0]) + (flatten(arr[1:]) if len(arr) > 1 else [])
|
||||||
if isinstance(arr, list) else [arr]
|
if isinstance(arr, list)
|
||||||
|
else [arr]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rotate(arr, k):
|
||||||
|
"""Rotates array"""
|
||||||
|
# Calculate the effective rotation amount (mod the array length)
|
||||||
|
k = k % len(arr)
|
||||||
|
# Rotate the array to the right
|
||||||
|
if k > 0:
|
||||||
|
arr = arr[-k:] + arr[:-k]
|
||||||
|
# Rotate the array to the left
|
||||||
|
elif k < 0:
|
||||||
|
arr = arr[-k:] + arr[:-k]
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def repeat_text(pos, neg, times):
|
||||||
|
"""Helper to repeat text"""
|
||||||
|
if times > 0:
|
||||||
|
return pos * times
|
||||||
|
if times < 0:
|
||||||
|
return neg * abs(times)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def sum_dict(arr: list[dict]) -> dict:
|
def sum_dict(arr: list[dict]) -> dict:
|
||||||
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
||||||
result = arr[0]
|
result = arr[0]
|
||||||
@ -17,3 +44,73 @@ def sum_dict(arr: list[dict]) -> dict:
|
|||||||
else:
|
else:
|
||||||
result[key] = element[key]
|
result[key] = element[key]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def string_rewrite(axiom: str, rules: dict):
|
||||||
|
"""String rewrite / Lindemeyer system for rule based text manipulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
axiom (str): Input string
|
||||||
|
rules (dict): String manipulation rules in dict:
|
||||||
|
|
||||||
|
Example:
|
||||||
|
rules = {
|
||||||
|
"1": "2",
|
||||||
|
"[2-9]": "45",
|
||||||
|
"4": lambda: str(randint(1, 7)),
|
||||||
|
"([1-9])(5)": lambda a, b: str(int(a)*int(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i range(10):
|
||||||
|
print(string_rewrite("1", rules))
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _apply_rules(match):
|
||||||
|
for key, value in rules.items():
|
||||||
|
if re.match(key, match.group(0)):
|
||||||
|
if callable(value):
|
||||||
|
yield value(
|
||||||
|
*match.groups()
|
||||||
|
) if value.__code__.co_argcount > 0 else value()
|
||||||
|
yield value
|
||||||
|
|
||||||
|
pattern = re.compile("|".join(rules.keys()))
|
||||||
|
return pattern.sub(lambda m: next(_apply_rules(m)), axiom)
|
||||||
|
|
||||||
|
|
||||||
|
def euclidian_rhythm(pulses: int, length: int, rot: int = 0):
|
||||||
|
"""Calculate Euclidean rhythms. Original algorithm by Thomas Morrill."""
|
||||||
|
|
||||||
|
def _starts_descent(arr, index):
|
||||||
|
length = len(arr)
|
||||||
|
next_index = (index + 1) % length
|
||||||
|
return arr[index] > arr[next_index]
|
||||||
|
|
||||||
|
def rotation(arr, idx):
|
||||||
|
return arr[-idx:] + arr[:-idx]
|
||||||
|
|
||||||
|
if pulses >= length:
|
||||||
|
return [True]
|
||||||
|
|
||||||
|
res_list = [pulses * t % length for t in range(-1, length - 1)]
|
||||||
|
bool_list = [_starts_descent(res_list, index) for index in range(length)]
|
||||||
|
|
||||||
|
return rotation(bool_list, rot)
|
||||||
|
|
||||||
|
|
||||||
|
def cyclic_zip(first: list, second: list) -> list:
|
||||||
|
"""Cyclic zip method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
first (list): First list is cycled
|
||||||
|
second (list): Second list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Cyclicly zipped list
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
s_length = len(second)
|
||||||
|
f_length = len(first)
|
||||||
|
for i in range(s_length):
|
||||||
|
result.append([first[i % f_length], second[i]])
|
||||||
|
return [deepcopy(item) for sublist in result for item in sublist]
|
||||||
|
|||||||
131
ziffers/converters.py
Normal file
131
ziffers/converters.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""Collection of converters"""
|
||||||
|
from ziffers import zparse, Ziffers, Pitch, Rest, Chord, accidentals_from_note_name, MODES, MODE_ACCIDENTALS
|
||||||
|
|
||||||
|
try:
|
||||||
|
from music21 import converter, note, stream, meter, chord, environment, tempo, key
|
||||||
|
music21_imported: bool = True
|
||||||
|
except ImportError:
|
||||||
|
music21_imported: bool = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctcsound
|
||||||
|
csound_imported: bool = True
|
||||||
|
except (ImportError, TypeError) as Error:
|
||||||
|
csound_imported: bool = False
|
||||||
|
|
||||||
|
def ziffers_to_csound_score(ziffers: Ziffers, bpm: int=80, amp: float=1500, instr: (int|str)=1) -> str:
|
||||||
|
""" Transform Ziffers object to Csound score in format:
|
||||||
|
i {instrument} {start time} {duration} {amplitude} {frequency} """
|
||||||
|
|
||||||
|
if not csound_imported:
|
||||||
|
raise ImportError("Install Csound")
|
||||||
|
|
||||||
|
score = ""
|
||||||
|
instr = f'"{instr}"' if isinstance(instr, str) else instr
|
||||||
|
start_time = 0
|
||||||
|
for item in ziffers.evaluated_values:
|
||||||
|
if isinstance(item, Chord):
|
||||||
|
for freq, dur in zip(item.get_freq(), item.get_duration()):
|
||||||
|
score += f"i {instr} {start_time} {dur * 4 * 60 / bpm} {amp} {freq:.2f}\n"
|
||||||
|
start_time += max(item.get_duration()) * 4 * 60 / bpm
|
||||||
|
elif isinstance(item, Rest):
|
||||||
|
score += f"i {instr} {start_time} {item.get_duration() * 4 * 60 / bpm} {amp} 0\n"
|
||||||
|
elif isinstance(item, Pitch):
|
||||||
|
score += f"i {instr} {start_time} {item.get_duration() * 4 * 60 / bpm} {amp} {item.get_freq():.2f}\n"
|
||||||
|
start_time += item.get_duration() * 4 * 60 / bpm
|
||||||
|
return score
|
||||||
|
|
||||||
|
def to_music21(expression: str | Ziffers, **options):
|
||||||
|
"""Helper for passing options to the parser"""
|
||||||
|
|
||||||
|
if not music21_imported:
|
||||||
|
raise ImportError("Install Music21 library")
|
||||||
|
|
||||||
|
# Register the ZiffersMusic21 converter
|
||||||
|
converter.registerSubConverter(ZiffersMusic21)
|
||||||
|
|
||||||
|
if isinstance(expression, Ziffers):
|
||||||
|
if options:
|
||||||
|
options["preparsed"] = expression
|
||||||
|
else:
|
||||||
|
options = {"preparsed": expression}
|
||||||
|
options = {"ziffers": options}
|
||||||
|
return converter.parse("PREPARSED", format="ziffers", keywords=options)
|
||||||
|
|
||||||
|
if options:
|
||||||
|
options = {"ziffers": options}
|
||||||
|
return converter.parse(expression, format="ziffers", keywords=options)
|
||||||
|
|
||||||
|
test = converter.parse(expression, format="ziffers")
|
||||||
|
return test
|
||||||
|
|
||||||
|
|
||||||
|
def set_musescore_path(path: str):
|
||||||
|
"""Helper for setting the Musescore path"""
|
||||||
|
settings = environment.UserSettings()
|
||||||
|
# Default windows path:
|
||||||
|
# 'C:\\Program Files\\MuseScore 3\\bin\\MuseScore3.exe'
|
||||||
|
settings["musicxmlPath"] = path
|
||||||
|
settings["musescoreDirectPNGPath"] = path
|
||||||
|
|
||||||
|
|
||||||
|
if music21_imported:
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, invalid-name, unused-argument, attribute-defined-outside-init
|
||||||
|
class ZiffersMusic21(converter.subConverters.SubConverter):
|
||||||
|
"""Ziffers converter to Music21"""
|
||||||
|
|
||||||
|
registerFormats = ("ziffers",)
|
||||||
|
registerInputExtensions = ("zf",)
|
||||||
|
|
||||||
|
def parseData(self, dataString, number=None):
|
||||||
|
"""Parses Ziffers string to Music21 object"""
|
||||||
|
# Look for options in keywords object
|
||||||
|
keywords = self.keywords["keywords"]
|
||||||
|
if "ziffers" in keywords:
|
||||||
|
options = keywords["ziffers"]
|
||||||
|
if "preparsed" in options:
|
||||||
|
parsed = options["preparsed"]
|
||||||
|
else:
|
||||||
|
parsed = zparse(dataString, **options)
|
||||||
|
else:
|
||||||
|
parsed = zparse(dataString)
|
||||||
|
|
||||||
|
note_stream = stream.Part()
|
||||||
|
|
||||||
|
if "time" in options:
|
||||||
|
m_item = meter.TimeSignature(options["time"]) # Common time
|
||||||
|
else:
|
||||||
|
m_item = meter.TimeSignature("c") # Common time
|
||||||
|
|
||||||
|
note_stream.insert(0, m_item)
|
||||||
|
|
||||||
|
if "key" in options:
|
||||||
|
accidentals = accidentals_from_note_name(options["key"])
|
||||||
|
else:
|
||||||
|
accidentals = 0
|
||||||
|
|
||||||
|
if "scale" in options:
|
||||||
|
scale_upper = options["scale"].upper()
|
||||||
|
scale_lower = options["scale"].lower()
|
||||||
|
if scale_upper in MODES:
|
||||||
|
accidentals += MODE_ACCIDENTALS[scale_upper]
|
||||||
|
note_stream.append(key.KeySignature(accidentals,mode=scale_lower))
|
||||||
|
else:
|
||||||
|
note_stream.append(key.KeySignature(accidentals))
|
||||||
|
|
||||||
|
if "bpm" in options:
|
||||||
|
note_stream.append(tempo.MetronomeMark(number=options["bpm"]))
|
||||||
|
|
||||||
|
for item in parsed:
|
||||||
|
if isinstance(item, Pitch):
|
||||||
|
m_item = note.Note(item.note)
|
||||||
|
m_item.duration.quarterLength = item.duration * 4
|
||||||
|
elif isinstance(item, Rest):
|
||||||
|
m_item = note.Rest(item.duration * 4)
|
||||||
|
elif isinstance(item, Chord):
|
||||||
|
m_item = chord.Chord(item.notes)
|
||||||
|
m_item.duration.quarterLength = item.duration * 4
|
||||||
|
note_stream.append(m_item)
|
||||||
|
# TODO: Is this ok?
|
||||||
|
self.stream = note_stream.makeMeasures()
|
||||||
3367
ziffers/defaults.py
3367
ziffers/defaults.py
File diff suppressed because it is too large
Load Diff
35
ziffers/generators.py
Normal file
35
ziffers/generators.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""Collection of generators"""
|
||||||
|
|
||||||
|
|
||||||
|
# Sieve of Eratosthenes
|
||||||
|
# Based on code by David Eppstein, UC Irvine, 28 Feb 2002
|
||||||
|
# http://code.activestate.com/recipes/117119/
|
||||||
|
def gen_primes():
|
||||||
|
"""Generate an infinite sequence of prime numbers."""
|
||||||
|
# Maps composites to primes witnessing their compositeness.
|
||||||
|
# This is memory efficient, as the sieve is not "run forward"
|
||||||
|
# indefinitely, but only as long as required by the current
|
||||||
|
# number being tested.
|
||||||
|
sieve = {}
|
||||||
|
|
||||||
|
# The running integer that's checked for primeness
|
||||||
|
current = 2
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if current not in sieve:
|
||||||
|
# current is a new prime.
|
||||||
|
# Yield it and mark its first multiple that isn't
|
||||||
|
# already marked in previous iterations
|
||||||
|
yield current
|
||||||
|
sieve[current * current] = [current]
|
||||||
|
else:
|
||||||
|
# current is composite. sieve[current] is the list of primes that
|
||||||
|
# divide it. Since we've reached current, we no longer
|
||||||
|
# need it in the map, but we'll mark the next
|
||||||
|
# multiples of its witnesses to prepare for larger
|
||||||
|
# numbers
|
||||||
|
for composite in sieve[current]:
|
||||||
|
sieve.setdefault(composite + current, []).append(composite)
|
||||||
|
del sieve[current]
|
||||||
|
|
||||||
|
current += 1
|
||||||
@ -1,85 +1,147 @@
|
|||||||
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
""" Lark transformer for mapping Lark tokens to Ziffers objects """
|
||||||
from typing import Optional
|
import random
|
||||||
from lark import Transformer
|
from lark import Transformer, Token
|
||||||
from .classes import (
|
from .scale import cents_to_semitones, ratio_to_cents, monzo_to_cents
|
||||||
Ziffers,
|
from .classes.root import Ziffers
|
||||||
|
from .classes.sequences import (
|
||||||
|
Sequence,
|
||||||
|
ListSequence,
|
||||||
|
RepeatedListSequence,
|
||||||
|
ListOperation,
|
||||||
|
RepeatedSequence,
|
||||||
|
Euclid,
|
||||||
|
Subdivision,
|
||||||
|
Eval,
|
||||||
|
Operation,
|
||||||
|
LispOperation,
|
||||||
|
)
|
||||||
|
from .classes.items import (
|
||||||
Whitespace,
|
Whitespace,
|
||||||
DurationChange,
|
DurationChange,
|
||||||
OctaveChange,
|
OctaveChange,
|
||||||
OctaveAdd,
|
OctaveAdd,
|
||||||
Pitch,
|
Pitch,
|
||||||
|
Rest,
|
||||||
RandomPitch,
|
RandomPitch,
|
||||||
RandomPercent,
|
RandomPercent,
|
||||||
Chord,
|
Chord,
|
||||||
RomanNumeral,
|
RomanNumeral,
|
||||||
Sequence,
|
|
||||||
ListSequence,
|
|
||||||
RepeatedListSequence,
|
|
||||||
Subdivision,
|
|
||||||
Cyclic,
|
Cyclic,
|
||||||
RandomInteger,
|
RandomInteger,
|
||||||
Range,
|
Range,
|
||||||
Operator,
|
Operator,
|
||||||
ListOperation,
|
|
||||||
Operation,
|
|
||||||
Eval,
|
|
||||||
Atom,
|
Atom,
|
||||||
Integer,
|
Integer,
|
||||||
Euclid,
|
VariableAssignment,
|
||||||
RepeatedSequence,
|
Variable,
|
||||||
|
VariableList,
|
||||||
|
Measure,
|
||||||
)
|
)
|
||||||
from .common import flatten, sum_dict
|
from .common import flatten, sum_dict
|
||||||
from .defaults import DEFAULT_DURS, OPERATORS
|
from .defaults import DEFAULT_DURS, OPERATORS
|
||||||
from .scale import parse_roman, chord_from_roman_numeral
|
from .scale import parse_roman
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
|
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name
|
||||||
class ZiffersTransformer(Transformer):
|
class ZiffersTransformer(Transformer):
|
||||||
"""Rules for transforming Ziffers expressions into tree."""
|
"""Rules for transforming Ziffers expressions into tree."""
|
||||||
|
|
||||||
def __init__(self, options: Optional[dict] = None):
|
|
||||||
super().__init__()
|
|
||||||
self.options = options
|
|
||||||
|
|
||||||
def start(self, items) -> Ziffers:
|
def start(self, items) -> Ziffers:
|
||||||
"""Root for the rules"""
|
"""Root for the rules"""
|
||||||
# seq = Sequence(values=items[0])
|
|
||||||
return Ziffers(values=items[0], options={})
|
return Ziffers(values=items[0], options={})
|
||||||
|
|
||||||
def sequence(self, items):
|
def sequence(self, items):
|
||||||
"""Flatten sequence"""
|
"""Flatten sequence"""
|
||||||
return flatten(items)
|
return flatten(items)
|
||||||
|
|
||||||
def random_integer(self, item) -> RandomInteger:
|
def rest(self, items):
|
||||||
"""Parses random integer syntax"""
|
"""Return duration event"""
|
||||||
val = item[0][1:-1].split(",")
|
if len(items) > 0:
|
||||||
return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value)
|
prefixes = sum_dict(items)
|
||||||
|
text_prefix = prefixes.pop("text")
|
||||||
|
prefixes["prefix"] = text_prefix
|
||||||
|
return Rest(text=text_prefix + "r", local_options=prefixes)
|
||||||
|
return Rest(text="r")
|
||||||
|
|
||||||
def range(self, item) -> Range:
|
def measure(self, items):
|
||||||
|
"""Return new measure"""
|
||||||
|
return Measure()
|
||||||
|
|
||||||
|
def random_integer(self, items) -> RandomInteger:
|
||||||
|
"""Parses random integer syntax"""
|
||||||
|
if len(items) > 1:
|
||||||
|
prefixes = sum_dict(items[0:-1]) # If there are prefixes
|
||||||
|
text_prefix = prefixes.pop("text")
|
||||||
|
prefixes["prefix"] = text_prefix
|
||||||
|
val = items[-1][1:-1].split(",")
|
||||||
|
return RandomInteger(
|
||||||
|
min=int(val[0]),
|
||||||
|
max=int(val[1]),
|
||||||
|
text=text_prefix + items[-1],
|
||||||
|
local_options=prefixes,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
val = items[0][1:-1].split(",")
|
||||||
|
return RandomInteger(min=int(val[0]), max=int(val[1]), text=items[0])
|
||||||
|
|
||||||
|
def random_integer_re(self, items):
|
||||||
|
"""Return random integer notation from regex"""
|
||||||
|
return items[0].value
|
||||||
|
|
||||||
|
def range(self, items) -> Range:
|
||||||
"""Parses range syntax"""
|
"""Parses range syntax"""
|
||||||
val = item[0].split("..")
|
if len(items) > 1:
|
||||||
return Range(start=val[0], end=val[1], text=item[0])
|
prefixes = sum_dict(items[0:-1]) # If there are prefixes
|
||||||
|
text_prefix = prefixes.pop("text")
|
||||||
|
prefixes["prefix"] = text_prefix
|
||||||
|
val = items[-1].split("..")
|
||||||
|
return Range(
|
||||||
|
start=int(val[0]),
|
||||||
|
end=int(val[1]),
|
||||||
|
text=text_prefix + items[-1],
|
||||||
|
local_options=prefixes,
|
||||||
|
)
|
||||||
|
# Else
|
||||||
|
val = items[0].split("..")
|
||||||
|
return Range(start=int(val[0]), end=int(val[1]), text=items[0])
|
||||||
|
|
||||||
|
def range_re(self, items):
|
||||||
|
"""Return range value from regex"""
|
||||||
|
return items[0].value
|
||||||
|
|
||||||
def cycle(self, items) -> Cyclic:
|
def cycle(self, items) -> Cyclic:
|
||||||
"""Parses cycle"""
|
"""Parses cycle"""
|
||||||
values = items[0]
|
values = items[0]
|
||||||
return Cyclic(values=values)
|
return Cyclic(values=values)
|
||||||
|
|
||||||
def pitch_class(self, item):
|
def pitch_class(self, items):
|
||||||
"""Parses pitch class"""
|
"""Parses pitch class"""
|
||||||
|
|
||||||
# If there are prefixes
|
# If there are prefixes
|
||||||
if len(item) > 1:
|
if len(items) > 1:
|
||||||
# Collect&sum prefixes from any order: _qee^s4 etc.
|
# Collect&sum prefixes from any order: _qee^s4 etc.
|
||||||
result = sum_dict(item)
|
prefixes = sum_dict(items[0:-1]) # If there are prefixes
|
||||||
return Pitch(**result)
|
text_prefix = prefixes.pop("text")
|
||||||
|
prefixes["prefix"] = text_prefix
|
||||||
|
pitch = Pitch(
|
||||||
|
pitch_class=items[-1]["pitch_class"],
|
||||||
|
text=text_prefix + items[-1]["text"],
|
||||||
|
local_options=prefixes,
|
||||||
|
)
|
||||||
|
return pitch
|
||||||
|
|
||||||
val = item[0]
|
val = items[0]
|
||||||
return Pitch(**val)
|
return Pitch(**val)
|
||||||
|
|
||||||
def pitch(self, items):
|
def pitch(self, items):
|
||||||
"""Return pitch class info"""
|
"""Return pitch class info"""
|
||||||
return {"pitch_class": int(items[0].value), "text": items[0].value}
|
text_value = items[0].value.replace("T", "10").replace("E", "11")
|
||||||
|
return {"pitch_class": int(text_value), "text": items[0].value}
|
||||||
|
|
||||||
|
def escaped_pitch(self, items):
|
||||||
|
"""Return escaped pitch"""
|
||||||
|
val = items[0].value[1:-1]
|
||||||
|
return {"pitch_class": int(val), "text": val}
|
||||||
|
|
||||||
def prefix(self, items):
|
def prefix(self, items):
|
||||||
"""Return prefix"""
|
"""Return prefix"""
|
||||||
@ -88,7 +150,10 @@ class ZiffersTransformer(Transformer):
|
|||||||
def oct_change(self, items):
|
def oct_change(self, items):
|
||||||
"""Parses octave change"""
|
"""Parses octave change"""
|
||||||
octave = items[0]
|
octave = items[0]
|
||||||
return [OctaveChange(value=octave["octave"], text=octave["text"]), items[1]]
|
return [
|
||||||
|
OctaveChange(value=octave["octave_change"], text=octave["text"]),
|
||||||
|
items[1],
|
||||||
|
]
|
||||||
|
|
||||||
def oct_mod(self, items):
|
def oct_mod(self, items):
|
||||||
"""Parses octave modification"""
|
"""Parses octave modification"""
|
||||||
@ -98,7 +163,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
def escaped_octave(self, items):
|
def escaped_octave(self, items):
|
||||||
"""Return octave info"""
|
"""Return octave info"""
|
||||||
value = items[0][1:-1]
|
value = items[0][1:-1]
|
||||||
return {"octave": int(value), "text": items[0].value}
|
return {"octave_change": int(value), "text": items[0].value}
|
||||||
|
|
||||||
def octave(self, items):
|
def octave(self, items):
|
||||||
"""Return octaves ^ and _"""
|
"""Return octaves ^ and _"""
|
||||||
@ -108,27 +173,49 @@ class ZiffersTransformer(Transformer):
|
|||||||
def modifier(self, items):
|
def modifier(self, items):
|
||||||
"""Return modifiers # and b"""
|
"""Return modifiers # and b"""
|
||||||
value = 1 if items[0].value == "#" else -1
|
value = 1 if items[0].value == "#" else -1
|
||||||
return {"modifier": value}
|
return {"modifier": value, "text": items[0].value}
|
||||||
|
|
||||||
def chord(self, items):
|
def chord(self, items):
|
||||||
"""Parses chord"""
|
"""Parses chord"""
|
||||||
|
if isinstance(items[-1], Token):
|
||||||
|
return Chord(
|
||||||
|
pitch_classes=items[0:-1],
|
||||||
|
text="".join([val.text for val in items[0:-1]]),
|
||||||
|
inversions=int(items[-1].value[1:]),
|
||||||
|
)
|
||||||
return Chord(pitch_classes=items, text="".join([val.text for val in items]))
|
return Chord(pitch_classes=items, text="".join([val.text for val in items]))
|
||||||
|
|
||||||
|
def invert(self, items):
|
||||||
|
"""Return chord inversion"""
|
||||||
|
return items[0]
|
||||||
|
|
||||||
def named_roman(self, items) -> RomanNumeral:
|
def named_roman(self, items) -> RomanNumeral:
|
||||||
"""Parse chord from roman numeral"""
|
"""Parse chord from roman numeral"""
|
||||||
numeral = items[0].value
|
numeral = items[0].value
|
||||||
if len(items) > 1:
|
# TODO: Refactor this and the rule
|
||||||
|
if len(items) == 1:
|
||||||
|
return RomanNumeral(value=parse_roman(numeral), text=numeral)
|
||||||
|
if len(items) > 2:
|
||||||
name = items[1]
|
name = items[1]
|
||||||
chord_notes = chord_from_roman_numeral(numeral, name)
|
inversions = int(items[-1].value[1:])
|
||||||
parsed_number = parse_roman(numeral)
|
|
||||||
return RomanNumeral(
|
return RomanNumeral(
|
||||||
text=numeral, value=parsed_number, chord_type=name, notes=chord_notes
|
text=numeral,
|
||||||
|
value=parse_roman(numeral),
|
||||||
|
chord_type=name,
|
||||||
|
inversions=inversions,
|
||||||
)
|
)
|
||||||
return RomanNumeral(
|
elif len(items) == 2:
|
||||||
value=parse_roman(numeral),
|
if isinstance(items[-1], Token):
|
||||||
text=numeral,
|
inversions = int(items[-1].value[1:])
|
||||||
notes=chord_from_roman_numeral(numeral),
|
return RomanNumeral(
|
||||||
)
|
value=parse_roman(numeral),
|
||||||
|
text=numeral,
|
||||||
|
inversions=inversions,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return RomanNumeral(
|
||||||
|
value=parse_roman(numeral), text=numeral, chord_type=items[1]
|
||||||
|
)
|
||||||
|
|
||||||
def chord_name(self, item):
|
def chord_name(self, item):
|
||||||
"""Return name for chord"""
|
"""Return name for chord"""
|
||||||
@ -136,7 +223,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
def roman_number(self, item):
|
def roman_number(self, item):
|
||||||
"""Return roman numeral"""
|
"""Return roman numeral"""
|
||||||
return item.value
|
return item[0]
|
||||||
|
|
||||||
def dur_change(self, items):
|
def dur_change(self, items):
|
||||||
"""Parses duration change"""
|
"""Parses duration change"""
|
||||||
@ -213,9 +300,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
def subdivision(self, items):
|
def subdivision(self, items):
|
||||||
"""Parse subdivision"""
|
"""Parse subdivision"""
|
||||||
values = flatten(items[0])
|
values = flatten(items[0])
|
||||||
return Subdivision(
|
return Subdivision(values=values, wrap_start="[", wrap_end="]")
|
||||||
values=values, text="[" + "".join([val.text for val in values]) + "]"
|
|
||||||
)
|
|
||||||
|
|
||||||
def subitems(self, items):
|
def subitems(self, items):
|
||||||
"""Return subdivision items"""
|
"""Return subdivision items"""
|
||||||
@ -225,8 +310,7 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
def eval(self, items):
|
def eval(self, items):
|
||||||
"""Parse eval"""
|
"""Parse eval"""
|
||||||
val = items[0]
|
return Eval(values=items)
|
||||||
return Eval(values=val)
|
|
||||||
|
|
||||||
def sub_operations(self, items):
|
def sub_operations(self, items):
|
||||||
"""Returns list of operations"""
|
"""Returns list of operations"""
|
||||||
@ -234,12 +318,52 @@ class ZiffersTransformer(Transformer):
|
|||||||
|
|
||||||
def operation(self, items):
|
def operation(self, items):
|
||||||
"""Return partial eval operations"""
|
"""Return partial eval operations"""
|
||||||
return flatten(items)
|
if isinstance(items[0], dict):
|
||||||
|
local_opts = items[0]
|
||||||
|
del local_opts["text"]
|
||||||
|
return Operation(values=flatten(items[1:]), local_options=items[0])
|
||||||
|
return Operation(values=flatten(items))
|
||||||
|
|
||||||
def atom(self, token):
|
def atom(self, token):
|
||||||
"""Return partial eval item"""
|
"""Return partial eval item"""
|
||||||
val = token[0].value
|
val = token[0].value
|
||||||
return Atom(value=val, text=val)
|
return Atom(value=val, text=str(val))
|
||||||
|
|
||||||
|
# Variable assignment
|
||||||
|
|
||||||
|
def assignment(self, items):
|
||||||
|
"""Creates variable assignment"""
|
||||||
|
var = items[0]
|
||||||
|
op = items[1]
|
||||||
|
content = items[2]
|
||||||
|
return VariableAssignment(
|
||||||
|
variable=var,
|
||||||
|
value=content,
|
||||||
|
text=var.text + "=" + content.text,
|
||||||
|
pre_eval=True if op == "=" else False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def ass_op(self, items):
|
||||||
|
"""Return parsed type for assignment: = or ~"""
|
||||||
|
return items[0].value
|
||||||
|
|
||||||
|
def variable(self, items):
|
||||||
|
"""Return variable"""
|
||||||
|
if len(items) > 1:
|
||||||
|
prefixes = sum_dict(items[0:-1])
|
||||||
|
text_prefix = prefixes.pop("text")
|
||||||
|
return Variable(
|
||||||
|
name=items[-1], text=text_prefix + items[-1], local_options=prefixes
|
||||||
|
)
|
||||||
|
return Variable(name=items[0], text=items[0])
|
||||||
|
|
||||||
|
def variable_char(self, items):
|
||||||
|
"""Return parsed variable name"""
|
||||||
|
return items[0].value # Variable(name=items[0].value, text=items[0].value)
|
||||||
|
|
||||||
|
def variablelist(self, items):
|
||||||
|
"""Return list of variables"""
|
||||||
|
return VariableList(values=items, text="".join([item.text for item in items]))
|
||||||
|
|
||||||
# List rules
|
# List rules
|
||||||
|
|
||||||
@ -247,9 +371,14 @@ class ZiffersTransformer(Transformer):
|
|||||||
"""Parse list sequence notation, ex: (1 2 3)"""
|
"""Parse list sequence notation, ex: (1 2 3)"""
|
||||||
if len(items) > 1:
|
if len(items) > 1:
|
||||||
prefixes = sum_dict(items[0:-1])
|
prefixes = sum_dict(items[0:-1])
|
||||||
|
text_prefix = prefixes.pop("text")
|
||||||
|
prefixes["prefix"] = text_prefix
|
||||||
values = items[-1]
|
values = items[-1]
|
||||||
seq = ListSequence(values=values, wrap_start=prefixes["text"] + "(")
|
seq = ListSequence(
|
||||||
seq.update_values(prefixes)
|
values=values,
|
||||||
|
wrap_start=prefixes["prefix"] + "(",
|
||||||
|
local_options=prefixes,
|
||||||
|
)
|
||||||
return seq
|
return seq
|
||||||
else:
|
else:
|
||||||
seq = ListSequence(values=items[0])
|
seq = ListSequence(values=items[0])
|
||||||
@ -264,12 +393,13 @@ class ZiffersTransformer(Transformer):
|
|||||||
values=items[-2],
|
values=items[-2],
|
||||||
repeats=items[-1],
|
repeats=items[-1],
|
||||||
wrap_end=":" + items[-1].text + ")",
|
wrap_end=":" + items[-1].text + ")",
|
||||||
|
local_options=prefixes,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
seq = RepeatedListSequence(
|
seq = RepeatedListSequence(
|
||||||
values=items[-2], repeats=Integer(text="1", value=1)
|
values=items[-2],
|
||||||
|
repeats=Integer(text="2", value=2, local_options=prefixes),
|
||||||
)
|
)
|
||||||
seq.update_values(prefixes)
|
|
||||||
return seq
|
return seq
|
||||||
else:
|
else:
|
||||||
if items[-1] is not None:
|
if items[-1] is not None:
|
||||||
@ -280,13 +410,14 @@ class ZiffersTransformer(Transformer):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
seq = RepeatedListSequence(
|
seq = RepeatedListSequence(
|
||||||
values=items[-2], repeats=Integer(text="1", value=1)
|
values=items[-2], repeats=Integer(text="2", value=2)
|
||||||
)
|
)
|
||||||
return seq
|
return seq
|
||||||
|
|
||||||
def NUMBER(self, token):
|
def integer(self, items):
|
||||||
"""Parse integer"""
|
"""Parses integer from single ints"""
|
||||||
val = token.value
|
concatted = sum_dict(items)
|
||||||
|
val = concatted["text"]
|
||||||
return Integer(text=val, value=int(val))
|
return Integer(text=val, value=int(val))
|
||||||
|
|
||||||
def number(self, item):
|
def number(self, item):
|
||||||
@ -300,8 +431,8 @@ class ZiffersTransformer(Transformer):
|
|||||||
def lisp_operation(self, items):
|
def lisp_operation(self, items):
|
||||||
"""Parse lisp like list operation"""
|
"""Parse lisp like list operation"""
|
||||||
op = items[0]
|
op = items[0]
|
||||||
values = items[1:]
|
values = items[2]
|
||||||
return Operation(
|
return LispOperation(
|
||||||
operator=op,
|
operator=op,
|
||||||
values=values,
|
values=values,
|
||||||
text="(+" + "".join([v.text for v in values]) + ")",
|
text="(+" + "".join([v.text for v in values]) + ")",
|
||||||
@ -312,6 +443,11 @@ class ZiffersTransformer(Transformer):
|
|||||||
val = token[0].value
|
val = token[0].value
|
||||||
return Operator(text=val, value=OPERATORS[val])
|
return Operator(text=val, value=OPERATORS[val])
|
||||||
|
|
||||||
|
def list_operator(self, token):
|
||||||
|
"""Parse list operator"""
|
||||||
|
val = token[0].value
|
||||||
|
return Operator(text=val, value=OPERATORS[val])
|
||||||
|
|
||||||
def list_items(self, items):
|
def list_items(self, items):
|
||||||
"""Parse sequence"""
|
"""Parse sequence"""
|
||||||
return Sequence(values=items)
|
return Sequence(values=items)
|
||||||
@ -320,17 +456,17 @@ class ZiffersTransformer(Transformer):
|
|||||||
"""Parse list operation"""
|
"""Parse list operation"""
|
||||||
return ListOperation(values=items)
|
return ListOperation(values=items)
|
||||||
|
|
||||||
def right_op(self,items):
|
def right_op(self, items):
|
||||||
"""Get right value for the operation"""
|
"""Get right value for the operation"""
|
||||||
return items[0]
|
return items[0]
|
||||||
|
|
||||||
def euclid(self, items):
|
def euclid(self, items):
|
||||||
"""Parse euclid notation"""
|
"""Parse euclid notation"""
|
||||||
params = items[1][1:-1].split(",")
|
params = items[1][1:-1].split(",")
|
||||||
init = {"onset": items[0], "pulses": params[0], "length": params[1]}
|
init = {"onset": items[0], "pulses": int(params[0]), "length": int(params[1])}
|
||||||
text = items[0].text + items[1]
|
text = items[0].text + items[1]
|
||||||
if len(params) > 2:
|
if len(params) > 2:
|
||||||
init["rotate"] = params[2]
|
init["rotate"] = int(params[2])
|
||||||
if len(items) > 2:
|
if len(items) > 2:
|
||||||
init["offset"] = items[2]
|
init["offset"] = items[2]
|
||||||
text = text + items[2].text
|
text = text + items[2].text
|
||||||
@ -348,4 +484,97 @@ class ZiffersTransformer(Transformer):
|
|||||||
values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]"
|
values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return RepeatedSequence(values=items[0], repeats=Integer(value=1, text="1"))
|
return RepeatedSequence(values=items[0], repeats=Integer(value=2, text="2"))
|
||||||
|
|
||||||
|
def repeat_item(self, items):
|
||||||
|
"""Parse repeat item syntax to sequence, ex: 1:4 (1 2 3):5"""
|
||||||
|
return RepeatedListSequence(
|
||||||
|
values=[items[0]],
|
||||||
|
repeats=items[1],
|
||||||
|
wrap_start="",
|
||||||
|
wrap_end=":" + items[1].text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=locally-disabled, unused-argument, too-many-public-methods, invalid-name, eval-used
|
||||||
|
class ScalaTransformer(Transformer):
|
||||||
|
"""Transformer for scala scales"""
|
||||||
|
|
||||||
|
def lines(self, items):
|
||||||
|
"""Transforms cents to semitones"""
|
||||||
|
cents = [
|
||||||
|
ratio_to_cents(item) if isinstance(item, int) else item for item in items
|
||||||
|
]
|
||||||
|
return cents_to_semitones(cents)
|
||||||
|
|
||||||
|
def operation(self, items):
|
||||||
|
"""Get operation"""
|
||||||
|
# Safe eval. Items are pre-parsed.
|
||||||
|
val = eval("".join(str(item) for item in items))
|
||||||
|
return val
|
||||||
|
|
||||||
|
def operator(self, items):
|
||||||
|
"""Get operator"""
|
||||||
|
return items[0].value
|
||||||
|
|
||||||
|
def sub_operations(self, items):
|
||||||
|
"""Get sub-operation"""
|
||||||
|
return "(" + items[0] + ")"
|
||||||
|
|
||||||
|
def frac_ratio(self, items):
|
||||||
|
"""Get ration as fraction"""
|
||||||
|
ratio = items[0] / items[1]
|
||||||
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def decimal_ratio(self, items):
|
||||||
|
"""Get ratio as decimal"""
|
||||||
|
ratio = float(str(items[0]) + "." + str(items[1]))
|
||||||
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def monzo(self, items):
|
||||||
|
"""Get monzo ratio"""
|
||||||
|
return monzo_to_cents(items)
|
||||||
|
|
||||||
|
def edo_ratio(self, items):
|
||||||
|
"""Get EDO ratio"""
|
||||||
|
ratio = pow(2, items[0] / items[1])
|
||||||
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def edji_ratio(self, items):
|
||||||
|
"""Get EDJI ratio"""
|
||||||
|
if len(items) > 3:
|
||||||
|
power = items[2] / items[3]
|
||||||
|
else:
|
||||||
|
power = items[2]
|
||||||
|
ratio = pow(power, items[0] / items[1])
|
||||||
|
return ratio_to_cents(ratio)
|
||||||
|
|
||||||
|
def int(self, items):
|
||||||
|
"""Get integer"""
|
||||||
|
return int(items[0].value)
|
||||||
|
|
||||||
|
def float(self, items):
|
||||||
|
"""Get float"""
|
||||||
|
return float(items[0].value)
|
||||||
|
|
||||||
|
def random_int(self, items):
|
||||||
|
"""Get random integer"""
|
||||||
|
|
||||||
|
def _rand_between(start, end):
|
||||||
|
return random.randint(min(start, end), max(start, end))
|
||||||
|
|
||||||
|
start = items[0]
|
||||||
|
end = items[1]
|
||||||
|
rand_val = _rand_between(start, end)
|
||||||
|
return rand_val
|
||||||
|
|
||||||
|
def random_decimal(self, items):
|
||||||
|
"""Get random decimal"""
|
||||||
|
|
||||||
|
def _rand_between(start, end):
|
||||||
|
return random.uniform(min(start, end), max(start, end))
|
||||||
|
|
||||||
|
start = items[0]
|
||||||
|
end = items[1]
|
||||||
|
rand_val = _rand_between(start, end)
|
||||||
|
return rand_val
|
||||||
|
|||||||
@ -1,21 +1,45 @@
|
|||||||
""" Module for the parser """
|
""" Module for the parser """
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from functools import lru_cache
|
||||||
from lark import Lark
|
from lark import Lark
|
||||||
from .classes import Ziffers
|
from .classes.root import Ziffers
|
||||||
from .mapper import ZiffersTransformer
|
from .mapper import ZiffersTransformer, ScalaTransformer
|
||||||
|
|
||||||
|
|
||||||
grammar_path = Path(__file__).parent
|
grammar_path = Path(__file__).parent
|
||||||
grammar = grammar_path / "ziffers.lark"
|
grammar_folder = Path.joinpath(grammar_path, "spec")
|
||||||
|
ziffers_grammar = Path.joinpath(grammar_folder, "ziffers.lark")
|
||||||
|
scala_grammar = Path.joinpath(grammar_folder, "scala.lark")
|
||||||
|
|
||||||
ziffers_parser = Lark.open(
|
ziffers_parser = Lark.open(
|
||||||
grammar,
|
str(ziffers_grammar),
|
||||||
rel_to=__file__,
|
rel_to=__file__,
|
||||||
start="root",
|
start="root",
|
||||||
parser="lalr",
|
parser="lalr",
|
||||||
transformer=ZiffersTransformer(),
|
transformer=ZiffersTransformer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
scala_parser = Lark.open(
|
||||||
|
str(scala_grammar),
|
||||||
|
rel_to=__file__,
|
||||||
|
start="root",
|
||||||
|
parser="lalr",
|
||||||
|
transformer=ScalaTransformer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_scala(expr: str):
|
||||||
|
"""Parse an expression using the Ziffers parser
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (str): Ziffers expression as a string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ziffers: Reutrns Ziffers iterable
|
||||||
|
"""
|
||||||
|
# Ignore everything before last comment "!"
|
||||||
|
values = expr.split("!")[-1]
|
||||||
|
return scala_parser.parse(values)
|
||||||
|
|
||||||
|
|
||||||
def parse_expression(expr: str) -> Ziffers:
|
def parse_expression(expr: str) -> Ziffers:
|
||||||
"""Parse an expression using the Ziffers parser
|
"""Parse an expression using the Ziffers parser
|
||||||
@ -28,7 +52,7 @@ def parse_expression(expr: str) -> Ziffers:
|
|||||||
"""
|
"""
|
||||||
return ziffers_parser.parse(expr)
|
return ziffers_parser.parse(expr)
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
def zparse(expr: str, **opts) -> Ziffers:
|
def zparse(expr: str, **opts) -> Ziffers:
|
||||||
"""Parses ziffers expression with options
|
"""Parses ziffers expression with options
|
||||||
|
|
||||||
@ -39,14 +63,32 @@ def zparse(expr: str, **opts) -> Ziffers:
|
|||||||
Returns:
|
Returns:
|
||||||
Ziffers: Returns Ziffers iterable parsed with the given options
|
Ziffers: Returns Ziffers iterable parsed with the given options
|
||||||
"""
|
"""
|
||||||
|
if "scale" in opts:
|
||||||
|
scale = opts["scale"]
|
||||||
|
if isinstance(scale,str) and not scale.isalpha():
|
||||||
|
parsed_scale = parse_scala(scale)
|
||||||
|
opts["scale"] = parsed_scale
|
||||||
|
|
||||||
parsed = parse_expression(expr)
|
parsed = parse_expression(expr)
|
||||||
parsed.init_opts(opts)
|
parsed.init_opts(opts)
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
def z(expr: str, **opts) -> Ziffers:
|
def z(expr: str, **opts) -> Ziffers:
|
||||||
"""Shortened method name for zparse"""
|
"""Shortened method name for zparse"""
|
||||||
return zparse(expr, **opts)
|
return zparse(expr, **opts)
|
||||||
|
|
||||||
|
def yield_items(gen: Ziffers, num: int, key: str = None) -> list:
|
||||||
|
"""Yield n items from parsed Ziffers"""
|
||||||
|
for i in range(num):
|
||||||
|
if key is not None:
|
||||||
|
yield getattr(gen[i],key,None)
|
||||||
|
else:
|
||||||
|
yield gen[i]
|
||||||
|
|
||||||
|
def get_items(gen: Ziffers, num: int, key: str = None) -> list:
|
||||||
|
"""Get n-item from parsed Ziffers. Functional alternative to Ziffers-object collect method."""
|
||||||
|
return list(yield_items(gen,num,key))
|
||||||
|
|
||||||
233
ziffers/scale.py
233
ziffers/scale.py
@ -2,9 +2,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=locally-disabled, no-name-in-module
|
# pylint: disable=locally-disabled, no-name-in-module
|
||||||
import re
|
import re
|
||||||
from math import floor
|
from math import log2
|
||||||
|
from itertools import islice
|
||||||
|
from .generators import gen_primes
|
||||||
|
from .common import repeat_text
|
||||||
from .defaults import (
|
from .defaults import (
|
||||||
DEFAULT_OCTAVE,
|
|
||||||
SCALES,
|
SCALES,
|
||||||
MODIFIERS,
|
MODIFIERS,
|
||||||
NOTES_TO_INTERVALS,
|
NOTES_TO_INTERVALS,
|
||||||
@ -45,6 +47,12 @@ def note_name_to_interval(name: str) -> int:
|
|||||||
return interval + modifier
|
return interval + modifier
|
||||||
|
|
||||||
|
|
||||||
|
def midi_to_freq(note: int) -> float:
|
||||||
|
"""Transform midi to frequency"""
|
||||||
|
freq = 440 # Frequency of A
|
||||||
|
return (freq / 32) * (2 ** ((note - 9) / 12))
|
||||||
|
|
||||||
|
|
||||||
def note_name_to_midi(name: str) -> int:
|
def note_name_to_midi(name: str) -> int:
|
||||||
"""Parse note name to midi
|
"""Parse note name to midi
|
||||||
|
|
||||||
@ -64,7 +72,7 @@ def note_name_to_midi(name: str) -> int:
|
|||||||
return 12 + octave * 12 + interval + modifier
|
return 12 + octave * 12 + interval + modifier
|
||||||
|
|
||||||
|
|
||||||
def get_scale(name: str) -> list[int]:
|
def get_scale(scale: str) -> list[int]:
|
||||||
"""Get a scale from the global scale list
|
"""Get a scale from the global scale list
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -73,18 +81,83 @@ def get_scale(name: str) -> list[int]:
|
|||||||
Returns:
|
Returns:
|
||||||
list: List of intervals in the scale
|
list: List of intervals in the scale
|
||||||
"""
|
"""
|
||||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"])
|
if isinstance(scale, (list, tuple)):
|
||||||
return list(map(int, str(scale)))
|
return scale
|
||||||
|
|
||||||
|
scale = SCALES.get(scale.lower().capitalize(), SCALES["Ionian"])
|
||||||
|
return scale
|
||||||
|
|
||||||
|
|
||||||
|
def get_scale_notes(name: str, root: int = 60, num_octaves: int = 1) -> list[int]:
|
||||||
|
"""Return notes for the scale
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the scale
|
||||||
|
root (int, optional): Root note. Defaults to 60.
|
||||||
|
num_octaves (int, optional): Number of octaves. Defaults to 1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: List of notes
|
||||||
|
"""
|
||||||
|
scale = get_scale(name)
|
||||||
|
scale_notes = [root]
|
||||||
|
for _ in range(num_octaves):
|
||||||
|
scale_notes = scale_notes + [root := root + semitone for semitone in scale]
|
||||||
|
return scale_notes
|
||||||
|
|
||||||
|
|
||||||
|
def get_chord_from_scale(
|
||||||
|
degree: int,
|
||||||
|
root: int = 60,
|
||||||
|
scale: str | tuple = "Major",
|
||||||
|
num_notes: int = 3,
|
||||||
|
skip: int = 2,
|
||||||
|
) -> list[int]:
|
||||||
|
"""Generate chord from the scale by skipping notes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
degree (int): Degree of scale to start on
|
||||||
|
root (int, optional): Root for the scale. Defaults to 60.
|
||||||
|
scale (str, optional): Name of the scale. Defaults to "Major".
|
||||||
|
num_notes (int, optional): Number of notes. Defaults to 3.
|
||||||
|
skip (int, optional): Takes every n from the scale. Defaults to 2.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: List of midi notes
|
||||||
|
"""
|
||||||
|
if isinstance(scale, str):
|
||||||
|
scale_length = get_scale_length(scale)
|
||||||
|
else:
|
||||||
|
scale_length = len(scale)
|
||||||
|
|
||||||
|
num_of_octaves = ((num_notes * skip + degree) // scale_length) + 1
|
||||||
|
scale_notes = get_scale_notes(scale, root, num_of_octaves)
|
||||||
|
return scale_notes[degree - 1 :: skip][:num_notes]
|
||||||
|
|
||||||
|
|
||||||
|
def get_scale_length(scale: str) -> int:
|
||||||
|
"""Get length of the scale
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the scale
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Length of the scale
|
||||||
|
"""
|
||||||
|
if isinstance(scale, (list, tuple)):
|
||||||
|
return len(scale)
|
||||||
|
|
||||||
|
return len(SCALES.get(scale.lower().capitalize(), SCALES["Ionian"]))
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=locally-disabled, too-many-arguments
|
# pylint: disable=locally-disabled, too-many-arguments
|
||||||
def note_from_pc(
|
def note_from_pc(
|
||||||
root: int | str,
|
root: int | str,
|
||||||
pitch_class: int,
|
pitch_class: int,
|
||||||
intervals: str | list[int | float],
|
intervals: str | tuple[int | float],
|
||||||
cents: bool = False,
|
|
||||||
octave: int = 0,
|
octave: int = 0,
|
||||||
modifier: int = 0,
|
modifier: int = 0,
|
||||||
|
degrees: bool = False
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Resolve a pitch class into a note from a scale
|
"""Resolve a pitch class into a note from a scale
|
||||||
|
|
||||||
@ -101,14 +174,14 @@ def note_from_pc(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Initialization
|
# Initialization
|
||||||
|
pitch_class = pitch_class-1 if degrees and pitch_class>0 else pitch_class
|
||||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||||
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
|
intervals = get_scale(intervals) if isinstance(intervals, str) else intervals
|
||||||
intervals = list(map(lambda x: x / 100), intervals) if cents else intervals
|
|
||||||
scale_length = len(intervals)
|
scale_length = len(intervals)
|
||||||
|
|
||||||
# Resolve pitch classes to the scale and calculate octave
|
# Resolve pitch classes to the scale and calculate octave
|
||||||
if pitch_class >= scale_length or pitch_class < 0:
|
if pitch_class >= scale_length or pitch_class < 0:
|
||||||
octave += floor(pitch_class / scale_length)
|
octave += pitch_class // scale_length
|
||||||
pitch_class = (
|
pitch_class = (
|
||||||
scale_length - (abs(pitch_class) % scale_length)
|
scale_length - (abs(pitch_class) % scale_length)
|
||||||
if pitch_class < 0
|
if pitch_class < 0
|
||||||
@ -120,7 +193,12 @@ def note_from_pc(
|
|||||||
# Computing the result
|
# Computing the result
|
||||||
note = root + sum(intervals[0:pitch_class])
|
note = root + sum(intervals[0:pitch_class])
|
||||||
|
|
||||||
return note + (octave * sum(intervals)) + modifier
|
note = note + (octave * sum(intervals)) + modifier
|
||||||
|
|
||||||
|
if isinstance(note, float):
|
||||||
|
return resolve_pitch_bend(note)
|
||||||
|
|
||||||
|
return (note, None)
|
||||||
|
|
||||||
|
|
||||||
def parse_roman(numeral: str) -> int:
|
def parse_roman(numeral: str) -> int:
|
||||||
@ -154,7 +232,10 @@ def accidentals_from_note_name(name: str) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Integer representing number of flats or sharps: -7 flat to 7 sharp.
|
int: Integer representing number of flats or sharps: -7 flat to 7 sharp.
|
||||||
"""
|
"""
|
||||||
idx = CIRCLE_OF_FIFTHS.index(name.upper())
|
if name not in CIRCLE_OF_FIFTHS:
|
||||||
|
name = midi_to_note_name(note_name_to_midi(name))
|
||||||
|
|
||||||
|
idx = CIRCLE_OF_FIFTHS.index(name)
|
||||||
return idx - 6
|
return idx - 6
|
||||||
|
|
||||||
|
|
||||||
@ -182,7 +263,7 @@ def midi_to_tpc(note: int, key: str | int):
|
|||||||
_type_: Tonal Pitch Class value for the note
|
_type_: Tonal Pitch Class value for the note
|
||||||
"""
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
acc = accidentals_from_note_name(key)
|
acc = accidentals_from_note_name(key[0])
|
||||||
else:
|
else:
|
||||||
acc = accidentals_from_midi_note(key)
|
acc = accidentals_from_midi_note(key)
|
||||||
return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc)
|
return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc)
|
||||||
@ -197,7 +278,7 @@ def midi_to_octave(note: int) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
int: Returns default octave in Ziffers where C4 is in octave 0
|
int: Returns default octave in Ziffers where C4 is in octave 0
|
||||||
"""
|
"""
|
||||||
return 0 if note <= 0 else floor(note / 12)
|
return 0 if note <= 0 else note // 12
|
||||||
|
|
||||||
|
|
||||||
def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||||
@ -211,10 +292,10 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: Returns dict containing pitch-class values
|
tuple: Returns dict containing pitch-class values
|
||||||
"""
|
"""
|
||||||
pitch_class = note % 12
|
pitch_class = int(note % 12) # Cast to int "fixes" microtonal scales
|
||||||
octave = midi_to_octave(note) - 5
|
octave = midi_to_octave(note) - 5
|
||||||
if scale.upper() == "CHROMATIC":
|
if isinstance(scale, str) and scale.upper() == "CHROMATIC":
|
||||||
return (str(pitch_class), pitch_class, octave)
|
return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave}
|
||||||
|
|
||||||
sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"]
|
sharps = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"]
|
||||||
flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"]
|
flats = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"]
|
||||||
@ -227,18 +308,57 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
|||||||
npc = sharps[pitch_class]
|
npc = sharps[pitch_class]
|
||||||
|
|
||||||
if len(npc) > 1:
|
if len(npc) > 1:
|
||||||
|
modifier = 1 if (npc[0] == "#") else -1
|
||||||
return {
|
return {
|
||||||
"text": npc,
|
"text": repeat_text("^", "_", octave) + npc,
|
||||||
"pitch_class": int(npc[1]),
|
"pitch_class": int(npc[1]),
|
||||||
"octave": octave,
|
"octave": octave,
|
||||||
"modifier": 1 if (npc[0] == "#") else -1,
|
"modifier": modifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"text": npc, "pitch_class": int(npc), "octave": octave}
|
return {
|
||||||
|
"text": repeat_text("^", "_", octave) + npc,
|
||||||
|
"pitch_class": int(npc),
|
||||||
|
"octave": octave,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def chord_from_roman_numeral(
|
def chord_from_degree(
|
||||||
roman: str, name: str = "major", num_octaves: int = 1
|
degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1
|
||||||
|
) -> list[int]:
|
||||||
|
"""Generate chord from scale
|
||||||
|
|
||||||
|
Args:
|
||||||
|
degree (int): Chord degree
|
||||||
|
name (str): Chord type
|
||||||
|
scale (str): Scale name
|
||||||
|
root (str | int): Root for the chord
|
||||||
|
num_octaves (int, optional): Number of octaves. Defaults to 1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: Created chord as list of midi notes
|
||||||
|
"""
|
||||||
|
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||||
|
|
||||||
|
if (
|
||||||
|
name is None
|
||||||
|
and isinstance(scale, str)
|
||||||
|
and scale.lower().capitalize() == "Chromatic"
|
||||||
|
):
|
||||||
|
name = "major"
|
||||||
|
|
||||||
|
if name:
|
||||||
|
return named_chord_from_degree(degree, name, root, scale, num_octaves)
|
||||||
|
else:
|
||||||
|
return get_chord_from_scale(degree, root, scale)
|
||||||
|
|
||||||
|
|
||||||
|
def named_chord_from_degree(
|
||||||
|
degree: int,
|
||||||
|
name: str = "major",
|
||||||
|
root: int = 60,
|
||||||
|
scale: str = "Major",
|
||||||
|
num_octaves: int = 1,
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Generates chord from given roman numeral and chord name
|
"""Generates chord from given roman numeral and chord name
|
||||||
|
|
||||||
@ -250,11 +370,74 @@ def chord_from_roman_numeral(
|
|||||||
Returns:
|
Returns:
|
||||||
list[int]: _description_
|
list[int]: _description_
|
||||||
"""
|
"""
|
||||||
root = parse_roman(roman) - 1
|
|
||||||
tonic = (DEFAULT_OCTAVE * 12) + root + 12
|
|
||||||
intervals = CHORDS.get(name, CHORDS["major"])
|
intervals = CHORDS.get(name, CHORDS["major"])
|
||||||
|
scale_degree = get_scale_notes(scale, root)[degree - 1]
|
||||||
notes = []
|
notes = []
|
||||||
for cur_oct in range(num_octaves):
|
for cur_oct in range(num_octaves):
|
||||||
for iterval in intervals:
|
for interval in intervals:
|
||||||
notes.append(tonic + iterval + (cur_oct * 12))
|
notes.append(scale_degree + interval + (cur_oct * 12))
|
||||||
return notes
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_pitch_bend(note_value: float, semitones: int = 1) -> int:
|
||||||
|
"""Resolves pitch bend value from float midi note
|
||||||
|
|
||||||
|
Args:
|
||||||
|
note_value (float): Note value as float, eg. 60.41123
|
||||||
|
semitones (int, optional): Number of semitones to scale the pitch bend. Defaults to 1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Returns pitch bend value ranging from 0 to 16383. 8192 means no bend.
|
||||||
|
"""
|
||||||
|
midi_bend_value = 8192
|
||||||
|
if isinstance(note_value, float) and note_value % 1 != 0.0:
|
||||||
|
start_value = (
|
||||||
|
note_value if note_value > round(note_value) else round(note_value)
|
||||||
|
)
|
||||||
|
end_value = round(note_value) if note_value > round(note_value) else note_value
|
||||||
|
bend_diff = midi_to_freq(start_value) / midi_to_freq(end_value)
|
||||||
|
bend_target = 1200 * log2(bend_diff)
|
||||||
|
# https://www.cs.cmu.edu/~rbd/doc/cmt/part7.html
|
||||||
|
midi_bend_value = 8192 + int(8191 * (bend_target / (100 * semitones)))
|
||||||
|
return (note_value, midi_bend_value)
|
||||||
|
|
||||||
|
|
||||||
|
def cents_to_semitones(cents: list) -> tuple[float]:
|
||||||
|
"""Tranform cents to semitones"""
|
||||||
|
if cents[0] != 0.0:
|
||||||
|
cents = [0.0] + cents
|
||||||
|
semitone_scale = []
|
||||||
|
for i, cent in enumerate(cents[:-1]):
|
||||||
|
semitone_interval = (cents[i + 1] - cent) / 100
|
||||||
|
semitone_scale.append(semitone_interval)
|
||||||
|
return tuple(semitone_scale)
|
||||||
|
|
||||||
|
|
||||||
|
def ratio_to_cents(ratio: float) -> float:
|
||||||
|
"""Transform ratio to cents"""
|
||||||
|
return 1200.0 * log2(float(ratio))
|
||||||
|
|
||||||
|
|
||||||
|
def monzo_to_cents(monzo) -> float:
|
||||||
|
"""
|
||||||
|
Convert a monzo to cents using the prime factorization method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
monzo (list): A list of integers representing the exponents of the prime factorization
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The value in cents
|
||||||
|
"""
|
||||||
|
# Calculate the prime factors of the indices in the monzo
|
||||||
|
max_index = len(monzo)
|
||||||
|
primes = list(islice(gen_primes(), max_index + 1))
|
||||||
|
|
||||||
|
# Product of the prime factors raised to the corresponding exponents
|
||||||
|
ratio = 1
|
||||||
|
for i in range(max_index):
|
||||||
|
ratio *= primes[i] ** monzo[i]
|
||||||
|
|
||||||
|
# Frequency ratio to cents
|
||||||
|
cents = 1200 * log2(ratio)
|
||||||
|
|
||||||
|
return cents
|
||||||
|
|||||||
0
ziffers/spec/__init__.py
Normal file
0
ziffers/spec/__init__.py
Normal file
24
ziffers/spec/scala.lark
Normal file
24
ziffers/spec/scala.lark
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
?root: lines
|
||||||
|
|
||||||
|
lines: (number | ratio | monzo | operation)+
|
||||||
|
|
||||||
|
?number: float | int | random_int | random_float
|
||||||
|
random_int: "(" int "," int ")"
|
||||||
|
random_float: "(" float "," float ")"
|
||||||
|
float: /(-?[0-9]+\.[0-9]*)|(\.[0-9]+)/
|
||||||
|
int: /-?[0-9]+/
|
||||||
|
|
||||||
|
?ratio: frac_ratio | edo_ratio | edji_ratio | decimal_ratio
|
||||||
|
frac_ratio: (int | random_int) "/" (int | random_int)
|
||||||
|
edo_ratio: (int | random_int) "\\" (int | random_int)
|
||||||
|
edji_ratio: (int | random_int) "\\" (int | random_int) "<" (int | random_int) "/"? (int | random_int)? ">"
|
||||||
|
decimal_ratio: int "," int
|
||||||
|
|
||||||
|
monzo: "[" int+ ">"
|
||||||
|
|
||||||
|
operation: (number | ratio | monzo) (operator ((number | ratio | monzo) | sub_operations | operation))+
|
||||||
|
!operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>"
|
||||||
|
sub_operations: "(" operation ")"
|
||||||
|
|
||||||
|
%import common.WS
|
||||||
|
%ignore WS
|
||||||
99
ziffers/spec/ziffers.lark
Normal file
99
ziffers/spec/ziffers.lark
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Root for the rules
|
||||||
|
?root: sequence -> start
|
||||||
|
sequence: (pitch_class | repeat_item | assignment | variable | variablelist | rest | dur_change | oct_mod | oct_change | WS | measure | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
|
||||||
|
|
||||||
|
// Pitch classes
|
||||||
|
pitch_class: prefix* (pitch | escaped_pitch)
|
||||||
|
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
||||||
|
pitch: /-?[0-9TE]/
|
||||||
|
escaped_decimal: "<" decimal ">"
|
||||||
|
escaped_octave: /<-?[0-9]>/
|
||||||
|
octave: /[_^]+/
|
||||||
|
modifier: /[#b]/
|
||||||
|
|
||||||
|
measure: "|"
|
||||||
|
|
||||||
|
// Variable assignment
|
||||||
|
assignment: variable ass_op (list | pitch_class | random_integer | random_pitch | cycle | list_op | repeat_item)
|
||||||
|
ass_op: /[=~]/
|
||||||
|
variable: prefix* variable_char
|
||||||
|
variable_char: /[A-Z]/
|
||||||
|
|
||||||
|
variablelist: variable variable+
|
||||||
|
|
||||||
|
// Durations
|
||||||
|
duration_chars: dotted_dur+
|
||||||
|
dotted_dur: dchar dot*
|
||||||
|
decimal: /-?[0-9]+\.[0-9]+/
|
||||||
|
dchar: /[mklpdcwyhnqaefsxtgujzo]/
|
||||||
|
dot: "."
|
||||||
|
|
||||||
|
rest: prefix* "r"
|
||||||
|
|
||||||
|
// Chords
|
||||||
|
chord: pitch_class pitch_class+ invert?
|
||||||
|
named_roman: roman_number (("^" chord_name))? invert? // TODO: Add | ("+" number)
|
||||||
|
chord_name: /[a-zA-Z0-9]+/
|
||||||
|
!roman_number: "i" | "ii" | "iii" | "iv" | "v" | "vi" | "vii"
|
||||||
|
|
||||||
|
invert: /%-?[0-9][0-9]*/
|
||||||
|
|
||||||
|
// Valid as integer
|
||||||
|
number: integer | random_integer | cycle
|
||||||
|
integer: pitch+
|
||||||
|
escaped_pitch: /{-?[0-9]+}/
|
||||||
|
|
||||||
|
// Repeats
|
||||||
|
repeat: "[:" sequence ":" [number] "]"
|
||||||
|
repeat_item: (pitch_class | list | list_op | random_integer | cycle | rest | subdivision | chord | named_roman | variable | range) ":" number
|
||||||
|
|
||||||
|
// List
|
||||||
|
list: prefix* "(" sequence ")"
|
||||||
|
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
||||||
|
|
||||||
|
// Right recursive list operation
|
||||||
|
list_op: list ((operator | list_operator) right_op)+
|
||||||
|
right_op: list | number
|
||||||
|
// Operators that work only on lists: | << >>
|
||||||
|
list_operator: /(\||<<|>>|<>|#|@)(?=[(\d])/
|
||||||
|
// /(\||<<|>>|<>|#|@)(?=[(\d])/
|
||||||
|
// Common operators that works with numbers 3+5 3-5 etc.
|
||||||
|
operator: /(\+|-|\*|\/|%|&|\?|~)/
|
||||||
|
|
||||||
|
// Euclidean cycles
|
||||||
|
// TODO: Support randomization etc.
|
||||||
|
//euclid_operator: (">" | "<") number "," number ["," number] (">" | "<")
|
||||||
|
euclid: list euclid_operator list?
|
||||||
|
?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/
|
||||||
|
|
||||||
|
// Lisp like list operation
|
||||||
|
lisp_operator: /([\+\-\*\/%\&])/
|
||||||
|
lisp_operation: "(" lisp_operator WS sequence ")"
|
||||||
|
|
||||||
|
// Subdivision
|
||||||
|
subdivision: "[" subitems "]"
|
||||||
|
subitems: (pitch_class | random_integer | random_pitch | rest | oct_mod | oct_change | WS | chord | named_roman | cycle | subdivision | list | list_op | range | repeat_item)*
|
||||||
|
|
||||||
|
// Control characters modifying future events
|
||||||
|
oct_mod: octave WS
|
||||||
|
oct_change: escaped_octave WS
|
||||||
|
dur_change: (decimal | char_change)
|
||||||
|
char_change: dchar_not_prefix+
|
||||||
|
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >)])/
|
||||||
|
|
||||||
|
// Generative rules
|
||||||
|
random_integer: prefix* random_integer_re
|
||||||
|
random_integer_re: /\(-?[0-9]+,-?[0-9]+\)/
|
||||||
|
range: prefix* range_re
|
||||||
|
range_re: /-?[0-9]+\.\.-?[0-9]+/
|
||||||
|
cycle: "<" sequence ">"
|
||||||
|
random_pitch: /(\?)(?!\d)/
|
||||||
|
random_percent: /(%)(?!\d)/
|
||||||
|
|
||||||
|
// Rules for evaluating clauses inside {}
|
||||||
|
eval: "{" ((operation | rest) WS?)+ "}"
|
||||||
|
operation: prefix? atom (operator (sub_operations | operation))*
|
||||||
|
sub_operations: "(" operation ")"
|
||||||
|
atom: number
|
||||||
|
|
||||||
|
%import common.WS
|
||||||
@ -1,81 +0,0 @@
|
|||||||
// Root for the rules
|
|
||||||
?root: sequence -> start
|
|
||||||
sequence: (pitch_class | dur_change | oct_mod | oct_change | WS | chord | named_roman | cycle | random_integer | random_pitch | random_percent | range | list | repeated_list | lisp_operation | list_op | subdivision | eval | euclid | repeat)*
|
|
||||||
|
|
||||||
// Pitch classes
|
|
||||||
pitch_class: prefix* pitch
|
|
||||||
prefix: (octave | duration_chars | escaped_decimal | escaped_octave | modifier)
|
|
||||||
pitch: /-?[0-9TE]/
|
|
||||||
escaped_decimal: "<" decimal ">"
|
|
||||||
escaped_octave: /<-?[0-9]>/
|
|
||||||
octave: /[_^]+/
|
|
||||||
modifier: /[#b]/
|
|
||||||
|
|
||||||
// Chords
|
|
||||||
chord: pitch_class pitch_class+
|
|
||||||
named_roman: roman_number (("^" chord_name))? // TODO: Add | ("+" number)
|
|
||||||
chord_name: /[a-zA-Z0-9]+/
|
|
||||||
?roman_number: /iv|v|v?i{1,3}/
|
|
||||||
|
|
||||||
// Valid as integer
|
|
||||||
number: NUMBER | random_integer | cyclic_number
|
|
||||||
cyclic_number: "<" number (WS number)* ">"
|
|
||||||
|
|
||||||
// Repeats
|
|
||||||
repeat: "[:" sequence ":" [number] "]"
|
|
||||||
|
|
||||||
// List
|
|
||||||
list: prefix* "(" sequence ")"
|
|
||||||
repeated_list: prefix* "(:" sequence ":" [number] ")"
|
|
||||||
|
|
||||||
// Right recursive list operation
|
|
||||||
list_op: list (operator right_op)+
|
|
||||||
right_op: list | number
|
|
||||||
operator: /([\+\-\*\/%]|<<|>>)/
|
|
||||||
|
|
||||||
// Euclidean cycles
|
|
||||||
euclid: list euclid_operator list?
|
|
||||||
?euclid_operator: /<[0-9]+,[0-9]+(,[0-9])?>/
|
|
||||||
|
|
||||||
// TODO: Support randomization etc.
|
|
||||||
//euclid_operator: (">" | "<") number "," number ["," number] (">" | "<")
|
|
||||||
|
|
||||||
// Lisp like list operation
|
|
||||||
lisp_operation: "(" operator WS sequence ")"
|
|
||||||
|
|
||||||
// Durations
|
|
||||||
duration_chars: dotted_dur+
|
|
||||||
dotted_dur: dchar dot*
|
|
||||||
decimal: /-?[0-9]+\.[0-9]+/
|
|
||||||
dchar: /[mklpdcwyhnqaefsxtgujzo]/
|
|
||||||
dot: "."
|
|
||||||
|
|
||||||
// Subdivision
|
|
||||||
subdivision: "[" subitems "]"
|
|
||||||
subitems: (pitch_class | WS | chord | cycle | subdivision)*
|
|
||||||
|
|
||||||
// Control characters modifying future events
|
|
||||||
oct_mod: octave WS
|
|
||||||
oct_change: escaped_octave WS
|
|
||||||
dur_change: (decimal | char_change)
|
|
||||||
char_change: dchar_not_prefix+
|
|
||||||
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?!\d)/
|
|
||||||
|
|
||||||
// Generative rules
|
|
||||||
random_integer: /\(-?[0-9]+,-?[0-9]+\)/
|
|
||||||
range: /-?[0-9]\.\.-?[0-9]/
|
|
||||||
cycle: "<" sequence ">"
|
|
||||||
random_pitch: /(\?)(?!\d)/
|
|
||||||
random_percent: /(%)(?!\d)/
|
|
||||||
|
|
||||||
// Rules for evaluating clauses inside {}
|
|
||||||
// TODO: Support for parenthesis?
|
|
||||||
eval: "{" operation+ "}"
|
|
||||||
operation: atom (operator (sub_operations | operation))*
|
|
||||||
sub_operations: "(" operation ")"
|
|
||||||
atom: (NUMBER | DECIMAL)
|
|
||||||
|
|
||||||
%import common.NUMBER
|
|
||||||
//%import common.SIGNED_NUMBER
|
|
||||||
%import common.DECIMAL
|
|
||||||
%import common.WS
|
|
||||||
Reference in New Issue
Block a user