你会如何编制钢琴八度?

时间:2010-12-15 20:49:32

标签: c# algorithm f#

我想写一个algorythm,如果不是所有可能的音符被覆盖的话,在给定的八度音程中建立常规的3键钢琴和弦进行到下一个。例如:

Cmaj键将提供其中所有音符/和弦的进展,因为开始音符是八度音阶的开始,它将在下一个C结束。但是如果我从同一个八度音阶的B音符开始,它将结束与B在下一个也。

我想为主要和次要音阶构建它,能够在将来扩展它为7和9型和弦。

这不是作业,我想使用c#然后在f#中重新编写它以更多地学习语言。

修改

我的问题是: 我应该为八度音程(C到C)使用什么数据结构:LinkedList List或者这可能需要完全不同的结构?

Edit2: 所以如果我们索引这样的笔记,我不确定它是否是正确的方法: 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 个答案:

答案 0 :(得分:3)

对此进行建模的最简单方法可能是使用midi note mapping的概念,因为枚举键并且来自给定根的第一个反转三元组将是

root, root + 4, root + 7

下一次反转将是

root + 4, root + 7, root + 12

下一次反转将是

root + 7, root + 12, root + 16

其中root是你的root的midi音符编号。

事实上,考虑到第一次反转的和弦,通过删除第一个条目,将它放在最后并添加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。然后你将root的值添加到输出和ta-da!你有你的和弦,现在很容易输出,好吧,midi。

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;
}

答案 1 :(得分:3)

几年前,当我在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 - 存储Note和Octave
  4. ScalePattern - 编码从一个音高到下一个音高的半步数(例如,大音阶为[0,2,4,5,7,9,11,12])
  5. Scale - 存储初始注释和ScalePattern
  6. 此设计也可以轻松定义和使用ChordPatterns

答案 2 :(得分:2)

顺便说一句,我喜欢音乐理论,数学和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的基础数据结构(请注意,我不太了解这个模块,我宁愿在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的集合(注意在特定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.ionianMode.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非常简单,仅包含一系列比例间隔。但是,当在PitchNote的上下文中查看时,会产生我们的所有结果。我将指出,在PitchNote的隔离中,Scale确实具有一个有趣的特征,即它会产生从比例间隔派生的无限序列RelativeIndices。使用此方法,我们可以从给定PitcheScale)开始,从Pitch生成GetPitches的无限序列。但现在对于最有趣的方法:GetNotePitchTuples,它产生无限序列的NotePitch元组,其中Note被启发式选择(请参阅该方法的注释)了解更多信息)。 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 s)隔离,又在Scale与给定根Note的上下文中{ {1}}。我没有过多考虑是否有必要进行封装,但是将Scale增强到(例如)返回和弦的进度(例如每个Note列表是非常简单的。给定起始Note和和弦模式(例如三元组)的比例Note

答案 3 :(得分:1)

您尝试生成的确切输出尚不清楚。但是,让我们记住一个比例是什么样的:

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

(其中T表示音符之间有两个半音,S表示一个半音。)

知道,按比例生成每个音符都很简单。

一旦你有一个音阶,你可以拉出1-3-5,然后2-4-6等,以获得所有的和弦。

编辑:音阶中有一定数量的音符,您希望能够通过索引获取音符。只需使用数组。

答案 4 :(得分:0)

我只想使用一个整数,其中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

答案 5 :(得分:0)

我想,取决于你想用八度音调信息做什么。但是,如果你想从八度音符中的音符组中提取特定音符,并且在设置八度音阶后你不打算添加音符,我认为最好使用一个能给你带来好处的音乐随机访问,如Array

答案 6 :(得分:0)

在某些时候,您可能需要MIDI音符到频率转换的公式。 IIRC中间C是MIDI音符60,每个积分步长代表半音。这是一些代码:

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

答案 7 :(得分:0)

您要使用音高类集表示法的和弦。这意味着我们不在乎音符的功能或名称,而只在乎与每个和弦的音调有关的音高等级。

算法

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