From 2de3dab48fbd20f2e08cff3c372b32a3e1950fe0 Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Sat, 15 Jul 2017 23:16:40 -0400 Subject: [PATCH 1/8] Add modulo to Row struct, allowing rows to have a modulo value other than 12 --- README.md | 11 ++++-- lib/webern.ex | 56 +++++++++++++++++++++-------- lib/webern/row.ex | 69 ++++++++++++++++++++++++----------- test/webern/row_test.exs | 18 ++++++---- test/webern_test.exs | 77 +++++++++++++++++++++++++++++++--------- 5 files changed, 171 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 57956e5..4d047ab 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,15 @@ 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.row/2` provides a helper function to generate a new tone row consisting +of pitch classes from the 12-tone semitone chromatic scale. 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 other than 12. 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. + `Webern.matrix/1` accepts a row as an argument and returns a displayable matrix based on the row. diff --git a/lib/webern.ex b/lib/webern.ex index 55a0bfa..1cb7d1e 100644 --- a/lib/webern.ex +++ b/lib/webern.ex @@ -4,8 +4,15 @@ 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 tone row consisting + of pitch classes from the 12-tone semitone chromatic scale. 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. `matrix/1` accepts a row as an argument and returns a displayable matrix based on the row. @@ -55,10 +62,19 @@ defmodule Webern do 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. + + A keyword list argument containing a value for the key `:modulo` can be passed + as a second argument to set the modulo value 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. + """ - @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 +86,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 +111,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 +137,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 +165,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 +193,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/row.ex b/lib/webern/row.ex index d098382..924f0a7 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -23,9 +23,9 @@ 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 ) @doc """ @@ -36,13 +36,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 +75,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 +94,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 +112,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 +131,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 +150,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 } """ @@ -182,24 +209,24 @@ defmodule Webern.Row do Enum.map(pcs, &Enum.at(@pitch_classes, &1)) end - defp zero(%__MODULE__{pitch_classes: [h | _] = pitch_classes}) do - new(Enum.map(pitch_classes, &(normalize(&1 - h)))) + 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, 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 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..e42e1d9 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,43 @@ 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/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 +107,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 +137,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 From cacd9cb9586151cca36a511ee251edd8811dbca8 Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Mon, 17 Jul 2017 08:05:49 -0400 Subject: [PATCH 2/8] Update documentation --- README.md | 12 ++++++------ lib/webern/row.ex | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4d047ab..ac21610 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ Elixir library for creating and working with 12-tone rows of pitch classes from the 12-tone semitone chromatic scale. 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 other than 12. 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. +* `: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. - -`Webern.matrix/1` accepts a row as an argument and returns a displayable matrix +`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/row.ex b/lib/webern/row.ex index 924f0a7..7e2a899 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 From 298119af1088b2dde29af49010156a829d56e9e3 Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Mon, 17 Jul 2017 08:06:56 -0400 Subject: [PATCH 3/8] More documentation tweaking --- README.md | 6 +++--- lib/webern.ex | 25 ++++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ac21610..b71aa20 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Elixir library for creating and working with 12-tone rows ### Creating objects -`Webern.row/2` provides a helper function to generate a new tone row consisting -of pitch classes from the 12-tone semitone chromatic scale. It can also take -an optional keyword list as a second argument, with the following keys +`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 diff --git a/lib/webern.ex b/lib/webern.ex index 1cb7d1e..fbedb19 100644 --- a/lib/webern.ex +++ b/lib/webern.ex @@ -4,9 +4,9 @@ defmodule Webern do ## Creating objects - `row/2` provides a helper function to generate a new tone row consisting - of pitch classes from the 12-tone semitone chromatic scale. It can also take - an optional keyword list as a second argument, with the following keys + `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 @@ -59,15 +59,18 @@ 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.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], + modulo: 12 + } - A keyword list argument containing a value for the key `:modulo` can be passed - as a second argument to set the modulo value 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. + A shorter row can be supplied, in which case the row's modulo will be + An optional keyword list second argument can be passed + #as a second argument to set the modulo value 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. """ @spec row([integer], Keyword.t | nil) :: row From 423b6d88eb94d51cf4245105790146c8c100a19f Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Mon, 17 Jul 2017 08:23:07 -0400 Subject: [PATCH 4/8] Document :allthethings: --- lib/webern.ex | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/webern.ex b/lib/webern.ex index fbedb19..2d4c2f6 100644 --- a/lib/webern.ex +++ b/lib/webern.ex @@ -59,18 +59,31 @@ defmodule Webern do The input `source_row` should be a 12-tone row defined by an ordering of the integers `0`..`11`. - iex> Webern.Row.new([0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11]) + 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 - An optional keyword list second argument can be passed - #as a second argument to set the modulo value 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. + 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 + } """ @spec row([integer], Keyword.t | nil) :: row From ff2d4a0191c1a162201d2c59aa5de8b730689b46 Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Mon, 17 Jul 2017 11:30:43 -0400 Subject: [PATCH 5/8] Allow `modulo: :infinity` to bypass performing any modulo on row transformations --- README.md | 4 +++- lib/webern.ex | 14 +++++++++++++- lib/webern/row.ex | 1 + test/webern_test.exs | 7 +++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b71aa20..2186a6f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ following keys `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. + 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. diff --git a/lib/webern.ex b/lib/webern.ex index 2d4c2f6..1ac4ff2 100644 --- a/lib/webern.ex +++ b/lib/webern.ex @@ -12,7 +12,9 @@ defmodule Webern do `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. + 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. @@ -85,6 +87,16 @@ defmodule Webern do 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], Keyword.t | nil) :: row def row(source_row, opts \\ []) when is_list(source_row) do diff --git a/lib/webern/row.ex b/lib/webern/row.ex index 7e2a899..bb55279 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -225,6 +225,7 @@ defmodule Webern.Row do new(Enum.map(pcs, &(normalize(&1 + interval, m))), m) end + 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 diff --git a/test/webern_test.exs b/test/webern_test.exs index e42e1d9..aa5b468 100644 --- a/test/webern_test.exs +++ b/test/webern_test.exs @@ -96,6 +96,13 @@ defmodule WebernTest do } 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], From 4306f6066dd5035147339b7375f60cd037b2bf15 Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Mon, 17 Jul 2017 16:48:02 -0400 Subject: [PATCH 6/8] Add non-equal-temperament pitches into the mix [ci skip] --- lib/webern/row.ex | 27 ++++++++++++++++++++++++++- test/webern_test.exs | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/webern/row.ex b/lib/webern/row.ex index bb55279..c5bcd51 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -27,6 +27,13 @@ defmodule Webern.Row do @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 @@ -206,9 +213,27 @@ 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)) + IO.puts p + pc = Map.get(@all_pitch_classes, n - (12 * octave)) + "#{pc}#{octave_string(octave)} ^\\markup { \\box #{p} }" + end + + 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 diff --git a/test/webern_test.exs b/test/webern_test.exs index aa5b468..e2d4df9 100644 --- a/test/webern_test.exs +++ b/test/webern_test.exs @@ -178,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 From 7fb7e390fddad62c686ac1b9b59bef26996b035c Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Tue, 18 Jul 2017 22:37:01 -0400 Subject: [PATCH 7/8] Fix to_lily for pitches with markup --- lib/webern/lilypond/utils.ex | 1 + lib/webern/row.ex | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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 c5bcd51..6dae58e 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -259,7 +259,7 @@ 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 @@ -284,7 +284,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 From b16ca963acc898a24bc4631f1009795b5c2f627e Mon Sep 17 00:00:00 2001 From: Michael Berkowitz Date: Wed, 19 Jul 2017 13:04:18 -0400 Subject: [PATCH 8/8] Cleanup --- lib/webern/row.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/webern/row.ex b/lib/webern/row.ex index 6dae58e..d077e25 100644 --- a/lib/webern/row.ex +++ b/lib/webern/row.ex @@ -223,9 +223,8 @@ defmodule Webern.Row do 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 + n = Float.round(12 * :math.log2(p / c0) / 0.5, 0) * 0.5 octave = round(Float.floor(n / 12)) - IO.puts p pc = Map.get(@all_pitch_classes, n - (12 * octave)) "#{pc}#{octave_string(octave)} ^\\markup { \\box #{p} }" end