Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 72 additions & 18 deletions lib/webern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 """
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/webern/lilypond/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 "|."
Expand Down
109 changes: 84 additions & 25 deletions lib/webern/row.ex
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 """
Expand All @@ -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
}

"""
Expand All @@ -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
}

"""
Expand All @@ -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
}

"""
Expand All @@ -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
}

"""
Expand All @@ -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
}

"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Loading