Coder Social home page Coder Social logo

tonic's Introduction

Tonic

Demo

Swift library for music theory, currently focused on chords/harmony.

Tonic answers musical questions, such as:

  • What's the note for this pitch in this key?

    Note(pitch: Pitch(midiNoteNumber), key: .Bb)

  • What's the name of a chord?

    Chord(notes: notes).description

  • What chords are in this key?

    Key.Cm.chords

  • What chords in this key contain this note?

    Key.C.chords.filter { $0.noteClasses.contains(.C) }

  • What notes do these keys have in common?

    Key.C.noteSet.intersection(Key.Cm.noteSet)

  • What notes don't these keys have in common?

    Key.C.noteSet.symmetricDifference(Key.Cm.noteSet)

These questions are all tested in our unit tests explicitly.

Goals

  • Correctness. Try to be as correct with respect to music theory as possible.
  • Strong typing. Use types to prevent errors (e.g. Pitch instead of UInt8).
  • Good performance. Tonic uses bit sets to represent pitch sets and note sets.

Documentation

The documentation is host on the AudioKit.io Website. The package includes a demo project as well.

Install

Install using Swift Package Manager.

tonic's People

Contributors

aure avatar bdrelling avatar brindy avatar jnpdx avatar maksutovic avatar matt54 avatar orchetect avatar saito-arch avatar sebastianboldt avatar wtholliday avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tonic's Issues

Query the notes of Key with correct octave information

Description

It may be incorrect, but is extracting the correct notes with their respective octaves from a Key object impossible?

I tried to obtain the correct notes and pitches for a Key by
mapping all the notes from the noteSet, since it was the only property available in the chord object.
I used the following code:

key.noteSet.array.map {
     var note = $0
     note.octave = 0
     return note.pitch 
}

Unfortunately, the noteClasses do not contain the octave information,
making it impossible for me to obtain the correct pitches for a specific Key.

po Key.init(root: .init(.D, accidental: .natural), scale: .major).noteSet.array
▿ 7 elements
  ▿ 0 : C♯4
    ▿ noteClass : C♯
      - letter : Tonic.Letter.C
      - accidental : ♯
    - octave : 4
  ▿ 1 : D4
    ▿ noteClass : D
      - letter : Tonic.Letter.D
      - accidental : 
    - octave : 4
  ▿ 2 : E4
    ▿ noteClass : E
      - letter : Tonic.Letter.E
      - accidental : 
    - octave : 4
  ▿ 3 : F♯4
    ▿ noteClass : F♯
      - letter : Tonic.Letter.F
      - accidental : ♯
    - octave : 4
  ▿ 4 : G4
    ▿ noteClass : G
      - letter : Tonic.Letter.G
      - accidental : 
    - octave : 4
  ▿ 5 : A4
    ▿ noteClass : A
      - letter : Tonic.Letter.A
      - accidental : 
    - octave : 4
  ▿ 6 : B4
    ▿ noteClass : B
      - letter : Tonic.Letter.B
      - accidental : 
    - octave : 4

Proposed Solution

I would like to be able to do something like this:

key.pitches(octave: 1)

or maybe something like this:

key.notes(octave: 1) 

So having access to notes with the correct octaves would be awesome

In this code snippet, I am increasing the octave for all notes that are lower than the pitch of the root note. Please note that I am not entirely sure if this approach will work for all possible Scale Types, but at first glance, it appears to be a promising solution.

extension Key {
    func notes(octave: Int) -> [Note] {
        var orderedNotes: [Note] = []

        for note in noteSet.array {
            let shift = note.noteClass.canonicalNote.pitch < root.canonicalNote.pitch
            let shiftedNote = Note(note.letter, accidental: note.accidental, octave: shift ? octave + 1 : octave)
            orderedNotes.append(shiftedNote)
        }

        return orderedNotes.sorted()
    }
}

Describe Alternatives You've Considered

No alternatives considered

Additional Context

No response

CI doesn't know visionos

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

See CI logs:

warning: unknown operating system for build configuration 'os'
#if os(macOS) || os(iOS) || os(visionOS)

Crash Logs, Screenshots or Other Attachments (if applicable)

No response

Key.primaryTriads missing augmented chords

macOS Version(s) Used to Build

macOS 12 Monterey

Xcode Version(s)

Xcode 14

Description

primaryTriads on Key only contains major, minor and diminished triads, which is fine for major (Ionian) and natural minor (Aeolian) modes, but when using a different mode (e.g. Harmonic Minor, which is not an uncommon mode to use) the augmented triad is missing.

For example:
Key(root: .C, scale: .harmonicMinor).primaryTriads

Expected:
Cm, D°, E♭⁺, Fm, G, A♭, B°

Actual:
Cm, D°, Fm, G, A♭, B°

Possible solution 1:
Change allowablePrimaryTriads Key.swift:45 to include .augmentedTriad.

The existing tests still pass when this is present.

Possible solution 2:
Add a function to Key that takes the allowable triad types and generates the full set of triads on the fly by duplicating a subset of the logic from Key's init func, e.g. something like:

public func triads(withTypes allowedTypes: [ChordType]) -> [Chord] {
    var triads: [Chord] = []
    for chord in chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) {
        if allowedTypes.contains(chord.type) {
            triads.append(Chord(chord.root, type: chord.type))
        }
    }

    let triadsStartingWithC = triads.sorted(by: { $0.root.letter < $1.root.letter })
    let rootPosition = triadsStartingWithC.firstIndex(where: { $0.root == root }) ?? 0
    return Array(triadsStartingWithC.rotatingLeft(positions: rootPosition))
}

Clients can also add this function as an extension in their own code base, though they would have to implement/copy the rotatingLeft func of RangeReplaceableCollection extension themselves.

Possible solution: 3
Make ChordTable public and allow it to be constructed with the allowed triads and accidentals for generating chords. This will allow clients to make an extension function, like in possible solution 2, with more precision:

init(allowedTriadTypes: [ChordType], allowedAccidentals: [Accidental])

Crash Logs, Screenshots or Other Attachments (if applicable)

No response

shiftUp by an octave

Description

shiftUp only works with intervals but octave is not part of intervals

Proposed Solution

include octave in intervals

Describe Alternatives You've Considered

create additional function to shiftUpBySemitones(12)

Additional Context

No response

Codable Support for all Types

Description

Wouldn't it be nice if all the types are codable by default so we can easily persist them on disk,
create json representations etc. ?

Proposed Solution

I guess there is not a lot to do to make this possible because most of the types already use basic foundation types under the hood.

Describe Alternatives You've Considered

Implementing on my own.

import Tonic

extension Tonic.Chord: Codable {
    private enum CodingKeys: String, CodingKey {
        case root
        case type
        case inversion
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let root = try container.decode(NoteClass.self, forKey: .root)
        let type = try container.decode(ChordType.self, forKey: .type)
        let inversion = try container.decode(Int.self, forKey: .inversion)
        self.init(root, type: type, inversion: inversion)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(root, forKey: .root)
        try container.encode(type, forKey: .type)
        try container.encode(inversion, forKey: .inversion)
    }
}

extension Tonic.NoteClass: Codable {
    private enum CodingKeys: String, CodingKey {
        case accidental
        case letter
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let accidental = try container.decode(Accidental.self, forKey: .accidental)
        let letter = try container.decode(Letter.self, forKey: .letter)
        self.init(letter, accidental: accidental)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(accidental, forKey: .accidental)
        try container.encode(letter, forKey: .letter)
    }
}

extension Tonic.Letter: Codable {}
extension Tonic.Accidental: Codable {}
extension Tonic.ChordType: Codable {}

Additional Context

Unfortunately conformance can not be synthesized automatically in an extra file so I have to implement manually as shown above.
Bildschirmfoto 2024-01-25 um 21 57 08

demo is slow in debug build

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

Demo is slow to recognize chords in debug build. To reproduce just run and play a chord on your midi controller. Could switch the demo to release by default, or try to make it faster in debug.

Crash Logs, Screenshots or Other Attachments (if applicable)

No response

No sus2 Chordtype available

Description

Currently the Chordtype-Type only provides a suspendedTriad which technically is a sus4 chord because it consists of the following intervals [.P4, .P5]. Wouldn't it be nice to also have a sus2 Type?
Is there any specific reason this is missing 🤔 ?

Proposed Solution

Splitting the suspendedTriad into suspendedSecondTriad and suspendedFourthTriad

case .suspendedFourth return [.P4, .P5]
case .suspendedSecond return [.M2, P5]

Describe Alternatives You've Considered

Wrapping my own Chordtype around the Tonic.Chordtype

If shiftup results in b#, octave is wrong

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

I tried to create the correct notes from an augmentedTriad starting on E as a root note.

var notes: [Note] = []
for interval in ChordType.augmentedTriad.intervals {
    // Create the next note by using the shiftup function
    if let shifted = Note(.E, accidental: .natural, octave: 0).shiftUp(interval) {
        notes.append(shifted)
    }
}

The first interval will be applied correctly
but the last one will result in a B# in octave 1 which results in the wrong note, one octave too high.

 3 elements
  0 : E0
    noteClass : E
     - letter : Tonic.Letter.E
     - accidental : 
   - octave : 0
  1 : G0
    noteClass : G
     - letter : Tonic.Letter.G
     - accidental : 
   - octave : 0
  2 : B0
    noteClass : B
     - letter : Tonic.Letter.B
     - accidental : 
   - octave : 1

Midinotenumbers will be:

 3 elements
 - 0 : 16
 - 1 : 20
 - 2 : 36

But should be:

 3 elements
 - 0 : 16
 - 1 : 20
 - 2 : 24

I did not fully grasp the shiftUp function but my quick fix was to check if the new note is b# and if it matches we just
decrease the octave by 1.

public func shiftUp(_ shift: Interval) -> Note? {
    var newNote = Note(.C, accidental: .natural, octave: 0)
    let newLetterIndex = (noteClass.letter.rawValue + (shift.degree - 1))
    let newLetter = Letter(rawValue: newLetterIndex % Letter.allCases.count)!
    let newOctave = (Int(pitch.midiNoteNumber) + shift.semitones) / 12 - 1
    for accidental in Accidental.allCases {
        newNote = Note(newLetter, accidental: accidental, octave: newOctave)
        // special b# handling 
        if newNote.letter == .B && newNote.accidental == .sharp {
            newNote.octave = newNote.octave - 1
        }
        if (newNote.noteNumber % 12) == ((Int(noteNumber) + shift.semitones) % 12) {
            return newNote
        }
    }
    return nil
}

I am also not sure if maybe similar issues arise with the shift down function.

Crash Logs, Screenshots or Other Attachments (if applicable)

No response

Query the correct notes or pitches of a chord

Description

It may be incorrect, but is it impossible to extract the correct notes with their respective octaves from a chord object for a specific inversion?

I tried to obtain the correct notes and pitches for a chord by mapping all the note classes, since it was the only property available in the chord object.
I used the following code:

chord.noteClasses.map {
    Note($0.letter, accidental: $0.accidental, octave: 0).pitch
}

Unfortunately, the noteClasses do not contain the octave information, making it impossible for me to obtain the correct pitches for a specific chord inversion.

Proposed Solution

I would like to be able to do something like this:

chord.pitches(octave: 1) // Return the correct pitches taking into account the inversion

or maybe something like this:

chord.notes(octave: 1) // Return the correct notes taking into account the inversion

So having access to notes with the correct octaves would be awesome

Describe Alternatives You've Considered

Implementing it on my side.

extension Chord {
    func pitches(octave: Int) -> [Pitch] {
        var notes = noteClasses.map {
            Note($0.letter, accidental: $0.accidental, octave: octave)
        }

        for step in 0..<inversion {
            let index = step % notes.count
            notes[index].octave += 1
        }

        return notes.map {
            $0.pitch
        }
    }
}

Additional Context

No response

chord table collisions

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

Add code to check for collisions. You'll find multiple chords map to the same NoteSets so the table will need to contain multiple chords for each NoteSet ([NoteSet: [Chord]]). There aren't actually any hash collisions.

For example:

notes for C𝄫sus4: [C𝄫, F𝄫, G𝄫]
notes for F𝄫sus2: [C𝄫, F𝄫, G𝄫]

Crash Logs, Screenshots or Other Attachments (if applicable)

No response

Lazy Chord loading on Key Type

Description

From what I understand, when you initialize a Key, all the chords are generated inside the init.
However, if you want to create all the available keys for a certain note, the performance is pretty bad.

Here is a piece of code that illustrates this concept:

let newKeys = Scale.allCases.map {
    Key(root: note, scale: $0)
}

This code creates an array of keys for all the scales of a given note.

Proposed Solution

I would suggest implementing lazy chord creation. This would allow users to request the chords for a specific key only when needed, instead of generating them automatically during initialization. I believe that generating chords during initialization is unnecessarily complex and pollutes the domain of the Key Object, in addition, it also impacts the performance.

/// All the traditional triads representable root, third, and fifth from each note in the key
public func primaryTriads() ->[Chord] {
    let table = ChordTable.shared
    var primaryTriads: [Chord] = []
    let allowablePrimaryTriads: [ChordType] = [.majorTriad, .minorTriad, .diminishedTriad, .augmentedTriad]

    for (_, chord) in table.chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) {
        if allowablePrimaryTriads.contains(chord.type) {
            primaryTriads.append(Chord(chord.root, type: chord.type))
        }
    }

    let primaryTriadsStartingWithC = primaryTriads.sorted(by: { $0.root.letter < $1.root.letter })
    let rootPosition = primaryTriadsStartingWithC.firstIndex(where: { $0.root == root }) ?? 0
    return Array(primaryTriadsStartingWithC.rotatingLeft(positions: rootPosition))
}

/// All chords that fit in the key
public func chords() -> [Chord] {
    let table = ChordTable.shared
    var chords: [Chord] = []
    for (_, chord) in table.chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) {
        chords.append(Chord(chord.root, type: chord.type))
    }
    return chords
}

We could also think about adding a static global cache, similar to Chordtable, that stores a key and all the corresponding chords globally for faster access.

Wdyt?

C4 and Cb4 are not 11 semitones apart.

Description

"Accidentals applied to a note do not have an effect on its ASPN number. For example, B♯3 and C4 have different octave numbers despite being enharmonically equivalent, because the B♯ is still considered part of the lower octave."

https://viva.pressbooks.pub/openmusictheory/chapter/aspn/

Proposed Solution

Not sure how this assumption affects the rest of your calculations, but C4 and Cb4 are not 11 semitones apart.

Describe Alternatives You've Considered

There are no alternative facts or alternative schools of thought about the octave designations of pitches.

Additional Context

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.