More examples and some minor fixes

This commit is contained in:
2023-06-29 00:03:45 +03:00
parent 7d6ba407bd
commit 9bd4ec0ff0
14 changed files with 336 additions and 66 deletions

View File

@ -8,6 +8,8 @@ Ziffers python supports following live coding and computer-aided composition env
* [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:

View File

@ -1,3 +1,4 @@
"""Example of using ziffers with Csound score."""
try:
import ctcsound
except (ImportError,TypeError):
@ -5,30 +6,35 @@ except (ImportError,TypeError):
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
parsed = zparse("w 0 1 q 0 1 2 3 r e 5 3 9 2 q r 0")
# 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.")
# Convert to csound score
score = to_csound_score(parsed, 180, 1500, "Meep")
# Outputs: i {instrument} {start time} {duration} {amplitude} {pitch}
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 Meep
instr FooBar
out(linen(oscili(p4,p5),0.1,p3,0.1))
endin
"""
# Run score with Csound
c = ctcsound.Csound()
c.setOption("-odac") # Using SetOption() to configure Csound
c.setOption("-odac")
c.compileOrc(orc)
c.readScore(score)
c.start()
c.perform()
c.stop()

View 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
}

File diff suppressed because one or more lines are too long

View File

@ -1,20 +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"
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)
parsed_bass = zparse(bass_line)
# Convert to music21 object
s2 = to_music21(parsed, time="4/4")
# Convert to music21 objects
part1 = to_music21(parsed, time="4/4")
part2 = to_music21(parsed_bass, time="4/4")
# Merge melody and bass line
#s2.append(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
s2.write('midi', fp='examples/music21/output/ziffers_example.mid')
song.write('midi', fp='examples/music21/output/ziffers_example.mid')

View File

@ -2,7 +2,7 @@ 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)', scale="Lydian", time="4/4")
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

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

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

View File

@ -188,6 +188,16 @@ def test_romans(pattern: str, expected: list):
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",
[

View File

@ -224,6 +224,10 @@ class Pitch(Event):
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"""

View File

@ -555,6 +555,8 @@ class ListOperation(Sequence):
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(

View File

@ -1,8 +1,8 @@
"""Collection of converters"""
from ziffers import zparse, Ziffers, Pitch, Rest, Chord
from ziffers import zparse, Ziffers, Pitch, Rest, Chord, accidentals_from_note_name
try:
from music21 import converter, note, stream, meter, chord, environment
from music21 import converter, note, stream, meter, chord, environment, tempo, key
music21_imported: bool = True
except ImportError:
music21_imported: bool = False
@ -13,52 +13,26 @@ try:
except (ImportError, TypeError) as Error:
csound_imported: bool = False
def freq_to_pch(freq: float) -> str:
"Format frequency to Csound PCH format"
return f"{freq:.2f}"
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")
# Csound score example: i1 0 1 0.5 8.00
# 1. The first number is the instrument number. In this case it is instrument 1.
# 2. The second number is the start time in seconds. In this case it is 0 seconds.
# 3. The third number is the duration in seconds. In this case it is 1 second.
# 4. The fourth number is the amplitude. In this case it is 0.5.
# 5. The fifth number is the frequency. In this case it is 8 Hz.
def freqs_and_durations_to_csound_score(pairs: list[list[float|list[float], float|list[float]]], bpm: int=80, amp: float=0.5, instr: (int|str)=1) -> str:
"""Tranforms list of lists containing frequencies and note lengths to csound score format.
Note lengths are transformed in seconds with the bpm.
Start time in seconds is calculated and summed from the note lengths.
If frequency is None, then it is a rest.
If frequency is a list, then it is a chord.
Example input: [[261.6255653005986, 0.5], [None, 0.25], [440.0, 0.125], [[261.6255653005986, 329.62755691286986, 391.9954359817492], [0.25, 0.125, 0.25]]]"""
score = ""
instr = f'"{instr}"' if isinstance(instr, str) else instr
start_time = 0
for pair in pairs:
if isinstance(pair[0], list):
for freq, dur in zip(pair[0], pair[1]):
score += f"i {instr} {start_time} {dur * 4 * 60 / bpm} {amp} {freq_to_pch(freq)}\n"
start_time += max(pair[1]) * 4 * 60 / bpm
else:
if pair[0] is None:
score += f"i {instr} {start_time} {pair[1] * 4 * 60 / bpm} {amp} 0\n"
else:
score += f"i {instr} {start_time} {pair[1] * 4 * 60 / bpm} {amp} {freq_to_pch(pair[0])}\n"
start_time += pair[1] * 4 * 60 / bpm
return score
def to_csound_score(expression: str | Ziffers, bpm: int=80, amp: float=0.5, instr: (int|str)=1) -> str:
""" Transform Ziffers object to Csound score """
if not csound_imported:
raise ImportError("Install Csound library")
if isinstance(expression, Ziffers):
score = freqs_and_durations_to_csound_score(expression.freq_pairs(),bpm,amp,instr)
else:
parsed = zparse(expression)
score = freqs_and_durations_to_csound_score(parsed.freq_pairs(),bpm,amp,instr)
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):
@ -68,7 +42,7 @@ def to_music21(expression: str | Ziffers, **options):
raise ImportError("Install Music21 library")
# Register the ZiffersMusic21 converter
converter.registerSubconverter(ZiffersMusic21)
converter.registerSubConverter(ZiffersMusic21)
if isinstance(expression, Ziffers):
if options:
@ -118,12 +92,23 @@ if music21_imported:
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"])
if "scale" in options and options["scale"].upper() == "MINOR":
accidentals-=3 # If minor, subtract 3 from accidentals
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)

View File

@ -231,7 +231,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