Кстати, я люблю теорию музыки, математику и F #, поэтому я не удержался от изучения этой проблемы.
Сначала я попробовал чисто функциональное решение, используя только модули, функции F # и базовые структуры данных, но это быстро вышло из-под контроля (поскольку я искал некоторые довольно амбициозные цели, включая поддержку произвольных масштабов, а не только "основных" "и" несовершеннолетний "). Далее следуют мои первые «серьезные» усилия по «программированию в среде» на F # с использованием объектной ориентации. Как я уже говорил ранее, я думал, что мог бы избежать этого, но оказалось, что использование объектной ориентации в F # на самом деле работает довольно хорошо и не сильно подрывает красоту и лаконичность (особенно когда мы игнорируем потребляемость другими языками .NET ).
Utils.fs
Для начала, у меня есть пара полезных функций, которые я буду использовать:
module MusicTheory.Utils
open System
let rotate (arr:_[]) start =
[|start..arr.Length + start - 1|] |> Array.map (fun i -> arr.[i% arr.Length])
//http://stackoverflow.com/questions/833180/handy-f-snippets/851449#851449
let memoize f =
let cache = Collections.Generic.Dictionary<_,_>(HashIdentity.Structural)
fun x ->
match cache.TryGetValue(x) with
| true, res -> res
| _ -> let res = f x
cache.[x] <- res
res
Note.fs
Тип Note
инкапсулирует музыкальную ноту, включая ее имя, знак (NoteSign
) и ее положение относительно других нот. Но это мало что делает. Модуль Aux
содержит некоторые базовые структуры данных, используемые для создания и проверки Note
s (обратите внимание, что я не слишком нашел этот модуль, я бы скорее использовал частные статические поля для типа Note, но F # не поддерживать частные статические поля. И поскольку я использую пространство имен вместо модуля для хранения моих типов (так что я могу использовать объявления файлов в верхней части), я не могу использовать свободно плавающие привязки let). Я думаю, что сопоставление с образцом для извлечения NoteSign
особенно аккуратно.
namespace MusicTheory
open Utils
open System
///want to use public static field on Note, but don't exist
module Aux =
let indexedNoteNames =
let arr = [|
["B#"; "C"] //flip this order?
["C#";"Db"]
["D"]
["D#";"Eb"]
["E";"Fb"]
["E#";"F" ] //flip this order?
["F#";"Gb"]
["G"]
["G#";"Ab"]
["A"]
["A#";"Bb"]
["B";"Cb"]
|]
Array.AsReadOnly(arr)
let noteNames = indexedNoteNames |> Seq.concat |> Seq.toList
let indexedSignlessNoteNames = [|'A';'B';'C';'D';'E';'F';'G'|]
open Aux
type NoteSign =
| Flat
| Sharp
| Natural
//Represents a note name and it's relative position (index)
type Note(name:string) =
let name =
match noteNames |> List.exists ((=) name) with
| true -> name
| false -> failwith "invalid note name: %s" name
let sign =
match name |> Seq.toArray with
| [|_|] -> NoteSign.Natural
| [|_;'#'|] -> NoteSign.Sharp
| [|_;'b'|] -> NoteSign.Flat
| _ -> failwith "invalid note name sign" //not possible
let index =
indexedNoteNames
|> Seq.findIndex (fun names -> names |> List.exists ((=) name))
with
member self.Name = name
member self.SignlessName = name.[0]
member self.Sign = sign
member self.Index = index
override self.ToString() = name
override self.GetHashCode() = name.GetHashCode()
override self.Equals(other:obj) =
match other with
| :? Note as otherNote -> otherNote.Name = self.Name
| _ -> false
///memoized instances of Note
static member get = memoize (fun name -> Note(name))
Pitch.fs
Следующим является Pitch
, который инкапсулирует конкретную частоту в хроматической шкале относительно некоторой начальной точки, 0 (C). Он выставляет расчеты, для которых лежит октава, а также набор Note
s, которые могут его описать (отмечая, что вне контекста шкалы, начинающейся с определенного Note
, одинаково действительны).
namespace MusicTheory
open Utils
open Aux
open System
///A note is a value 0-11 corresponding to positions in the chromatic scale.
///A pitch is any value relative to a starting point of the chromatic scale
type Pitch (pitchIndex:int) =
let pitchIndex = pitchIndex
let noteIndex = Math.Abs(pitchIndex % 12)
let octave =
if pitchIndex >= 0 then (pitchIndex / 12) + 1
else (pitchIndex / 12) - 1
let notes = indexedNoteNames.[noteIndex] |> List.map Note.get
with
member self.Notes = notes
member self.PitchIndex = pitchIndex
member self.NoteIndex = noteIndex
///e.g. pitchIndex = 5 -> 1, pitchIndex = -5 -> -1, pitchIndex = 13 -> 2
member self.Octave = octave
override self.ToString() = sprintf "Notes = %A, PitchIndex = %i, NoteIndex = %i, Octave = %i" notes noteIndex pitchIndex octave
override self.GetHashCode() = pitchIndex
override self.Equals(other:obj) =
match other with
| :? Pitch as otherPitch -> otherPitch.PitchIndex = self.PitchIndex
| _ -> false
///memoized instances of Pitch
static member get = memoize (fun index -> Pitch(index))
///get the first octave pitch for the given note
static member getByNote (note:Note) = note.Index |> Pitch.get
///get the first octave pitch for the given note name
static member getByNoteName name = name |> Note.get |> Pitch.getByNote
ScaleIntervals.fs
В ожидании нашего предстоящего типа Scale
у нас есть модуль ScaleIntervals
, заполненный подмодулями, заполненными списками интервалов между шагами, которые описывают шкалы (обратите внимание, что это отличается от представления на основе индекса, которое использовали другие) , Для вашего интереса обратите внимание, что Mode.ionian
и Mode.aeolian
соответствуют «мажорной» и «минорной» шкалам соответственно. На практике вы, вероятно, захотите использовать некоторые внешние средства для загрузки интервалов масштабирования во время выполнения.
//could encapsulate as a type, instead of checking in Scale constructors
///define modes by chromatic interval sequence
module MusicTheory.ScaleIntervals
open Utils
module Mode =
let ionian = [|2;2;1;2;2;2;1|] //i.e. "Major"
let dorian = Utils.rotate ionian 1
let phrygian = Utils.rotate ionian 2
let lydian = Utils.rotate ionian 3
let mixolydian = Utils.rotate ionian 4
let aeolian = Utils.rotate ionian 5 //i.e. "Minor
let locrian = Utils.rotate ionian 6
module EqualTone =
let half = [|1;1;1;1;1;1;1;1;1;1;1;1|]
let whole = [|2;2;2;2;2;2|]
module Pentatonic =
let major = [|2;2;3;2;3|]
let minor = Utils.rotate major 4 //not sure
Scale.fs
Здесь лежит сердце нашего решения. Сам по себе Scale
довольно прост, просто заключает в себе последовательность интервалов масштабирования. Но если смотреть в контексте Pitch
или Note
, мы получим все наши результаты. Я укажу, что в отличие от Pitch
или Note
, Scale
имеет интересную особенность, которая дает бесконечную последовательность RelativeIndices
, полученную из интервалов шкалы. Используя это, мы можем получить бесконечную последовательность Pitche
с, построенную из этого Scale
, начиная с данного Pitch
(GetPitches
). Но теперь для самого интересного метода: GetNotePitchTuples
, который дает бесконечную последовательность Note
, Pitch
кортежей, где Note
s эвристически выбраны (см. Комментарии к этому методу для получения дополнительной информации). Scale также предоставляет несколько перегрузок для более легкого доступа к последовательностям Note
, включая перегрузку ToString(string)
, которая принимает имя string
Note
и возвращает string
со списком первой октавы из Note
имен.
namespace MusicTheory
open Utils
open System
///A Scale is a set of intervals within an octave together with a root pitch
type Scale(intervals:seq<int>) =
let intervals =
if intervals |> Seq.sum <> 12 then
failwith "intervals invalid, do not sum to 12"
else
intervals
let relativeIndices =
let infiniteIntervals = Seq.initInfinite (fun _ -> intervals) |> Seq.concat
infiniteIntervals |> Seq.scan (fun pos cur -> pos+cur) 0
with
member self.Intervals = intervals
member self.RelativeIndices = relativeIndices
override self.ToString() = sprintf "%A" intervals
override self.GetHashCode() = intervals.GetHashCode()
override self.Equals(other:obj) =
match other with
| :? Scale as otherScale -> otherScale.Intervals = self.Intervals
| _ -> false
///Infinite sequence of pitches for this scale starting at rootPitch
member self.GetPitches(rootPitch:Pitch) =
relativeIndices
|> Seq.map (fun i -> Pitch.get (rootPitch.PitchIndex + i))
///Infinite sequence of Note, Pitch tuples for this scale starting at rootPitch.
///Notes are selected heuristically: works perfectly for Modes, but needs some work
///for Pentatonic and EqualTone (perhaps introduce some kind of Sign bias or explicit classification).
member self.GetNotePitchTuples(rootNote:Note, rootPitch:Pitch) =
let selectNextNote (prevNote:Note) (curPitch:Pitch) =
//make sure octave note same as root note
if curPitch.Notes |> List.exists ((=) rootNote) then
rootNote
else
//take the note with the least distance (signless name wise) from the root note
//but not if the distance is 0. assumes curPitch.Notes ordered asc in this way.
//also assumes that curPitch.Notes of length 1 or 2.
match curPitch.Notes with
| [single] -> single
| [first;second] when first.SignlessName = prevNote.SignlessName -> second
| [first;_] -> first
self.GetPitches(rootPitch)
|> Seq.scan
(fun prev curPitch ->
match prev with
| None -> Some(rootNote, rootPitch) //first
| Some(prevNote,_) -> Some(selectNextNote prevNote curPitch, curPitch)) //subsequent
None
|> Seq.choose id
member self.GetNotePitchTuples(rootNote:Note) =
self.GetNotePitchTuples(rootNote, Pitch.getByNote rootNote)
member self.GetNotePitchTuples(rootNoteName:string) =
self.GetNotePitchTuples(Note.get rootNoteName)
///return a string representation of the notes of this scale in an octave for the given note
member self.ToString(note:Note) =
let notes =
(Scale(intervals).GetNotePitchTuples(note))
|> Seq.take (Seq.length intervals + 1)
|> Seq.toList
|> List.map (fst)
sprintf "%A" notes
///return a string representation of the notes of this scale in an octave for the given noteName
member self.ToString(noteName:string) =
self.ToString(Note.get noteName)
Вот демонстрация:
open MusicTheory
open Aux
open ScaleIntervals
let testScaleNoteHeuristics intervals =
let printNotes (noteName:string) =
printfn "%A" (Scale(intervals).ToString(noteName))
noteNames
|> Seq.iter printNotes
//> testScaleNoteHeuristics Mode.ionian;;
//"[B#; D; E; F; G; A; B; B#]"
//"[C; D; E; F; G; A; B; C]"
//"[C#; D#; E#; F#; G#; A#; B#; C#]"
//"[Db; Eb; F; Gb; Ab; Bb; C; Db]"
//"[D; E; F#; G; A; B; C#; D]"
//"[D#; E#; G; Ab; Bb; C; D; D#]"
//"[Eb; F; G; Ab; Bb; C; D; Eb]"
//"[E; F#; G#; A; B; C#; D#; E]"
//"[Fb; Gb; Ab; A; B; C#; D#; Fb]"
//"[E#; G; A; Bb; C; D; E; E#]"
//"[F; G; A; Bb; C; D; E; F]"
//"[F#; G#; A#; B; C#; D#; E#; F#]"
//"[Gb; Ab; Bb; Cb; Db; Eb; F; Gb]"
//"[G; A; B; C; D; E; F#; G]"
//"[G#; A#; B#; C#; D#; E#; G; G#]"
//"[Ab; Bb; C; Db; Eb; F; G; Ab]"
//"[A; B; C#; D; E; F#; G#; A]"
//"[A#; B#; D; Eb; F; G; A; A#]"
//"[Bb; C; D; Eb; F; G; A; Bb]"
//"[B; C#; D#; E; F#; G#; A#; B]"
//"[Cb; Db; Eb; Fb; Gb; Ab; Bb; Cb]"
//val it : unit = ()
Аккорды
Следующим шагом является поддержка концепции аккорда, как изолированного от Scale
(набора Pitche
с), так и в контексте Scale
с заданным корнем Note
. Я не слишком задумывался о том, оправдана ли здесь какая-либо инкапсуляция, но было бы очень просто улучшить Scale
, чтобы, скажем, вернуть последовательность аккордов (например, список Note
для каждого Note
в масштаб) с учетом начального Note
и паттерна аккордов (например, триада).