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
|
||||
|
||||
# Debugging file
|
||||
debug.py
|
||||
# Debugging files
|
||||
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+
|
||||
|
||||
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))> :]",
|
||||
"Lists": "h 1 q(0 1 2 3) 2",
|
||||
"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",
|
||||
"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)",
|
||||
@ -36,7 +36,6 @@ if __name__ == "__main__":
|
||||
}
|
||||
for ex in expressions:
|
||||
try:
|
||||
print("Parsed: " + parse_expression(expressions[ex]).text)
|
||||
print(f"{ex}: " + parse_expression(expressions[ex]).text)
|
||||
except Exception as e:
|
||||
print(f"[red]Failed on {ex}[/red]")
|
||||
# print(f"[red]Failed on {ex}[/red]: {str(e)[0:40]}...")
|
||||
print(f"[red]Failed on {ex}[/red]: "+expressions[ex])
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "python-ziffers"
|
||||
version = "0.1.0"
|
||||
name = "ziffers"
|
||||
version = "0.0.1"
|
||||
description = "Port of the Ziffers numerical notation for Python"
|
||||
authors = [
|
||||
{name = "Raphaël Forment", email="raphael.forment@gmail.com"},
|
||||
@ -13,7 +13,10 @@ requires-python = ">=3.10"
|
||||
keywords = ["mininotation", "algorithmic music", "parser"]
|
||||
|
||||
classifiers = [
|
||||
"Topic :: Software Development"
|
||||
"Topic :: Artistic Software",
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
@ -22,12 +25,34 @@ dependencies = [
|
||||
"pytest>=7.2.1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||
documentation = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||
repository = "https://github.com/Bubobubobubobubo/ziffers-python"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ['ziffers','ziffers.classes','ziffers.spec']
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target_version = ['py310']
|
||||
target_version = ['py311']
|
||||
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
|
||||
from ziffers import scale
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,expected",
|
||||
[
|
||||
@ -49,5 +47,5 @@ def test_notenames(name: str, expected: int):
|
||||
)
|
||||
def test_note_to_midi(pitch_classes: str, expected: int):
|
||||
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
|
||||
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 .mapper import *
|
||||
from .classes import *
|
||||
from .common import *
|
||||
from .defaults 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 """
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
def flatten(arr: list) -> list:
|
||||
"""Flattens array"""
|
||||
return (
|
||||
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:
|
||||
"""Sums a list of dicts: [{a:3,b:3},{b:1}] -> {a:3,b:4}"""
|
||||
result = arr[0]
|
||||
@ -17,3 +44,73 @@ def sum_dict(arr: list[dict]) -> dict:
|
||||
else:
|
||||
result[key] = element[key]
|
||||
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()
|
||||
3365
ziffers/defaults.py
3365
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 """
|
||||
from typing import Optional
|
||||
from lark import Transformer
|
||||
from .classes import (
|
||||
Ziffers,
|
||||
import random
|
||||
from lark import Transformer, Token
|
||||
from .scale import cents_to_semitones, ratio_to_cents, monzo_to_cents
|
||||
from .classes.root import Ziffers
|
||||
from .classes.sequences import (
|
||||
Sequence,
|
||||
ListSequence,
|
||||
RepeatedListSequence,
|
||||
ListOperation,
|
||||
RepeatedSequence,
|
||||
Euclid,
|
||||
Subdivision,
|
||||
Eval,
|
||||
Operation,
|
||||
LispOperation,
|
||||
)
|
||||
from .classes.items import (
|
||||
Whitespace,
|
||||
DurationChange,
|
||||
OctaveChange,
|
||||
OctaveAdd,
|
||||
Pitch,
|
||||
Rest,
|
||||
RandomPitch,
|
||||
RandomPercent,
|
||||
Chord,
|
||||
RomanNumeral,
|
||||
Sequence,
|
||||
ListSequence,
|
||||
RepeatedListSequence,
|
||||
Subdivision,
|
||||
Cyclic,
|
||||
RandomInteger,
|
||||
Range,
|
||||
Operator,
|
||||
ListOperation,
|
||||
Operation,
|
||||
Eval,
|
||||
Atom,
|
||||
Integer,
|
||||
Euclid,
|
||||
RepeatedSequence,
|
||||
VariableAssignment,
|
||||
Variable,
|
||||
VariableList,
|
||||
Measure,
|
||||
)
|
||||
from .common import flatten, sum_dict
|
||||
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
|
||||
class ZiffersTransformer(Transformer):
|
||||
"""Rules for transforming Ziffers expressions into tree."""
|
||||
|
||||
def __init__(self, options: Optional[dict] = None):
|
||||
super().__init__()
|
||||
self.options = options
|
||||
|
||||
def start(self, items) -> Ziffers:
|
||||
"""Root for the rules"""
|
||||
# seq = Sequence(values=items[0])
|
||||
return Ziffers(values=items[0], options={})
|
||||
|
||||
def sequence(self, items):
|
||||
"""Flatten sequence"""
|
||||
return flatten(items)
|
||||
|
||||
def random_integer(self, item) -> RandomInteger:
|
||||
"""Parses random integer syntax"""
|
||||
val = item[0][1:-1].split(",")
|
||||
return RandomInteger(min=int(val[0]), max=int(val[1]), text=item[0].value)
|
||||
def rest(self, items):
|
||||
"""Return duration event"""
|
||||
if len(items) > 0:
|
||||
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"""
|
||||
val = item[0].split("..")
|
||||
return Range(start=val[0], end=val[1], text=item[0])
|
||||
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].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:
|
||||
"""Parses cycle"""
|
||||
values = items[0]
|
||||
return Cyclic(values=values)
|
||||
|
||||
def pitch_class(self, item):
|
||||
def pitch_class(self, items):
|
||||
"""Parses pitch class"""
|
||||
|
||||
# If there are prefixes
|
||||
if len(item) > 1:
|
||||
if len(items) > 1:
|
||||
# Collect&sum prefixes from any order: _qee^s4 etc.
|
||||
result = sum_dict(item)
|
||||
return Pitch(**result)
|
||||
prefixes = sum_dict(items[0:-1]) # If there are prefixes
|
||||
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)
|
||||
|
||||
def pitch(self, items):
|
||||
"""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):
|
||||
"""Return prefix"""
|
||||
@ -88,7 +150,10 @@ class ZiffersTransformer(Transformer):
|
||||
def oct_change(self, items):
|
||||
"""Parses octave change"""
|
||||
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):
|
||||
"""Parses octave modification"""
|
||||
@ -98,7 +163,7 @@ class ZiffersTransformer(Transformer):
|
||||
def escaped_octave(self, items):
|
||||
"""Return octave info"""
|
||||
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):
|
||||
"""Return octaves ^ and _"""
|
||||
@ -108,26 +173,48 @@ class ZiffersTransformer(Transformer):
|
||||
def modifier(self, items):
|
||||
"""Return modifiers # and b"""
|
||||
value = 1 if items[0].value == "#" else -1
|
||||
return {"modifier": value}
|
||||
return {"modifier": value, "text": items[0].value}
|
||||
|
||||
def chord(self, items):
|
||||
"""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]))
|
||||
|
||||
def invert(self, items):
|
||||
"""Return chord inversion"""
|
||||
return items[0]
|
||||
|
||||
def named_roman(self, items) -> RomanNumeral:
|
||||
"""Parse chord from roman numeral"""
|
||||
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]
|
||||
chord_notes = chord_from_roman_numeral(numeral, name)
|
||||
parsed_number = parse_roman(numeral)
|
||||
inversions = int(items[-1].value[1:])
|
||||
return RomanNumeral(
|
||||
text=numeral, value=parsed_number, chord_type=name, notes=chord_notes
|
||||
text=numeral,
|
||||
value=parse_roman(numeral),
|
||||
chord_type=name,
|
||||
inversions=inversions,
|
||||
)
|
||||
elif len(items) == 2:
|
||||
if isinstance(items[-1], Token):
|
||||
inversions = int(items[-1].value[1:])
|
||||
return RomanNumeral(
|
||||
value=parse_roman(numeral),
|
||||
text=numeral,
|
||||
notes=chord_from_roman_numeral(numeral),
|
||||
inversions=inversions,
|
||||
)
|
||||
else:
|
||||
return RomanNumeral(
|
||||
value=parse_roman(numeral), text=numeral, chord_type=items[1]
|
||||
)
|
||||
|
||||
def chord_name(self, item):
|
||||
@ -136,7 +223,7 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def roman_number(self, item):
|
||||
"""Return roman numeral"""
|
||||
return item.value
|
||||
return item[0]
|
||||
|
||||
def dur_change(self, items):
|
||||
"""Parses duration change"""
|
||||
@ -213,9 +300,7 @@ class ZiffersTransformer(Transformer):
|
||||
def subdivision(self, items):
|
||||
"""Parse subdivision"""
|
||||
values = flatten(items[0])
|
||||
return Subdivision(
|
||||
values=values, text="[" + "".join([val.text for val in values]) + "]"
|
||||
)
|
||||
return Subdivision(values=values, wrap_start="[", wrap_end="]")
|
||||
|
||||
def subitems(self, items):
|
||||
"""Return subdivision items"""
|
||||
@ -225,8 +310,7 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def eval(self, items):
|
||||
"""Parse eval"""
|
||||
val = items[0]
|
||||
return Eval(values=val)
|
||||
return Eval(values=items)
|
||||
|
||||
def sub_operations(self, items):
|
||||
"""Returns list of operations"""
|
||||
@ -234,12 +318,52 @@ class ZiffersTransformer(Transformer):
|
||||
|
||||
def operation(self, items):
|
||||
"""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):
|
||||
"""Return partial eval item"""
|
||||
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
|
||||
|
||||
@ -247,9 +371,14 @@ class ZiffersTransformer(Transformer):
|
||||
"""Parse list sequence notation, ex: (1 2 3)"""
|
||||
if len(items) > 1:
|
||||
prefixes = sum_dict(items[0:-1])
|
||||
text_prefix = prefixes.pop("text")
|
||||
prefixes["prefix"] = text_prefix
|
||||
values = items[-1]
|
||||
seq = ListSequence(values=values, wrap_start=prefixes["text"] + "(")
|
||||
seq.update_values(prefixes)
|
||||
seq = ListSequence(
|
||||
values=values,
|
||||
wrap_start=prefixes["prefix"] + "(",
|
||||
local_options=prefixes,
|
||||
)
|
||||
return seq
|
||||
else:
|
||||
seq = ListSequence(values=items[0])
|
||||
@ -264,12 +393,13 @@ class ZiffersTransformer(Transformer):
|
||||
values=items[-2],
|
||||
repeats=items[-1],
|
||||
wrap_end=":" + items[-1].text + ")",
|
||||
local_options=prefixes,
|
||||
)
|
||||
else:
|
||||
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
|
||||
else:
|
||||
if items[-1] is not None:
|
||||
@ -280,13 +410,14 @@ class ZiffersTransformer(Transformer):
|
||||
)
|
||||
else:
|
||||
seq = RepeatedListSequence(
|
||||
values=items[-2], repeats=Integer(text="1", value=1)
|
||||
values=items[-2], repeats=Integer(text="2", value=2)
|
||||
)
|
||||
return seq
|
||||
|
||||
def NUMBER(self, token):
|
||||
"""Parse integer"""
|
||||
val = token.value
|
||||
def integer(self, items):
|
||||
"""Parses integer from single ints"""
|
||||
concatted = sum_dict(items)
|
||||
val = concatted["text"]
|
||||
return Integer(text=val, value=int(val))
|
||||
|
||||
def number(self, item):
|
||||
@ -300,8 +431,8 @@ class ZiffersTransformer(Transformer):
|
||||
def lisp_operation(self, items):
|
||||
"""Parse lisp like list operation"""
|
||||
op = items[0]
|
||||
values = items[1:]
|
||||
return Operation(
|
||||
values = items[2]
|
||||
return LispOperation(
|
||||
operator=op,
|
||||
values=values,
|
||||
text="(+" + "".join([v.text for v in values]) + ")",
|
||||
@ -312,6 +443,11 @@ class ZiffersTransformer(Transformer):
|
||||
val = token[0].value
|
||||
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):
|
||||
"""Parse sequence"""
|
||||
return Sequence(values=items)
|
||||
@ -320,17 +456,17 @@ class ZiffersTransformer(Transformer):
|
||||
"""Parse list operation"""
|
||||
return ListOperation(values=items)
|
||||
|
||||
def right_op(self,items):
|
||||
def right_op(self, items):
|
||||
"""Get right value for the operation"""
|
||||
return items[0]
|
||||
|
||||
def euclid(self, items):
|
||||
"""Parse euclid notation"""
|
||||
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]
|
||||
if len(params) > 2:
|
||||
init["rotate"] = params[2]
|
||||
init["rotate"] = int(params[2])
|
||||
if len(items) > 2:
|
||||
init["offset"] = items[2]
|
||||
text = text + items[2].text
|
||||
@ -348,4 +484,97 @@ class ZiffersTransformer(Transformer):
|
||||
values=items[0], repeats=items[-1], wrap_end=":" + items[-1].text + "]"
|
||||
)
|
||||
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 """
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
from lark import Lark
|
||||
from .classes import Ziffers
|
||||
from .mapper import ZiffersTransformer
|
||||
from .classes.root import Ziffers
|
||||
from .mapper import ZiffersTransformer, ScalaTransformer
|
||||
|
||||
|
||||
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(
|
||||
grammar,
|
||||
str(ziffers_grammar),
|
||||
rel_to=__file__,
|
||||
start="root",
|
||||
parser="lalr",
|
||||
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:
|
||||
"""Parse an expression using the Ziffers parser
|
||||
@ -28,7 +52,7 @@ def parse_expression(expr: str) -> Ziffers:
|
||||
"""
|
||||
return ziffers_parser.parse(expr)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def zparse(expr: str, **opts) -> Ziffers:
|
||||
"""Parses ziffers expression with options
|
||||
|
||||
@ -39,14 +63,32 @@ def zparse(expr: str, **opts) -> Ziffers:
|
||||
Returns:
|
||||
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.init_opts(opts)
|
||||
return parsed
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
def z(expr: str, **opts) -> Ziffers:
|
||||
"""Shortened method name for zparse"""
|
||||
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
|
||||
# pylint: disable=locally-disabled, no-name-in-module
|
||||
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 (
|
||||
DEFAULT_OCTAVE,
|
||||
SCALES,
|
||||
MODIFIERS,
|
||||
NOTES_TO_INTERVALS,
|
||||
@ -45,6 +47,12 @@ def note_name_to_interval(name: str) -> int:
|
||||
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:
|
||||
"""Parse note name to midi
|
||||
|
||||
@ -64,7 +72,7 @@ def note_name_to_midi(name: str) -> int:
|
||||
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
|
||||
|
||||
Args:
|
||||
@ -73,18 +81,83 @@ def get_scale(name: str) -> list[int]:
|
||||
Returns:
|
||||
list: List of intervals in the scale
|
||||
"""
|
||||
scale = SCALES.get(name.lower().capitalize(), SCALES["Chromatic"])
|
||||
return list(map(int, str(scale)))
|
||||
if isinstance(scale, (list, tuple)):
|
||||
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
|
||||
def note_from_pc(
|
||||
root: int | str,
|
||||
pitch_class: int,
|
||||
intervals: str | list[int | float],
|
||||
cents: bool = False,
|
||||
intervals: str | tuple[int | float],
|
||||
octave: int = 0,
|
||||
modifier: int = 0,
|
||||
degrees: bool = False
|
||||
) -> int:
|
||||
"""Resolve a pitch class into a note from a scale
|
||||
|
||||
@ -101,14 +174,14 @@ def note_from_pc(
|
||||
"""
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
# Resolve pitch classes to the scale and calculate octave
|
||||
if pitch_class >= scale_length or pitch_class < 0:
|
||||
octave += floor(pitch_class / scale_length)
|
||||
octave += pitch_class // scale_length
|
||||
pitch_class = (
|
||||
scale_length - (abs(pitch_class) % scale_length)
|
||||
if pitch_class < 0
|
||||
@ -120,7 +193,12 @@ def note_from_pc(
|
||||
# Computing the result
|
||||
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:
|
||||
@ -154,7 +232,10 @@ def accidentals_from_note_name(name: str) -> int:
|
||||
Returns:
|
||||
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
|
||||
|
||||
|
||||
@ -182,7 +263,7 @@ def midi_to_tpc(note: int, key: str | int):
|
||||
_type_: Tonal Pitch Class value for the note
|
||||
"""
|
||||
if isinstance(key, str):
|
||||
acc = accidentals_from_note_name(key)
|
||||
acc = accidentals_from_note_name(key[0])
|
||||
else:
|
||||
acc = accidentals_from_midi_note(key)
|
||||
return (note * 7 + 26 - (11 + acc)) % 12 + (11 + acc)
|
||||
@ -197,7 +278,7 @@ def midi_to_octave(note: int) -> int:
|
||||
Returns:
|
||||
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:
|
||||
@ -211,10 +292,10 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||
Returns:
|
||||
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
|
||||
if scale.upper() == "CHROMATIC":
|
||||
return (str(pitch_class), pitch_class, octave)
|
||||
if isinstance(scale, str) and scale.upper() == "CHROMATIC":
|
||||
return {"text": str(pitch_class), "pitch_class": pitch_class, "octave": octave}
|
||||
|
||||
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"]
|
||||
@ -227,18 +308,57 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
||||
npc = sharps[pitch_class]
|
||||
|
||||
if len(npc) > 1:
|
||||
modifier = 1 if (npc[0] == "#") else -1
|
||||
return {
|
||||
"text": npc,
|
||||
"text": repeat_text("^", "_", octave) + npc,
|
||||
"pitch_class": int(npc[1]),
|
||||
"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(
|
||||
roman: str, name: str = "major", num_octaves: int = 1
|
||||
def chord_from_degree(
|
||||
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]:
|
||||
"""Generates chord from given roman numeral and chord name
|
||||
|
||||
@ -250,11 +370,74 @@ def chord_from_roman_numeral(
|
||||
Returns:
|
||||
list[int]: _description_
|
||||
"""
|
||||
root = parse_roman(roman) - 1
|
||||
tonic = (DEFAULT_OCTAVE * 12) + root + 12
|
||||
intervals = CHORDS.get(name, CHORDS["major"])
|
||||
scale_degree = get_scale_notes(scale, root)[degree - 1]
|
||||
notes = []
|
||||
for cur_oct in range(num_octaves):
|
||||
for iterval in intervals:
|
||||
notes.append(tonic + iterval + (cur_oct * 12))
|
||||
for interval in intervals:
|
||||
notes.append(scale_degree + interval + (cur_oct * 12))
|
||||
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