accraze / python-twelve-tone

:notes: 12-tone matrix to generate dodecaphonic melodies :notes:
https://pypi.python.org/pypi/twelve-tone
BSD 2-Clause "Simplified" License
71 stars 5 forks source link

Alternative implementations of a twelve tone matrix and possible design benefits #33

Open jgarte opened 1 year ago

jgarte commented 1 year ago

Hi,

What do you think if we were to re-implement a twelve tone matrix without needing to use numpy.zeros?

For example we could generate the 12X12 matrix like this:

>>> from pprint import PrettyPrinter
>>> matrix = [[0] * 12] * 12
>>> pp = PrettyPrinter(indent=4)
>>> pp.pprint(matrix)

   [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

We can then proceed to implement the compose function in terms of mutating the above matrix list variable to produce a matrix with the canonical row forms: P, R, I, and RI.

One possible algorithm:

We can loosely implement the steps as described in this instructable:

https://www.instructables.com/Create-a-Twelve-Tone-melody-with-a-Twelve-Tone-Mat/

  1. Compute the interval adjacencies of the initial row and store that in a variable.
  2. Compute the inversion of the initial row using step 1 and store that in a variable.
  3. Use the List[int] variables created in steps 1 and 2 to mutate/compute the rest of the 12 X 12 List[List[int]].

Another alternative could be to outsource the matrix generation logic to the music21 library:

https://web.mit.edu/music21/doc/moduleReference/moduleSerial.html?highlight=row#music21.serial.ToneRow.row

Using music21 would be an implementation detail of twelve-tone.

The cool thing that might arise from using music21 is that we could piggyback on its features to add/invent CLI flags for twelve-tone such as --historical that let's the user ask for a historical tone row, which is kind of cute:

> bergLyric = serial.getHistoricalRowByName('BergLyricSuite')
> bergLyric.pitchClasses()
[5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11]

twelve-tone using music21's getHistoricalRowByName method

$ twelve-tone --historical

F E C A G D G♯ C♯ D♯ F♯ A♯ B

or with --verbose

$ twelve-tone --historical --verbose

🚀 The tone row from Berg's Lyric Suite!
F E C A G D G♯ C♯ D♯ F♯ A♯ B
jgarte commented 1 year ago

A third implementation is to use the abjad library to render the twelve-tone row.

I think this is our best option to easily support features I have mentioned here and in other issues #31 #29 #30 #32 .

Using abjad will make it very easy to generate a twelve-tone row:

>>> import abjad

>>> numbers = [1, 11, 9, 3, 6, 7, 5, 4, 10, 2, 8, 0]
>>> row = abjad.TwelveToneRow(numbers)
>>> row.as_midi(row, "row.midi")

>>> print(row)
PC<1, 11, 9, 3, 6, 7, 5, 4, 10, 2, 8, 0>

>>> # row is an object with methods
>>> row.
row.count(               row.index(               row.items                row.retrograde(          row.to_pitches(          row.voice_vertically(    
row.from_selection(      row.invert(              row.multiply(            row.rotate(              row.transpose(           
row.has_duplicates(      row.item_class           row.permute(             row.to_pitch_classes(    row.voice_horizontally(

>>> row.to_pitch_classes()
TwelveToneRow([1, 11, 9, 3, 6, 7, 5, 4, 10, 2, 8, 0])

>>> abjad.persist.as_midi(row, "row.midi")
('row.midi', 0.007839679718017578, 9.847418308258057, True)

>>> abjad.persist.as_png(row, "row.png")
(('row.png',), 0.008437156677246094, 2.8052899837493896, True)

>>> abjad.persist.as_ly(row, "row.ly")
('row.ly', 0.008110284805297852)

abjad uses lilypond to produce the midi file. We would be able to drop numpy and miditime as dependencies of python-twelve-tone if we use abjad.

abjad is also smaller as a library than numpy but we'll be getting a lot more use out of it than using numpy and as shown in these examples..

Outputs from REPL session above

row.ly

%! abjad.LilyPondFile._get_format_pieces()
\version "2.24.1"
%! abjad.LilyPondFile._get_format_pieces()
\language "english"

\layout {
    \accidentalStyle forget
    indent = 0
    \context {
        \Score
        \override BarLine.transparent = ##t
        \override BarNumber.stencil = ##f
        \override Beam.stencil = ##f
        \override Flag.stencil = ##f
        \override Stem.stencil = ##f
        \override TimeSignature.stencil = ##f
        proportionalNotationDuration = #(ly:make-moment 1 12)
    }
}

\paper {
    markup-system-spacing.padding = 8
    system-system-spacing.padding = 10
    top-markup-spacing.padding = 4
}
\new Score
<<
    \new Staff
    {
        \new Voice
        {
            cs'8
            b'8
            a'8
            ef'8
            fs'8
            g'8
            f'8
            e'8
            bf'8
            d'8
            af'8
            c'8
            %! SCORE_1
            \bar "|."
            \override Score.BarLine.transparent = ##f
        }
    }
>>

With the below png output we could add a feature to render the musical score fragment of the twelve-tone row in the terminal with terminal-img, for example.

from image import DrawImage

# feeding it the png output from abjad
image = DrawImage("row.png")

row.png

row

The score fragment output can be easily styled to not produce the footer.

jgarte commented 1 year ago

@accraze Would you like me to draft a PR using abjad/lilypond to update the MIDI functionality mentioned in #31?

I can close #29 and #32 as well in this PR.

I would not be implementing the row score image terminal rendering mentioned above in this PR but I would be happy to do that in a near future PR.