diff --git a/README.md b/README.md index 57956e5..2186a6f 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,19 @@ Elixir library for creating and working with 12-tone rows ### Creating objects -`Webern.row/1` provides a helper function to generate a new tone row consisting -of pitch classes from the 12-tone semitone chromatic scale - -`Webern.matrix/1` accepts a row as an argument and returns a displayable matrix +`Webern.row/2` provides a helper function to generate a new serializeable row. It +can also take an optional keyword list as a second argument, with the +following keys + +* `:modulo` specifies the modulo value for the row. If it is not provided, + `Webern` will assume that the highest possible value is present in the row + and base the modulo value on that. In cases of integral pitch classes, the + modulo will be the highest value in the row + 1. Non-integral rows should + specify their modulo to avoid unexpected transformations. `:modulo` can + also be set to `:infinity` to indicate that no modulo operation should + be performed. + +`Webern.matrix/1` accepts a row as an argument and returns a printable matrix based on the row. ### Transforming tone rows diff --git a/lib/webern.ex b/lib/webern.ex index 55a0bfa..1ac4ff2 100644 --- a/lib/webern.ex +++ b/lib/webern.ex @@ -4,8 +4,17 @@ defmodule Webern do ## Creating objects - `row/1` provides a helper function to generate a new tone row consisting - of pitch classes from the 12-tone semitone chromatic scale + `row/2` provides a helper function to generate a new serializeable row. It + can also take an optional keyword list as a second argument, with the + following keys + + * `:modulo` specifies the modulo value for the row. If it is not provided, + `Webern` will assume that the highest possible value is present in the row + and base the modulo value on that. In cases of integral pitch classes, the + modulo will be the highest value in the row + 1. Non-integral rows should + specify their modulo to avoid unexpected transformations. `:modulo` can + also be set to `:infinity` to indicate that no modulo operation should + be performed. `matrix/1` accepts a row as an argument and returns a displayable matrix based on the row. @@ -52,13 +61,48 @@ defmodule Webern do The input `source_row` should be a 12-tone row defined by an ordering of the integers `0`..`11`. - A shorter row can also be supplied, but the modulo - point of the generated row will still be 12, allowing the possibility for - row permutations to include pitches not present in the original row. + iex> Webern.row([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) + %Webern.Row{ + pitch_classes: [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11], + modulo: 12 + } + + A shorter row can be supplied, in which case the row's modulo will be + determined by the highest value in the given row plus 1. + + iex> Webern.row([0, 2, 1, 3, 4, 6]) + %Webern.Row{ + pitch_classes: [0, 2, 1, 3, 4, 6], + modulo: 7 + } + + A modulo value can be specified for a row by passing it as a value for + `:modulo` in a keyword list secord argument. For partial rows that do not + contain the highest possible value, or for rows using non-integral sets, + it is recommended you specify this value to avoid unexpected transformations. + + iex> Webern.row([0, 2, 1, 3, 4, 6], modulo: 12) + %Webern.Row{ + pitch_classes: [0, 2, 1, 3, 4, 6], + modulo: 12 + } + + `:modulo` can also be set explicitly to `:infinity` to provide no modulo for + a row. This can be useful when working with a source set using absolute + pitches or raw Hz values: + + iex> Webern.row([440.0, 493.9, 554.4, 587.3, 659.3], modulo: :infinity) + %Webern.Row{ + pitch_classes: [440.0, 493.9, 554.4, 587.3, 659.3], + modulo: :infinity + } + """ - @spec row([integer]) :: row - def row(source_row) when is_list(source_row) do - Row.new(source_row) + @spec row([integer], Keyword.t | nil) :: row + def row(source_row, opts \\ []) when is_list(source_row) do + with modulo <- Keyword.get(opts, :modulo, Enum.max(source_row) + 1) do + Row.new(source_row, modulo) + end end @doc """ @@ -70,11 +114,13 @@ defmodule Webern do iex> row = Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) iex> Webern.prime(row) %Webern.Row{ - pitch_classes: [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11] + pitch_classes: [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11], + modulo: 12 } iex> Webern.prime(row, 3) %Webern.Row{ - pitch_classes: [3, 5, 4, 6, 7, 9, 8, 10, 11, 1, 0, 2] + pitch_classes: [3, 5, 4, 6, 7, 9, 8, 10, 11, 1, 0, 2], + modulo: 12 } See [above](#module-transforming-tone-rows) for details about the prime form @@ -93,11 +139,13 @@ defmodule Webern do iex> row = Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) iex> Webern.retrograde(row) %Webern.Row{ - pitch_classes: [11, 9, 10, 8, 7, 5, 6, 4, 3, 1, 2, 0] + pitch_classes: [11, 9, 10, 8, 7, 5, 6, 4, 3, 1, 2, 0], + modulo: 12 } iex> Webern.retrograde(row, 4) %Webern.Row{ - pitch_classes: [3, 1, 2, 0, 11, 9, 10, 8, 7, 5, 6, 4] + pitch_classes: [3, 1, 2, 0, 11, 9, 10, 8, 7, 5, 6, 4], + modulo: 12 } See [above](#module-transforming-tone-rows) for details about @@ -117,11 +165,13 @@ defmodule Webern do iex> row = Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) iex> Webern.inverse(row) %Webern.Row{ - pitch_classes: [0, 10, 11, 9, 8, 6, 7, 5, 4, 2, 3, 1] + pitch_classes: [0, 10, 11, 9, 8, 6, 7, 5, 4, 2, 3, 1], + modulo: 12 } iex> Webern.inverse(row, 1) %Webern.Row{ - pitch_classes: [1, 11, 0, 10, 9, 7, 8, 6, 5, 3, 4, 2] + pitch_classes: [1, 11, 0, 10, 9, 7, 8, 6, 5, 3, 4, 2], + modulo: 12 } See [above](#module-transforming-tone-rows) for details about the inverse @@ -143,11 +193,13 @@ defmodule Webern do iex> row = Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) iex> Webern.retrograde_inverse(row) %Webern.Row{ - pitch_classes: [1, 3, 2, 4, 5, 7, 6, 8, 9, 11, 10, 0] + pitch_classes: [1, 3, 2, 4, 5, 7, 6, 8, 9, 11, 10, 0], + modulo: 12 } iex> Webern.retrograde_inverse(row, 5) %Webern.Row{ - pitch_classes: [6, 8, 7, 9, 10, 0, 11, 1, 2, 4, 3, 5] + pitch_classes: [6, 8, 7, 9, 10, 0, 11, 1, 2, 4, 3, 5], + modulo: 12 } See [above](#module-transforming-tone-rows) for details about the retrograde @@ -169,11 +221,13 @@ defmodule Webern do iex> row = Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) iex> Webern.inverse_retrograde(row) %Webern.Row{ - pitch_classes: [11, 1, 0, 2, 3, 5, 4, 6, 7, 9, 8, 10] + pitch_classes: [11, 1, 0, 2, 3, 5, 4, 6, 7, 9, 8, 10], + modulo: 12 } iex> Webern.inverse_retrograde(row, 8) %Webern.Row{ - pitch_classes: [7, 9, 8, 10, 11, 1, 0, 2, 3, 5, 4, 6] + pitch_classes: [7, 9, 8, 10, 11, 1, 0, 2, 3, 5, 4, 6], + modulo: 12 } See [above](#module-transforming-tone-rows) for details about the inverse diff --git a/lib/webern/lilypond/utils.ex b/lib/webern/lilypond/utils.ex index 8ac91c3..77ddcbb 100644 --- a/lib/webern/lilypond/utils.ex +++ b/lib/webern/lilypond/utils.ex @@ -12,6 +12,7 @@ defmodule Webern.Lilypond.Utils do \\override Staff.TimeSignature #'stencil = ##f \\override Staff.Stem #'transparent = ##t \\accidentalStyle Score.dodecaphonic + \\textLengthOn \\time 12/4 #{tone_row_lily} \\bar "|." diff --git a/lib/webern/row.ex b/lib/webern/row.ex index d098382..d077e25 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -1,6 +1,6 @@ defmodule Webern.Row do @moduledoc """ - Models a tone row built from pitches from the 12 semitone chromatic scale. + Models a serializeable row. N.B. Although the main functionality for row creation/transformation is defined in this module, the `Webern` module provides a more user-friendly @@ -23,10 +23,17 @@ defmodule Webern.Row do """ - defstruct [:pitch_classes] + defstruct [:pitch_classes, :modulo] - @type t :: %__MODULE__{pitch_classes: [integer]} + @type t :: %__MODULE__{pitch_classes: [integer], modulo: integer | nil} @pitch_classes ~w( c cs d ef e f fs g af a bf b ) + @all_pitch_classes %{ + 0.0 => "c", 0.5 => "cqs", 1.0 => "cs", 1.5 => "ctqs", 2.0 => "d", + 2.5 => "etqf", 3.0 => "ef", 3.5 => "eqf", 4.0 => "e", 4.5 => "esq", + 5.0 => "f", 5.5 => "fqs", 6.0 => "fs", 6.5 => "ftqs", 7.0 => "g", + 7.5 => "atqf", 8.0 => "af", 8.5 => "aqf", 9.0 => "a", 9.5 => "btqf", + 10.0 => "bf", 10.5 => "bqf", 11.0 => "b", 11.5 => "bqs" + } @doc """ Accepts `pitch_classes`, a list of integers between 0 and 11, and returns @@ -36,13 +43,35 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) %Webern.Row{ - pitch_classes: [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11] + pitch_classes: [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11], + modulo: 12 + } + + `new/2` can also take an optional second argument specifying the modulo + for the row. If not given, this defaults to the highest value in the row + 1. + This assumes an integral-based row of pitch classes. A row using non-integral + values should always specify a modulo to avoid unexpected transformations. + + ## Example + + iex> Webern.Row.new([0, 2, 1, 3, 4, 5]) + %Webern.Row{ + pitch_classes: [0, 2, 1, 3, 4, 5], + modulo: 6 + } + + iex> Webern.Row.new([0, 2, 1, 3, 4, 5], 7) + %Webern.Row{ + pitch_classes: [0, 2, 1, 3, 4, 5], + modulo: 7 } """ - @spec new([integer]) :: __MODULE__.t - def new(pitch_classes) do - %__MODULE__{pitch_classes: pitch_classes} + @spec new([integer], integer | nil) :: __MODULE__.t + def new(pitch_classes, modulo \\ nil) do + with modulo <- modulo || Enum.max(pitch_classes) + 1 do + %__MODULE__{pitch_classes: pitch_classes, modulo: modulo} + end end @doc """ @@ -53,7 +82,8 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) ...> |> Row.prime(3) %Webern.Row{ - pitch_classes: [3, 5, 4, 6, 7, 9, 8, 10, 11, 1, 0, 2] + pitch_classes: [3, 5, 4, 6, 7, 9, 8, 10, 11, 1, 0, 2], + modulo: 12 } """ @@ -71,7 +101,8 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) ...> |> Row.retrograde(4) %Webern.Row{ - pitch_classes: [3, 1, 2, 0, 11, 9, 10, 8, 7, 5, 6, 4] + pitch_classes: [3, 1, 2, 0, 11, 9, 10, 8, 7, 5, 6, 4], + modulo: 12 } """ @@ -88,7 +119,8 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) ...> |> Row.inverse(1) %Webern.Row{ - pitch_classes: [1, 11, 0, 10, 9, 7, 8, 6, 5, 3, 4, 2] + pitch_classes: [1, 11, 0, 10, 9, 7, 8, 6, 5, 3, 4, 2], + modulo: 12 } """ @@ -106,7 +138,8 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) ...> |> Row.retrograde_inverse(5) %Webern.Row{ - pitch_classes: [6, 8, 7, 9, 10, 0, 11, 1, 2, 4, 3, 5] + pitch_classes: [6, 8, 7, 9, 10, 0, 11, 1, 2, 4, 3, 5], + modulo: 12 } """ @@ -124,7 +157,8 @@ defmodule Webern.Row do iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) ...> |> Row.inverse_retrograde(8) %Webern.Row{ - pitch_classes: [7, 9, 8, 10, 11, 1, 0, 2, 3, 5, 4, 6] + pitch_classes: [7, 9, 8, 10, 11, 1, 0, 2, 3, 5, 4, 6], + modulo: 12 } """ @@ -179,34 +213,52 @@ defmodule Webern.Row do end defp to_pitch_classes(%__MODULE__{pitch_classes: pcs}) do - Enum.map(pcs, &Enum.at(@pitch_classes, &1)) + Enum.map(pcs, fn pc -> + case is_integer(pc) do + true -> Enum.at(@pitch_classes, pc, pc) + false -> to_pitch_with_frequency_annotation(pc) + end + end) + end + + defp to_pitch_with_frequency_annotation(p) do + c0 = 440.0 * :math.pow(2, -4.75) + n = Float.round(12 * :math.log2(p / c0) / 0.5, 0) * 0.5 + octave = round(Float.floor(n / 12)) + pc = Map.get(@all_pitch_classes, n - (12 * octave)) + "#{pc}#{octave_string(octave)} ^\\markup { \\box #{p} }" end - defp zero(%__MODULE__{pitch_classes: [h | _] = pitch_classes}) do - new(Enum.map(pitch_classes, &(normalize(&1 - h)))) + defp octave_string(3), do: "" + defp octave_string(o) when o < 3, do: String.duplicate(",", 3 - o) + defp octave_string(o) when o > 3, do: String.duplicate("'", o - 3) + + defp zero(%__MODULE__{pitch_classes: [h | _] = pitch_classes, modulo: m}) do + new(Enum.map(pitch_classes, &(normalize(&1 - h, m))), m) end - defp _retrograde(%__MODULE__{pitch_classes: pitch_classes}) do - new(Enum.reverse(pitch_classes)) + defp _retrograde(%__MODULE__{pitch_classes: pitch_classes, modulo: m}) do + new(Enum.reverse(pitch_classes), m) end - defp _inverse(%__MODULE__{pitch_classes: [h | _] = pitch_classes}) do - new(Enum.map(pitch_classes, &(normalize(2 * h - &1)))) + defp _inverse(%__MODULE__{pitch_classes: [h | _] = pcs, modulo: m}) do + new(Enum.map(pcs, &normalize(2 * h - &1, m)), m) end - defp transpose(%__MODULE__{pitch_classes: pitch_classes}, interval) do - new(Enum.map(pitch_classes, &(normalize(&1 + interval)))) + defp transpose(%__MODULE__{pitch_classes: pcs, modulo: m}, interval) do + new(Enum.map(pcs, &(normalize(&1 + interval, m))), m) end - defp normalize(n) when n < 0, do: normalize(n + (-n * 12)) - defp normalize(n), do: rem(n, 12) + defp normalize(n, :infinity), do: Float.round(n, 1) + defp normalize(n, m) when n < 0, do: normalize(n + (-n * m), m) + defp normalize(n, m), do: rem(n, m) end defimpl String.Chars, for: Webern.Row do def to_string(row = %Webern.Row{}) do row |> Webern.Row.to_list(to_pitches: true) - |> Enum.map(&String.ljust(&1, 4)) + |> Enum.map(&" #{String.ljust(&1, 3)}") |> Enum.join("") |> String.strip end @@ -231,7 +283,14 @@ defimpl Webern.Lilypond, for: Webern.Row do def row_pitches_to_lily(row = %Webern.Row{}) do row |> Webern.Row.to_list(to_pitches: true) - |> Enum.map(&"#{&1}'") + |> Enum.map(&"#{&1}#{octave_mark(&1)}") |> Enum.join(" ") end + + defp octave_mark(str) do + case Regex.match?(~r/markup/, str) do + true -> "" + false -> "'" + end + end end diff --git a/test/webern/row_test.exs b/test/webern/row_test.exs index 2cdff52..2df9fda 100644 --- a/test/webern/row_test.exs +++ b/test/webern/row_test.exs @@ -7,37 +7,43 @@ defmodule Webern.RowTest do test ".new/1 returns a row from the given pitch class list" do assert Row.new(@op_28) == %Webern.Row{ - pitch_classes: [10, 9, 0, 11, 3, 4, 1, 2, 6, 5, 8, 7] + pitch_classes: [10, 9, 0, 11, 3, 4, 1, 2, 6, 5, 8, 7], + modulo: 12 } end test ".prime/2 returns the prime version of a row starting at a given step" do assert Row.new(@op_28) |> Row.prime(2) == %Webern.Row{ - pitch_classes: [2, 1, 4, 3, 7, 8, 5, 6, 10, 9, 0, 11] + pitch_classes: [2, 1, 4, 3, 7, 8, 5, 6, 10, 9, 0, 11], + modulo: 12 } end test ".retrograde/2 returns the retrograde of a row that starts at the given step" do assert Row.new(@op_28) |> Row.retrograde(5) == %Webern.Row{ - pitch_classes: [2, 3, 0, 1, 9, 8, 11, 10, 6, 7, 4, 5] + pitch_classes: [2, 3, 0, 1, 9, 8, 11, 10, 6, 7, 4, 5], + modulo: 12 } end test ".inverse/2 returns an inverse version of a row starting at a given step" do assert Row.new(@op_28) |> Row.inverse(6) == %Webern.Row{ - pitch_classes: [6, 7, 4, 5, 1, 0, 3, 2, 10, 11, 8, 9] + pitch_classes: [6, 7, 4, 5, 1, 0, 3, 2, 10, 11, 8, 9], + modulo: 12 } end test ".retrograde_inverse/2 returns the retrograde of an inverted row that starts at a given step" do assert Row.new(@op_28) |> Row.retrograde_inverse(9) == %Webern.Row{ - pitch_classes: [0, 11, 2, 1, 5, 6, 3, 4, 8, 7, 10, 9] + pitch_classes: [0, 11, 2, 1, 5, 6, 3, 4, 8, 7, 10, 9], + modulo: 12 } end test ".inverse_retrograde/2 returns the inverse of the retrograde of a row starting at a given step" do assert Row.new(@op_28) |> Row.inverse_retrograde(5) == %Webern.Row{ - pitch_classes: [2, 1, 4, 3, 7, 8, 5, 6, 10, 9, 0, 11] + pitch_classes: [2, 1, 4, 3, 7, 8, 5, 6, 10, 9, 0, 11], + modulo: 12 } end diff --git a/test/webern_test.exs b/test/webern_test.exs index 0bef5f5..e2d4df9 100644 --- a/test/webern_test.exs +++ b/test/webern_test.exs @@ -5,29 +5,34 @@ defmodule WebernTest do @op_24 [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9] @short_row [11, 2, 7, 8, 5, 1] + @small_row [2, 3, 0, 1, 4, 5] test ".row/1 returns a row from a given pitch class list" do assert row(@op_24) == %Webern.Row{ - pitch_classes: [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9] + pitch_classes: [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9], + modulo: 12 } end describe ".prime" do test ".prime/1 returns the original prime form of the row" do assert prime(row(@op_24)) == %Webern.Row{ - pitch_classes: [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9] + pitch_classes: [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9], + modulo: 12 } end test ".prime/1 returns the original prime form for a short row" do assert prime(row(@short_row)) == %Webern.Row{ - pitch_classes: [11, 2, 7, 8, 5, 1] + pitch_classes: [11, 2, 7, 8, 5, 1], + modulo: 12 } end test ".prime/2 returns the prime form of the row starting at the given step" do assert prime(row(@op_24), 3) == %Webern.Row{ - pitch_classes: [3, 2, 6, 7, 11, 10, 0, 8, 9, 4, 5, 1] + pitch_classes: [3, 2, 6, 7, 11, 10, 0, 8, 9, 4, 5, 1], + modulo: 12 } end end @@ -35,19 +40,22 @@ defmodule WebernTest do describe ".retrograde" do test ".retrograde/1 returns the retrograde form of the row" do assert retrograde(row(@op_24)) == %Webern.Row{ - pitch_classes: [9, 1, 0, 5, 4, 8, 6, 7, 3, 2, 10, 11] + pitch_classes: [9, 1, 0, 5, 4, 8, 6, 7, 3, 2, 10, 11], + modulo: 12 } end test ".retrograde/1 returns the retrograde form of a short row" do assert retrograde(row(@short_row)) == %Webern.Row{ - pitch_classes: [1, 5, 8, 7, 2, 11] + pitch_classes: [1, 5, 8, 7, 2, 11], + modulo: 12 } end test ".retrograde/2 returns the retrograde form for the row starting at the given step" do assert retrograde(row(@op_24), 3) == %Webern.Row{ - pitch_classes: [1, 5, 4, 9, 8, 0, 10, 11, 7, 6, 2, 3] + pitch_classes: [1, 5, 4, 9, 8, 0, 10, 11, 7, 6, 2, 3], + modulo: 12 } end end @@ -55,19 +63,50 @@ defmodule WebernTest do describe ".inverse" do test ".inverse/1 returns the inverse form of the row" do assert inverse(row(@op_24)) == %Webern.Row{ - pitch_classes: [11, 0, 8, 7, 3, 4, 2, 6, 5, 10, 9, 1] + pitch_classes: [11, 0, 8, 7, 3, 4, 2, 6, 5, 10, 9, 1], + modulo: 12 } end test ".inverse/1 returns the inverse form of a short row" do assert inverse(row(@short_row)) == %Webern.Row{ - pitch_classes: [11, 8, 3, 2, 5, 9] + pitch_classes: [11, 8, 3, 2, 5, 9], + modulo: 12 + } + end + + test ".inverse/1 assumes a modulo based on the largest value in a row" do + assert inverse(row([10, 8, 3, 2, 5, 9])) == %Webern.Row{ + pitch_classes: [10, 1, 6, 7, 4, 0], + modulo: 11 + } + end + + test ".inverse/1 returns the inverse of a short row with a non-12 modulo point" do + assert inverse(row(@small_row)) == %Webern.Row{ + pitch_classes: [2, 1, 4, 3, 0, 5], + modulo: 6 + } + end + + test ".inverse/1 returns the inverse of a row with a custom modulo point" do + assert inverse(row([0, 1, 2, 3, 4], modulo: 7)) == %Webern.Row{ + pitch_classes: [0, 6, 5, 4, 3], + modulo: 7 + } + end + + test ".inverse/1 behaves correctly with `modulo: :infinity`" do + assert inverse(row([440.0, 493.9, 554.4, 587.3, 659.3], modulo: :infinity)) == %Webern.Row{ + pitch_classes: [440.0, 386.1, 325.6, 292.7, 220.7], + modulo: :infinity } end test ".inverse/2 returns the inverse form of the row starting at the given step" do assert inverse(row(@op_24), 3) == %Webern.Row{ - pitch_classes: [3, 4, 0, 11, 7, 8, 6, 10, 9, 2, 1, 5] + pitch_classes: [3, 4, 0, 11, 7, 8, 6, 10, 9, 2, 1, 5], + modulo: 12 } end end @@ -75,19 +114,29 @@ defmodule WebernTest do describe ".retrograde_inverse" do test ".retrograde_inverse/1 returns the retrograde inverse form of the row" do assert retrograde_inverse(row(@op_24)) == %Webern.Row{ - pitch_classes: [1, 9, 10, 5, 6, 2, 4, 3, 7, 8, 0, 11] + pitch_classes: [1, 9, 10, 5, 6, 2, 4, 3, 7, 8, 0, 11], + modulo: 12 } end test ".retrograde_inverse/1 returns the retrograde inverse form of a short row" do assert retrograde_inverse(row(@short_row)) == %Webern.Row{ - pitch_classes: [9, 5, 2, 3, 8, 11] + pitch_classes: [9, 5, 2, 3, 8, 11], + modulo: 12 + } + end + + test ".retrograde_inverse/1 returns the retrograde inverse form of a row with custom modulo" do + assert retrograde_inverse(row([1, 3, 2, 4, 0], modulo: 7)) == %Webern.Row{ + pitch_classes: [2, 5, 0, 6, 1], + modulo: 7 } end test ".retrograde_inverse/2 returns the retrograde inverse form for the row starting at the given step" do assert retrograde_inverse(row(@op_24), 3) == %Webern.Row{ - pitch_classes: [5, 1, 2, 9, 10, 6, 8, 7, 11, 0, 4, 3] + pitch_classes: [5, 1, 2, 9, 10, 6, 8, 7, 11, 0, 4, 3], + modulo: 12 } end end @@ -95,19 +144,22 @@ defmodule WebernTest do describe ".inverse_retrograde" do test ".inverse_retrograde/1 returns the inverse of the retrograde of the row" do assert inverse_retrograde(row(@op_24)) == %Webern.Row{ - pitch_classes: [9, 5, 6, 1, 2, 10, 0, 11, 3, 4, 8, 7] + pitch_classes: [9, 5, 6, 1, 2, 10, 0, 11, 3, 4, 8, 7], + modulo: 12 } end test ".inverse_retrograde/1 returns the inverse of the retrograde of a short row" do assert inverse_retrograde(row(@short_row)) == %Webern.Row{ - pitch_classes: [1, 9, 6, 7, 0, 3] + pitch_classes: [1, 9, 6, 7, 0, 3], + modulo: 12 } end test ".inverse_retrograde/2 returns the inverse of the retrograde of a row starting at a given step" do assert inverse_retrograde(row(@op_24), 3) == %Webern.Row{ - pitch_classes: [1, 9, 10, 5, 6, 2, 4, 3, 7, 8, 0, 11] + pitch_classes: [1, 9, 10, 5, 6, 2, 4, 3, 7, 8, 0, 11], + modulo: 12 } end end @@ -126,6 +178,8 @@ defmodule WebernTest do describe ".to_string/1" do test "it returns a space separated row when called with a row" do assert to_string(row(@op_24)) == "b bf d ef g fs af e f c cs a" + assert to_string(row([440.0, 492.0, 544.4, 600.3, 640.3], modulo: :infinity)) == + "a' ^\\markup { \\box 440.0 } b' ^\\markup { \\box 492.0 } cqs'' ^\\markup { \\box 544.4 } etqf'' ^\\markup { \\box 600.3 } eqf'' ^\\markup { \\box 640.3 }" end test "it returns a space separated matrix when called with a matrix" do