Как бы вы запрограммировали октаву для фортепиано? - PullRequest
2 голосов
/ 15 декабря 2010

Я хотел бы написать алгоритм, который строит правильную последовательность трех клавишных аккордов для фортепиано в данной октаве, переходя к следующей, если не все возможные ноты были охвачены. Например:

Клавиша Cmaj выдаст все ноты / аккорды в своей прогрессии, поскольку начальная нота является началом октавы, она заканчивается на следующей C. Но если я начну с ноты B той же октавы, она закончится с буквой B в следующем.

Я хотел бы построить его как для мажорной, так и для минорной гаммы с возможностью расширения в будущем для аккордов типа 7 и 9.

Это не домашнее задание, я хотел бы использовать c #, а затем переписать его на f #, чтобы немного больше выучить язык.

Edit:

Мой вопрос таков: Какую структуру данных я должен использовать для октавы (от C до C): список LinkedList или, может быть, для этого потребуется совершенно другая структура?

Редактировать2: Так что, если мы будем индексировать заметки, как это, я не уверен, что это правильный подход: 0 1 2 3 4 5 6 7 8 9 10 11 12

Input: Note = C (0), Scale = Maj Output: 0 4 7, 2 5 9, 4 7 12, etc.

Ответы [ 8 ]

3 голосов
/ 16 декабря 2010

Когда я делал это на Java несколько лет назад, я делал следующие классы:

  1. Note - представляет каждую из 12 различных нот в любой октаве ([C, C # / Db, D, D # / Eb, E, F, F # / Gb, G, G # / Ab, A, A # / Bb, B])
  2. Octave - хранит целое число, чтобы различать другие октавы
  3. Pitch - хранит ноты и октавы
  4. ScalePattern - кодирует количество полушагов от одного шага к следующему (например, Major Scale - [0,2,4,5,7,9,11,12])
  5. Scale - сохраняет начальную ноту и ScalePattern

Этот дизайн также позволяет легко определять и использовать ChordPatterns.

3 голосов
/ 16 декабря 2010

Самый простой способ смоделировать это, возможно, использовать понятие midi note mapping , так как ключи перечислены, и первая триада инверсии от данного корня будет

root, root + 4, root + 7

следующая инверсия будет

root + 4, root + 7, root + 12

следующая инверсия будет

root + 7, root + 12, root + 16

где root - номер миди-ноты для вашего корня.

Фактически, учитывая аккорд в первой инверсии, генерировать все остальные инверсии тривиально, удаляя первую запись, помещая ее в конец и добавляя 12. Таким образом, ваши аккорды действительно начнут выглядеть так:

public int GetChord(ChordName chord)
{
    switch (chord) {
    case ChordName.Major: return new int[] { 0, 4, 7 };
    case ChordName.Minor: return new int[] { 0, 3, 7 };
    case ChordName.Augmented: return new int[] { 0, 4, 8 };
    case ChordName.Dominant7: return new int[] { 0, 4, 7, 10 };
    case ChordName.Maj7: return new int[] { 0, 4, 7, 11 };
    // etc
 }

Тогда что бы ни возвращалось отсюда (и, вероятно, было бы лучше использовать List), вы можете написать IEnumerable, который возвращает каждую инверсию. Затем вы добавляете значение корня к выводу и та-да! у вас есть свой аккорд, который теперь чрезвычайно легко вывести как, ну, в общем, миди.

public int[] InvertChord(int[] chord)
{
    int[] inversion = new int[chord.Length];
    for (int i = 1; i < chord.Length; i++) {
        inversion[i-1] = chord[i];
    }
    inversion[inversion.Length-1] = chord[0] + 12;
    return inversion;
}

public int[][] ChordAndAllInversions(int[] chord)
{
    int[][] inversions = new int[chord.Length][];
    inversions[0] = chord;
    for (int i=1; i < chord.Length; i++) {
        inversions[i] = InvertChord(inversions[i - 1]);
    }
    return inversions;
}
2 голосов
/ 19 декабря 2010

Кстати, я люблю теорию музыки, математику и 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 и паттерна аккордов (например, триада).

1 голос
/ 15 декабря 2010

Точный вывод, который вы пытаетесь сгенерировать, неясен. Однако давайте вспомним, как выглядит масштаб:

 T T S T T T S
C D E F G A B C

(где T указывают два полутона между нотами, а S указывают один).

Зная это, легко сгенерировать каждую ноту в масштабе.

Когда у вас есть гамма, вы можете вытянуть 1-3-5, затем 2-4-6 и т. Д., Чтобы получить все аккорды.

РЕДАКТИРОВАТЬ: в шкале есть фиксированное количество нот, и вы хотите иметь возможность выбирать заметки по индексу. Просто используйте массив.

0 голосов
/ 01 июля 2019

Вы запрашиваете аккорды в наборе тонов .Это означает, что мы не заботимся о функции или названии нот, только о классах высоты тона по отношению к тонике каждого аккорда.

Алгоритм

static IEnumerable<IEnumerable<int>> GetChords(int[] scale, int extension)
{
    foreach (var degree in Enumerable.Range(0, scale.Length))
    {
        yield return GetChord(scale, extension, degree);
    }
}

static IEnumerable<int> GetChord(int[] scale, int extension, int degree)
{
    var d = degree;
    var m = extension - 1;
    var k = 2;

    do
    {
        yield return scale[d];
        d += k;
        d %= scale.Length;
        m -= k;

    } while (m >= 0);
}

Проверка

static void Main(string[] args)
{
    var major = new[] { 0, 2, 4, 5, 7, 8, 11 };
    var text = new StringBuilder();

    text.AppendLine(Print(GetChords(major, 5)));   // triads
    text.AppendLine(Print(GetChords(major, 7)));   // 7ths
    text.AppendLine(Print(GetChords(major, 9)));   // 9ths
    text.AppendLine(Print(GetChords(major, 11)));  // 11ths
    text.AppendLine(Print(GetChords(major, 13)));  // 13ths

    var rendered = text.ToString();
    Console.WriteLine(rendered);

    Console.ReadKey();
}

static string Print(IEnumerable<IEnumerable<int>> chords)
{
    return string.Join(",", chords.Select(chord => string.Join(" ", chord.Select(x => x))));
}

выход

- 0 4 7,2 5 8,4 7 11,5 8 0,7 11 2,8 0 4,11 2 5

- 0 4 7 11,2 5 8 0,4 7 11 2,5 8 0 4,7 11 2 5,8 0 4 7,11 2 5 8

- 0 4 7 11 2,2 5 8 0 4,4 7 11 2 5,5 8 0 4 7,7 11 2 5 8,8 0 4 7 11,11 2 5 8 0

- 0 4 7 11 2 5,2 5 8 0 4 7,4 7 11 2 5 8,5 8 0 4 7 11,7 11 2 5 8 0,8 0 4 7 11 2,11 2 5 8 0 4

- 0 4 7 11 2 5 8,2 5 8 0 4 7 11,4 7 11 2 5 8 0,5 8 0 4 7 11 2,7 11 2 5 8 0 4,8 0 4 7 11 2 5,11 2 5 8 0 4 7
0 голосов
/ 16 декабря 2010

В какой-то момент вам, вероятно, понадобится формула для преобразования MIDI-ноты в частоту.Средняя С IIRC - это нота MIDI 60, и каждый интегральный шаг представляет полутон.Вот некоторый код:

http://www.musicdsp.org/showone.php?id=125

0 голосов
/ 16 декабря 2010

Зависит от того, что вы хотите сделать с информацией о октаве, я думаю.Однако, если вы хотите извлечь определенные заметки из набора нот в октаве, и вы не хотите добавлять заметки после настройки октавы, я думаю, что было бы лучше использовать класс, который дал бы вам хорошеепроизвольный доступ, как Array.

0 голосов
/ 15 декабря 2010

Я бы просто использовал целое число, где 0 - самая низкая клавиша на клавиатуре. Каждый шаг представляет собой полутон выше. Затем разбейте аккорды на интервалы, например:

type  1  3  5  7
----------------
maj = 0  4  3  4
min = 0  3  4  3
dom = 0  4  3  3
dim = 0  3  3  3
...

Тогда вы можете сделать простое дополнение, чтобы получить все ноты аккорда. Итак, начиная с ноты 43, доминирующим аккордом будут ноты:

43  47  50  53
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...