diff --git a/example.musicxml b/example.musicxml new file mode 100644 index 0000000..5dafd65 --- /dev/null +++ b/example.musicxml @@ -0,0 +1,101 @@ + + + + Music21 Fragment + + Music21 + + 2023-06-27 + music21 v.9.1.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + + G + 2 + + + + + D + 0 + 4 + + 10080 + quarter + + + + E + 0 + 4 + + 10080 + quarter + + + + 10080 + quarter + + + + D + 0 + 4 + + 5040 + eighth + + + + + E + 0 + 4 + + 5040 + eighth + natural + + + + + G + 0 + 4 + + 5040 + eighth + + + + 5040 + eighth + + + light-heavy + + + + \ No newline at end of file diff --git a/examples/csound/play_with_csound.py b/examples/csound/play_with_csound.py new file mode 100644 index 0000000..95ebb66 --- /dev/null +++ b/examples/csound/play_with_csound.py @@ -0,0 +1,36 @@ +try: + import ctcsound +except (ImportError,TypeError): + csound_imported = false + +from ziffers import * + +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") + + # Convert to csound score + score = to_csound_score(parsed, 180, 1500, "Meep") + + # Outputs: i {instrument} {start time} {duration} {amplitude} {pitch} + print(score) + + orc = """ + instr Meep + out(linen(oscili(p4,p5),0.1,p3,0.1)) + endin + """ + + c = ctcsound.Csound() + c.setOption("-odac") # Using SetOption() to configure Csound + + 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'.") \ No newline at end of file diff --git a/examples/music21/create_midi_file.py b/examples/music21/create_midi_file.py new file mode 100644 index 0000000..9423d5c --- /dev/null +++ b/examples/music21/create_midi_file.py @@ -0,0 +1,20 @@ +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 object +s2 = to_music21(parsed, time="4/4") + +# Merge melody and bass line +#s2.append(to_music21(parsed_bass, time="4/4")) + +# Write to midi file under examples/music21/midi folder +s2.write('midi', fp='examples/music21/output/ziffers_example.mid') \ No newline at end of file diff --git a/examples/music21/create_musicxml.py b/examples/music21/create_musicxml.py new file mode 100644 index 0000000..6a34e28 --- /dev/null +++ b/examples/music21/create_musicxml.py @@ -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') \ No newline at end of file diff --git a/examples/music21/create_png.py b/examples/music21/create_png.py new file mode 100644 index 0000000..1f6893f --- /dev/null +++ b/examples/music21/create_png.py @@ -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") \ No newline at end of file diff --git a/examples/music21/music21_example.py b/examples/music21/music21_example.py deleted file mode 100644 index c53f80d..0000000 --- a/examples/music21/music21_example.py +++ /dev/null @@ -1,8 +0,0 @@ -from music21 import * -from sardine import * -from ziffers import * - - -s = to_music21('(i v vi vii^dim)@(q0 e2 s1 012)',time="4/4") - -s.show('midi') \ No newline at end of file diff --git a/examples/music21/music21_example_2.py b/examples/music21/music21_example_2.py deleted file mode 100644 index 6b8321b..0000000 --- a/examples/music21/music21_example_2.py +++ /dev/null @@ -1,9 +0,0 @@ -from music21 import * -from sardine import * -from ziffers import * - -parsed = zparse('1 2 qr e 124') -s2 = to_music21(parsed,time="4/4") - -s2.show() -s2.show('midi') \ No newline at end of file diff --git a/examples/music21/output/example-1.png b/examples/music21/output/example-1.png new file mode 100644 index 0000000..211c450 Binary files /dev/null and b/examples/music21/output/example-1.png differ diff --git a/examples/music21/output/example.musicxml b/examples/music21/output/example.musicxml new file mode 100644 index 0000000..bda8da7 --- /dev/null +++ b/examples/music21/output/example.musicxml @@ -0,0 +1,99 @@ + + + + + + 2023-06-27 + music21 v.9.1.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + + G + 2 + + + + + D + 0 + 4 + + 10080 + quarter + + + + E + 0 + 4 + + 10080 + quarter + + + + 10080 + quarter + + + + D + 0 + 4 + + 5040 + eighth + + + + + E + 0 + 4 + + 5040 + eighth + natural + + + + + G + 0 + 4 + + 5040 + eighth + + + + 5040 + eighth + + + light-heavy + + + + \ No newline at end of file diff --git a/examples/music21/output/ziffers_example.mid b/examples/music21/output/ziffers_example.mid new file mode 100644 index 0000000..b1902c5 Binary files /dev/null and b/examples/music21/output/ziffers_example.mid differ diff --git a/examples/music21/output/ziffers_example.xml b/examples/music21/output/ziffers_example.xml new file mode 100644 index 0000000..11e60ed --- /dev/null +++ b/examples/music21/output/ziffers_example.xml @@ -0,0 +1,228 @@ + + + + Music21 Fragment + + Music21 + + 2023-06-27 + music21 v.9.1.0 + + + + + 7 + 40 + + + + + + + + + + + + + 10080 + + + G + 2 + + + + + C + 1 + 4 + + 10080 + quarter + sharp + + + + C + 0 + 4 + + 10080 + quarter + natural + + + + + F + 0 + 4 + + 10080 + quarter + + + + + A + 0 + 4 + + 10080 + quarter + + + + C + 0 + 5 + + 10080 + quarter + + + + F + 0 + 4 + + 10080 + quarter + + + + + G + 0 + 4 + + 10080 + quarter + + + + + C + 0 + 5 + + 10080 + quarter + natural + + + + + + + C + 0 + 4 + + 20160 + half + natural + + + + + F + 0 + 4 + + 20160 + half + natural + + + + + G + 0 + 4 + + 20160 + half + natural + + + + + A + 0 + 4 + + 20160 + half + natural + + + + + C + 0 + 5 + + 20160 + half + natural + + + + + D + -1 + 5 + + 20160 + half + flat + + + + + F + 0 + 5 + + 20160 + half + natural + + + + + G + 0 + 5 + + 20160 + half + natural + + + + + F + 0 + 6 + + 20160 + half + natural + + + + 20160 + half + + + light-heavy + + + + \ No newline at end of file diff --git a/examples/music21/show_in_musescore.py b/examples/music21/show_in_musescore.py new file mode 100644 index 0000000..910eaac --- /dev/null +++ b/examples/music21/show_in_musescore.py @@ -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)', scale="Lydian", time="4/4") + +# See https://web.mit.edu/music21/doc/installing/installAdditional.html +# Attempt to open / show the midi in MuseScore +s.show() \ No newline at end of file diff --git a/ziffers/classes/root.py b/ziffers/classes/root.py index 165914a..939a174 100644 --- a/ziffers/classes/root.py +++ b/ziffers/classes/root.py @@ -153,9 +153,17 @@ class Ziffers(Sequence): def pairs(self) -> list[tuple]: """Return list of pitches and durations""" return [ - (val.get_pitch_class(), val.get_duration()) + [val.get_pitch_class(), val.get_duration()] for val in self.evaluated_values - if isinstance(val, Pitch) + 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]: @@ -194,4 +202,4 @@ class Ziffers(Sequence): return all_items if len(all_items) == 1: return all_items[0] - return None + return None \ No newline at end of file diff --git a/ziffers/converters.py b/ziffers/converters.py index 36b124a..1c0418f 100644 --- a/ziffers/converters.py +++ b/ziffers/converters.py @@ -7,12 +7,67 @@ try: except ImportError: music21_imported: bool = False +try: + import ctcsound + csound_imported: bool = True +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}" + +# 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) + + 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):