├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ └── Support_question.md └── workflows │ ├── build-test.xxxyml │ ├── llvm-cov.xxxyml │ └── swiftlint.xxxyml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── music-notation │ ├── Accent.swift │ ├── Accidental.swift │ ├── Bend.swift │ ├── Clef.swift │ ├── Collection+Helpers.swift │ ├── DiatonicIntervalQuality.swift │ ├── Dynamics.swift │ ├── Element.swift │ ├── Enharmonic.swift │ ├── ImmutableMeasure.swift │ ├── Instrument.swift │ ├── Interval.swift │ ├── IntervalQuality.swift │ ├── Jump.swift │ ├── Key.swift │ ├── Marker.swift │ ├── Measure.swift │ ├── MeasureDurationValidator.swift │ ├── MeasureRepeat.swift │ ├── Note.swift │ ├── NoteCollection.swift │ ├── NoteDuration.swift │ ├── NoteLetter.swift │ ├── NotesHolder.swift │ ├── Octave.swift │ ├── PageFooter.swift │ ├── PageHeader.swift │ ├── Part.swift │ ├── RepeatedMeasure.swift │ ├── Score.swift │ ├── SpelledPitch.swift │ ├── Staff.swift │ ├── StaffLocation.swift │ ├── Striking.swift │ ├── Stylesheet.swift │ ├── Tempo.swift │ ├── Tie.swift │ ├── TimeSignature.swift │ ├── Tuplet.swift │ └── Version.swift ├── Tests └── MusicNotationTests │ ├── ClefTests.swift │ ├── Collection+HelpersTests.swift │ ├── Dynamics.swift │ ├── IntervalQualityTests.swift │ ├── IntervalTests.swift │ ├── KeyTests.swift │ ├── MarkerTests.swift │ ├── MeasureDurationValidatorTests.swift │ ├── MeasureRepeatTests.swift │ ├── MeasureTests.swift │ ├── NoteDurationTests.swift │ ├── NoteTests.swift │ ├── PartTests.swift │ ├── PitchTests.swift │ ├── ScoreTests.swift │ ├── StaffTests.swift │ ├── TempoTests.swift │ └── TupletTests.swift └── docs ├── UML Class Diagram.pdf ├── UML Class Diagram.uxf └── images ├── All_clefs.svg ├── UMLClassDiagram.png ├── all-clefs.png ├── alto-clef.png ├── bass-clef.png ├── chord.png ├── g-clef.png ├── grand-staff.png ├── lyrics.png ├── measure.png ├── multi-staff.png ├── multi-voice.png ├── pitch.png ├── rests.png ├── single-staff.png ├── single-tab-stash.png ├── staff.png └── symphony-9.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | # Common styles for most types of files 5 | [*] 6 | indent_size = 4 7 | tab_width = 4 8 | end_of_line = lf 9 | 10 | [*.swift] 11 | indent_style = tab 12 | insert_final_newline = true 13 | max_line_length = 120 14 | trim_trailing_whitespace = true 15 | 16 | [{*.yml,*.yaml}] 17 | indent_size = 2 18 | 19 | # Use tabs for property lists 20 | [*.plist] 21 | indent_style = tab 22 | 23 | # Makefiles always use tabs for indentation 24 | [Makefile] 25 | indent_style = tab 26 | 27 | # Markdown 28 | # Trailing whitespace means line break instead of full paragraph 29 | # https://arcticicestudio.github.io/styleguide-markdown/rules/whitespace.html#trailing 30 | [*.md] 31 | indent_size = 2 32 | trim_trailing_whitespace = false 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug Report" 3 | about: Create a report to help us to improve MusicNotation 4 | 5 | --- 6 | 7 | # Summary 8 | 9 | 10 | # Steps to Reproduce 11 | 12 | 13 | # Expected Results 14 | 15 | 16 | # Actual Results 17 | 18 | 19 | # Regression & Version 20 | 21 | 22 | # Related Link 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F528 Feature Request" 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Support_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Support Question" 3 | about: Ask here if you have a question about the usage of MusicNotation 4 | 5 | --- 6 | 7 | 11 | -------------------------------------------------------------------------------- /.github/workflows/build-test.xxxyml: -------------------------------------------------------------------------------- 1 | # Build and test the code in the music-notation swift package. 2 | 3 | name: Build & Test 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | macos: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Build 19 | run: swift build 20 | - name: Run tests 21 | run: swift test 22 | 23 | linux: 24 | strategy: 25 | matrix: 26 | tag: ['5.5'] 27 | runs-on: ubuntu-latest 28 | 29 | container: 30 | image: swift:${{ matrix.tag }} 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Build 36 | run: swift build 37 | # - name: Test 38 | # run: swift test --enable-test-discovery 39 | -------------------------------------------------------------------------------- /.github/workflows/llvm-cov.xxxyml: -------------------------------------------------------------------------------- 1 | # Generate code coverage metrics on the code in the music-notation swift package. 2 | 3 | name: Coverage 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | Coverage: 13 | runs-on: macos-11 14 | 15 | steps: 16 | - name: Checkout the Git repository 17 | uses: actions/checkout@v2 18 | 19 | - name: Run tests 20 | run: swift test -c debug -Xswiftc -enable-testing --enable-code-coverage 21 | 22 | - name: Gather test coverage 23 | run: xcrun llvm-cov export -summary-only -ignore-filename-regex MusicNotationTests .build/debug/music-notationPackageTests.xctest/Contents/MacOS/music-notationPackageTests -instr-profile .build/debug/codecov/default.profdata > .build/coverage.json 24 | 25 | - name: Get Coverage Total Percent 26 | run: | 27 | # Push the coverage number into the COVERAGE environment variable 28 | RAW_COVERAGE=$(cat .build/coverage.json|jq '.data[0]["totals"]["lines"]["percent"]') 29 | echo "COVERAGE=$(awk 'BEGIN { rounded = sprintf("%.1f", '$RAW_COVERAGE'); print rounded }')" >> $GITHUB_ENV 30 | 31 | # Use this hack to get to the branch 32 | REF=${{ github.ref }} 33 | echo "github.ref: $REF" 34 | IFS='/' read -ra PATHS <<< "$REF" 35 | BRANCH_NAME="${PATHS[1]}_${PATHS[2]}" 36 | echo $BRANCH_NAME 37 | echo "BRANCH=$(echo ${BRANCH_NAME})" >> $GITHUB_ENV 38 | 39 | - name: Create The Badge 40 | uses: schneegans/dynamic-badges-action@v1.0.0 41 | with: 42 | auth: ${{ secrets.GIST_SECRET }} 43 | gistID: b9f858cfba09911bd1755bdc40dd5a35 44 | filename: music-notation__${{ env.BRANCH }}.json 45 | label: Coverage 46 | message: ${{ env.COVERAGE }} 47 | color: green 48 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.xxxyml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | jobs: 7 | SwiftLint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: SwiftLint 12 | uses: norio-nomura/action-swiftlint@3.2.1 13 | with: 14 | args: --strict 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | DerivedData/ 5 | 6 | ## Various settings 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata/ 16 | xcuserdata 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | *.lcov 29 | 30 | ### Xcode Patch ### 31 | *.xcodeproj/* 32 | !*.xcodeproj/project.pbxproj 33 | !*.xcodeproj/xcshareddata/ 34 | !*.xcworkspace/contents.xcworkspacedata 35 | /*.gcno 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | .build/ 47 | .swiftpm/xcode/ 48 | 49 | # fastlane 50 | # 51 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 52 | # screenshots whenever they are needed. 53 | # For more information about the recommended setup visit: 54 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 55 | 56 | fastlane/report.xml 57 | fastlane/Preview.html 58 | fastlane/screenshots 59 | fastlane/test_output 60 | 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --indent tabs 2 | --tabwidth 4 3 | 4 | --modifierorder public,private,static,private(set) 5 | 6 | --decimalgrouping 3 7 | 8 | --enable isEmpty 9 | --disable preferKeyPath 10 | --disable trailingCommas 11 | --disable blankLinesAroundMark 12 | --disable consecutiveSpaces 13 | --disable unusedArguments 14 | --disable redundantFileprivate 15 | --disable wrapMultilineStatementBraces 16 | --disable wrapArguments 17 | --disable indent 18 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - inclusive_language 3 | - todo 4 | - opening_brace 5 | 6 | opt_in_rules: 7 | - anyobject_protocol 8 | - array_init 9 | - attributes 10 | - closure_end_indentation 11 | - closure_spacing 12 | - collection_alignment 13 | - contains_over_first_not_nil 14 | - empty_count 15 | - empty_string 16 | - empty_xctest_method 17 | - explicit_init 18 | - fallthrough 19 | - fatal_error_message 20 | - file_header 21 | - first_where 22 | - identical_operands 23 | - implicit_return 24 | - implicitly_unwrapped_optional 25 | - joined_default_parameter 26 | - literal_expression_end_indentation 27 | - lower_acl_than_parent 28 | - nimble_operator 29 | - object_literal 30 | - operator_usage_whitespace 31 | - overridden_super_call 32 | - override_in_extension 33 | - pattern_matching_keywords 34 | - private_action 35 | - private_outlet 36 | - prohibited_interface_builder 37 | - prohibited_super_call 38 | - quick_discouraged_call 39 | - quick_discouraged_focused_test 40 | - quick_discouraged_pending_test 41 | - redundant_nil_coalescing 42 | - redundant_type_annotation 43 | - single_test_class 44 | - sorted_first_last 45 | - sorted_imports 46 | - unavailable_function 47 | - unneeded_parentheses_in_closure_argument 48 | - untyped_error_in_catch 49 | - vertical_parameter_alignment_on_call 50 | - vertical_whitespace_closing_braces 51 | - vertical_whitespace_opening_braces 52 | - yoda_condition 53 | 54 | excluded: 55 | - SwiftLint/Common/3rdPartyLib 56 | - .build 57 | - Package.swift 58 | - Tests/LinuxMain.swift 59 | - Tests/MusicNotationTests/XCTestManifests.swift 60 | 61 | analyzer_rules: 62 | - capture_variable 63 | - unused_declaration 64 | - unused_import 65 | 66 | line_length: 67 | warning: 2000 68 | error: 240 69 | ignores_function_declarations: true 70 | ignores_comments: true 71 | ignores_urls: true 72 | 73 | function_body_length: 74 | warning: 500 75 | error: 700 76 | 77 | function_parameter_count: 78 | warning: 6 79 | error: 8 80 | 81 | type_body_length: 82 | warning: 500 83 | error: 800 84 | 85 | file_length: 86 | warning: 1000 87 | error: 1500 88 | ignore_comment_only_lines: true 89 | 90 | cyclomatic_complexity: 91 | warning: 22 92 | error: 25 93 | 94 | identifier_name: 95 | max_length: 96 | warning: 60 97 | error: 80 98 | min_length: 99 | error: 3 100 | excluded: 101 | - up 102 | - row 103 | - key 104 | - id 105 | - url 106 | - uri 107 | - URI 108 | - URL 109 | - b5 110 | - a3 111 | - a4 112 | - b4 113 | - c5 114 | - pp 115 | - p 116 | - mp 117 | - mf 118 | - f 119 | - ff 120 | 121 | reporter: "xcode" 122 | 123 | file_header: 124 | required_pattern: | 125 | \/\/ 126 | \/\/\t.*?\.swift 127 | \/\/\t.*? 128 | \/\/ 129 | \/\/\tCreated by .*? on \d{4}-\d{2}-\d{2}\. 130 | \/\/\tCopyright © \d{4}(-\d{4})* .*?\. All rights reserved\. 131 | \/\/ 132 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kyledsherman@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Kyle Sherman 2 | Copyright (c) 2021 Steven Woolgar 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // 3 | // Package.swift 4 | // MusicNotation 5 | // 6 | // Created by Steven Woolgar on 07/30/2021. 7 | // Copyright © 2021 Steven Woolgar. All rights reserved. 8 | // 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "music-notation", 13 | platforms: [ 14 | .macOS(.v14), 15 | .iOS(.v17), 16 | .tvOS(.v17), 17 | .watchOS(.v10) 18 | ], 19 | 20 | products: [ 21 | .library(name: "MusicNotation", targets: ["MusicNotation"])], 22 | 23 | targets: [ 24 | .target(name: "MusicNotation", path: "Sources", exclude: ["../docs"]), 25 | .testTarget(name: "MusicNotationTests", dependencies: ["MusicNotation"]) 26 | ], 27 | 28 | swiftLanguageVersions: [.v6] 29 | ) 30 | -------------------------------------------------------------------------------- /Sources/music-notation/Accent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accent.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum Accent: Sendable { 10 | case standard 11 | case strong 12 | case ghost 13 | } 14 | -------------------------------------------------------------------------------- /Sources/music-notation/Accidental.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accidental.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | // MARK: - 10 | public enum Accidental: Sendable, Element { 11 | case sharp 12 | case doubleSharp 13 | case flat 14 | case doubleFlat 15 | case natural 16 | } 17 | 18 | // MARK: - 19 | extension Accidental: CustomDebugStringConvertible { 20 | public var debugDescription: String { 21 | switch self { 22 | case .sharp: return "♯" 23 | case .doubleSharp: return "𝄪" 24 | case .flat: return "♭" 25 | case .doubleFlat: return "𝄫" 26 | case .natural: return "♮" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/music-notation/Bend.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bend.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-07-30. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public struct Bend: Sendable { 10 | let interval: Int 11 | let duration: Int 12 | } 13 | 14 | public enum BendType: Sendable { 15 | case bend(bendValue: Bend) 16 | case release(releaseValue: Bend) 17 | case bendAndRelease(bendValue: Bend, holdDuration: Int, releaseValue: Bend) 18 | case hold 19 | case prebend(interval: Int) 20 | case prebendAndBend(interval: Int, bendValue: Bend) 21 | case prebendAndRelease(interval: Int, releaseValue: Bend) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/music-notation/Clef.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clef.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-10-16. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A clef (from French: clef **'key'**) is a musical symbol used to indicate which notes are represented 12 | /// by the lines and spaces on a musical staff. Placing a clef on a staff assigns a particular pitch 13 | /// to one of the five lines or four spaces, which defines the pitches on the remaining lines and spaces. 14 | /// 15 | /// The three clef symbols used in modern music notation are the **G-clef**, **F-clef**, and **C-clef**. 16 | /// Placing these clefs on a line fixes a reference note to that line—an F-clef fixes the F below 17 | /// middle C, a C-clef fixes middle C, and a G-clef fixes the G above middle C. 18 | /// 19 | /// In modern music notation, the G-clef is most frequently seen as treble clef (placing G4 on the 20 | /// second line of the staff), and the F-clef as bass clef (placing F3 on the fourth line). 21 | /// 22 | /// The C-clef is mostly encountered as alto clef (placing middle C on the third line) or tenor 23 | /// clef (middle C on the fourth line). 24 | /// A clef may be placed on a space instead of a line, but this is rare. 25 | /// 26 | /// The use of different clefs makes it possible to write music for all instruments and voices, 27 | /// regardless of differences in range. Using different clefs for different instruments and 28 | /// voices allows each part to be written comfortably on a staff with a minimum of ledger lines. 29 | /// To this end, the G-clef is used for high parts, the C-clef for middle parts, and the F-clef for 30 | /// low parts. Transposing instruments can be an exception to this—the same clef is generally used 31 | /// for all instruments in a family, regardless of their sounding pitch. For example, even the low 32 | /// saxophones read in treble clef. 33 | /// 34 | /// - note: Please see the description as seen in the README.md file. You can see the graphic representations 35 | /// of each of the clefs. 36 | /// 37 | public struct Clef: Sendable { 38 | /// The pitch that defines the clef. This pitch is specified to be at a 39 | /// certain `StaffLocation` using the `staffLocation` property. 40 | /// 41 | public let pitch: SpelledPitch? 42 | 43 | /// The location on the staff at which the pitch for the clef is located. 44 | public let staffLocation: StaffLocation 45 | 46 | /// You can create a custom clef by providing a pitch and line number. 47 | /// 48 | /// - parameter pitch: The pitch that the clef represents. Pitch is optional to support un-pitched (i.e. drums) 49 | /// - parameter location: The location on the staff 50 | /// 51 | public init(pitch: SpelledPitch?, location: StaffLocation) { 52 | self.pitch = pitch 53 | staffLocation = location 54 | } 55 | 56 | /// Calculates the pitch for the given staff location for this Clef. 57 | /// 58 | /// - parameter location: The location on the staff for which you would like to know the pitch. 59 | /// - returns: The pitch at the given staff location or nil if the `Clef` is un-pitched. 60 | /// - throws: 61 | /// - `ClefError.internal`: Logic error with math 62 | /// - `ClefError.octaveOutOfRange` 63 | /// 64 | internal func pitch(at location: StaffLocation) throws -> SpelledPitch? { 65 | guard let pitch = pitch else { return nil } 66 | 67 | let largestNoteLetter = NoteLetter.b.rawValue 68 | let delta = location.halfSteps - staffLocation.halfSteps 69 | guard delta != 0 else { return pitch } 70 | 71 | let clefPitchRawValue = pitch.noteLetter.rawValue 72 | 73 | // Add the delta to the clef raw value 74 | let newPitchRawWithDelta = clefPitchRawValue + delta 75 | 76 | /// If requested location is increase (delta > 0), you are adding onto 0. 77 | /// If it's a decrease, you are subtracting from the largestNoteLetter. 78 | /// 79 | let startingPitchValue = newPitchRawWithDelta > 0 ? 0 : largestNoteLetter 80 | 81 | // Perform modulus to find out which `NoteLetter` it is. 82 | let newPitchRawValue = newPitchRawWithDelta % largestNoteLetter 83 | 84 | // If modulus is 0, it was the last letter. Otherwise, take the new raw value and add it to the starting value 85 | guard let newNoteLetter = NoteLetter(rawValue: newPitchRawValue == 0 ? largestNoteLetter : startingPitchValue + newPitchRawValue) else { 86 | assertionFailure("modulus failed, because logic is flawed") 87 | throw ClefError.internalError 88 | } 89 | 90 | // Figure out the delta by looking at how many times the new pitch has multipled the base noteLetter 91 | let octaveDeltaRaw = Double(newPitchRawWithDelta) / Double(largestNoteLetter) 92 | let octaveDelta: Int = { 93 | // TODO: Use `.rounded(_:)` instead of floor here and below. 94 | // https://github.com/drumnkyle/music-notation-core/issues/146 95 | if octaveDeltaRaw == floor(octaveDeltaRaw) { 96 | /// If the value is an exact multiple, it has not crossed the octave border yet. 97 | /// Therefore, subtract one from the value for the octave delta. 98 | return Int(octaveDeltaRaw - 1) 99 | } 100 | 101 | /// In any other case, just floor the value and that is the delta for the octave. 102 | return Int(floor(octaveDeltaRaw)) 103 | }() 104 | let newOctaveValue = pitch.octave.rawValue + octaveDelta 105 | guard let newOctave = Octave(rawValue: newOctaveValue) else { throw ClefError.octaveOutOfRange } 106 | return SpelledPitch(newNoteLetter, newOctave) 107 | } 108 | } 109 | 110 | // MARK: - Common clefs 111 | extension Clef { 112 | // Common clef convenience constructors 113 | public static let treble = Clef(pitch: SpelledPitch(.g, .octave4), location: StaffLocation(.line, 1)) 114 | public static let bass = Clef(pitch: SpelledPitch(.f, .octave3), location: StaffLocation(.line, 3)) 115 | public static let tenor = Clef(pitch: SpelledPitch(.c, .octave4), location: StaffLocation(.line, 3)) 116 | public static let alto = Clef(pitch: SpelledPitch(.c, .octave4), location: StaffLocation(.line, 2)) 117 | 118 | /// Un-pitched (drums, percussion, etc.) 119 | public static let neutral = Clef(pitch: nil, location: StaffLocation(.line, 2)) 120 | 121 | /// For tablature (guitar, etc.) 122 | public static let tab = Clef(pitch: nil, location: StaffLocation(.line, 2)) 123 | 124 | // Less common 125 | public static let frenchViolin = Clef(pitch: SpelledPitch(.g, .octave4), location: StaffLocation(.line, 0)) 126 | public static let soprano = Clef(pitch: SpelledPitch(.c, .octave4), location: StaffLocation(.line, 0)) 127 | public static let mezzoSoprano = Clef(pitch: SpelledPitch(.c, .octave4), location: StaffLocation(.line, 1)) 128 | public static let baritoneF = Clef(pitch: SpelledPitch(.f, .octave3), location: StaffLocation(.line, 2)) 129 | public static let baritoneC = Clef(pitch: SpelledPitch(.c, .octave4), location: StaffLocation(.line, 4)) 130 | public static let subBase = Clef(pitch: SpelledPitch(.f, .octave3), location: StaffLocation(.line, 5)) 131 | 132 | // TODO: Is this one correct? 133 | /// Starting in the 18th century, music for some instruments (such as guitar) and for the tenor voice 134 | /// have used treble clef, although they sound an octave lower. To avoid ambiguity, modified clefs 135 | /// are sometimes used, especially in choral writing. Using a C-clef on the third space places the 136 | /// notes identically, but this notation is much less common[9][10] as it is easily confused with the 137 | /// alto and tenor clefs. 138 | public static let suboctaveTreble = Clef(pitch: SpelledPitch(.g, .octave3), location: StaffLocation(.line, 1)) 139 | 140 | } 141 | 142 | // MARK: - Errors 143 | 144 | public enum ClefError: Error { 145 | case octaveOutOfRange 146 | case internalError 147 | } 148 | 149 | // MARK: - Equality 150 | 151 | extension Clef: Equatable { 152 | public static func == (lhs: Clef, rhs: Clef) -> Bool { 153 | lhs.pitch == rhs.pitch && lhs.staffLocation.halfSteps == rhs.staffLocation.halfSteps 154 | } 155 | } 156 | 157 | // MARK: - Debug 158 | 159 | extension Clef: CustomDebugStringConvertible { 160 | public var debugDescription: String { 161 | switch self { 162 | case Clef.treble: return "treble" 163 | case Clef.bass: return "bass" 164 | case Clef.tenor: return "tenor" 165 | case Clef.alto: return "alto" 166 | case Clef.neutral: return "neutral" 167 | case Clef.frenchViolin: return "frenchViolin" 168 | case Clef.soprano: return "soprano" 169 | case Clef.mezzoSoprano: return "mezzoSoprano" 170 | case Clef.baritoneF: return "baritoneF" 171 | case Clef.baritoneC: return "baritoneC" 172 | case Clef.suboctaveTreble: return "suboctaveTreble" 173 | case let clef where clef.pitch == nil: return "neutral" 174 | default: 175 | return "\(pitch!)@\(staffLocation.locationType)\(staffLocation.number)" 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/music-notation/Collection+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+Helpers.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-10-15. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public extension Collection { 10 | var lastIndex: Self.Index { 11 | guard endIndex != startIndex else { return startIndex } 12 | return index(endIndex, offsetBy: -1) 13 | } 14 | 15 | func isValidIndex(_ index: Self.Index) -> Bool { 16 | if isEmpty { 17 | return false 18 | } 19 | return index >= startIndex && index < endIndex 20 | } 21 | 22 | func isValidIndexRange(_ range: Range) -> Bool { 23 | if isEmpty { 24 | return false 25 | } 26 | let upperBound = range ~= range.upperBound ? range.upperBound : index(range.upperBound, offsetBy: -1) 27 | return range.lowerBound >= startIndex && upperBound < endIndex 28 | } 29 | 30 | subscript(safe index: Self.Index) -> Self.Iterator.Element? { 31 | guard isValidIndex(index) else { return nil } 32 | return self[index] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/music-notation/DiatonicIntervalQuality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiatonicIntervalQuality.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2025-01-19. 6 | // Copyright © 2025 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum DiatonicIntervalQuality { 10 | case unison 11 | case second 12 | case third 13 | case fourth 14 | case fifth 15 | case sixth 16 | case seventh 17 | case octave 18 | 19 | init(with interval: Interval) throws { 20 | switch (interval.diatonic, interval.semitones()) { 21 | case (0, 0), (0, 1): 22 | self = .unison 23 | 24 | case (1, 0), (1, 1), (1, 2), (1, 3): 25 | self = .second 26 | 27 | case (2, 2), (2, 3), (2, 4), (2, 5): 28 | self = .third 29 | 30 | case (3, 4), (3, 5), (3, 6): 31 | self = .fourth 32 | 33 | case (4, 6), (4, 7), (4, 8): 34 | self = .fifth 35 | 36 | case (5, 7), (5, 8), (5, 9), (5, 10): 37 | self = .sixth 38 | 39 | case (6, 9), (6, 10), (6, 11), (6, 12): 40 | self = .seventh 41 | 42 | case (7, 11), (7, 12): 43 | self = .octave 44 | 45 | default: 46 | throw IntervalError.intervalOutOfBounds 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/music-notation/Dynamics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dynamics.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum Dynamics: CaseIterable, Sendable { 10 | case ppp 11 | case pp 12 | case p 13 | case mp 14 | case mf 15 | case f 16 | case ff 17 | case fff 18 | } 19 | 20 | extension Dynamics { 21 | func name() -> String { 22 | switch self { 23 | case .ppp: return "pianississimo" 24 | case .pp: return "pianissimo" 25 | case .p: return "piano" 26 | case .mp: return "mezzo-piano" 27 | case .mf: return "mezzo-forte" 28 | case .f: return "forte" 29 | case .ff: return "fortissimo" 30 | case .fff: return "fortississimo" 31 | } 32 | } 33 | 34 | // LOC! 35 | func level() -> String { 36 | switch self { 37 | case .ppp: return "very very quiet" 38 | case .pp: return "very quiet" 39 | case .p: return "quiet" 40 | case .mp: return "moderately quiet" 41 | case .mf: return "moderately loud" 42 | case .f: return "loud" 43 | case .ff: return "very loud" 44 | case .fff: return "very very loud" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/music-notation/Element.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Element.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-08-30. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public protocol Element {} 10 | -------------------------------------------------------------------------------- /Sources/music-notation/Enharmonic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enharmonic.swift 3 | // music-notation 4 | // 5 | // Created by Rob Hudson on 2016-09-16. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public protocol Enharmonic: Equatable { 10 | func isEnharmonic(with other: Self) -> Bool 11 | } 12 | -------------------------------------------------------------------------------- /Sources/music-notation/ImmutableMeasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImmutableMeasure.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-03-06. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public protocol ImmutableMeasure: NotesHolder { 10 | var timeSignature: TimeSignature { get } 11 | var key: Key? { get } 12 | var notes: [[NoteCollection]] { get } 13 | var noteCount: [Int] { get } 14 | 15 | /// Stores all clef changes that took place in this measure 16 | var clefs: [Double: Clef] { get } 17 | 18 | /// Stores the last clef used in the measure 19 | var lastClef: Clef? { get } 20 | 21 | /// Stores the clef used when the measure was created or inserted into the Staff 22 | var originalClef: Clef? { get } 23 | 24 | // Collection Conformance 25 | var startIndex: Int { get } 26 | var endIndex: Int { get } 27 | func index(after index: Int) -> Int 28 | func index(before index: Int) -> Int 29 | 30 | init(timeSignature: TimeSignature, key: Key?) 31 | init(timeSignature: TimeSignature, key: Key?, notes: [[NoteCollection]]) 32 | } 33 | 34 | public func == (lhs: T, rhs: T) -> Bool { 35 | guard lhs.timeSignature == rhs.timeSignature, 36 | lhs.key == rhs.key, 37 | lhs.notes.count == rhs.notes.count, 38 | lhs.clefs == rhs.clefs, 39 | lhs.lastClef == rhs.lastClef else { 40 | return false 41 | } 42 | 43 | for index in 0 ..< lhs.notes.count { 44 | guard lhs.notes[index].count == rhs.notes[index].count else { return false } 45 | 46 | for inner in 0 ..< lhs.notes[index].count { 47 | if lhs.notes[index][inner] == rhs.notes[index][inner] { 48 | continue 49 | } else { 50 | return false 51 | } 52 | } 53 | } 54 | return true 55 | } 56 | 57 | // MARK: - Collection Conformance Helpers 58 | 59 | /// One slice of `NoteCollection` from a note set at a particular time 60 | public struct MeasureSlice: Equatable { 61 | public let noteSetIndex: Int 62 | public let noteCollection: NoteCollection 63 | public static func == (lhs: MeasureSlice, rhs: MeasureSlice) -> Bool { 64 | lhs.noteSetIndex == rhs.noteSetIndex && 65 | lhs.noteCollection == rhs.noteCollection 66 | } 67 | } 68 | 69 | public extension ImmutableMeasure { 70 | var startIndex: Int { 0 } 71 | var endIndex: Int { notes.map { $0.endIndex }.max() ?? 0 } 72 | func index(after index: Int) -> Int { notes.index(after: index) } 73 | func index(before index: Int) -> Int { notes.index(before: index) } 74 | 75 | internal static func measureSlices(at position: Int, in notes: [[NoteCollection]]) -> [MeasureSlice]? { 76 | notes.enumerated().compactMap { noteSetIndex, noteCollections in 77 | guard let noteCollection = noteCollections[safe: position] else { return nil } 78 | return MeasureSlice(noteSetIndex: noteSetIndex, noteCollection: noteCollection) 79 | } 80 | } 81 | } 82 | 83 | public struct MeasureIterator: IteratorProtocol { 84 | var currentIndex: Int = 0 85 | let notes: [[NoteCollection]] 86 | let endIndex: Int 87 | 88 | init(_ measure: T) { 89 | notes = measure.notes 90 | endIndex = measure.endIndex 91 | } 92 | 93 | public mutating func next() -> [MeasureSlice]? { 94 | defer { currentIndex += 1 } 95 | if currentIndex >= endIndex { 96 | return nil 97 | } 98 | return Measure.measureSlices(at: currentIndex, in: notes) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/music-notation/Instrument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Instrument.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-03-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public struct Instrument: Sendable { 10 | /// Number of steps that this instrument is transposed. `chromatic` is measured in semitones. 11 | public let name: String 12 | public let shortName: String 13 | 14 | public let chromaticTransposition: Int 15 | public let diatonicTransposition: Int 16 | 17 | public init( 18 | name: String = "", 19 | shortName: String = "", 20 | chromaticTransposition: Int = 0, 21 | diatonicTransposition: Int = 0 22 | ) { 23 | self.name = name 24 | self.shortName = shortName 25 | self.chromaticTransposition = chromaticTransposition 26 | self.diatonicTransposition = diatonicTransposition 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/music-notation/Interval.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Interval.swift 3 | // music-notation 4 | // 5 | // Created by Rob Hudson on 2016-08-01. 6 | // Rewritten by Steven Woolgar on 2025-01-19. 7 | // Copyright © 2016 Kyle Sherman. All rights reserved. 8 | // 9 | 10 | public struct Interval { 11 | enum Constants { 12 | static let semitonesPerOctave = 12 13 | static let diatonicQualitiesPerOctave = 7 14 | } 15 | 16 | public let diatonic: Int 17 | internal let _semitones: Int 18 | 19 | public init(diatonic: Int, semitones: Int) throws { 20 | guard semitones >= 0 else { throw IntervalError.semitoneNumberNotPositive } 21 | guard diatonic >= 0 else { throw IntervalError.diatonicNumberNotPositive } 22 | 23 | self.diatonic = diatonic 24 | self._semitones = semitones 25 | 26 | // Check the validity of the interval by attempting to calculate these qualities. 27 | _ = try self.intervalQuality() 28 | _ = try self.diatonicIntervalQuality() 29 | } 30 | 31 | public init(diatonicIntervalQuality: DiatonicIntervalQuality, semitones: Int) throws { 32 | guard semitones >= 0 else { throw IntervalError.semitoneNumberNotPositive } 33 | 34 | let diatonic = switch diatonicIntervalQuality { 35 | case .unison: 0 36 | case .second: 1 37 | case .third: 2 38 | case .fourth: 3 39 | case .fifth: 4 40 | case .sixth: 5 41 | case .seventh: 6 42 | case .octave: 7 43 | } 44 | 45 | try self.init(diatonic: diatonic, semitones: semitones) 46 | } 47 | 48 | public init(intervalQuality: IntervalQuality) throws { 49 | switch intervalQuality { 50 | case .perfectUnison: 51 | try self .init(diatonic: 0, semitones: 0) 52 | 53 | case .augmentedUnison: 54 | try self .init(diatonic: 0, semitones: 1) 55 | 56 | // Seconds 57 | case .diminishedSecond: 58 | try self .init(diatonic: 1, semitones: 0) 59 | case .minorSecond: 60 | try self .init(diatonic: 1, semitones: 1) 61 | case .majorSecond: 62 | try self .init(diatonic: 1, semitones: 2) 63 | case .augmentedSecond: 64 | try self .init(diatonic: 1, semitones: 3) 65 | 66 | // Thirds 67 | case .diminishedThird: 68 | try self .init(diatonic: 2, semitones: 2) 69 | case .minorThird: 70 | try self .init(diatonic: 2, semitones: 3) 71 | case .majorThird: 72 | try self .init(diatonic: 2, semitones: 4) 73 | case .augmentedThird: 74 | try self .init(diatonic: 2, semitones: 5) 75 | 76 | // Fourths 77 | case .diminishedFourth: 78 | try self .init(diatonic: 3, semitones: 4) 79 | case .perfectFourth: 80 | try self .init(diatonic: 3, semitones: 5) 81 | case .augmentedFourth: 82 | try self .init(diatonic: 3, semitones: 6) 83 | 84 | // Fifths 85 | case .diminishedFifth: 86 | try self .init(diatonic: 4, semitones: 6) 87 | case .perfectFifth: 88 | try self .init(diatonic: 4, semitones: 7) 89 | case .augmentedFifth: 90 | try self .init(diatonic: 4, semitones: 8) 91 | 92 | // Sixths 93 | case .diminishedSixth: 94 | try self .init(diatonic: 5, semitones: 7) 95 | case .minorSixth: 96 | try self .init(diatonic: 5, semitones: 8) 97 | case .majorSixth: 98 | try self .init(diatonic: 5, semitones: 9) 99 | case .augmentedSixth: 100 | try self .init(diatonic: 5, semitones: 10) 101 | 102 | // Sevenths 103 | case .diminishedSeventh: 104 | try self .init(diatonic: 6, semitones: 9) 105 | case .minorSeventh: 106 | try self .init(diatonic: 6, semitones: 10) 107 | case .majorSeventh: 108 | try self .init(diatonic: 6, semitones: 11) 109 | case .augmentedSeventh: 110 | try self .init(diatonic: 6, semitones: 12) 111 | 112 | // Octaves 113 | case .diminishedOctave: 114 | try self .init(diatonic: 7, semitones: 11) 115 | case .perfectOctave: 116 | try self .init(diatonic: 7, semitones: 12) 117 | } 118 | } 119 | 120 | // Normalized number of semitones. This allows use to support multi-octal intervals and yet 121 | // still know what the intra-octave values are. 122 | func semitones() -> Int { 123 | ((_semitones - 1) % Constants.semitonesPerOctave) + 1 124 | } 125 | 126 | func diatonicIntervalQuality() throws -> DiatonicIntervalQuality { 127 | try DiatonicIntervalQuality(with: self) 128 | } 129 | 130 | func intervalQuality() throws -> IntervalQuality { 131 | try IntervalQuality(with: self) 132 | } 133 | } 134 | 135 | public enum IntervalError: Error { 136 | case semitoneNumberNotPositive 137 | case diatonicNumberNotPositive 138 | case intervalOutOfBounds 139 | } 140 | -------------------------------------------------------------------------------- /Sources/music-notation/IntervalQuality.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntervalQuality.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2025-01-19. 6 | // Copyright © 2025 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum IntervalQuality { 10 | // Unisons 11 | case perfectUnison 12 | case augmentedUnison 13 | 14 | // Seconds 15 | case diminishedSecond 16 | case minorSecond 17 | case majorSecond 18 | case augmentedSecond 19 | 20 | // Thirds 21 | case diminishedThird 22 | case minorThird 23 | case majorThird 24 | case augmentedThird 25 | 26 | // Fourths 27 | case diminishedFourth 28 | case perfectFourth 29 | case augmentedFourth 30 | 31 | // Fifths 32 | case diminishedFifth 33 | case perfectFifth 34 | case augmentedFifth 35 | 36 | // Sixths 37 | case diminishedSixth 38 | case minorSixth 39 | case majorSixth 40 | case augmentedSixth 41 | 42 | // Sevenths 43 | case diminishedSeventh 44 | case minorSeventh 45 | case majorSeventh 46 | case augmentedSeventh 47 | 48 | // Octaves 49 | case diminishedOctave 50 | case perfectOctave 51 | 52 | init(with interval: Interval) throws { 53 | switch (interval.diatonic, interval.semitones()) { 54 | case (0, 0): 55 | self = .perfectUnison 56 | case (0, 1): 57 | self = .augmentedUnison 58 | 59 | case (1, 0): 60 | self = .diminishedSecond 61 | case (1, 1): 62 | self = .minorSecond 63 | case (1, 2): 64 | self = .majorSecond 65 | case (1, 3): 66 | self = .augmentedSecond 67 | 68 | case (2, 2): 69 | self = .diminishedThird 70 | case (2, 3): 71 | self = .minorThird 72 | case (2, 4): 73 | self = .majorThird 74 | case (2, 5): 75 | self = .augmentedThird 76 | 77 | case (3, 4): 78 | self = .diminishedFourth 79 | case (3, 5): 80 | self = .perfectFourth 81 | case (3, 6): 82 | self = .augmentedFourth 83 | 84 | case (4, 6): 85 | self = .diminishedFifth 86 | case (4, 7): 87 | self = .perfectFifth 88 | case (4, 8): 89 | self = .augmentedFifth 90 | 91 | case (5, 7): 92 | self = .diminishedSixth 93 | case (5, 8): 94 | self = .minorSixth 95 | case (5, 9): 96 | self = .majorSixth 97 | case (5, 10): 98 | self = .augmentedSixth 99 | 100 | case (6, 9): 101 | self = .diminishedSeventh 102 | case (6, 10): 103 | self = .minorSeventh 104 | case (6, 11): 105 | self = .majorSeventh 106 | case (6, 12): 107 | self = .augmentedSeventh 108 | 109 | case (7, 11): 110 | self = .diminishedOctave 111 | case (7, 12): 112 | self = .perfectOctave 113 | 114 | default: 115 | throw IntervalError.intervalOutOfBounds 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/music-notation/Jump.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Jump.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-08-27. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum JumpType { 10 | case daCapo 11 | case daCapoAlFine 12 | case daCapoAlCoda 13 | case dalSegnoAlCoda 14 | case dalSegnoAlFine 15 | case dalSegno 16 | case daCapoAlDoppiaCoda 17 | case dalSegnoAlDoppiaCoda 18 | case dalDoppioSegno 19 | case dalDoppioSegnoAlCoda 20 | case dalDoppioSegnoAlDoppiaCODA 21 | case dalDoppioSegnoAlFine 22 | case userJump 23 | } 24 | -------------------------------------------------------------------------------- /Sources/music-notation/Key.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Key.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-07-11. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public enum KeyType: Sendable { 10 | case major 11 | case minor 12 | } 13 | 14 | public struct Key: Sendable { 15 | fileprivate let type: KeyType 16 | fileprivate let noteLetter: NoteLetter 17 | fileprivate let accidental: Accidental 18 | 19 | public init(noteLetter: NoteLetter, accidental: Accidental = .natural, type: KeyType = .major) { 20 | self.noteLetter = noteLetter 21 | self.accidental = accidental 22 | self.type = type 23 | } 24 | } 25 | 26 | extension Key: CustomDebugStringConvertible { 27 | public var debugDescription: String { 28 | "\(noteLetter)\(accidental.debugDescription) \(type)" 29 | } 30 | } 31 | 32 | extension Key: Equatable { 33 | public static func == (lhs: Key, rhs: Key) -> Bool { 34 | if lhs.type == rhs.type, 35 | lhs.noteLetter == rhs.noteLetter, 36 | lhs.accidental == rhs.accidental { 37 | return true 38 | } 39 | return false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/music-notation/Marker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Jump.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-08-27. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum MarkerType: CaseIterable { 10 | case segno 11 | case varSegno 12 | case coda 13 | case varCoda 14 | case codetta 15 | case fine 16 | case toCoda 17 | case toCodaSymbol 18 | case daCoda 19 | case dalDoppioSegnoAlCoda 20 | case daDoppiaCoda 21 | case userMarker 22 | 23 | static func marker(from string: String) throws -> MarkerType { 24 | switch string { 25 | case "segno": 26 | return .segno 27 | case "varsegno": 28 | return .varSegno 29 | case "coda": 30 | return .coda 31 | case "Var Coda": 32 | return .varCoda 33 | case "Codetta": 34 | return .codetta 35 | case "Fine": 36 | return .fine 37 | case "To Coda": 38 | return .toCoda 39 | case "To Coda Symbol": 40 | return .toCodaSymbol 41 | case "Da Coda": 42 | return .daCoda 43 | case "Dal Doppio Segno Al Coda": 44 | return .dalDoppioSegnoAlCoda 45 | case "Da Doppia Coda": 46 | return .daDoppiaCoda 47 | case "userMarker": 48 | return .userMarker 49 | default: 50 | throw MarkerTypeError.MarkerTypeStringConversionError 51 | } 52 | } 53 | } 54 | 55 | public enum MarkerTypeError: Error { 56 | case MarkerTypeStringConversionError 57 | } 58 | -------------------------------------------------------------------------------- /Sources/music-notation/MeasureDurationValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureDurationValidator.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-08-06. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | import Darwin.C.math 10 | 11 | /// This is a collection of static functions that will give information about the completeness of the duration of a 12 | /// `Measure`. A measure must have a certain number of notes according to its `TimeSignature` in order to be valid. 13 | /// `MeasureDurationValidator` provides information as to the validitiy of the measure and any information that can be used 14 | /// in order to be able to modify the measure so that it can be made valid. 15 | /// 16 | public enum MeasureDurationValidator { 17 | /// This represents the state of a measure's duration. It represents whether the `Measure` is full, notFull, 18 | /// overfilled, or somehow invalid. The fullness is dictated by the time signature and how many notes are in the 19 | /// measure when it is checked. 20 | /// 21 | /// - notFull: The measure doesn't have all the notes needed to be full. The `availableNotes` associated value will give a 22 | /// dictionary of notes that will fit. It gives the smallest number of notes possible to fill. 23 | /// - full: The measure is complete and cannot hold any more or less notes. 24 | /// - overfilled: The measure has too many notes to be complete. The `overflowingNotes` associated value will give the range 25 | /// of notes that would need to be removed for the measure to not be full anymore. Note that if the range of notes 26 | /// is removed, it could be `full` or `notFull` depending on how large the last note removed was. 27 | /// 28 | public enum CompletionState: Equatable { 29 | case notFull(availableNotes: [NoteDuration: Int]) 30 | case full 31 | case overfilled(overflowingNotes: CountableRange) 32 | case invalid 33 | 34 | public static func == (lhs: CompletionState, rhs: CompletionState) -> Bool { 35 | switch (lhs, rhs) { 36 | case (.full, .full): 37 | return true 38 | case let (.notFull(lhsValue), .notFull(rhsValue)) where lhsValue == rhsValue: 39 | return true 40 | case let (.overfilled(lhsValue), .overfilled(rhsValue)) where lhsValue == rhsValue: 41 | return true 42 | case (.invalid, .invalid): 43 | return true 44 | default: 45 | return false 46 | } 47 | } 48 | } 49 | 50 | /// For the given measure, returns an array of `CompletionState` for each set in the measure in order. 51 | /// 52 | /// - parameter measure: The measure for which the `CompletionState` should be calculated. 53 | /// - returns: The `CompletionState` for each set of notes in the measure in order. 54 | /// 55 | public static func completionState(of measure: ImmutableMeasure) -> [CompletionState] { 56 | let baseDuration: NoteDuration 57 | do { 58 | baseDuration = try baseNoteDuration(from: measure) 59 | } catch { 60 | return [.invalid] 61 | } 62 | let fullMeasureTicksBudget = Double(measure.timeSignature.numerator) * baseDuration.ticks 63 | 64 | // Validate each set separately 65 | return measure.notes.enumerated().map { setIndex, noteCollection in 66 | var overFilledStartIndex: Int? 67 | let filledTicks = noteCollection.enumerated().reduce(0.0) { prev, indexAndCollection in 68 | let (index, currentCollection) = indexAndCollection 69 | let newTicks = prev + currentCollection.ticks 70 | if newTicks > fullMeasureTicksBudget, overFilledStartIndex == nil { 71 | overFilledStartIndex = index 72 | } 73 | return newTicks 74 | } 75 | if filledTicks == fullMeasureTicksBudget { 76 | return .full 77 | } else if let overFilledStartIndex = overFilledStartIndex { 78 | return .overfilled(overflowingNotes: CountableRange(uncheckedBounds: (overFilledStartIndex, 79 | measure.noteCount[setIndex]))) 80 | } else if filledTicks < fullMeasureTicksBudget { 81 | return .notFull(availableNotes: availableNotes(within: fullMeasureTicksBudget - filledTicks)) 82 | } else { 83 | return .invalid 84 | } 85 | } 86 | } 87 | 88 | /// Returns the number of a certain `NoteDuration` that will fit in a measure in the specified note set. 89 | /// 90 | /// - parameter noteDuration: The note duration to check 91 | /// - parameter measure: The measure to check 92 | /// - parameter setIndex: The index of the note set to check. Default is 0. 93 | /// - returns: The number of the specified duration that will fit in the measure within the specificed note set. 94 | /// 95 | public static func number(of noteDuration: NoteDuration, fittingIn measure: ImmutableMeasure, inSet setIndex: Int = 0) -> Int { 96 | let baseDuration: NoteDuration 97 | do { 98 | baseDuration = try baseNoteDuration(from: measure) 99 | } catch { 100 | // TODO: Write TimeSignature validation, so this isn't possible 101 | return 0 102 | } 103 | let fullMeasureTicksBudget = Double(measure.timeSignature.numerator) * baseDuration.ticks 104 | let alreadyFilledTicks = measure.notes[setIndex].reduce(0.0) { prev, currentCollection in 105 | prev + currentCollection.ticks 106 | } 107 | let availableTicks = fullMeasureTicksBudget - alreadyFilledTicks 108 | guard availableTicks > 0 else { return 0 } 109 | return Int(availableTicks / noteDuration.ticks) 110 | } 111 | 112 | /// Calculates the `NoteDuration` that is associated with the bottom number of a `TimeSignature`. 113 | /// This handles irrational time signatures. 114 | /// 115 | /// - parameter measure: The measure for which the base duration should be derived. 116 | /// - returns: The duration that is equivalent to the bottom number of the time signature associated with the specified 117 | /// measure. 118 | /// - throws: 119 | /// - MeasureDurationValidatorError.invalidBottomNumber 120 | /// 121 | internal static func baseNoteDuration(from measure: ImmutableMeasure) throws -> NoteDuration { 122 | let denominator = measure.timeSignature.denominator 123 | 124 | // TODO: Replace `pow`, `floor`, and `log` 125 | // https://github.com/drumnkyle/music-notation-core/issues/146 126 | let rationalizedDenominator = Int(pow(2, floor(log(Double(denominator)) / log(2)))) 127 | 128 | // TODO: (Kyle) We should validate in TimeSignature to make sure the number 129 | // isn't too large. Then I guess we can make this a force unwrap, because the math above 130 | // means it will always be a power of 2 and NoteDuration is always power of 2. 131 | if let timeSignatureValue = NoteDuration.TimeSignatureValue(rawValue: rationalizedDenominator) { 132 | return NoteDuration(timeSignatureValue: timeSignatureValue) 133 | } else { 134 | throw MeasureDurationValidatorError.invalidBottomNumber 135 | } 136 | } 137 | 138 | private static func availableNotes(within ticks: Double) -> [NoteDuration: Int] { 139 | var ticksLeft = ticks 140 | var availableNotes: [NoteDuration: Int] = [:] 141 | while ticksLeft != 0 { 142 | let duration = findLargestDuration(lessThan: ticksLeft) 143 | let noteCount = Int(ticksLeft / duration.ticks) 144 | availableNotes[duration] = noteCount 145 | ticksLeft -= Double(noteCount) * duration.ticks 146 | } 147 | return availableNotes 148 | } 149 | 150 | // TODO: Refactor - move allDurations and function findLargest(start:, end:) -> NoteDuration out of this function 151 | // https://github.com/drumnkyle/music-notation-core/issues/141 152 | private static func findLargestDuration(lessThan ticks: Double) -> NoteDuration { 153 | let allDurations: [NoteDuration] = [ 154 | .large, .long, .doubleWhole, .whole, .half, .quarter, .eighth, .sixteenth, 155 | .thirtySecond, .sixtyFourth, .oneTwentyEighth, .twoFiftySixth 156 | ] 157 | let allTicks = allDurations.map { $0.ticks } 158 | func findLargest(start: Int, end: Int) -> NoteDuration { 159 | guard end - start > 1 else { return allDurations[end] } 160 | 161 | let mid = (start + end) / 2 162 | if allTicks[mid] < ticks { 163 | return findLargest(start: start, end: mid) 164 | } else if allTicks[mid] > ticks { 165 | return findLargest(start: mid, end: end) 166 | } else { 167 | return allDurations[mid] 168 | } 169 | } 170 | return findLargest(start: 0, end: allTicks.count - 1) 171 | } 172 | } 173 | 174 | public enum MeasureDurationValidatorError: Error { 175 | case invalidBottomNumber 176 | case internalError 177 | } 178 | -------------------------------------------------------------------------------- /Sources/music-notation/MeasureRepeat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureRepeat.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-06-15. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// This represents a set of one or more measures that is repeated `repeatCount` times. 10 | public struct MeasureRepeat { 11 | public var repeatCount: Int { 12 | didSet { 13 | recalculateMeasureCount() 14 | } 15 | } 16 | 17 | public var measures: [Measure] { 18 | didSet { 19 | recalculateMeasureCount() 20 | } 21 | } 22 | 23 | /// The number of measures, including repeated measures 24 | public private(set) var measureCount: Int 25 | 26 | public init(measures: [Measure], repeatCount: Int = 1) throws { 27 | guard !measures.isEmpty else { throw MeasureRepeatError.noMeasures } 28 | guard repeatCount > 0 else { throw MeasureRepeatError.invalidRepeatCount } 29 | self.measures = measures 30 | self.repeatCount = repeatCount 31 | measureCount = MeasureRepeat.measureCount(forMeasures: measures, repeatCount: repeatCount) 32 | } 33 | 34 | /// Inserts a measure into the repeat. The index can only be within the count of original measures or 35 | /// equal to the count. If it is equal to the count, the measure will be added to the end of the 36 | /// measures to be repeated. 37 | /// 38 | /// - parameter measure: The measure to be inserted. 39 | /// - parameter index: The index at which to insert the measure within the measures to be repeated. 40 | /// - throws: 41 | /// - `MeasureRepeatError.indexOutOfRange` 42 | /// - `MeasureRepeatError.cannotModifyRepeatedMeasures` 43 | /// 44 | public mutating func insertMeasure(_ measure: Measure, at index: Int) throws { 45 | guard index >= 0 else { throw MeasureRepeatError.indexOutOfRange } 46 | guard index <= measures.count else { throw MeasureRepeatError.cannotModifyRepeatedMeasures } 47 | measures.insert(measure, at: index) 48 | } 49 | 50 | internal func expand() -> [ImmutableMeasure] { 51 | let repeatedMeasuresHolders = measures.map { 52 | RepeatedMeasure(immutableMeasure: $0) as ImmutableMeasure 53 | } 54 | let measuresHolders = measures.map { $0 as ImmutableMeasure } 55 | var allMeasures: [ImmutableMeasure] = measuresHolders 56 | for _ in 0 ..< repeatCount { 57 | allMeasures += repeatedMeasuresHolders 58 | } 59 | return allMeasures 60 | } 61 | 62 | private mutating func recalculateMeasureCount() { 63 | measureCount = MeasureRepeat.measureCount(forMeasures: measures, repeatCount: repeatCount) 64 | } 65 | 66 | private static func measureCount(forMeasures measures: [Measure], repeatCount: Int) -> Int { 67 | measures.count + repeatCount * measures.count 68 | } 69 | } 70 | 71 | extension MeasureRepeat: CustomDebugStringConvertible { 72 | public var debugDescription: String { 73 | let measuresDescription = measures.map { $0.debugDescription }.joined(separator: ", ") 74 | 75 | return "[ \(measuresDescription) ] × \(repeatCount + 1)" 76 | } 77 | } 78 | 79 | extension MeasureRepeat: NotesHolder {} 80 | 81 | extension MeasureRepeat: Equatable { 82 | public static func == (lhs: MeasureRepeat, rhs: MeasureRepeat) -> Bool { 83 | guard lhs.repeatCount == rhs.repeatCount else { return false } 84 | guard lhs.measures == rhs.measures else { return false } 85 | return true 86 | } 87 | } 88 | 89 | public enum MeasureRepeatError: Error { 90 | case noMeasures 91 | case invalidRepeatCount 92 | case cannotModifyRepeatedMeasures 93 | case indexOutOfRange 94 | } 95 | -------------------------------------------------------------------------------- /Sources/music-notation/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2015-06-12. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public struct Note: NoteCollection, Sendable { 10 | public let noteCount = 1 11 | public let noteDuration: NoteDuration 12 | public let noteTimingCount = 1 13 | public let groupingOrder = 1 14 | public var first: Note? { self } 15 | public var last: Note? { self } 16 | 17 | public let pitches: [SpelledPitch] 18 | 19 | public let isRest: Bool 20 | 21 | public var accent: Accent? 22 | public var isStaccato = false 23 | public var dynamics: Dynamics? 24 | public var striking: Striking? 25 | public var hidden = false 26 | 27 | // These two are used for tablature. You can not infer them from a note. 28 | // ?But then fret and string without a tuning doesn't give us much? 29 | public var fret: Int? = nil 30 | public var string: Int? = nil 31 | 32 | // Which voice will this note be a part of. 33 | public var voice: Int = 1 34 | 35 | internal var tie: Tie? 36 | 37 | /// Initialize a rest. 38 | public init(_ restDuration: NoteDuration) { 39 | noteDuration = restDuration 40 | pitches = [] 41 | isRest = true 42 | } 43 | 44 | /// Initialize a note with a single pitch. 45 | public init(_ noteDuration: NoteDuration, pitch: SpelledPitch) { 46 | self.noteDuration = noteDuration 47 | pitches = [pitch] 48 | isRest = false 49 | } 50 | 51 | /// Initialize a note with multiple pitches (chord). 52 | public init(_ noteDuration: NoteDuration, pitches: [SpelledPitch]) { 53 | isRest = false 54 | self.noteDuration = noteDuration 55 | self.pitches = pitches 56 | } 57 | 58 | // MARK: - Methods 59 | 60 | // MARK: Public 61 | 62 | public func note(at index: Int) throws -> Note { 63 | guard index == 0 else { throw NoteError.invalidNoteIndex } 64 | return self 65 | } 66 | 67 | internal mutating func modifyTie(_ request: Tie) throws { 68 | // Nothing to do if it's the same value 69 | guard tie != request else { return } 70 | switch (tie, request) { 71 | case (.begin?, .end), (.end?, .begin): 72 | tie = .beginAndEnd 73 | case (nil, let value): 74 | tie = value 75 | default: 76 | throw NoteError.invalidRequestedTieState 77 | } 78 | } 79 | 80 | /// Remove the tie from the note. 81 | /// 82 | /// - parameter currentTie: What part of the tie on the note the caller wants to remove. This is important if the 83 | /// note is both the beginning and end of a tie 84 | /// - throws: 85 | /// - `NoteError.invalidRequestedTieState` 86 | /// 87 | internal mutating func removeTie(_ currentTie: Tie) throws { 88 | switch (currentTie, tie) { 89 | case (.beginAndEnd, _): 90 | throw NoteError.invalidRequestedTieState 91 | case (_, nil): 92 | return 93 | case let (request, current?) where request == current: 94 | tie = nil 95 | case (.begin, .beginAndEnd?): 96 | tie = .end 97 | case (.end, .beginAndEnd?): 98 | tie = .begin 99 | default: 100 | throw NoteError.invalidRequestedTieState 101 | } 102 | } 103 | } 104 | 105 | extension Note: Equatable { 106 | public static func == (lhs: Note, rhs: Note) -> Bool { 107 | if lhs.noteDuration == rhs.noteDuration, 108 | lhs.pitches == rhs.pitches, 109 | lhs.isRest == rhs.isRest, 110 | lhs.accent == rhs.accent, 111 | lhs.isStaccato == rhs.isStaccato, 112 | lhs.dynamics == rhs.dynamics, 113 | lhs.striking == rhs.striking, 114 | lhs.tie == rhs.tie { 115 | return true 116 | } else { 117 | return false 118 | } 119 | } 120 | } 121 | 122 | extension Note: CustomDebugStringConvertible { 123 | public var debugDescription: String { 124 | let pitchesString: String 125 | if pitches.count > 1 { 126 | pitchesString = "\(pitches)" 127 | } else { 128 | if let pitch = pitches.first { 129 | pitchesString = "\(pitch)" 130 | } else { 131 | pitchesString = "" 132 | } 133 | } 134 | return "\(tie == .end || tie == .beginAndEnd ? "_" : "")\(noteDuration)\(pitchesString)\(isRest ? "R" : "")\(tie == .begin || tie == .beginAndEnd ? "_" : "")" 135 | } 136 | } 137 | 138 | public enum NoteError: Error { 139 | case invalidRequestedTieState 140 | case invalidNoteIndex 141 | } 142 | -------------------------------------------------------------------------------- /Sources/music-notation/NoteCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteCollection.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2015-06-15. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// 10 | /// This is a protocol that represents anything that represents one or more notes. 11 | /// i.e. `Note` and `Tuplet` both conform to this. 12 | public protocol NoteCollection { 13 | /// The count of actual notes in this `NoteCollection` 14 | var noteCount: Int { get } 15 | 16 | /// The duration of the note that in combination with `noteTimingCount` 17 | /// will give you the amount of time this `NoteCollection` occupies. 18 | var noteDuration: NoteDuration { get } 19 | 20 | /// The number of notes to indicate the amount of time occupied by this 21 | /// `NoteCollection`. Combine this with `noteDuration`. 22 | var noteTimingCount: Int { get } 23 | 24 | /// The grouping order defined for this `NoteCollection` 25 | var groupingOrder: Int { get } 26 | 27 | var first: Note? { get } 28 | var last: Note? { get } 29 | 30 | var ticks: Double { get } 31 | 32 | /// - returns: The note at the given index. 33 | func note(at index: Int) throws -> Note 34 | } 35 | 36 | public extension NoteCollection { 37 | var ticks: Double { Double(noteTimingCount) * noteDuration.ticks } 38 | } 39 | 40 | public func == (lhs: NoteCollection, rhs: NoteCollection) -> Bool { 41 | if let left = lhs as? Note, 42 | let right = rhs as? Note, 43 | left == right { 44 | return true 45 | } else if let left = lhs as? Tuplet, 46 | let right = rhs as? Tuplet, 47 | left == right { 48 | return true 49 | } else { 50 | return false 51 | } 52 | } 53 | 54 | public func != (lhs: NoteCollection, rhs: NoteCollection) -> Bool { 55 | !(lhs == rhs) 56 | } 57 | -------------------------------------------------------------------------------- /Sources/music-notation/NoteDuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteDuration.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-08-20. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | public struct NoteDuration: Hashable, Sendable { 10 | public enum Value: CustomDebugStringConvertible, Hashable, Sendable { 11 | case large 12 | case long 13 | case doubleWhole 14 | case whole 15 | case half 16 | case quarter 17 | case eighth 18 | case sixteenth 19 | case thirtySecond 20 | case sixtyFourth 21 | case oneTwentyEighth 22 | case twoFiftySixth 23 | 24 | public var debugDescription: String { 25 | switch self { 26 | case .large: return "8" 27 | case .long: return "4" 28 | case .doubleWhole: return "2" 29 | case .whole: return "1" 30 | case .half: return "1/2" 31 | case .quarter: return "1/4" 32 | case .eighth: return "1/8" 33 | case .sixteenth: return "1/16" 34 | case .thirtySecond: return "1/32" 35 | case .sixtyFourth: return "1/64" 36 | case .oneTwentyEighth: return "1/128" 37 | case .twoFiftySixth: return "1/256" 38 | } 39 | } 40 | } 41 | 42 | /// This holds the value for the bottom number of the time signature and relates the `NoteDuration.Value` to this 43 | /// number or nil if it cannot be used for the bottom number of a time signature. 44 | /// 45 | public enum TimeSignatureValue: Int, Hashable, Sendable { 46 | case whole = 1 47 | case half = 2 48 | case quarter = 4 49 | case eighth = 8 50 | case sixteenth = 16 51 | case thirtySecond = 32 52 | case sixtyFourth = 64 53 | case oneTwentyEighth = 128 54 | // As far as I can tell, 128th note is the largest allowed 55 | 56 | init?(value: Value) { 57 | switch value { 58 | case .whole: self = .whole 59 | case .half: self = .half 60 | case .quarter: self = .quarter 61 | case .eighth: self = .eighth 62 | case .sixteenth: self = .sixteenth 63 | case .thirtySecond: self = .thirtySecond 64 | case .sixtyFourth: self = .sixtyFourth 65 | case .oneTwentyEighth: self = .oneTwentyEighth 66 | default: return nil 67 | } 68 | } 69 | 70 | fileprivate var duration: NoteDuration { 71 | switch self { 72 | case .whole: return .whole 73 | case .half: return .half 74 | case .quarter: return .quarter 75 | case .eighth: return .eighth 76 | case .sixteenth: return .sixteenth 77 | case .thirtySecond: return .thirtySecond 78 | case .sixtyFourth: return .sixtyFourth 79 | case .oneTwentyEighth: return .oneTwentyEighth 80 | } 81 | } 82 | } 83 | 84 | /// The duration value of the `NoteDuration`. i.e. eighth, sixteenth, etc. 85 | public let value: Value 86 | /// The number of dots for this `NoteDuration`. 87 | public let dotCount: Int 88 | 89 | /// The value for which the bottom number of time signature will be if this duration value is used. 90 | /// 91 | public let timeSignatureValue: TimeSignatureValue? 92 | 93 | /// This is the number of ticks for the duration with the `dotCount` taken into account. This is a mathematical 94 | /// representation of a `NoteDuration` that can be used for different calculations of equivalence. 95 | /// 96 | internal var ticks: Double { 97 | var ticks: Double = 0 98 | let baseTicks: Double = { 99 | switch value { 100 | case .large: return 65_536 101 | case .long: return 32_768 102 | case .doubleWhole: return 16_384 103 | case .whole: return 8_192 104 | case .half: return 4_096 105 | case .quarter: return 2_048 106 | case .eighth: return 1_024 107 | case .sixteenth: return 512 108 | case .thirtySecond: return 256 109 | case .sixtyFourth: return 128 110 | case .oneTwentyEighth: return 64 111 | case .twoFiftySixth: return 32 112 | } 113 | }() 114 | ticks += baseTicks 115 | var dotValue = baseTicks / 2 116 | for _ in 0 ..< dotCount { 117 | ticks += dotValue 118 | dotValue /= 2 119 | } 120 | return ticks 121 | } 122 | 123 | private init(value: Value) { 124 | self.value = value 125 | dotCount = 0 126 | timeSignatureValue = TimeSignatureValue(value: value) 127 | } 128 | 129 | /// Use this initializer if you would like to create a `NoteDuration` with 1 or more dots. 130 | /// Otherwise, use the static properties if you do not need any dots. 131 | /// 132 | /// - parameter value: The value of the duration. i.e. whole, quarter, eighth, etc. 133 | /// - parameter dotCount: The number of dots for this duration. 134 | /// - throws: 135 | /// - `NoteDurationError.negativeDotCountInvalid` 136 | /// 137 | public init(value: Value, dotCount: Int) throws { 138 | guard dotCount >= 0 else { throw NoteDurationError.negativeDotCountInvalid } 139 | self.value = value 140 | self.dotCount = dotCount 141 | timeSignatureValue = TimeSignatureValue(value: value) 142 | } 143 | 144 | /// Initialize a `NoteDuration` from a `TimeSignatureValue` which encapsulates the bottom number of a `TimeSignature`. 145 | public init(timeSignatureValue: TimeSignatureValue) { 146 | self = timeSignatureValue.duration 147 | } 148 | 149 | public static let large = NoteDuration(value: .large) 150 | public static let long = NoteDuration(value: .long) 151 | public static let doubleWhole = NoteDuration(value: .doubleWhole) 152 | public static let whole = NoteDuration(value: .whole) 153 | public static let half = NoteDuration(value: .half) 154 | public static let quarter = NoteDuration(value: .quarter) 155 | public static let eighth = NoteDuration(value: .eighth) 156 | public static let sixteenth = NoteDuration(value: .sixteenth) 157 | public static let thirtySecond = NoteDuration(value: .thirtySecond) 158 | public static let sixtyFourth = NoteDuration(value: .sixtyFourth) 159 | public static let oneTwentyEighth = NoteDuration(value: .oneTwentyEighth) 160 | public static let twoFiftySixth = NoteDuration(value: .twoFiftySixth) 161 | 162 | /// This can be used to find out how many of a certain duration fits within another a duration. This takes into account 163 | /// the `dotCount` as well. 164 | /// 165 | /// For example: How many eighth notes fit within a quarter note? 166 | /// `NoteDuration.number(of: .eighth, within: .quarter)` = 2.0 167 | /// 168 | /// - parameter noteDuration: the `NoteDuration` you would like to see how many would fit. 169 | /// - parameter baseNoteDuration: the `NoteDuration` that you would to see how many of the first duration will fit into. 170 | /// - returns: A `Double` representing how many of the first duration fit within the second. If the first duration is 171 | /// larger than the second, it will be a Double number less than 0. 172 | /// 173 | public static func number(of noteDuration: NoteDuration, within baseNoteDuration: NoteDuration) -> Double { 174 | baseNoteDuration.ticks / noteDuration.ticks 175 | } 176 | } 177 | 178 | extension NoteDuration: Equatable { 179 | public static func == (lhs: NoteDuration, rhs: NoteDuration) -> Bool { 180 | lhs.value == rhs.value && lhs.dotCount == rhs.dotCount 181 | } 182 | } 183 | 184 | extension NoteDuration: CustomDebugStringConvertible { 185 | public var debugDescription: String { 186 | let dots = String(repeating: ".", count: dotCount) 187 | return "\(value.debugDescription)\(dots)" 188 | } 189 | } 190 | 191 | public enum NoteDurationError: Error { 192 | case negativeDotCountInvalid 193 | } 194 | -------------------------------------------------------------------------------- /Sources/music-notation/NoteLetter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteLetter.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | // swiftlint:disable identifier_name 10 | public enum NoteLetter: Int, Sendable { 11 | case c = 1 12 | case d 13 | case e 14 | case f 15 | case g 16 | case a 17 | case b 18 | } 19 | -------------------------------------------------------------------------------- /Sources/music-notation/NotesHolder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesHolder.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2015-06-15. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// This is a protocol that represents anything that can directly hold multiple notes. 10 | /// i.e. `Measure` and `MeasureRepeat` both conform to this. 11 | /// 12 | public protocol NotesHolder: CustomDebugStringConvertible { 13 | var measureCount: Int { get } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/music-notation/Octave.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Octave.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum Octave: Int, Sendable { 10 | case octaveNegative1 = -1 11 | case octave0 = 0 12 | case octave1 = 1 13 | case octave2 = 2 14 | case octave3 = 3 15 | case octave4 = 4 16 | case octave5 = 5 17 | case octave6 = 6 18 | case octave7 = 7 19 | case octave8 = 8 20 | case octave9 = 9 21 | } 22 | -------------------------------------------------------------------------------- /Sources/music-notation/PageFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageFooter.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2021-01-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | /// Placeholder for eventual staff layout description of a page footer. 10 | public struct PageFooter {} 11 | -------------------------------------------------------------------------------- /Sources/music-notation/PageHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageHeader.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2021-01-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | /// Placeholder for eventual staff layout description of a page header. 10 | public struct PageHeader {} 11 | -------------------------------------------------------------------------------- /Sources/music-notation/Part.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Part.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2021-01-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public struct Part { 10 | // MARK: - Main Properties 11 | 12 | public let instrument: Instrument? 13 | internal private(set) var staves: [Staff] = [] 14 | 15 | public init( 16 | instrument: Instrument? = nil, 17 | staves: [Staff] = [] 18 | ) { 19 | self.instrument = instrument 20 | self.staves = staves 21 | } 22 | } 23 | 24 | // MARK: - Collection Conformance 25 | 26 | extension Part: RandomAccessCollection { 27 | public typealias Index = Int 28 | public var startIndex: Int { staves.startIndex } 29 | public var endIndex: Int { staves.endIndex } 30 | 31 | public func index(after index: Int) -> Index { staves.index(after: index) } 32 | public typealias Iterator = IndexingIterator<[Staff]> 33 | public func makeIterator() -> Iterator { staves.makeIterator() } 34 | } 35 | 36 | // MARK: - BidirectionalCollection Conformance 37 | 38 | extension Part: BidirectionalCollection { 39 | public func index(before index: Int) -> Index { staves.index(before: index) } 40 | } 41 | 42 | // MARK: - RangeReplaceableCollection Conformance 43 | 44 | extension Part: RangeReplaceableCollection { 45 | public init() { 46 | self.init(instrument: nil) 47 | } 48 | 49 | mutating public func replaceSubrange( 50 | _ subrange: Range, 51 | with newElements: C) where C : Collection, Self.Element == C.Element { 52 | staves.replaceSubrange(subrange.relative(to: staves), with: newElements) 53 | } 54 | } 55 | 56 | // MARK: - MutableCollection Conformance 57 | 58 | extension Part: MutableCollection { 59 | public subscript(position: Int) -> Iterator.Element { 60 | get { staves[position] } 61 | set(newValue) { staves[position] = newValue } 62 | } 63 | } 64 | 65 | extension Part: CustomDebugStringConvertible { 66 | public var debugDescription: String { 67 | let stavesDescription = staves.map { $0.debugDescription }.joined(separator: "\n") 68 | let instrumentName = if let name = instrument?.name { 69 | "instrument `\(name)`" 70 | } else { 71 | "" 72 | } 73 | 74 | return "Part(\(instrumentName)), staves(\(stavesDescription))" 75 | } 76 | } 77 | 78 | public enum PartError: Error { 79 | case removeOutOfRange 80 | case staffIndexOutOfRange 81 | case badInstrument 82 | } 83 | -------------------------------------------------------------------------------- /Sources/music-notation/RepeatedMeasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepeatedMeasure.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-03-09. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// This represents a measure that is part of a repeat. This is not one of the original measures that 10 | /// will be repeated. This represents a measure that is a repeat of one of the original measures to 11 | /// be repeated. 12 | /// 13 | /// i.e. If we want to repeat measure A 2 times, you would have 14 | /// 15 | /// | A, repeated A, repeated A | 16 | /// 17 | /// This structure represents those repeated measures "repeated A", so that they can be differentiated from the 18 | /// original measure "A". 19 | /// 20 | /// - Note: Look at `MeasureRepeat.expand()` to see how this is used. 21 | /// 22 | public struct RepeatedMeasure: ImmutableMeasure, Equatable, RandomAccessCollection { 23 | // MARK: - Collection Conformance 24 | 25 | public typealias Index = Int 26 | public subscript(position: Index) -> Iterator.Element { 27 | Measure.measureSlices(at: position, in: notes)! 28 | } 29 | 30 | public typealias Iterator = MeasureIterator 31 | public func makeIterator() -> Iterator { 32 | MeasureIterator(self) 33 | } 34 | 35 | // MARK: - Main Properties 36 | 37 | public let timeSignature: TimeSignature 38 | public let key: Key? 39 | public let notes: [[NoteCollection]] 40 | public let measureCount: Int = 1 41 | public let noteCount: [Int] 42 | public let clefs: [Double: Clef] 43 | public let lastClef: Clef? 44 | public let originalClef: Clef? 45 | 46 | public init( 47 | timeSignature: TimeSignature, 48 | key: Key? = nil, 49 | notes: [[NoteCollection]] = [], 50 | lastClef: Clef? = nil, 51 | originalClef: Clef? = nil, 52 | clefs: [Double: Clef] = [:] 53 | ) { 54 | self.lastClef = lastClef 55 | self.originalClef = originalClef 56 | self.clefs = clefs 57 | self.timeSignature = timeSignature 58 | self.key = key 59 | self.notes = notes 60 | noteCount = notes.map { 61 | $0.reduce(0) { prev, noteCollection in 62 | prev + noteCollection.noteCount 63 | } 64 | } 65 | } 66 | 67 | public init(timeSignature: TimeSignature, key: Key? = nil) { 68 | self.init(timeSignature: timeSignature, key: key, notes: [[]]) 69 | } 70 | 71 | public init(timeSignature: TimeSignature, key: Key? = nil, notes: [[NoteCollection]]) { 72 | self.timeSignature = timeSignature 73 | self.key = key 74 | self.notes = notes 75 | clefs = [:] 76 | noteCount = notes.map { 77 | $0.reduce(0) { prev, noteCollection in 78 | prev + noteCollection.noteCount 79 | } 80 | } 81 | lastClef = nil 82 | originalClef = nil 83 | } 84 | 85 | internal init(immutableMeasure: ImmutableMeasure) { 86 | timeSignature = immutableMeasure.timeSignature 87 | key = immutableMeasure.key 88 | notes = immutableMeasure.notes 89 | lastClef = immutableMeasure.lastClef 90 | originalClef = immutableMeasure.originalClef 91 | clefs = immutableMeasure.clefs 92 | noteCount = immutableMeasure.noteCount 93 | } 94 | } 95 | 96 | extension RepeatedMeasure: CustomDebugStringConvertible { 97 | public var debugDescription: String { 98 | let notesString = notes.map { "\($0)" }.joined(separator: ",") 99 | 100 | return "|\(timeSignature): \(notesString)|" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/music-notation/Score.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Score.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2021-01-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | /// A `score` can contain 0 or more parts. Each part can have a name, color, and position 10 | /// within the score. 11 | /// 12 | /// `score` is also full collection of parts. See the extensions for the comformances. 13 | /// 14 | /// A `score` will also be the container for stylesheets, as well as overall data for the 15 | /// entire score. 16 | public struct Score { 17 | // MARK: - Main Properties 18 | 19 | // This should be part of the io portion of the library. 20 | // Note: This should be tied to the swift package, but I don't know of a way to push this into here. 21 | public var version = Version(major: 0, minor: 3, revision: 0) 22 | 23 | internal private(set) var parts: [Part] = [] 24 | 25 | public var title: String = "" 26 | public var subtitle: String = "" 27 | public var artist: String = "" 28 | public var album: String = "" 29 | public var words: String = "" 30 | public var music: String = "" 31 | public var wordsAndMusic: String = "" 32 | public var transcriber: String = "" 33 | public var instructions: String = "" 34 | public var notices: String = "" 35 | 36 | public var firstPageHeader: PageHeader? 37 | public var pageHeader: PageHeader? 38 | public var pageFooter: PageFooter? 39 | 40 | // Holds information global to all parts of the score (for instance the sections) 41 | public var masterPart: Part? 42 | 43 | public init( 44 | parts: [Part] = [], 45 | title: String = "", 46 | subtitle: String = "", 47 | artist: String = "", 48 | album: String = "", 49 | words: String = "", 50 | music: String = "", 51 | wordsAndMusic: String = "", 52 | transcriber: String = "", 53 | instructions: String = "", 54 | notices: String = "", 55 | firstPageHeader: PageHeader? = nil, 56 | pageHeader: PageHeader? = nil, 57 | pageFooter: PageFooter? = nil, 58 | masterPart: Part? = nil 59 | ) { 60 | self.parts = parts 61 | self.title = title 62 | self.subtitle = subtitle 63 | self.artist = artist 64 | self.album = album 65 | self.words = words 66 | self.music = music 67 | self.wordsAndMusic = wordsAndMusic 68 | self.transcriber = transcriber 69 | self.instructions = instructions 70 | self.notices = notices 71 | self.firstPageHeader = firstPageHeader 72 | self.pageHeader = pageHeader 73 | self.pageFooter = pageFooter 74 | self.masterPart = masterPart 75 | } 76 | } 77 | 78 | // MARK: - RandomAccessCollection Conformance 79 | 80 | extension Score: RandomAccessCollection { 81 | public typealias Index = Int 82 | public var startIndex: Int { parts.startIndex } 83 | public var endIndex: Int { parts.endIndex } 84 | public func index(after index: Int) -> Int { parts.index(after: index) } 85 | public typealias Iterator = IndexingIterator<[Part]> 86 | public func makeIterator() -> Iterator { parts.makeIterator() } 87 | } 88 | 89 | // MARK: - BidirectionalCollection Conformance 90 | 91 | extension Score: BidirectionalCollection { 92 | public func index(before index: Int) -> Int { parts.index(before: index) } 93 | } 94 | 95 | // MARK: - RangeReplaceableCollection Conformance 96 | 97 | extension Score: RangeReplaceableCollection { 98 | public init() { 99 | self.init(parts: []) 100 | } 101 | 102 | mutating public func replaceSubrange( 103 | _ subrange: Range, 104 | with newElements: C) where C : Collection, Self.Element == C.Element { 105 | parts.replaceSubrange(subrange.relative(to: parts), with: newElements) 106 | } 107 | } 108 | 109 | // MARK: - MutableCollection Conformance 110 | 111 | extension Score: MutableCollection { 112 | public subscript(position: Int) -> Part { 113 | get { parts[position] } 114 | set(newValue) { parts[position] = newValue } 115 | } 116 | } 117 | 118 | // MARK: - CustomDebugStringConvertible Conformance 119 | 120 | extension Score: CustomDebugStringConvertible { 121 | public var debugDescription: String { 122 | "Score version: \(version.major).\(version.minor).\(version.revision), parts(\(parts.map { $0.debugDescription }.joined(separator: ", ")))" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/music-notation/SpelledPitch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpelledPitch.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2015-06-15. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// A pitch indicates the note letter (a-g), octave, and accidental. 10 | /// Since certain pitches are the same, but can be shown differently (like a sharp and b flat), 11 | /// the "spelling" of the pitch is important. 12 | /// This struct represents a pitch that is already in the desired spelling. 13 | /// So, this assumes the user of this struct knows which spelling to pick. 14 | /// 15 | public struct SpelledPitch: Sendable { 16 | public let noteLetter: NoteLetter 17 | public let accidental: Accidental 18 | public let octave: Octave 19 | 20 | public init(_ noteLetter: NoteLetter, accidental: Accidental = .natural, _ octave: Octave) { 21 | self.noteLetter = noteLetter 22 | self.accidental = accidental 23 | self.octave = octave 24 | } 25 | } 26 | 27 | extension SpelledPitch: CustomDebugStringConvertible { 28 | public var debugDescription: String { 29 | switch accidental { 30 | case .natural: 31 | return "\(noteLetter)\(octave.rawValue)" 32 | default: 33 | return "\(noteLetter)\(accidental)\(octave.rawValue)" 34 | } 35 | } 36 | } 37 | 38 | extension SpelledPitch: Equatable { 39 | public static func == (lhs: SpelledPitch, rhs: SpelledPitch) -> Bool { 40 | if lhs.accidental == rhs.accidental, 41 | lhs.noteLetter == rhs.noteLetter, 42 | lhs.octave == rhs.octave { 43 | return true 44 | } else { 45 | return false 46 | } 47 | } 48 | } 49 | 50 | public extension SpelledPitch { 51 | var midiNoteNumber: Int { 52 | var result = (octave.rawValue + 1) * 12 53 | 54 | switch noteLetter { 55 | case .c: break 56 | case .d: result += 2 57 | case .e: result += 4 58 | case .f: result += 5 59 | case .g: result += 7 60 | case .a: result += 9 61 | case .b: result += 11 62 | } 63 | 64 | switch accidental { 65 | case .natural: 66 | break 67 | case .flat: 68 | result -= 1 69 | case .sharp: 70 | result += 1 71 | case .doubleFlat: 72 | result -= 2 73 | case .doubleSharp: 74 | result += 2 75 | } 76 | 77 | return result 78 | } 79 | } 80 | 81 | extension SpelledPitch: Enharmonic { 82 | public func isEnharmonic(with other: SpelledPitch) -> Bool { 83 | midiNoteNumber == other.midiNoteNumber 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/music-notation/StaffLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaffLocation.swift 3 | // music-notation 4 | // 5 | // Created by Kyle Sherman on 2016-10-30. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// Represents a 0-based location on the staff where 0 is the first line/space from the bottom. 10 | /// Negative numbers represents ledger lines/spaces below the first line/space of the staff. 11 | /// 12 | public struct StaffLocation: Sendable { 13 | public enum LocationType: Sendable { 14 | case line 15 | case space 16 | } 17 | 18 | public let locationType: LocationType 19 | 20 | /// 0-based location on the staff where 0 is the first line/space from the bottom. 21 | /// Negative numbers represent ledger lines/spaces below the first one. 22 | /// 23 | public let number: Int 24 | 25 | /// Starts from 0 on the first line (from the bottom). Ledger lines below that are negative. 26 | /// Each increase by 1 moves a half step. i.e. 1 is the first space on the staff. 27 | /// 28 | internal var halfSteps: Int { 29 | switch locationType { 30 | case .space: 31 | return number * 2 + 1 32 | case .line: 33 | return number * 2 34 | } 35 | } 36 | 37 | public init(_ type: LocationType, _ number: Int) { 38 | locationType = type 39 | self.number = number 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/music-notation/Striking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Striking.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum Striking: Sendable { 10 | case left 11 | case up 12 | case right 13 | case down 14 | } 15 | -------------------------------------------------------------------------------- /Sources/music-notation/Stylesheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stylesheet.swift 3 | // music-notation 4 | // 5 | // Created by Steven Woolgar on 2021-01-30. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum MeasurementSystem { 10 | case metric 11 | case imperial 12 | } 13 | 14 | public enum PageFormats { 15 | case b5 16 | case legal 17 | case a3 18 | case a4 19 | case semiLetter 20 | case b4 21 | case fifteen 22 | case executive 23 | case folio 24 | case letter 25 | case c5 26 | case tabloid 27 | case userDefined(width: Double, height: Double, measurementSystem: MeasurementSystem) 28 | } 29 | 30 | public enum PageOrientation { 31 | case portrait 32 | case landscape 33 | } 34 | 35 | public enum TuningPosition { 36 | case onTopOfScore 37 | case beforeTablature 38 | } 39 | 40 | public enum ColumnsLayout { 41 | case twoColumns 42 | case horizontally 43 | } 44 | 45 | /// Placeholder for eventual staff layout description of a page header. 46 | public struct Stylesheet { 47 | /// Page & Score format 48 | /// Page related settings 49 | public var pageFormat: PageFormats 50 | public var orientation: PageOrientation 51 | public var topMargin: Int 52 | public var rightMargin: Int 53 | public var bottomMargin: Int 54 | public var leftMargin: Int 55 | 56 | /// Size settings 57 | public var globalScoreProportions: Double // In millimeters 58 | public var affectsFontsAndChordSizes: Bool 59 | public var rhythmProportions: Double 60 | 61 | /// Tuning settings 62 | public var displayTunings: Bool 63 | public var tuningPosition: TuningPosition 64 | public var columns: ColumnsLayout 65 | 66 | /// Systems & Staves 67 | public var firstSystemIndented: Bool 68 | public var firstSystemIndentedValue: Int // In millimeters 69 | 70 | /// Header & Footer 71 | 72 | /// Texts & Styles 73 | 74 | /// Notation 75 | public var hideRhythmInTrblatureWhenUsingStandardNotation: Bool 76 | } 77 | -------------------------------------------------------------------------------- /Sources/music-notation/Tempo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tempo.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 07/18/2024. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | /// Represents the tempo of the section at the point of attachment. 10 | /// 11 | /// In musical terminology, tempo (Italian for 'time'; plural 'tempos', or tempi from the Italian plural), 12 | /// also known as beats per minute, is the speed or pace of a given composition. In classical music, 13 | /// tempo is typically indicated with an instruction at the start of a piece (often using conventional 14 | /// Italian terms) and is usually measured in beats per minute (BPM). In modern classical compositions, 15 | /// a "metronome mark" in beats per minute may supplement or replace the normal tempo marking, while in 16 | /// modern genres like electronic dance music, tempo will typically simply be stated in BPM. 17 | /// 18 | public struct Tempo: Sendable { 19 | public enum TempoType: Sendable { 20 | case undefined 21 | case pause 22 | case linear 23 | case ramp 24 | } 25 | 26 | public enum NoteUnit: Int, Sendable { 27 | case eight = 1 28 | case quarter 29 | case dottedQuarter 30 | case half 31 | case dottedHalf 32 | } 33 | 34 | public let type: TempoType 35 | public let position: Double 36 | public let value: Int 37 | public let unit: NoteUnit 38 | public let text: String? 39 | 40 | public init( 41 | type: TempoType, 42 | position: Double, 43 | value: Int, 44 | unit: NoteUnit, 45 | text: String? = nil 46 | ) { 47 | self.type = type 48 | self.position = position 49 | self.value = value 50 | self.unit = unit 51 | self.text = text 52 | } 53 | } 54 | 55 | extension Tempo: Equatable { 56 | public static func == (lhs: Tempo, rhs: Tempo) -> Bool { 57 | guard lhs.type == rhs.type, 58 | lhs.position == rhs.position, 59 | lhs.value == rhs.value, 60 | lhs.unit == rhs.unit, 61 | lhs.text == rhs.text else { return false } 62 | return true 63 | } 64 | } 65 | // Mark: - Debug 66 | 67 | extension Tempo: CustomDebugStringConvertible { 68 | public var debugDescription: String { 69 | if let text { 70 | return "type: \(type), position: \(position), value: \(value), unit: \(unit), text: \(text)" 71 | } else { 72 | return "type: \(type), position: \(position), value: \(value), unit: \(unit)" 73 | } 74 | } 75 | } 76 | 77 | public enum TempoError: Error { 78 | case invalidTempoIndex 79 | } 80 | -------------------------------------------------------------------------------- /Sources/music-notation/Tie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tie.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2021-04-06. 6 | // Copyright © 2021 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | public enum Tie: Sendable { 10 | case begin 11 | case end 12 | case beginAndEnd 13 | } 14 | -------------------------------------------------------------------------------- /Sources/music-notation/TimeSignature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeSignature.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2015-06-12. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | /// The time signature (also known as `meter signature`, `metre signature`, or `measure signature`) is a notational convention used 10 | /// in Western musical notation to specify how many beats (pulses) are contained in each measure (bar), and which note value 11 | /// is equivalent to a beat. 12 | /// 13 | /// In a music score, the time signature appears at the beginning as a time symbol or stacked numerals, such as common time 14 | /// or `3/4` (read common time and three-four time, respectively), immediately following the key signature (or immediately 15 | /// following the clef symbol if the key signature is empty). A mid-score time signature, usually immediately following a 16 | /// barline, indicates a change of meter. 17 | /// 18 | /// There are various types of time signatures, depending on whether the music follows regular (or symmetrical) beat patterns, 19 | /// including simple (e.g., `3/4` and `4/4`), and compound (e.g., `9/8` and `12/8`); or involves shifting beat patterns, including 20 | /// complex (e.g., `5/4` or `7/8`), odd (e.g., `5/8` & `3/8` or `6/8` & `3/4`), additive (e.g., `3 + 2/8 + 3`), 21 | /// fractional (e.g., ​`2½/4`), and complex meters (e.g., `3/10` or `5/24`). 22 | /// 23 | /// - Note: 24 | /// A regular (`.simple`, `.compound`) time signature is one which represents 2, 3 or 4 main beats per bar. 25 | /// - **Duple** time means 2 main beats per bar 26 | /// - **Triple** time means 3 main beats per bar 27 | /// - **Quadruple** time means 4 main beats per bar 28 | /// 29 | /// `.simple` time signatures have a main beat which divides into **two** 1st level sub-beats. 30 | /// `.compound` time signatures have a main beat which divides into **three** 1st level sub-beats. 31 | /// In both `.simple` and `.compound` time, 2nd level sub-beats always subdivide by two (never by three). 32 | /// 33 | /// 34 | /// # Meter and time signatures # 35 | /// 36 | /// Meter involves the way multiple pulse layers work together to organize music in time. Standard meters in Western 37 | /// music can be classified into _simple meters_ and _compound meters_, as well as _duple_, _triple_, and _quadruple meters_. 38 | /// 39 | /// Duple, triple, and quadruple classifications result from the relationship between the counting pulse and the pulses 40 | /// that are slower than the counting pulse. In other words, it is a question of grouping: how many beats occur in each bar. 41 | /// If counting-pulse beats group into twos, we have duple meter; groups of three, triple meter; groups of four, 42 | /// quadruple meter. Conducting patterns are determined based on these classifications. 43 | /// 44 | /// Simple and compound classifications result from the relationship between the counting pulse and the pulses that are 45 | /// faster than the counting pulse. In other words, it is a question of division: does each beat divide into two equal 46 | /// parts, or three equal parts. Meters that divide the beat into two equal parts are simple meters; meters that divide 47 | /// the beat into three equal parts are compound meters. 48 | /// 49 | /// Thus, there are six types of standard meter in Western music: 50 | /// 51 | /// - simple duple (beats group into two, divide into two) 52 | /// - simple triple (beats group into three, divide into two) 53 | /// - simple quadruple (beats group into four, divide into two) 54 | /// - compound duple (beats group into two, divide into three) 55 | /// - compound triple (beats group into three, divide into three) 56 | /// - compound quadruple (beats group into four, divide into three) 57 | /// 58 | /// In a time signature, the top number (and the top number only!) describes the type of meter. Following are the top 59 | /// numbers that always correspond to each type of meter: 60 | /// 61 | /// - simple duple: 2 62 | /// - simple triple: 3 63 | /// - simple quadruple: 4 64 | /// - compound duple: 6 65 | /// - compound triple: 9 66 | /// - compound quadruple: 12 67 | /// 68 | /// ## Notating meter ## 69 | /// 70 | /// As far as I can tell, there can be little to infer about a meter from simply the time signature (especially it's odd'ness). 71 | /// For example, 8/8 is an odd meter which is counted as **1**-2-3, **2**-2-3, **3**-2 and 4/4 would be **1** &, **2** &, **3** &, **4** &. 72 | /// And yet 9/8 is an even meter (**1**-2-3, **2**-2-3, **3**-2-3) 73 | /// 74 | /// In _simple meters_, the bottom number of the time signature corresponds to the type of note corresponding to a 75 | /// single beat. If a simple meter is notated such that each quarter note corresponds to a beat, the bottom 76 | /// number of the time signature is 4. If a simple meter is notated such that each half note corresponds 77 | /// to a beat, the bottom number of the time signature is 2. If a simple meter is notated such that each eighth 78 | /// note corresponds to a beat, the bottom number of the time signature is 8. And so on. 79 | /// 80 | /// In _compound meters_, the bottom number of the time signature corresponds to the type of note corresponding to 81 | /// a _single division of the beat_. If a compound meter is notated such that each dotted-quarter note corresponds to 82 | /// a beat, the eighth note is the division of the beat, and thus the bottom number of the time signature is 8. 83 | /// If a compound meter is notated such that each dotted-half note corresponds to a beat, the quarter note is the 84 | /// division of the beat, and thus the bottom number of the time signature is 4. Note that because the beat is 85 | /// divided into three in a compound meter, the beat is always three times as long as the division note, and 86 | /// _the beat is always dotted_. 87 | /// 88 | public struct TimeSignature: Sendable { 89 | public let numerator: Int 90 | public let denominator: Int 91 | public let tempo: Int 92 | 93 | public init(numerator: Int, denominator: Int, tempo: Int) { 94 | // TODO: Check the validity of all these values 95 | self.numerator = numerator 96 | self.denominator = denominator 97 | self.tempo = tempo 98 | } 99 | } 100 | 101 | extension TimeSignature: CustomDebugStringConvertible { 102 | public var debugDescription: String { 103 | "\(numerator)/\(denominator)" 104 | } 105 | } 106 | 107 | extension TimeSignature: Equatable { 108 | public static func == (lhs: TimeSignature, rhs: TimeSignature) -> Bool { 109 | if lhs.numerator == rhs.numerator, 110 | lhs.denominator == rhs.denominator, 111 | lhs.tempo == rhs.tempo { 112 | return true 113 | } else { 114 | return false 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/music-notation/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2025-01-04. 6 | // Copyright © 2025 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | /// A definition of the version of music notation version that created the Score 10 | /// (the score has a version). 11 | public struct Version: Sendable { 12 | let major: Int 13 | let minor: Int 14 | let revision: Int 15 | } 16 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/ClefTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClefTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-10-16. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class ClefTests { 13 | @Test func initForCustomOnLine() async throws { 14 | let clef = Clef(pitch: SpelledPitch(.c, .octave4), 15 | location: StaffLocation(.line, 0)) 16 | #expect(clef.staffLocation.halfSteps == 0) 17 | } 18 | 19 | @Test func initForCustomOnSpace() async throws { 20 | let clef = Clef(pitch: SpelledPitch(.g, .octave4), 21 | location: StaffLocation(.space, 1)) 22 | #expect(clef.staffLocation.halfSteps == 3) 23 | } 24 | 25 | @Test func initForCustomNegativeLedger() async throws { 26 | let clef = Clef(pitch: SpelledPitch(.g, .octave3), 27 | location: StaffLocation(.line, -2)) 28 | #expect(clef.staffLocation.halfSteps == -4) 29 | } 30 | 31 | @Test func initForCustomPositiveLedger() async throws { 32 | let clef = Clef(pitch: SpelledPitch(.a, .octave4), 33 | location: StaffLocation(.line, 7)) 34 | #expect(clef.staffLocation.halfSteps == 14) 35 | } 36 | 37 | @Test func pitchAtOctaveOutOfRange() async throws { 38 | #expect(throws: ClefError.octaveOutOfRange) { 39 | try Clef.treble.pitch(at: StaffLocation(.space, 300)) 40 | } 41 | 42 | #expect(throws: ClefError.octaveOutOfRange) { 43 | try Clef.treble.pitch(at: StaffLocation(.line, 300)) 44 | } 45 | 46 | #expect(throws: ClefError.octaveOutOfRange) { 47 | try Clef.treble.pitch(at: StaffLocation(.space, -300)) 48 | } 49 | 50 | #expect(throws: ClefError.octaveOutOfRange) { 51 | try Clef.treble.pitch(at: StaffLocation(.line, -300)) 52 | } 53 | } 54 | 55 | @Test func pitchAtUnpitched() async throws { 56 | var neutral: SpelledPitch? 57 | #expect(throws: Never.self) { neutral = try Clef.neutral.pitch(at: StaffLocation(.space, 1)) } 58 | #expect(neutral == nil) 59 | 60 | var tab: SpelledPitch? 61 | #expect(throws: Never.self) { 62 | tab = try Clef.tab.pitch(at: StaffLocation(.space, 1)) 63 | } 64 | #expect(tab == nil) 65 | } 66 | 67 | @Test func pitchAtLocationWithinStaffIncrease() async throws { 68 | #expect(try Clef.treble.pitch(at: StaffLocation(.space, 2)) == SpelledPitch(.c, .octave5)) 69 | #expect(try Clef.treble.pitch(at: StaffLocation(.line, 2)) == SpelledPitch(.b, .octave4)) 70 | #expect(try Clef.bass.pitch(at: StaffLocation(.space, 3)) == SpelledPitch(.g, .octave3)) 71 | #expect(try Clef.alto.pitch(at: StaffLocation(.line, 4)) == SpelledPitch(.g, .octave4)) 72 | #expect(try Clef.soprano.pitch(at: StaffLocation(.space, 3)) == SpelledPitch(.c, .octave5)) 73 | 74 | let customBClef = Clef( 75 | pitch: SpelledPitch(.b, .octave3), 76 | location: StaffLocation(.line, 2) 77 | ) 78 | #expect(try customBClef.pitch(at: StaffLocation(.space, 2)) == SpelledPitch(.c, .octave4)) 79 | } 80 | 81 | @Test func pitchAtLocationDecrease() async throws { 82 | #expect(try Clef.treble.pitch(at: StaffLocation(.line, 0)) == SpelledPitch(.e, .octave4)) 83 | #expect(try Clef.treble.pitch(at: StaffLocation(.space, -1)) == SpelledPitch(.d, .octave4)) 84 | #expect(try Clef.alto.pitch(at: StaffLocation(.line, -3)) == SpelledPitch(.g, .octave2)) 85 | #expect(try Clef.alto.pitch(at: StaffLocation(.line, -2)) == SpelledPitch(.b, .octave2)) 86 | #expect(try Clef.alto.pitch(at: StaffLocation(.space, 1)) == SpelledPitch(.b, .octave3)) 87 | #expect(try Clef.bass.pitch(at: StaffLocation(.line, 1)) == SpelledPitch(.b, .octave2)) 88 | } 89 | 90 | @Test func pitchAtSamePitchAsClef() async throws { 91 | #expect(try Clef.treble.pitch(at: StaffLocation(.line, 1)) == 92 | SpelledPitch(.g, .octave4)) 93 | #expect(try Clef.soprano.pitch(at: StaffLocation(.line, 0)) == 94 | SpelledPitch(.c, .octave4)) 95 | } 96 | 97 | @Test func pitchAtNegativeClefDecrease() async throws { 98 | let negativeClef = Clef(pitch: SpelledPitch(.d, .octave3), location: StaffLocation(.line, -1)) 99 | #expect(try negativeClef.pitch(at: StaffLocation(.line, -2)) == SpelledPitch(.b, .octave2)) 100 | } 101 | 102 | @Test func equalityFailStandard() async throws { 103 | #expect(Clef.treble != Clef.bass) 104 | } 105 | 106 | @Test func equalityFailDifferentPitch() async throws { 107 | let custom1 = Clef( 108 | pitch: SpelledPitch(.a, .octave3), 109 | location: StaffLocation(.line, 1) 110 | ) 111 | let custom2 = Clef( 112 | pitch: SpelledPitch(.a, .octave2), 113 | location: StaffLocation(.line, 1) 114 | ) 115 | #expect(custom1 != custom2) 116 | } 117 | 118 | @Test func equalityFailDifferentLineNumber() async throws { 119 | let custom1 = Clef( 120 | pitch: SpelledPitch(.a, .octave2), 121 | location: StaffLocation(.space, 1) 122 | ) 123 | let custom2 = Clef( 124 | pitch: SpelledPitch(.a, .octave2), 125 | location: StaffLocation(.space, 2) 126 | ) 127 | #expect(custom1 != custom2) 128 | } 129 | 130 | @Test func equalityStandard() async throws { 131 | #expect(Clef.treble == Clef.treble) 132 | } 133 | 134 | @Test func equalityCustom() async throws { 135 | let custom1 = Clef( 136 | pitch: SpelledPitch(.a, .octave2), 137 | location: StaffLocation(.line, 1) 138 | ) 139 | let custom2 = Clef( 140 | pitch: SpelledPitch(.a, .octave2), 141 | location: StaffLocation(.line, 1) 142 | ) 143 | #expect(custom1 == custom2) 144 | } 145 | 146 | @Test func equalityCustomWithStandard() async throws { 147 | let treble = Clef( 148 | pitch: SpelledPitch(.g, .octave4), 149 | location: StaffLocation(.line, 1) 150 | ) 151 | #expect(treble == Clef.treble) 152 | } 153 | 154 | @Test func descriptionStandard() async throws { 155 | #expect(Clef.treble.debugDescription == "treble") 156 | #expect(Clef.bass.debugDescription == "bass") 157 | #expect(Clef.tenor.debugDescription == "tenor") 158 | #expect(Clef.alto.debugDescription == "alto") 159 | #expect(Clef.neutral.debugDescription == "neutral") 160 | #expect(Clef.tab.debugDescription == "neutral") 161 | #expect(Clef.frenchViolin.debugDescription == "frenchViolin") 162 | #expect(Clef.soprano.debugDescription == "soprano") 163 | #expect(Clef.mezzoSoprano.debugDescription == "mezzoSoprano") 164 | #expect(Clef.baritoneF.debugDescription == "baritoneF") 165 | #expect(Clef.baritoneC.debugDescription == "baritoneC") 166 | #expect(Clef.suboctaveTreble.debugDescription == "suboctaveTreble") 167 | } 168 | 169 | @Test func descriptionCustom() async throws { 170 | let custom = Clef( 171 | pitch: SpelledPitch(.a, .octave3), 172 | location: StaffLocation(.line, 1) 173 | ) 174 | #expect(custom.debugDescription == "a3@line1") 175 | 176 | let customNeutral = Clef( 177 | pitch: nil, 178 | location: StaffLocation(.space, 3) 179 | ) 180 | #expect(customNeutral.debugDescription == "neutral") 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/Collection+HelpersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+HelpersTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-10-30. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class CollectionHelpersTests { 13 | var emptyArray: [Int]! 14 | var singleElementArray: [Int]! 15 | var multipleElementArray: [Int]! 16 | 17 | init() { 18 | emptyArray = [] 19 | singleElementArray = [1] 20 | multipleElementArray = [1, 2, 3, 4, 5] 21 | } 22 | 23 | deinit { 24 | emptyArray = nil 25 | singleElementArray = nil 26 | multipleElementArray = nil 27 | } 28 | 29 | // MARK: - Collection 30 | 31 | // MARK: lastIndex 32 | 33 | @Test func lastIndex1Item() async throws { 34 | #expect(singleElementArray.lastIndex == 0) 35 | } 36 | 37 | @Test func lastIndexEmpty() async throws { 38 | #expect(emptyArray.lastIndex == 0) 39 | } 40 | 41 | @Test func lastIndexManyItems() async throws { 42 | #expect(multipleElementArray.lastIndex == 4) 43 | } 44 | 45 | // MARK: isValidIndex() 46 | 47 | @Test func isValidIndexInvalid() async throws { 48 | #expect(!emptyArray.isValidIndex(0)) 49 | #expect(!emptyArray.isValidIndex(1)) 50 | #expect(!singleElementArray.isValidIndex(1)) 51 | #expect(!singleElementArray.isValidIndex(-1)) 52 | #expect(!multipleElementArray.isValidIndex(7)) 53 | } 54 | 55 | @Test func isValidIndexValid() async throws { 56 | #expect(singleElementArray.isValidIndex(0)) 57 | #expect(multipleElementArray.isValidIndex(0)) 58 | #expect(multipleElementArray.isValidIndex(1)) 59 | #expect(multipleElementArray.isValidIndex(2)) 60 | #expect(multipleElementArray.isValidIndex(3)) 61 | #expect(multipleElementArray.isValidIndex(4)) 62 | } 63 | 64 | // MARK: isValidIndexRange() 65 | 66 | @Test func isValidIndexClosedValid() async throws { 67 | #expect(singleElementArray.isValidIndexRange(Range(0 ... 0))) 68 | #expect(multipleElementArray.isValidIndexRange(Range(0 ... 4))) 69 | } 70 | 71 | @Test func isValidIndexClosedInvalid() async throws { 72 | #expect(!emptyArray.isValidIndexRange(Range(0 ... 0))) 73 | #expect(!singleElementArray.isValidIndexRange(Range(0 ... 1))) 74 | #expect(!multipleElementArray.isValidIndexRange(Range(0 ... 5))) 75 | #expect(!multipleElementArray.isValidIndexRange(Range(-1 ... 4))) 76 | } 77 | 78 | @Test func isValidIndexNotClosedValid() async throws { 79 | #expect(singleElementArray.isValidIndexRange(0 ..< 1)) 80 | #expect(multipleElementArray.isValidIndexRange(0 ..< 5)) 81 | } 82 | 83 | @Test func isValidIndexNotClosedInvalid() async throws { 84 | #expect(!emptyArray.isValidIndexRange(0 ..< 1)) 85 | #expect(!singleElementArray.isValidIndexRange(0 ..< 2)) 86 | #expect(!multipleElementArray.isValidIndexRange(0 ..< 6)) 87 | #expect(!multipleElementArray.isValidIndexRange(-1 ..< 5)) 88 | } 89 | 90 | // MARK: subscript(safe:) 91 | 92 | @Test func subscriptInvalidIndex() async throws { 93 | #expect(emptyArray[safe: 0] == nil) 94 | #expect(singleElementArray[safe: 1] == nil) 95 | #expect(multipleElementArray[safe: 5] == nil) 96 | #expect(multipleElementArray[safe: -1] == nil) 97 | } 98 | 99 | @Test func subscriptValidIndex() async throws { 100 | #expect(singleElementArray[safe: 0] == .some(1)) 101 | #expect(multipleElementArray[safe: 0] == .some(1)) 102 | #expect(multipleElementArray[safe: 1] == .some(2)) 103 | #expect(multipleElementArray[safe: 2] == .some(3)) 104 | #expect(multipleElementArray[safe: 3] == .some(4)) 105 | #expect(multipleElementArray[safe: 4] == .some(5)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/Dynamics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicsTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-10-17. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class DynamicsTests { 13 | @Test func equalityFalse() async throws { 14 | let ppp = Dynamics.ppp 15 | let pp = Dynamics.pp 16 | #expect(ppp != pp) 17 | } 18 | 19 | @Test func equalityTrue() async throws { 20 | let ppp1 = Dynamics.ppp 21 | let ppp2 = Dynamics.ppp 22 | #expect(ppp1 == ppp2) 23 | } 24 | 25 | @Test func name() async throws { 26 | let ppp = Dynamics.ppp 27 | let pppString = ppp.name() 28 | #expect(pppString == "pianississimo") 29 | 30 | let pp = Dynamics.pp 31 | let ppString = pp.name() 32 | #expect(ppString == "pianissimo") 33 | 34 | let p = Dynamics.p 35 | let pString = p.name() 36 | #expect(pString == "piano") 37 | 38 | let mp = Dynamics.mp 39 | let mpString = mp.name() 40 | #expect(mpString == "mezzo-piano") 41 | 42 | let mf = Dynamics.mf 43 | let mfString = mf.name() 44 | #expect(mfString == "mezzo-forte") 45 | 46 | let f = Dynamics.f 47 | let fString = f.name() 48 | #expect(fString == "forte") 49 | 50 | let ff = Dynamics.ff 51 | let ffString = ff.name() 52 | #expect(ffString == "fortissimo") 53 | 54 | let fff = Dynamics.fff 55 | let fffString = fff.name() 56 | #expect(fffString == "fortississimo") 57 | 58 | #expect(Dynamics.allCases.count == 8) 59 | } 60 | 61 | @Test func level() async throws { 62 | let ppp = Dynamics.ppp 63 | let pppString = ppp.level() 64 | #expect(pppString == "very very quiet") 65 | 66 | let pp = Dynamics.pp 67 | let ppString = pp.level() 68 | #expect(ppString == "very quiet") 69 | 70 | let p = Dynamics.p 71 | let pString = p.level() 72 | #expect(pString == "quiet") 73 | 74 | let mp = Dynamics.mp 75 | let mpString = mp.level() 76 | #expect(mpString == "moderately quiet") 77 | 78 | let mf = Dynamics.mf 79 | let mfString = mf.level() 80 | #expect(mfString == "moderately loud") 81 | 82 | let f = Dynamics.f 83 | let fString = f.level() 84 | #expect(fString == "loud") 85 | 86 | let ff = Dynamics.ff 87 | let ffString = ff.level() 88 | #expect(ffString == "very loud") 89 | 90 | let fff = Dynamics.fff 91 | let fffString = fff.level() 92 | #expect(fffString == "very very loud") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/IntervalQualityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntervalQualityTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2025-01-19. 6 | // Copyright © 2025 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class IntervalQualityTests { 13 | @Test func fourthQuality() async throws { 14 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .fourth, semitones: 4)) 15 | #expect(quality == .diminishedFourth) 16 | } 17 | 18 | @Test func perfectFifthQuality() async throws { 19 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .fifth, semitones: 7)) 20 | #expect(quality == .perfectFifth) 21 | } 22 | 23 | @Test func augmentedFifthQuality() async throws { 24 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .fifth, semitones: 8)) 25 | #expect(quality == .augmentedFifth) 26 | } 27 | 28 | @Test func diminishedSixthQuality() async throws { 29 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .sixth, semitones: 7)) 30 | #expect(quality == .diminishedSixth) 31 | } 32 | 33 | @Test func minorSixthQuality() async throws { 34 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .sixth, semitones: 8)) 35 | #expect(quality == .minorSixth) 36 | } 37 | 38 | @Test func majorSixthQuality() async throws { 39 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .sixth, semitones: 9)) 40 | #expect(quality == .majorSixth) 41 | } 42 | 43 | @Test func augmentedSixthQuality() async throws { 44 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .sixth, semitones: 10)) 45 | #expect(quality == .augmentedSixth) 46 | } 47 | 48 | @Test func diminishedSeventhQuality() async throws { 49 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .seventh, semitones: 9)) 50 | #expect(quality == .diminishedSeventh) 51 | } 52 | 53 | @Test func minorSeventhQuality() async throws { 54 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .seventh, semitones: 10)) 55 | #expect(quality == .minorSeventh) 56 | } 57 | 58 | @Test func majorSeventhQuality() async throws { 59 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .seventh, semitones: 11)) 60 | #expect(quality == .majorSeventh) 61 | } 62 | 63 | @Test func augmentedSeventhQuality() async throws { 64 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .seventh, semitones: 12)) 65 | #expect(quality == .augmentedSeventh) 66 | } 67 | 68 | @Test func diminishedOctaveQuality() async throws { 69 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .octave, semitones: 11)) 70 | #expect(quality == .diminishedOctave) 71 | } 72 | 73 | @Test func perfectOctaveQuality() async throws { 74 | let quality = try IntervalQuality(with: try! Interval(diatonicIntervalQuality: .octave, semitones: 12)) 75 | #expect(quality == .perfectOctave) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/IntervalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntervalTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Rob Hudson on 2016-08-01. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class IntervalTests { 13 | @Test func unison() async throws { 14 | let interval = try! Interval(diatonic: 0, semitones: 0) 15 | #expect(try interval.diatonicIntervalQuality() == .unison) 16 | #expect(try interval.intervalQuality() == .perfectUnison) 17 | } 18 | 19 | @Test func unisonQuality() async throws { 20 | let interval = try! Interval(diatonicIntervalQuality: .unison, semitones: 0) 21 | #expect(try interval.diatonicIntervalQuality() == .unison) 22 | #expect(try interval.intervalQuality() == .perfectUnison) 23 | } 24 | 25 | @Test func minorSecond() async throws { 26 | let interval = try! Interval(diatonic: 1, semitones: 1) 27 | #expect(try interval.diatonicIntervalQuality() == .second) 28 | #expect(try interval.intervalQuality() == .minorSecond) 29 | } 30 | 31 | @Test func minorSecondDiatonicQuality() async throws { 32 | let interval = try! Interval(diatonicIntervalQuality: .second, semitones: 1) 33 | #expect(try interval.diatonicIntervalQuality() == .second) 34 | #expect(try interval.intervalQuality() == .minorSecond) 35 | } 36 | 37 | @Test func minorThird() async throws { 38 | let interval = try! Interval(diatonic: 2, semitones: 3) 39 | #expect(try interval.diatonicIntervalQuality() == .third) 40 | #expect(try interval.intervalQuality() == .minorThird) 41 | } 42 | 43 | @Test func minorThirdDiatonicQuality() async throws { 44 | let interval = try! Interval(diatonicIntervalQuality: .third, semitones: 3) 45 | #expect(try interval.diatonicIntervalQuality() == .third) 46 | #expect(try interval.intervalQuality() == .minorThird) 47 | } 48 | 49 | @Test func augmentedFourth() async throws { 50 | let interval = try! Interval(diatonic: 3, semitones: 6) 51 | #expect(try interval.diatonicIntervalQuality() == .fourth) 52 | #expect(try interval.intervalQuality() == .augmentedFourth) 53 | } 54 | 55 | @Test func augmentedFourthDiatonicQuality() async throws { 56 | let interval = try! Interval(diatonicIntervalQuality: .fourth, semitones: 6) 57 | #expect(try interval.diatonicIntervalQuality() == .fourth) 58 | #expect(try interval.intervalQuality() == .augmentedFourth) 59 | } 60 | 61 | @Test func diminishedFifth() async throws { 62 | let interval = try! Interval(diatonic: 4, semitones: 6) 63 | #expect(try interval.diatonicIntervalQuality() == .fifth) 64 | #expect(try interval.intervalQuality() == .diminishedFifth) 65 | } 66 | 67 | @Test func diminishedFifthDiatonicQuality() async throws { 68 | let interval = try! Interval(diatonicIntervalQuality: .fifth, semitones: 6) 69 | #expect(try interval.diatonicIntervalQuality() == .fifth) 70 | #expect(try interval.intervalQuality() == .diminishedFifth) 71 | } 72 | 73 | @Test func diminishedFifthDiatonicQualityOctave() async throws { 74 | let interval = try! Interval( 75 | diatonicIntervalQuality: .fifth, 76 | semitones: 6 + Interval.Constants.semitonesPerOctave 77 | ) 78 | #expect(try interval.diatonicIntervalQuality() == .fifth) 79 | #expect(try interval.intervalQuality() == .diminishedFifth) 80 | } 81 | 82 | @Test func octave() async throws { 83 | let interval = try! Interval(diatonic: 7, semitones: 12) 84 | #expect(try interval.diatonicIntervalQuality() == .octave) 85 | #expect(try interval.intervalQuality() == .perfectOctave) 86 | } 87 | 88 | @Test func octaveQuality() async throws { 89 | let interval = try! Interval(diatonicIntervalQuality: .octave, semitones: 12) 90 | #expect(try interval.diatonicIntervalQuality() == .octave) 91 | #expect(try interval.intervalQuality() == .perfectOctave) 92 | } 93 | 94 | @Test func largeInterval() async throws { 95 | let interval = try! Interval(diatonic: 5, semitones: 33) 96 | #expect(try interval.diatonicIntervalQuality() == .sixth) 97 | #expect(try interval.intervalQuality() == .majorSixth) 98 | } 99 | 100 | @Test func majorOctaveInvalid() async throws { 101 | #expect(throws: IntervalError.intervalOutOfBounds) { 102 | _ = try Interval(diatonic: 2, semitones: 8) 103 | } 104 | } 105 | 106 | @Test func perfectNinthInvalid() async throws { 107 | #expect(throws: IntervalError.intervalOutOfBounds) { 108 | _ = try Interval(diatonicIntervalQuality: .fifth, semitones: 9) 109 | } 110 | } 111 | 112 | @Test func nagativeSemitoneInvalid() async throws { 113 | #expect(throws: IntervalError.semitoneNumberNotPositive) { 114 | _ = try Interval(diatonicIntervalQuality: .octave, semitones: -1) 115 | } 116 | } 117 | 118 | @Test func nagativeDiatonicInvalid() async throws { 119 | #expect(throws: IntervalError.diatonicNumberNotPositive) { 120 | _ = try Interval(diatonic: -1, semitones: 0) 121 | } 122 | } 123 | 124 | // MARK: - IntervalQuality init 125 | 126 | @Test func perfectUnisonQuality() async throws { 127 | let interval = try! Interval(intervalQuality: .perfectUnison) 128 | #expect(interval.diatonic == 0) 129 | #expect(interval._semitones == 0) 130 | } 131 | 132 | @Test func augmentedUnisonQuality() async throws { 133 | let interval = try! Interval(intervalQuality: .augmentedUnison) 134 | #expect(interval.diatonic == 0) 135 | #expect(interval._semitones == 1) 136 | } 137 | 138 | @Test func diminishedSecondQuality() async throws { 139 | let interval = try! Interval(intervalQuality: .diminishedSecond) 140 | #expect(interval.diatonic == 1) 141 | #expect(interval._semitones == 0) 142 | } 143 | 144 | @Test func minorSecondQuality() async throws { 145 | let interval = try! Interval(intervalQuality: .minorSecond) 146 | #expect(interval.diatonic == 1) 147 | #expect(interval._semitones == 1) 148 | } 149 | 150 | @Test func majorSecondQuality() async throws { 151 | let interval = try! Interval(intervalQuality: .majorSecond) 152 | #expect(interval.diatonic == 1) 153 | #expect(interval._semitones == 2) 154 | } 155 | 156 | @Test func augmentedSecondQuality() async throws { 157 | let interval = try! Interval(intervalQuality: .augmentedSecond) 158 | #expect(interval.diatonic == 1) 159 | #expect(interval._semitones == 3) 160 | } 161 | 162 | @Test func diminishedThirdQuality() async throws { 163 | let interval = try! Interval(intervalQuality: .diminishedThird) 164 | #expect(interval.diatonic == 2) 165 | #expect(interval._semitones == 2) 166 | } 167 | 168 | @Test func minorThirdQuality() async throws { 169 | let interval = try! Interval(intervalQuality: .minorThird) 170 | #expect(interval.diatonic == 2) 171 | #expect(interval._semitones == 3) 172 | } 173 | 174 | @Test func majorThirdQuality() async throws { 175 | let interval = try! Interval(intervalQuality: .majorThird) 176 | #expect(interval.diatonic == 2) 177 | #expect(interval._semitones == 4) 178 | } 179 | 180 | @Test func augmentedThirdQuality() async throws { 181 | let interval = try! Interval(intervalQuality: .augmentedThird) 182 | #expect(interval.diatonic == 2) 183 | #expect(interval._semitones == 5) 184 | } 185 | 186 | @Test func diminishedFourthQuality() async throws { 187 | let interval = try! Interval(intervalQuality: .diminishedFourth) 188 | #expect(interval.diatonic == 3) 189 | #expect(interval._semitones == 4) 190 | } 191 | 192 | @Test func perfectFourthQuality() async throws { 193 | let interval = try! Interval(intervalQuality: .perfectFourth) 194 | #expect(interval.diatonic == 3) 195 | #expect(interval._semitones == 5) 196 | } 197 | 198 | @Test func augmentedFourthQuality() async throws { 199 | let interval = try! Interval(intervalQuality: .augmentedFourth) 200 | #expect(interval.diatonic == 3) 201 | #expect(interval._semitones == 6) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/KeyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-11-27. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class KeyTests { 13 | @Test func debugDescription() async throws { 14 | let naturalMajor = Key(noteLetter: .c) 15 | #expect(naturalMajor.debugDescription == "c♮ major") 16 | let naturalMinor = Key(noteLetter: .a, accidental: .natural, type: .minor) 17 | #expect(naturalMinor.debugDescription == "a♮ minor") 18 | let sharpMajor = Key(noteLetter: .b, accidental: .sharp, type: .major) 19 | #expect(sharpMajor.debugDescription == "b♯ major") 20 | let doubleFlatMinor = Key(noteLetter: .e, accidental: .doubleFlat, type: .minor) 21 | #expect(doubleFlatMinor.debugDescription == "e𝄫 minor") 22 | } 23 | 24 | @Test func equalityTrue() async throws { 25 | let sharpMinor = Key(noteLetter: .g, accidental: .sharp, type: .minor) 26 | let sharpMinor2 = Key(noteLetter: .g, accidental: .sharp, type: .minor) 27 | #expect(sharpMinor == sharpMinor2) 28 | } 29 | 30 | @Test func equalityFalse() async throws { 31 | let differentType = Key(noteLetter: .b, accidental: .natural, type: .major) 32 | let differentType2 = Key(noteLetter: .b, accidental: .natural, type: .minor) 33 | #expect(differentType != differentType2) 34 | 35 | let differentNoteLetter = Key(noteLetter: .a) 36 | let differentNoteLetter2 = Key(noteLetter: .f) 37 | #expect(differentNoteLetter != differentNoteLetter2) 38 | 39 | let differentAccidental = Key(noteLetter: .a, accidental: .sharp, type: .major) 40 | let differentAccidental2 = Key(noteLetter: .a, accidental: .doubleSharp, type: .major) 41 | #expect(differentAccidental != differentAccidental2) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/MarkerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkerTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-10-17. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class MarkerTests { 13 | @Test func equalityFalse() async throws { 14 | let segno = MarkerType.segno 15 | let varSegno = MarkerType.varSegno 16 | #expect(segno != varSegno) 17 | } 18 | 19 | @Test func equalityTrue() async throws { 20 | let segno1 = MarkerType.segno 21 | let segno2 = MarkerType.segno 22 | #expect(segno1 == segno2) 23 | } 24 | 25 | @Test func convenienceInit() async throws { 26 | let segnoFromString = try MarkerType.marker(from: "segno") 27 | #expect(segnoFromString == .segno) 28 | 29 | let varSegnoFromString = try MarkerType.marker(from: "varsegno") 30 | #expect(varSegnoFromString == .varSegno) 31 | 32 | let codaFromString = try MarkerType.marker(from: "coda") 33 | #expect(codaFromString == .coda) 34 | 35 | let varCodaFromString = try MarkerType.marker(from: "Var Coda") 36 | #expect(varCodaFromString == .varCoda) 37 | 38 | let codettaFromString = try MarkerType.marker(from: "Codetta") 39 | #expect(codettaFromString == .codetta) 40 | 41 | let fineFromString = try MarkerType.marker(from: "Fine") 42 | #expect(fineFromString == .fine) 43 | 44 | let toCodaFromString = try MarkerType.marker(from: "To Coda") 45 | #expect(toCodaFromString == .toCoda) 46 | 47 | let toCodaSymbolFromString = try MarkerType.marker(from: "To Coda Symbol") 48 | #expect(toCodaSymbolFromString == .toCodaSymbol) 49 | 50 | let daCodaFromString = try MarkerType.marker(from: "Da Coda") 51 | #expect(daCodaFromString == .daCoda) 52 | 53 | let dalDoppiaDelSegnoFromString = try MarkerType.marker(from: "Dal Doppio Segno Al Coda") 54 | #expect(dalDoppiaDelSegnoFromString == .dalDoppioSegnoAlCoda) 55 | 56 | let daDoppiaFromString = try MarkerType.marker(from: "Da Doppia Coda") 57 | #expect(daDoppiaFromString == .daDoppiaCoda) 58 | 59 | let userMarkerFromString = try MarkerType.marker(from: "userMarker") 60 | #expect(userMarkerFromString == .userMarker) 61 | 62 | #expect(throws: MarkerTypeError.self) { 63 | _ = try MarkerType.marker(from: "badMarkerType") 64 | } 65 | 66 | #expect(MarkerType.allCases.count == 12) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/MeasureDurationValidatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureDurationValidatorTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-08-06. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class MeasureDurationValidatorTests { 13 | static let standardTimeSignature = TimeSignature(numerator: 4, denominator: 4, tempo: 120) 14 | static let oddTimeSignature = TimeSignature(numerator: 11, denominator: 16, tempo: 86) 15 | static let irrationalTimeSignature = TimeSignature(numerator: 3, denominator: 6, tempo: 120) 16 | 17 | var fullMeasure: Measure! 18 | var notFullMeasure: Measure! 19 | var notFullMeasureDotted: Measure! 20 | var overfilledMeasure: Measure! 21 | // A measure where the overfill contains a dot, so really the dot part is the part that is overfilling it 22 | var overfilledWithDotMeasure: Measure! 23 | // A measure where if you remove the overfilled note, it is not full anymore 24 | var overfilledWithTooLargeMeasure: Measure! 25 | let emptyMeasure = Measure(timeSignature: standardTimeSignature, key: Key(noteLetter: .c)) 26 | 27 | var fullMeasureOddTimeSignature: Measure! 28 | var notFullMeasureOddTimeSignature: Measure! 29 | var overfilledMeasureOddTimeSignature: Measure! 30 | 31 | var fullMeasureIrrationalTimeSignature: Measure! 32 | var notFullMeasureIrrationalTimeSignature: Measure! 33 | var overfilledMeasureIrrationalTimeSignature: Measure! 34 | 35 | init() { 36 | let key = Key(noteLetter: .c) 37 | var staff = Staff(clef: .treble) 38 | let dotted16: Note = { 39 | Note(try! NoteDuration(value: .sixteenth, dotCount: 1), 40 | pitch: SpelledPitch(.c, .octave0)) 41 | }() 42 | let doubleDottedEighth: Note = { 43 | Note(try! NoteDuration(value: .eighth, dotCount: 2), 44 | pitch: SpelledPitch(.c, .octave0)) 45 | }() 46 | let quarter = Note(.quarter, pitch: SpelledPitch(.c, .octave0)) 47 | let thirtySecond = Note(.thirtySecond, pitch: SpelledPitch(.c, .octave0)) 48 | let halfRest = Note(.half) 49 | let quarterTriplet = try! Tuplet(3, .quarter, notes: [quarter, quarter, quarter]) 50 | 51 | fullMeasure = Measure( 52 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 53 | key: key, 54 | notes: [[quarter, quarter, thirtySecond, thirtySecond, thirtySecond, thirtySecond, quarter, dotted16, 55 | thirtySecond]] 56 | ) 57 | // Missing 1 3/4 beats 58 | notFullMeasure = Measure( 59 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 60 | key: key, 61 | notes: [[quarterTriplet, thirtySecond, thirtySecond]] 62 | ) 63 | // Missing 1 1/8 beats 64 | notFullMeasureDotted = Measure( 65 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 66 | key: key, 67 | notes: [[halfRest, doubleDottedEighth]] 68 | ) 69 | // Overfilled by the last 2 quarter notes. Full if they aren't there 70 | overfilledMeasure = Measure( 71 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 72 | key: key, 73 | notes: [[halfRest, quarter, dotted16, thirtySecond, thirtySecond, thirtySecond, thirtySecond, thirtySecond, 74 | quarter, quarter]] 75 | ) 76 | // The last sixteenth fills the measure, but the dot puts it over the edge 77 | overfilledWithDotMeasure = Measure( 78 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 79 | key: key, 80 | notes: [[halfRest, quarter, thirtySecond, thirtySecond, thirtySecond, thirtySecond, thirtySecond, 81 | thirtySecond, dotted16]] 82 | ) 83 | // Quarter is too much, but when removed, the measure is not full 84 | overfilledWithTooLargeMeasure = Measure( 85 | timeSignature: MeasureDurationValidatorTests.standardTimeSignature, 86 | key: key, 87 | notes: [[quarter, quarter, quarter, doubleDottedEighth, quarter]] 88 | ) 89 | fullMeasureOddTimeSignature = Measure( 90 | timeSignature: MeasureDurationValidatorTests.oddTimeSignature, 91 | key: key, 92 | notes: [[dotted16, thirtySecond, quarter, quarter, thirtySecond, thirtySecond]] 93 | ) 94 | // Missing a quarter note (4 beats) 95 | notFullMeasureOddTimeSignature = Measure( 96 | timeSignature: MeasureDurationValidatorTests.oddTimeSignature, 97 | key: key, 98 | notes: [[dotted16, thirtySecond, quarter, thirtySecond, thirtySecond]] 99 | ) 100 | // Overfilled by the half rest. Full if removed 101 | overfilledMeasureOddTimeSignature = Measure( 102 | timeSignature: MeasureDurationValidatorTests.oddTimeSignature, 103 | key: key, 104 | notes: [[dotted16, thirtySecond, quarter, thirtySecond, thirtySecond, quarter, halfRest]] 105 | ) 106 | fullMeasureIrrationalTimeSignature = Measure( 107 | timeSignature: MeasureDurationValidatorTests.irrationalTimeSignature, 108 | key: key, 109 | notes: [[quarter, quarter, quarter]] 110 | ) 111 | // Missing one quarter note 112 | notFullMeasureIrrationalTimeSignature = Measure( 113 | timeSignature: MeasureDurationValidatorTests.irrationalTimeSignature, 114 | key: key, 115 | notes: [[quarter, quarter]] 116 | ) 117 | // Overfilled by one quarter note 118 | overfilledMeasureIrrationalTimeSignature = Measure( 119 | timeSignature: MeasureDurationValidatorTests.irrationalTimeSignature, 120 | key: key, 121 | notes: [[quarter, quarter, quarter, quarter]] 122 | ) 123 | // Add all to staff 124 | staff.appendMeasure(fullMeasure) 125 | staff.appendMeasure(notFullMeasure) 126 | staff.appendMeasure(notFullMeasureDotted) 127 | staff.appendMeasure(overfilledMeasure) 128 | staff.appendMeasure(overfilledWithDotMeasure) 129 | staff.appendMeasure(overfilledWithTooLargeMeasure) 130 | staff.appendMeasure(emptyMeasure) 131 | staff.appendMeasure(fullMeasureOddTimeSignature) 132 | staff.appendMeasure(notFullMeasureOddTimeSignature) 133 | staff.appendMeasure(overfilledMeasureOddTimeSignature) 134 | staff.appendMeasure(fullMeasureIrrationalTimeSignature) 135 | staff.appendMeasure(notFullMeasureIrrationalTimeSignature) 136 | staff.appendMeasure(overfilledMeasureIrrationalTimeSignature) 137 | } 138 | 139 | // MARK: - completionState(of:) 140 | 141 | // MARK: .full 142 | 143 | @Test func completionStateFull() async throws { 144 | #expect( 145 | MeasureDurationValidator.completionState(of: fullMeasure) == 146 | [MeasureDurationValidator.CompletionState.full] 147 | ) 148 | #expect( 149 | MeasureDurationValidator.completionState(of: fullMeasureOddTimeSignature) == 150 | [MeasureDurationValidator.CompletionState.full] 151 | ) 152 | #expect( 153 | MeasureDurationValidator.completionState(of: fullMeasureIrrationalTimeSignature) == 154 | [MeasureDurationValidator.CompletionState.full] 155 | ) 156 | } 157 | 158 | // MARK: .notFull 159 | 160 | @Test func completionStateNotFullForEmpty() async throws { 161 | #expect( 162 | MeasureDurationValidator.completionState(of: emptyMeasure) == 163 | [MeasureDurationValidator.CompletionState.notFull(availableNotes: [.whole: 1])] 164 | ) 165 | } 166 | 167 | @Test func completionStateNotFullForStandard() async throws { 168 | #expect( 169 | MeasureDurationValidator.completionState(of: notFullMeasure) == 170 | [MeasureDurationValidator.CompletionState.notFull(availableNotes: [.quarter: 1, .eighth: 1, .sixteenth: 1])] 171 | ) 172 | } 173 | 174 | @Test func completionStateNotFullForDotted() async throws { 175 | #expect( 176 | MeasureDurationValidator.completionState(of: notFullMeasureDotted) == 177 | [MeasureDurationValidator.CompletionState.notFull(availableNotes: [.quarter: 1, .thirtySecond: 1])] 178 | ) 179 | } 180 | 181 | @Test func completionStateNotFullForOddTimeSignature() async throws { 182 | #expect( 183 | MeasureDurationValidator.completionState(of: notFullMeasureOddTimeSignature) == 184 | [MeasureDurationValidator.CompletionState.notFull(availableNotes: [.quarter: 1])] 185 | ) 186 | } 187 | 188 | @Test func completionStateNotFullForIrrationalTimeSignature() async throws { 189 | #expect( 190 | MeasureDurationValidator.completionState(of: notFullMeasureIrrationalTimeSignature) == 191 | [MeasureDurationValidator.CompletionState.notFull(availableNotes: [.quarter: 1])] 192 | ) 193 | } 194 | 195 | // MARK: .overfilled 196 | 197 | @Test func completionStateOverfilledForOneExtra() async throws { 198 | #expect( 199 | MeasureDurationValidator.completionState(of: overfilledWithTooLargeMeasure) == 200 | [MeasureDurationValidator.CompletionState.overfilled(overflowingNotes: 4 ..< 5)] 201 | ) 202 | } 203 | 204 | @Test func completionStateOverfilledForMultipleExtra() async throws { 205 | #expect( 206 | MeasureDurationValidator.completionState(of: overfilledMeasure) == 207 | [MeasureDurationValidator.CompletionState.overfilled(overflowingNotes: 8 ..< 10)] 208 | ) 209 | } 210 | 211 | @Test func completionStateOverfilledForSingleExtraOddTimeSignature() async throws { 212 | #expect( 213 | MeasureDurationValidator.completionState(of: overfilledMeasureOddTimeSignature) == 214 | [MeasureDurationValidator.CompletionState.overfilled(overflowingNotes: 6 ..< 7)] 215 | ) 216 | } 217 | 218 | @Test func completionStateOverfilledTooFullBecauseOfDot() async throws { 219 | #expect( 220 | MeasureDurationValidator.completionState(of: overfilledWithDotMeasure) == 221 | [MeasureDurationValidator.CompletionState.overfilled(overflowingNotes: 8 ..< 9)] 222 | ) 223 | } 224 | 225 | @Test func completionStateOverfilledForSingleExtraIrrationalTimeSignature() async throws { 226 | #expect( 227 | MeasureDurationValidator.completionState(of: overfilledMeasureIrrationalTimeSignature) == 228 | [MeasureDurationValidator.CompletionState.overfilled(overflowingNotes: 3 ..< 4)] 229 | ) 230 | } 231 | 232 | // MARK: - number(of:fittingIn:) 233 | 234 | @Test func numberOfFittingInForFull() async throws { 235 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: fullMeasure) == 0) 236 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: fullMeasure) == 0) 237 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: fullMeasure) == 0) 238 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: fullMeasureOddTimeSignature) == 0) 239 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: fullMeasure) == 0) 240 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: fullMeasure) == 0) 241 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: fullMeasure) == 0) 242 | } 243 | 244 | @Test func numberOfFittingInForEmptyStandardTimeSignature() async throws { 245 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: emptyMeasure) == 1) 246 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: emptyMeasure) == 2) 247 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: emptyMeasure) == 4) 248 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: emptyMeasure) == 8) 249 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: emptyMeasure) == 16) 250 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: emptyMeasure) == 32) 251 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: emptyMeasure) == 64) 252 | } 253 | 254 | @Test func numberOfFittingInForStandardTimeSignature() async throws { 255 | // 1 3/4 beats missing 256 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: notFullMeasure) == 0) 257 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: notFullMeasure) == 0) 258 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: notFullMeasure) == 1) 259 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: notFullMeasure) == 3) 260 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: notFullMeasure) == 7) 261 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: notFullMeasure) == 14) 262 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: notFullMeasure) == 28) 263 | 264 | // 1 1/8 beats missing 265 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: notFullMeasureDotted) == 0) 266 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: notFullMeasureDotted) == 0) 267 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: notFullMeasureDotted) == 1) 268 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: notFullMeasureDotted) == 2) 269 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: notFullMeasureDotted) == 4) 270 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: notFullMeasureDotted) == 9) 271 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: notFullMeasureDotted) == 18) 272 | } 273 | 274 | @Test func numberOfFittingInForOddTimeSignature() async throws { 275 | // 4 beats missing - 1 quarter note 276 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: notFullMeasureOddTimeSignature) == 0) 277 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: notFullMeasureOddTimeSignature) == 0) 278 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: notFullMeasureOddTimeSignature) == 1) 279 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: notFullMeasureOddTimeSignature) == 2) 280 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: notFullMeasureOddTimeSignature) == 4) 281 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: notFullMeasureOddTimeSignature) == 8) 282 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: notFullMeasureOddTimeSignature) == 16) 283 | } 284 | 285 | @Test func numberOfFittingInForOverfilled() async throws { 286 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: overfilledMeasure) == 0) 287 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: overfilledMeasure) == 0) 288 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: overfilledMeasure) == 0) 289 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: overfilledMeasure) == 0) 290 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: overfilledMeasure) == 0) 291 | #expect(MeasureDurationValidator.number(of: .thirtySecond, fittingIn: overfilledMeasure) == 0) 292 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, fittingIn: overfilledMeasure) == 0) 293 | } 294 | 295 | @Test func numberOfFittingInForFullIrrationalTimeSignature() async throws { 296 | #expect(MeasureDurationValidator.number(of: .whole, fittingIn: fullMeasureIrrationalTimeSignature) == 0) 297 | #expect(MeasureDurationValidator.number(of: .half, fittingIn: fullMeasureIrrationalTimeSignature) == 0) 298 | #expect(MeasureDurationValidator.number(of: .quarter, fittingIn: fullMeasureIrrationalTimeSignature) == 0) 299 | #expect(MeasureDurationValidator.number(of: .eighth, fittingIn: fullMeasureIrrationalTimeSignature) == 0) 300 | #expect(MeasureDurationValidator.number(of: .sixteenth, fittingIn: fullMeasureIrrationalTimeSignature) == 0) 301 | #expect(MeasureDurationValidator.number(of: .thirtySecond, 302 | fittingIn: fullMeasureIrrationalTimeSignature) == 0) 303 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, 304 | fittingIn: fullMeasureIrrationalTimeSignature) == 0) 305 | } 306 | 307 | @Test func numberOfFittingInForNotFullIrrationalTimeSignature() async throws { 308 | #expect(MeasureDurationValidator.number(of: .whole, 309 | fittingIn: notFullMeasureIrrationalTimeSignature) == 0) 310 | #expect(MeasureDurationValidator.number(of: .half, 311 | fittingIn: notFullMeasureIrrationalTimeSignature) == 0) 312 | #expect(MeasureDurationValidator.number(of: .quarter, 313 | fittingIn: notFullMeasureIrrationalTimeSignature) == 1) 314 | #expect(MeasureDurationValidator.number(of: .eighth, 315 | fittingIn: notFullMeasureIrrationalTimeSignature) == 2) 316 | #expect(MeasureDurationValidator.number(of: .sixteenth, 317 | fittingIn: notFullMeasureIrrationalTimeSignature) == 4) 318 | #expect(MeasureDurationValidator.number(of: .thirtySecond, 319 | fittingIn: notFullMeasureIrrationalTimeSignature) == 8) 320 | #expect(MeasureDurationValidator.number(of: .sixtyFourth, 321 | fittingIn: notFullMeasureIrrationalTimeSignature) == 16) 322 | } 323 | 324 | // MARK: - baseNoteDuration(from:) 325 | 326 | // MARK: Failures 327 | 328 | @Test func baseNoteDurationForTooLargeBottomNumber() async throws { 329 | let timeSignature = TimeSignature(numerator: 4, denominator: 256, tempo: 120) 330 | let measure = Measure(timeSignature: timeSignature, key: Key(noteLetter: .c)) 331 | #expect(throws: MeasureDurationValidatorError.invalidBottomNumber) { 332 | _ = try MeasureDurationValidator.baseNoteDuration(from: measure) 333 | } 334 | } 335 | 336 | // MARK: Successes 337 | 338 | @Test func baseNoteDurationForCommonBottomNumber() async throws { 339 | let baseNoteDuration = try MeasureDurationValidator.baseNoteDuration(from: fullMeasure) 340 | #expect(baseNoteDuration == .quarter) 341 | let baseNoteDurationOdd = try MeasureDurationValidator.baseNoteDuration(from: fullMeasureOddTimeSignature) 342 | #expect(baseNoteDurationOdd == .sixteenth) 343 | } 344 | 345 | @Test func baseNoteDurationForIrrationalBottomNumber() async throws { 346 | let baseNoteDurationIrrational = try MeasureDurationValidator.baseNoteDuration(from: fullMeasureIrrationalTimeSignature) 347 | #expect(baseNoteDurationIrrational == .quarter) 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/MeasureRepeatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureRepeatTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-03-09. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class MeasureRepeatTests { 13 | static let timeSignature = TimeSignature(numerator: 4, denominator: 4, tempo: 120) 14 | static let key = Key(noteLetter: .c) 15 | static let note1 = Note(.eighth, pitch: SpelledPitch(.c, .octave1)) 16 | static let note2 = Note(.quarter, pitch: SpelledPitch(.d, .octave1)) 17 | let measure1 = Measure(timeSignature: timeSignature, key: key, notes: [[note1, note1]]) 18 | let measure2 = Measure(timeSignature: timeSignature, key: key, notes: [[note2, note2]]) 19 | 20 | // MARK: - init(measures:repeateCount:) 21 | 22 | // MARK: Failures 23 | 24 | @Test func initInvalidRepeatCount() async throws { 25 | #expect(throws: MeasureRepeatError.invalidRepeatCount) { 26 | _ = try MeasureRepeat(measures: [self.measure1], repeatCount: -2) 27 | } 28 | } 29 | 30 | @Test func initNoMeasures() async throws { 31 | #expect(throws: MeasureRepeatError.noMeasures) { 32 | _ = try MeasureRepeat(measures: []) 33 | } 34 | } 35 | 36 | // MARK: Successes 37 | 38 | @Test func initNotSpecifiedRepeatCount() async throws { 39 | let measureRepeat = try MeasureRepeat(measures: [measure1]) 40 | #expect(measureRepeat.repeatCount == 1) 41 | } 42 | 43 | @Test func initSingleMeasure() async throws { 44 | _ = try MeasureRepeat(measures: [measure2], repeatCount: 3) 45 | } 46 | 47 | @Test func initMultipleMeasures() async throws { 48 | _ = try MeasureRepeat(measures: [measure1, measure2], repeatCount: 4) 49 | } 50 | 51 | // MARK: - expand() 52 | 53 | @Test func expandSingleMeasureRepeatedOnce() async throws { 54 | let measureRepeat = try MeasureRepeat(measures: [measure1], repeatCount: 1) 55 | let expected = [measure1, RepeatedMeasure(immutableMeasure: measure1)] as [ImmutableMeasure] 56 | let actual = measureRepeat.expand() 57 | guard actual.count == expected.count else { 58 | Issue.record("actual.count(\(actual.count)) == expected.count(\(expected.count))") 59 | return 60 | } 61 | #expect(compareImmutableMeasureArrays(actual: actual, expected: expected)) 62 | #expect(String(describing: measureRepeat) == "[ |4/4: [1/8c1, 1/8c1]| ] × 2") 63 | } 64 | 65 | @Test func expandSingleMeasureRepeatedMany() async throws { 66 | let measureRepeat = try MeasureRepeat(measures: [measure1], repeatCount: 3) 67 | let repeatedMeasure = RepeatedMeasure(immutableMeasure: measure1) 68 | let expected = [measure1, repeatedMeasure, repeatedMeasure, repeatedMeasure] as [ImmutableMeasure] 69 | let actual = measureRepeat.expand() 70 | guard actual.count == expected.count else { 71 | Issue.record("actual.count(\(actual.count)) == expected.count(\(expected.count))") 72 | return 73 | } 74 | #expect(compareImmutableMeasureArrays(actual: actual, expected: expected)) 75 | #expect(String(describing: measureRepeat) == "[ |4/4: [1/8c1, 1/8c1]| ] × 4") 76 | } 77 | 78 | @Test func expandManyMeasuresRepeatedOnce() async throws { 79 | let measureRepeat = try MeasureRepeat(measures: [measure1, measure2], repeatCount: 1) 80 | let repeatedMeasure1 = RepeatedMeasure(immutableMeasure: measure1) 81 | let repeatedMeasure2 = RepeatedMeasure(immutableMeasure: measure2) 82 | let expected = [measure1, measure2, repeatedMeasure1, repeatedMeasure2] as [ImmutableMeasure] 83 | let actual = measureRepeat.expand() 84 | guard actual.count == expected.count else { 85 | Issue.record("actual.count(\(actual.count)) == expected.count(\(expected.count))") 86 | return 87 | } 88 | #expect(compareImmutableMeasureArrays(actual: actual, expected: expected)) 89 | #expect(String(describing: measureRepeat) == "[ |4/4: [1/8c1, 1/8c1]|, |4/4: [1/4d1, 1/4d1]| ] × 2") 90 | } 91 | 92 | @Test func expandManyMeasuresRepeatedMany() async throws { 93 | let measureRepeat = try MeasureRepeat(measures: [measure1, measure2], repeatCount: 3) 94 | let repeatedMeasure1 = RepeatedMeasure(immutableMeasure: measure1) 95 | let repeatedMeasure2 = RepeatedMeasure(immutableMeasure: measure2) 96 | let expected = [ 97 | measure1, measure2, 98 | repeatedMeasure1, repeatedMeasure2, 99 | repeatedMeasure1, repeatedMeasure2, 100 | repeatedMeasure1, repeatedMeasure2, 101 | ] as [ImmutableMeasure] 102 | let actual = measureRepeat.expand() 103 | guard actual.count == expected.count else { 104 | Issue.record("actual.count(\(actual.count)) == expected.count(\(expected.count))") 105 | return 106 | } 107 | #expect(compareImmutableMeasureArrays(actual: actual, expected: expected)) 108 | #expect(String(describing: measureRepeat) == "[ |4/4: [1/8c1, 1/8c1]|, |4/4: [1/4d1, 1/4d1]| ] × 4") 109 | } 110 | 111 | // MARK: - == 112 | 113 | // MARK: Failures 114 | 115 | @Test func equalitySameMeasureCountDifferentMeasures() async throws { 116 | let measureRepeat1 = try MeasureRepeat(measures: [measure1, measure2, measure1]) 117 | let measureRepeat2 = try MeasureRepeat(measures: [measure2, measure1, measure2]) 118 | #expect(measureRepeat1 != measureRepeat2) 119 | } 120 | 121 | @Test func equalityDifferentMeasureCountSameMeasures() async throws { 122 | let measureRepeat1 = try MeasureRepeat(measures: [measure1, measure2]) 123 | let measureRepeat2 = try MeasureRepeat(measures: [measure1, measure2, measure1]) 124 | #expect(measureRepeat1 != measureRepeat2) 125 | } 126 | 127 | @Test func equalityDifferentMeasureCountDifferentMeasures() async throws { 128 | let measureRepeat1 = try MeasureRepeat(measures: [measure1, measure2]) 129 | let measureRepeat2 = try MeasureRepeat(measures: [measure2, measure1, measure2]) 130 | #expect(measureRepeat1 != measureRepeat2) 131 | } 132 | 133 | @Test func equalityDifferentRepeatCount() async throws { 134 | let measureRepeat1 = try MeasureRepeat(measures: [measure1, measure2], repeatCount: 2) 135 | let measureRepeat2 = try MeasureRepeat(measures: [measure1, measure2], repeatCount: 3) 136 | #expect(measureRepeat1 != measureRepeat2) 137 | } 138 | 139 | // MARK: Successes 140 | 141 | @Test func equalitySucceeds() async throws { 142 | let measureRepeat1 = try MeasureRepeat(measures: [measure1], repeatCount: 2) 143 | let measureRepeat2 = try MeasureRepeat(measures: [measure1], repeatCount: 2) 144 | #expect(measureRepeat1 == measureRepeat2) 145 | } 146 | 147 | // MARK: - Helpers 148 | 149 | private func compareImmutableMeasureArrays(actual: [ImmutableMeasure], expected: [ImmutableMeasure]) -> Bool { 150 | for (index, item) in actual.enumerated() { 151 | if let item = item as? RepeatedMeasure { 152 | if let expectedItem = expected[index] as? RepeatedMeasure { 153 | if item == expectedItem { 154 | continue 155 | } else { 156 | return false 157 | } 158 | } else { 159 | return false 160 | } 161 | } else if let item = item as? Measure { 162 | if let expectedItem = expected[index] as? Measure { 163 | if item == expectedItem { 164 | continue 165 | } else { 166 | return false 167 | } 168 | } else { 169 | return false 170 | } 171 | } else { 172 | Issue.record("actual.count(\(actual.count)) == expected.count(\(expected.count))") 173 | return false 174 | } 175 | } 176 | return true 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/NoteDurationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteDurationTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2016-08-21. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class NoteDurationTests { 13 | let allValues: [NoteDuration.Value] = [ 14 | .large, 15 | .long, 16 | .doubleWhole, 17 | .whole, 18 | .half, 19 | .quarter, 20 | .eighth, 21 | .sixteenth, 22 | .thirtySecond, 23 | .sixtyFourth, 24 | .oneTwentyEighth, 25 | .twoFiftySixth, 26 | ] 27 | 28 | // MARK: - init(value:dotCount:) 29 | 30 | // MARK: Failures 31 | 32 | @Test func initNegativeDotCount() async throws { 33 | #expect(throws: NoteDurationError.negativeDotCountInvalid) { 34 | _ = try NoteDuration(value: .quarter, dotCount: -1) 35 | } 36 | } 37 | 38 | // MARK: Successes 39 | 40 | @Test func initDotCountZero() async throws { 41 | let dotCount = 0 42 | 43 | try allValues.forEach { 44 | let duration = try NoteDuration(value: $0, dotCount: dotCount) 45 | #expect(duration.value == $0) 46 | #expect(duration.dotCount == dotCount) 47 | } 48 | } 49 | 50 | @Test func initDotCountNonZero() async throws { 51 | let dotCount = 2 52 | 53 | try allValues.forEach { 54 | let duration = try NoteDuration(value: $0, dotCount: dotCount) 55 | #expect(duration.value == $0) 56 | #expect(duration.dotCount == dotCount) 57 | } 58 | } 59 | 60 | @Test func initDotCountLargerThan4() async throws { 61 | let dotCount = 5 62 | 63 | try allValues.forEach { 64 | let duration = try NoteDuration(value: $0, dotCount: dotCount) 65 | #expect(duration.value == $0) 66 | #expect(duration.dotCount == dotCount) 67 | } 68 | } 69 | 70 | // MARK: - init(timeSignatureValue:) 71 | 72 | // Cannot fail 73 | 74 | func initTimeSignatureValue() async throws { 75 | #expect( 76 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .whole)!) == 77 | .whole 78 | ) 79 | #expect( 80 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .half)!) == 81 | .half 82 | ) 83 | #expect( 84 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .quarter)!) == 85 | .quarter 86 | ) 87 | #expect( 88 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .eighth)!) == 89 | .eighth 90 | ) 91 | #expect( 92 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .sixteenth)!) == 93 | .sixteenth 94 | ) 95 | #expect( 96 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .thirtySecond)!) == 97 | .thirtySecond 98 | ) 99 | #expect( 100 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .sixtyFourth)!) == 101 | .sixtyFourth 102 | ) 103 | #expect( 104 | NoteDuration(timeSignatureValue: NoteDuration.TimeSignatureValue(value: .oneTwentyEighth)!) == 105 | .oneTwentyEighth 106 | ) 107 | } 108 | 109 | // MARK: - number(of:equalTo:) 110 | 111 | // MARK: Successes 112 | 113 | func equalToForSameDuration() async throws { 114 | #expect(NoteDuration.number(of: .large, within: .large) == 1) 115 | #expect(NoteDuration.number(of: .long, within: .long) == 1) 116 | #expect(NoteDuration.number(of: .doubleWhole, within: .doubleWhole) == 1) 117 | #expect(NoteDuration.number(of: .whole, within: .whole) == 1) 118 | #expect(NoteDuration.number(of: .half, within: .half) == 1) 119 | #expect(NoteDuration.number(of: .quarter, within: .quarter) == 1) 120 | #expect(NoteDuration.number(of: .eighth, within: .eighth) == 1) 121 | #expect(NoteDuration.number(of: .sixteenth, within: .sixteenth) == 1) 122 | #expect(NoteDuration.number(of: .thirtySecond, within: .thirtySecond) == 1) 123 | #expect(NoteDuration.number(of: .sixtyFourth, within: .sixtyFourth) == 1) 124 | #expect(NoteDuration.number(of: .oneTwentyEighth, within: .oneTwentyEighth) == 1) 125 | #expect(NoteDuration.number(of: .twoFiftySixth, within: .twoFiftySixth) == 1) 126 | } 127 | 128 | @Test func equalToForSameDurationSingleDot() async throws { 129 | let noteDuration = try NoteDuration(value: .quarter, dotCount: 1) 130 | #expect(NoteDuration.number(of: try NoteDuration(value: .quarter, dotCount: 1), within: noteDuration) == 1) 131 | } 132 | 133 | @Test func equalToForSameDurationMultipleDot() async throws { 134 | let noteDuration = try NoteDuration(value: .quarter, dotCount: 3) 135 | #expect(NoteDuration.number(of: try NoteDuration(value: .quarter, dotCount: 3), within: noteDuration) == 1) 136 | } 137 | 138 | @Test func equalToForSmallerDuration() async throws { 139 | let noteDuration = NoteDuration.sixteenth 140 | #expect(NoteDuration.number(of: .sixtyFourth, within: noteDuration) == 4) 141 | } 142 | 143 | @Test func equalToForLargerDuration() async throws { 144 | let noteDuration = NoteDuration.quarter 145 | #expect(NoteDuration.number(of: .whole, within: noteDuration) == 0.25) 146 | } 147 | 148 | @Test func equalToForSmallerDurationSingleDotFromNoDot() async throws { 149 | let noteDuration = NoteDuration.quarter 150 | #expect(NoteDuration.number(of: try NoteDuration(value: .eighth, dotCount: 1), within: noteDuration) == Double(4) / 3) 151 | } 152 | 153 | @Test func equalToForSmallerDurationSingleDotFromSingleDot() async throws { 154 | let noteDuration = try NoteDuration(value: .quarter, dotCount: 1) 155 | #expect(NoteDuration.number(of: try NoteDuration(value: .sixteenth, dotCount: 1), within: noteDuration) == 4) 156 | } 157 | 158 | @Test func equalToForSmallerDurationDoubleDotFromDoubleDot() async throws { 159 | let noteDuration = try NoteDuration(value: .quarter, dotCount: 2) 160 | #expect(NoteDuration.number(of: try NoteDuration(value: .thirtySecond, dotCount: 2), within: noteDuration) == 8) 161 | } 162 | 163 | // MARK: - debugDescription 164 | 165 | @Test func debugDescriptionNoDot() async throws { 166 | #expect(NoteDuration.large.debugDescription == "8") 167 | #expect(NoteDuration.long.debugDescription == "4") 168 | #expect(NoteDuration.doubleWhole.debugDescription == "2") 169 | #expect(NoteDuration.whole.debugDescription == "1") 170 | #expect(NoteDuration.half.debugDescription == "1/2") 171 | #expect(NoteDuration.quarter.debugDescription == "1/4") 172 | #expect(NoteDuration.eighth.debugDescription == "1/8") 173 | #expect(NoteDuration.sixteenth.debugDescription == "1/16") 174 | #expect(NoteDuration.thirtySecond.debugDescription == "1/32") 175 | #expect(NoteDuration.sixtyFourth.debugDescription == "1/64") 176 | #expect(NoteDuration.oneTwentyEighth.debugDescription == "1/128") 177 | #expect(NoteDuration.twoFiftySixth.debugDescription == "1/256") 178 | } 179 | 180 | @Test func debugDescriptionSingleDot() async throws { 181 | #expect(try! NoteDuration(value: .quarter, dotCount: 1).debugDescription == "1/4.") 182 | } 183 | 184 | @Test func debugDescriptionMultipleDots() async throws { 185 | #expect(try! NoteDuration(value: .sixtyFourth, dotCount: 3).debugDescription == "1/64...") 186 | } 187 | 188 | // MARK: - timeSignatureValue 189 | 190 | @Test func timeSignatureValue() async throws { 191 | #expect(NoteDuration.large.timeSignatureValue == nil) 192 | #expect(NoteDuration.long.timeSignatureValue == nil) 193 | #expect(NoteDuration.doubleWhole.timeSignatureValue == nil) 194 | #expect(NoteDuration.whole.timeSignatureValue?.rawValue == 1) 195 | #expect(NoteDuration.half.timeSignatureValue?.rawValue == 2) 196 | #expect(NoteDuration.quarter.timeSignatureValue?.rawValue == 4) 197 | #expect(NoteDuration.eighth.timeSignatureValue?.rawValue == 8) 198 | #expect(NoteDuration.sixteenth.timeSignatureValue?.rawValue == 16) 199 | #expect(NoteDuration.thirtySecond.timeSignatureValue?.rawValue == 32) 200 | #expect(NoteDuration.sixtyFourth.timeSignatureValue?.rawValue == 64) 201 | #expect(NoteDuration.oneTwentyEighth.timeSignatureValue?.rawValue == 128) 202 | #expect(NoteDuration.twoFiftySixth.timeSignatureValue?.rawValue == nil) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/NoteTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Kyle Sherman on 2015-06-15. 6 | // Copyright © 2015 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class NoteTests { 13 | var note: Note! 14 | 15 | init() { 16 | note = Note(.eighth, pitch: SpelledPitch(.c, .octave1)) 17 | } 18 | 19 | // MARK: - modifyTie(_:) 20 | 21 | // MARK: Failures 22 | 23 | @Test func modifyTieBeginAndEndTryBegin() async throws { 24 | note.tie = .beginAndEnd 25 | #expect(throws: NoteError.invalidRequestedTieState) { 26 | try self.note.modifyTie(.begin) 27 | } 28 | } 29 | 30 | @Test func modifyTieBeginAndEndTryEnd() async throws { 31 | note.tie = .beginAndEnd 32 | #expect(throws: NoteError.invalidRequestedTieState) { 33 | try self.note.modifyTie(.end) 34 | } 35 | } 36 | 37 | // MARK: Successes 38 | 39 | @Test func modifyTieNilTryBegin() async throws { 40 | note.tie = nil 41 | try note.modifyTie(.begin) 42 | #expect(note.tie == .begin) 43 | } 44 | 45 | @Test func modifyTieNilTryEnd() async throws { 46 | note.tie = nil 47 | try note.modifyTie(.end) 48 | #expect(note.tie == .end) 49 | } 50 | 51 | @Test func modifyTieNilTryBeginAndEnd() async throws { 52 | note.tie = nil 53 | try note.modifyTie(.beginAndEnd) 54 | #expect(note.tie == .beginAndEnd) 55 | } 56 | 57 | @Test func modifyTieBeginTryEnd() async throws { 58 | note.tie = .begin 59 | try note.modifyTie(.end) 60 | #expect(note.tie == .beginAndEnd) 61 | } 62 | 63 | @Test func modifyTieEndTryBegin() async throws { 64 | note.tie = .end 65 | try note.modifyTie(.begin) 66 | #expect(note.tie == .beginAndEnd) 67 | } 68 | 69 | @Test func modifyTieBeginTryBegin() async throws { 70 | note.tie = .begin 71 | try note.modifyTie(.begin) 72 | #expect(note.tie == .begin) 73 | } 74 | 75 | @Test func modifyTieBeginAndEndTryBeginAndEnd() async throws { 76 | note.tie = .end 77 | try note.modifyTie(.end) 78 | #expect(note.tie == .end) 79 | } 80 | 81 | @Test func modifyTieEndTryEnd() async throws { 82 | note.tie = .beginAndEnd 83 | try note.modifyTie(.beginAndEnd) 84 | #expect(note.tie == .beginAndEnd) 85 | } 86 | 87 | // MARK: - removeTie() 88 | 89 | // MARK: Failures 90 | 91 | @Test func removeTieNilTryBeginAndEnd() async throws { 92 | note.tie = nil 93 | 94 | #expect(throws: NoteError.invalidRequestedTieState) { 95 | try self.note.removeTie(.beginAndEnd) 96 | } 97 | } 98 | 99 | @Test func removeTieEndTryBegin() async throws { 100 | // Requested state doesn't match 101 | note.tie = .end 102 | #expect(throws: NoteError.invalidRequestedTieState) { 103 | try self.note.removeTie(.begin) 104 | } 105 | } 106 | 107 | @Test func removeTieBeginTryEnd() async throws { 108 | // Requested state doesn't match 109 | note.tie = .begin 110 | #expect(throws: NoteError.invalidRequestedTieState) { 111 | try self.note.removeTie(.end) 112 | } 113 | } 114 | 115 | // MARK: Successes 116 | 117 | @Test func removeTieBegin() async throws { 118 | note.tie = .begin 119 | try note.removeTie(.begin) 120 | #expect(note.tie == nil) 121 | } 122 | 123 | @Test func removeTieEnd() async throws { 124 | note.tie = .end 125 | try note.removeTie(.end) 126 | #expect(note.tie == nil) 127 | } 128 | 129 | @Test func removeTieBeginAndEndTryBegin() async throws { 130 | note.tie = .beginAndEnd 131 | try note.removeTie(.begin) 132 | #expect(note.tie == .end) 133 | } 134 | 135 | @Test func removeTieBeginAndEndTryEnd() async throws { 136 | note.tie = .beginAndEnd 137 | try note.removeTie(.end) 138 | #expect(note.tie == .begin) 139 | } 140 | 141 | @Test func removeTieNilTryBegin() async throws { 142 | note.tie = nil 143 | try note.removeTie(.begin) 144 | #expect(note.tie == nil) 145 | } 146 | 147 | @Test func removeTieNilTryEnd() async throws { 148 | note.tie = nil 149 | try note.removeTie(.end) 150 | #expect(note.tie == nil) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/PartTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PartTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-098-23. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class PartTests { 13 | let testName = "testName" 14 | let testShortName = "testShortName" 15 | 16 | var staff1: Staff! 17 | var staff2: Staff! 18 | var part: Part! 19 | 20 | init() { 21 | staff1 = Staff(clef: .treble) 22 | staff2 = Staff(clef: .bass) 23 | part = Part(instrument: Instrument(name: testName, shortName: testShortName), staves: [staff1, staff2]) 24 | } 25 | 26 | deinit { 27 | part = nil 28 | } 29 | 30 | @Test func debugDescription() async throws { 31 | #expect(part!.debugDescription == "Part(instrument `testName`), staves(staff(treble )\nstaff(bass ))") 32 | } 33 | 34 | @Test func partNames() async throws { 35 | #expect(part!.instrument?.name == testName) 36 | #expect(part!.instrument?.shortName == testShortName) 37 | } 38 | 39 | @Test func iterator() async throws { 40 | var iterator = part.makeIterator() 41 | if let actual = iterator.next() { 42 | #expect(actual.clef == .treble) 43 | } else { 44 | Issue.record("Iterator didn't return correct value for next()") 45 | } 46 | } 47 | 48 | @Test func appendStaff() async throws { 49 | part.append(Staff(clef: .soprano)) 50 | #expect(part.staves.count == 3) 51 | } 52 | 53 | @Test func remove() async throws { 54 | part.remove(at: 0) 55 | #expect(part.staves.count == 1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/PitchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PitchTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Rob Hudson on 2016-07-29. 6 | // Copyright © 2016 Kyle Sherman. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class PitchTests { 13 | @Test func pitch1() async throws { 14 | let pitch = SpelledPitch(.c, .octave3) 15 | #expect(pitch.debugDescription == "c3") 16 | } 17 | 18 | @Test func pitch2() async throws { 19 | let pitch = SpelledPitch(.g, accidental: .sharp, .octave6) 20 | #expect(pitch.debugDescription == "g♯6") 21 | } 22 | 23 | @Test func pitch3() async throws { 24 | let pitch = SpelledPitch(.e, accidental: .flat, .octave2) 25 | #expect(pitch.debugDescription == "e♭2") 26 | } 27 | 28 | @Test func pitch4() async throws { 29 | let pitch = SpelledPitch(.a, accidental: .natural, .octave4) 30 | #expect(pitch.debugDescription == "a4") 31 | } 32 | 33 | @Test func pitch5() async throws { 34 | let pitch = SpelledPitch(.b, accidental: .doubleSharp, .octave5) 35 | #expect(pitch.debugDescription == "b𝄪5") 36 | } 37 | 38 | @Test func pitch6() async throws { 39 | let pitch = SpelledPitch(.f, accidental: .doubleFlat, .octave7) 40 | #expect(pitch.debugDescription == "f𝄫7") 41 | } 42 | 43 | // MARK: - == 44 | 45 | // MARK: Failures 46 | 47 | @Test func notEqual() async throws { 48 | let pitch1 = SpelledPitch(.b, accidental: .flat, .octave5) 49 | let pitch2 = SpelledPitch(.b, accidental: .flat, .octave4) 50 | 51 | #expect(pitch1 != pitch2) 52 | } 53 | 54 | // MARK: Successes 55 | 56 | @Test func equal() async throws { 57 | let pitch1 = SpelledPitch(.d, accidental: .sharp, .octave1) 58 | let pitch2 = SpelledPitch(.d, accidental: .sharp, .octave1) 59 | 60 | #expect(pitch1 == pitch2) 61 | } 62 | 63 | // MARK: - MIDI numbers 64 | 65 | // MARK: Successes 66 | 67 | @Test func ridiculouslyLowNote() async throws { 68 | let pitch = SpelledPitch(.c, accidental: .natural, .octaveNegative1) 69 | 70 | #expect(pitch.midiNoteNumber == 0) 71 | } 72 | 73 | @Test func lowNote() async throws { 74 | let pitch = SpelledPitch(.f, accidental: .sharp, .octave1) 75 | 76 | #expect(pitch.midiNoteNumber == 30) 77 | } 78 | 79 | @Test func midRangeNote() async throws { 80 | let pitch = SpelledPitch(.d, .octave4) 81 | 82 | #expect(pitch.midiNoteNumber == 62) 83 | } 84 | 85 | @Test func highNote() async throws { 86 | let pitch = SpelledPitch(.c, accidental: .flat, .octave8) 87 | 88 | #expect(pitch.midiNoteNumber == 107) 89 | } 90 | 91 | // MARK: - isEnharmonic(with:) 92 | 93 | // MARK: Failures 94 | 95 | @Test func differentAccidentals() async throws { 96 | let pitch1 = SpelledPitch(.d, accidental: .flat, .octave1) 97 | let pitch2 = SpelledPitch(.d, accidental: .sharp, .octave1) 98 | 99 | #expect(pitch1 != pitch2) 100 | #expect(!pitch1.isEnharmonic(with: pitch2)) 101 | } 102 | 103 | @Test func samePitchDifferentOctaves() async throws { 104 | let pitch1 = SpelledPitch(.e, accidental: .natural, .octave5) 105 | let pitch2 = SpelledPitch(.e, accidental: .natural, .octave6) 106 | 107 | #expect(pitch1 != pitch2) 108 | #expect(!pitch1.isEnharmonic(with: pitch2)) 109 | } 110 | 111 | @Test func enharmonicPitchDifferentOctaves() async throws { 112 | let pitch1 = SpelledPitch(.f, accidental: .doubleSharp, .octave2) 113 | let pitch2 = SpelledPitch(.g, accidental: .natural, .octave5) 114 | 115 | #expect(pitch1 != pitch2) 116 | #expect(!pitch1.isEnharmonic(with: pitch2)) 117 | } 118 | 119 | // MARK: Successes 120 | 121 | @Test func samePitchIsEnharmonic() async throws { 122 | let pitch1 = SpelledPitch(.g, accidental: .natural, .octave6) 123 | let pitch2 = SpelledPitch(.g, accidental: .natural, .octave6) 124 | 125 | #expect(pitch1 == pitch2) 126 | #expect(pitch1.isEnharmonic(with: pitch2)) 127 | // Transitive property 128 | #expect(pitch2.isEnharmonic(with: pitch1)) 129 | } 130 | 131 | @Test func enharmonicNotEquatable() async throws { 132 | let pitch1 = SpelledPitch(.a, accidental: .flat, .octave3) 133 | let pitch2 = SpelledPitch(.g, accidental: .sharp, .octave3) 134 | 135 | #expect(pitch1 != pitch2) 136 | #expect(pitch1.isEnharmonic(with: pitch2)) 137 | } 138 | 139 | @Test func naturalAndFlat() async throws { 140 | let pitch1 = SpelledPitch(.e, accidental: .natural, .octave4) 141 | let pitch2 = SpelledPitch(.f, accidental: .flat, .octave4) 142 | 143 | #expect(pitch1 != pitch2) 144 | #expect(pitch1.isEnharmonic(with: pitch2)) 145 | } 146 | 147 | @Test func doubleFlat() async throws { 148 | let pitch1 = SpelledPitch(.b, accidental: .doubleFlat, .octave2) 149 | let pitch2 = SpelledPitch(.a, .octave2) 150 | 151 | #expect(pitch1 != pitch2) 152 | #expect(pitch1.isEnharmonic(with: pitch2)) 153 | } 154 | 155 | @Test func differentOctaveNumbers() async throws { 156 | let pitch1 = SpelledPitch(.b, accidental: .sharp, .octave6) 157 | let pitch2 = SpelledPitch(.c, accidental: .natural, .octave7) 158 | 159 | #expect(pitch1 != pitch2) 160 | #expect(pitch1.isEnharmonic(with: pitch2)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/ScoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScoreTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 2024-08-23. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class ScoreTests { 13 | enum Constant { 14 | static let standardClef: Clef = .treble 15 | } 16 | 17 | var score: Score! 18 | var staff: Staff! 19 | 20 | var measure1: Measure! 21 | var measure2: Measure! 22 | var measure3: Measure! 23 | var measure4: Measure! 24 | var measure5: Measure! 25 | var measure6: Measure! 26 | var measure7: Measure! 27 | var measure8: Measure! 28 | var repeat1: MeasureRepeat! 29 | var repeat2: MeasureRepeat! 30 | 31 | init() { 32 | staff = Staff(clef: Constant.standardClef) 33 | let timeSignature = TimeSignature(numerator: 4, denominator: 4, tempo: 120) 34 | let key = Key(noteLetter: .c) 35 | let note = Note(.sixteenth, pitch: SpelledPitch(.c, .octave1)) 36 | let note2 = Note(.sixteenth, pitch: SpelledPitch(.a, .octave1)) 37 | let tuplet = try! Tuplet(3, .sixteenth, notes: [note, note, note]) 38 | let tuplet2 = try! Tuplet(3, .sixteenth, notes: [note2, note, note]) 39 | 40 | measure1 = Measure( 41 | timeSignature: timeSignature, 42 | key: key, 43 | notes: [[note, note, note, note, tuplet]] 44 | ) 45 | measure2 = Measure( 46 | timeSignature: timeSignature, 47 | key: key, 48 | notes: [[tuplet, note, note]] 49 | ) 50 | measure3 = Measure( 51 | timeSignature: timeSignature, 52 | key: key, 53 | notes: [[note, note, note, note, tuplet]] 54 | ) 55 | measure4 = Measure( 56 | timeSignature: timeSignature, 57 | key: key, 58 | notes: [[note, note, note, note]] 59 | ) 60 | measure5 = Measure( 61 | timeSignature: timeSignature, 62 | key: key, 63 | notes: [[tuplet, note, note, note, note]] 64 | ) 65 | measure6 = Measure( 66 | timeSignature: timeSignature, 67 | key: key, 68 | notes: [[tuplet, tuplet, note, note]] 69 | ) 70 | measure7 = Measure( 71 | timeSignature: timeSignature, 72 | key: key, 73 | notes: [[note2, tuplet, tuplet, note]] 74 | ) 75 | measure8 = Measure( 76 | timeSignature: timeSignature, 77 | key: key, 78 | notes: [[tuplet2, note, note]] 79 | ) 80 | repeat1 = try! MeasureRepeat(measures: [measure4]) 81 | repeat2 = try! MeasureRepeat(measures: [measure4, measure4], repeatCount: 2) 82 | staff.appendMeasure(measure1) 83 | staff.appendMeasure(measure2) 84 | staff.appendMeasure(measure3) 85 | staff.appendMeasure(measure4) 86 | staff.appendMeasure(measure5) 87 | staff.appendRepeat(repeat1) // index = 5 88 | staff.appendRepeat(repeat2) // index = 7 89 | staff.appendMeasure(measure6) // index = 13 90 | staff.appendMeasure(measure3) 91 | staff.appendMeasure(measure7) 92 | staff.appendMeasure(measure8) 93 | 94 | let instrument1 = Instrument(name: "instrument 1") 95 | let instrument2 = Instrument(name: "instrument 2") 96 | let instrument3 = Instrument(name: "instrument 3") 97 | let instrument4 = Instrument(name: "instrument 4") 98 | 99 | let part1 = Part(instrument: instrument1, staves: [staff]) 100 | let part2 = Part(instrument: instrument2) 101 | let part3 = Part(instrument: instrument3) 102 | let part4 = Part(instrument: instrument4) 103 | 104 | score = Score(parts: [part1, part2, part3, part4]) 105 | } 106 | 107 | deinit { 108 | score = nil 109 | staff = nil 110 | measure1 = nil 111 | measure2 = nil 112 | measure3 = nil 113 | measure4 = nil 114 | measure5 = nil 115 | measure6 = nil 116 | measure7 = nil 117 | measure8 = nil 118 | repeat1 = nil 119 | repeat2 = nil 120 | } 121 | 122 | // swiftlint:disable line_length 123 | @Test func debugDescription() async throws { 124 | #expect(staff!.debugDescription == "staff(treble |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]|, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1, 1/16c1, 1/16c1]|, [ |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]| ] × 2, [ |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]| ] × 3, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [1/16a1, 3[1/16c1, 1/16c1, 1/16c1], 3[1/16c1, 1/16c1, 1/16c1], 1/16c1]|, |4/4: [3[1/16a1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|)") 125 | } 126 | 127 | @Test func titleSetPostInit() async throws { 128 | score.title = "Xanadu" 129 | #expect(score.title == "Xanadu") 130 | } 131 | 132 | @Test func titleSetDuringInit() async throws { 133 | let titleScore = Score(title: "Xanadu") 134 | #expect(titleScore.title == "Xanadu") 135 | } 136 | 137 | @Test func subtitleSetPostInit() async throws { 138 | score.subtitle = "subtitle" 139 | #expect(score.subtitle == "subtitle") 140 | } 141 | 142 | @Test func subtitleSetDuringInit() async throws { 143 | let subtitleScore = Score(subtitle: "subtitle") 144 | #expect(subtitleScore.subtitle == "subtitle") 145 | } 146 | 147 | @Test func titlesSetPostInit() async throws { 148 | score.title = "Xanadu" 149 | score.subtitle = "subtitle" 150 | #expect(score.title == "Xanadu") 151 | #expect(score.subtitle == "subtitle") 152 | } 153 | 154 | @Test func titlesSetDuringInit() async throws { 155 | let subtitleScore = Score(title: "Xanadu", subtitle: "subtitle") 156 | #expect(subtitleScore.title == "Xanadu") 157 | #expect(subtitleScore.subtitle == "subtitle") 158 | } 159 | 160 | @Test func artistSetPostInit() async throws { 161 | score.artist = "Rush" 162 | #expect(score.artist == "Rush") 163 | } 164 | 165 | @Test func artistSetDuringInit() async throws { 166 | let artistScore = Score(artist: "Rush") 167 | #expect(artistScore.artist == "Rush") 168 | } 169 | 170 | @Test func albumSetPostInit() async throws { 171 | score.album = "Farewell To Kings" 172 | #expect(score.album == "Farewell To Kings") 173 | } 174 | 175 | @Test func albumSetDuringInit() async throws { 176 | let albumScore = Score(album: "Farewell To Kings") 177 | #expect(albumScore.album == "Farewell To Kings") 178 | } 179 | 180 | @Test func wordsSetPostInit() async throws { 181 | score.words = "Neil Peart" 182 | #expect(score.words == "Neil Peart") 183 | } 184 | 185 | @Test func wordsSetDuringInit() async throws { 186 | let wordsScore = Score(words: "Neil Peart") 187 | #expect(wordsScore.words == "Neil Peart") 188 | } 189 | 190 | @Test func musicSetPostInit() async throws { 191 | score.music = "Alex Lifeson, Geddy Lee" 192 | #expect(score.music == "Alex Lifeson, Geddy Lee") 193 | } 194 | 195 | @Test func musicSetDuringInit() async throws { 196 | let musicScore = Score(music: "Alex Lifeson, Geddy Lee") 197 | #expect(musicScore.music == "Alex Lifeson, Geddy Lee") 198 | } 199 | 200 | @Test func wordsAndMusicSetPostInit() async throws { 201 | score.wordsAndMusic = "wordsAndMusic" 202 | #expect(score.wordsAndMusic == "wordsAndMusic") 203 | } 204 | 205 | @Test func wordsAndMusicSetDuringInit() async throws { 206 | let wordsAndMusicScore = Score(wordsAndMusic: "wordsAndMusic") 207 | #expect(wordsAndMusicScore.wordsAndMusic == "wordsAndMusic") 208 | } 209 | 210 | @Test func transcriberSetPostInit() async throws { 211 | score.transcriber = "transcriber" 212 | #expect(score.transcriber == "transcriber") 213 | } 214 | 215 | @Test func transcriberSetDuringInit() async throws { 216 | let transcriberScore = Score(transcriber: "transcriber") 217 | #expect(transcriberScore.transcriber == "transcriber") 218 | } 219 | 220 | @Test func instructionsSetPostInit() async throws { 221 | score.instructions = "instructions" 222 | #expect(score.instructions == "instructions") 223 | } 224 | 225 | @Test func instructionsSetDuringInit() async throws { 226 | let instructionsScore = Score(instructions: "instructions") 227 | #expect(instructionsScore.instructions == "instructions") 228 | } 229 | 230 | @Test func noticesSetPostInit() async throws { 231 | score.notices = "notices" 232 | #expect(score.notices == "notices") 233 | } 234 | 235 | @Test func noticesSetDuringInit() async throws { 236 | let noticesScore = Score(notices: "notices") 237 | #expect(noticesScore.notices == "notices") 238 | } 239 | 240 | @Test func allTitlesSetDuringInit() async throws { 241 | let scoreAllTitles = Score( 242 | title: "title", 243 | subtitle: "subtitle", 244 | artist: "artist", 245 | album: "album", 246 | words: "words", 247 | music: "music", 248 | wordsAndMusic: "wordsAndMusic", 249 | transcriber: "transcriber", 250 | instructions: "instructions", 251 | notices: "notices" 252 | ) 253 | #expect(scoreAllTitles.title == "title") 254 | #expect(scoreAllTitles.subtitle == "subtitle") 255 | #expect(scoreAllTitles.artist == "artist") 256 | #expect(scoreAllTitles.album == "album") 257 | #expect(scoreAllTitles.words == "words") 258 | #expect(scoreAllTitles.music == "music") 259 | #expect(scoreAllTitles.wordsAndMusic == "wordsAndMusic") 260 | #expect(scoreAllTitles.transcriber == "transcriber") 261 | #expect(scoreAllTitles.instructions == "instructions") 262 | #expect(scoreAllTitles.notices == "notices") 263 | } 264 | 265 | // MARK: -- RandomAccess Conformance 266 | 267 | @Test func randomAccessStartIndex() async throws { 268 | #expect(score.startIndex == score.parts.startIndex) 269 | } 270 | 271 | @Test func randomAccessEndIndex() async throws { 272 | #expect(score.endIndex == score.parts.endIndex) 273 | } 274 | 275 | @Test func randomAccessIndexAfter() async throws { 276 | #expect(score.index(after: 2) == score.parts.index(after: 2)) 277 | } 278 | 279 | @Test func randomAccessIteration() async throws { 280 | var count = 0 281 | var names: Array = [] 282 | for part in score { 283 | count += 1 284 | let name = if let name = part.instrument?.name { 285 | name 286 | } else { 287 | "" 288 | } 289 | names.append(name) 290 | } 291 | 292 | #expect(count == 4) 293 | #expect(names == ["instrument 1", "instrument 2", "instrument 3", "instrument 4"]) 294 | } 295 | 296 | // MARK: -- BidirectionalCollection Conformance 297 | 298 | @Test func BidirectionalIndexBefore() async throws { 299 | #expect(score.index(before: 2) == score.parts.index(before: 2)) 300 | } 301 | 302 | // MARK: -- BidirectionalCollection Conformance 303 | 304 | @Test func rangeReplaceableEmptyInit() async throws { 305 | let emptyScore = Score() 306 | #expect(emptyScore.parts.count == 0) 307 | #expect(emptyScore.title == "") 308 | #expect(emptyScore.subtitle == "") 309 | #expect(emptyScore.firstPageHeader == nil) 310 | #expect(emptyScore.masterPart == nil) 311 | } 312 | 313 | @Test func rangeReplaceableAppend() async throws { 314 | let part5 = Part(instrument: Instrument(name: "instrument 5")) 315 | score.append(part5) 316 | 317 | #expect(score[4].instrument?.name == "instrument 5") 318 | } 319 | 320 | @Test func rangeReplaceableInsert() async throws { 321 | let part0 = Part(instrument: Instrument(name: "instrument 0")) 322 | score.insert(contentsOf: [part0], at: 0) 323 | #expect(score[0].instrument?.name == "instrument 0") 324 | } 325 | 326 | @Test func rangeReplaceableRemoveLast() async throws { 327 | score.removeLast() 328 | #expect(score.count == 3) 329 | } 330 | 331 | // MARK: -- MutableCollection Conformance 332 | 333 | @Test func mutableCollectionReplace() async throws { 334 | #expect(score[1].instrument?.name == "instrument 2") 335 | let newpart1 = Part(instrument: Instrument(name: "replaced instrument 2")) 336 | score[1] = newpart1 337 | #expect(score[1].instrument?.name == "replaced instrument 2") 338 | } 339 | 340 | // MARK: -- CustomDebugStringConvertible Conformance 341 | 342 | @Test func debugString() async throws { 343 | let string = "Score version: 0.3.0, parts(Part(instrument `instrument 1`), staves(staff(treble |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]|, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1, 1/16c1, 1/16c1]|, [ |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]| ] × 2, [ |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1]| ] × 3, |4/4: [3[1/16c1, 1/16c1, 1/16c1], 3[1/16c1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|, |4/4: [1/16c1, 1/16c1, 1/16c1, 1/16c1, 3[1/16c1, 1/16c1, 1/16c1]]|, |4/4: [1/16a1, 3[1/16c1, 1/16c1, 1/16c1], 3[1/16c1, 1/16c1, 1/16c1], 1/16c1]|, |4/4: [3[1/16a1, 1/16c1, 1/16c1], 1/16c1, 1/16c1]|)), Part(instrument `instrument 2`), staves(), Part(instrument `instrument 3`), staves(), Part(instrument `instrument 4`), staves())" 344 | #expect(score!.debugDescription == string) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /Tests/MusicNotationTests/TempoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TempoTests.swift 3 | // MusicNotation 4 | // 5 | // Created by Steven Woolgar on 07/29/2024. 6 | // Copyright © 2024 Steven Woolgar. All rights reserved. 7 | // 8 | 9 | @testable import MusicNotation 10 | import Testing 11 | 12 | @Suite final class TempoTests { 13 | @Test func tempoDebug() async throws { 14 | let tempo = Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter) 15 | #expect(tempo.debugDescription == "type: linear, position: 10.3, value: 120, unit: quarter") 16 | } 17 | 18 | @Test func tempoDebugWithLabel() async throws { 19 | let tempo = Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter, text: "Intro") 20 | #expect(tempo.debugDescription == "type: linear, position: 10.3, value: 120, unit: quarter, text: Intro") 21 | } 22 | 23 | @Test func tempoEqualityCheck() async throws { 24 | let tempo = Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter, text: "Intro") 25 | #expect(tempo == Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter, text: "Intro")) 26 | } 27 | 28 | @Test func tempoInequalityCheck() async throws { 29 | let tempo = Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter, text: "Intro") 30 | #expect(tempo != Tempo(type: .linear, position: 12.3, value: 120, unit: .quarter, text: "Intro")) 31 | #expect(tempo != Tempo(type: .undefined, position: 10.3, value: 120, unit: .quarter, text: "Intro")) 32 | #expect(tempo != Tempo(type: .pause, position: 10.3, value: 120, unit: .quarter, text: "Intro")) 33 | #expect(tempo != Tempo(type: .ramp, position: 10.3, value: 120, unit: .quarter, text: "Intro")) 34 | #expect(tempo != Tempo(type: .linear, position: 10.3, value: 110, unit: .quarter, text: "Intro")) 35 | #expect(tempo != Tempo(type: .linear, position: 10.3, value: 120, unit: .dottedHalf, text: "Intro")) 36 | #expect(tempo != Tempo(type: .linear, position: 10.3, value: 120, unit: .quarter)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/UML Class Diagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/UML Class Diagram.pdf -------------------------------------------------------------------------------- /docs/UML Class Diagram.uxf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 5 | com.baselet.element.old.element.Class 6 | 7 | 360 8 | 490 9 | 100 10 | 30 11 | 12 | Note 13 | 14 | 15 | 16 | com.baselet.element.old.element.Class 17 | 18 | 360 19 | 320 20 | 100 21 | 30 22 | 23 | Measure 24 | 25 | 26 | 27 | com.baselet.element.old.element.Class 28 | 29 | 370 30 | 120 31 | 100 32 | 30 33 | 34 | Staff 35 | 36 | 37 | 38 | com.baselet.element.old.element.Class 39 | 40 | 530 41 | 490 42 | 100 43 | 30 44 | 45 | Pitch 46 | 47 | 48 | 49 | com.baselet.element.old.element.Class 50 | 51 | 140 52 | 370 53 | 110 54 | 30 55 | 56 | TimeSignature 57 | 58 | 59 | 60 | UMLClass 61 | 62 | 590 63 | 560 64 | 130 65 | 50 66 | 67 | <<enum>> 68 | NoteLetter 69 | 70 | 71 | 72 | UMLClass 73 | 74 | 200 75 | 590 76 | 130 77 | 30 78 | 79 | NoteDuration 80 | 81 | 82 | 83 | UMLClass 84 | 85 | 400 86 | 580 87 | 130 88 | 50 89 | 90 | <<enum>> 91 | Striking 92 | 93 | 94 | 95 | UMLClass 96 | 97 | 500 98 | 320 99 | 130 100 | 50 101 | 102 | <<enum>> 103 | Accidental 104 | 105 | 106 | 107 | UMLClass 108 | 109 | 650 110 | 320 111 | 130 112 | 50 113 | 114 | <<enum>> 115 | Octave 116 | 117 | 118 | 119 | UMLClass 120 | 121 | 510 122 | 50 123 | 100 124 | 50 125 | 126 | <<enum>> 127 | Instrument 128 | 129 | 130 | 131 | UMLClass 132 | 133 | 200 134 | 50 135 | 100 136 | 50 137 | 138 | <<enum>> 139 | Clef 140 | 141 | 142 | 143 | com.baselet.element.old.element.Relation 144 | 145 | 270 146 | 40 147 | 130 148 | 100 149 | 150 | lt=<<<<-> 151 | m2= 1 152 | 110;80;110;30;30;30 153 | 154 | 155 | com.baselet.element.old.element.Relation 156 | 157 | 220 158 | 310 159 | 160 160 | 100 161 | 162 | lt=<<<<-> 163 | m2= 1 164 | 140;30;80;30;80;80;30;80 165 | 166 | 167 | com.baselet.element.old.element.Relation 168 | 169 | 430 170 | 480 171 | 120 172 | 50 173 | 174 | lt=<<<<-> 175 | m2=1 176 | 30;30;100;30 177 | 178 | 179 | com.baselet.element.old.element.Relation 180 | 181 | 430 182 | 40 183 | 100 184 | 100 185 | 186 | lt=<<<<-> 187 | m2=1 188 | 30;80;30;80;30;30;80;30 189 | 190 | 191 | com.baselet.element.old.element.Relation 192 | 193 | 580 194 | 340 195 | 110 196 | 170 197 | 198 | lt=<<<<-> 199 | m2= 1 200 | 30;150;30;50;90;50;90;30 201 | 202 | 203 | com.baselet.element.old.element.Relation 204 | 205 | 600 206 | 470 207 | 80 208 | 110 209 | 210 | lt=<<<<-> 211 | m2= 1 212 | 30;30;60;30;60;90 213 | 214 | 215 | com.baselet.element.old.element.Relation 216 | 217 | 530 218 | 340 219 | 50 220 | 170 221 | 222 | lt=<<<<-> 223 | m2= 1 224 | 30;150;30;30 225 | 226 | 227 | com.baselet.element.old.element.Relation 228 | 229 | 300 230 | 490 231 | 110 232 | 130 233 | 234 | lt=<<<<-> 235 | m2= 1 236 | 90;30;90;110;30;110 237 | 238 | 239 | com.baselet.element.old.element.Relation 240 | 241 | 410 242 | 490 243 | 50 244 | 110 245 | 246 | lt=<<<<-> 247 | m2= 1 248 | 30;30;30;90 249 | 250 | 251 | com.baselet.element.old.element.Class 252 | 253 | 80 254 | 490 255 | 110 256 | 30 257 | 258 | Tuplet 259 | 260 | 261 | 262 | com.baselet.element.old.element.Relation 263 | 264 | 160 265 | 480 266 | 220 267 | 50 268 | 269 | lt=<<<<-> 270 | m2=2..7 271 | 30;30;200;30 272 | 273 | 274 | com.baselet.element.old.element.Class 275 | 276 | 210 277 | 260 278 | 120 279 | 30 280 | 281 | MeasureRepeat 282 | 283 | 284 | 285 | com.baselet.element.old.element.Relation 286 | 287 | 400 288 | 120 289 | 50 290 | 100 291 | 292 | lt=<<<<-> 293 | m2= * 294 | 30;30;30;80 295 | 296 | 297 | com.baselet.element.old.element.Relation 298 | 299 | 300 300 | 240 301 | 100 302 | 100 303 | 304 | lt=<<<<-> 305 | m2= * 306 | 30;30;80;30;80;80 307 | 308 | 309 | com.baselet.element.old.element.Class 310 | 311 | 210 312 | 540 313 | 60 314 | 30 315 | 316 | Tie 317 | 318 | 319 | 320 | UMLClass 321 | 322 | 380 323 | 200 324 | 100 325 | 40 326 | 327 | <<interface>> 328 | NotesHolder 329 | 330 | 331 | 332 | Relation 333 | 334 | 260 335 | 210 336 | 140 337 | 70 338 | 339 | lt=<<. 340 | 120.0;10.0;10.0;10.0;10.0;50.0 341 | 342 | 343 | Relation 344 | 345 | 410 346 | 230 347 | 30 348 | 110 349 | 350 | lt=<<. 351 | 10.0;10.0;10.0;90.0 352 | 353 | 354 | UMLClass 355 | 356 | 360 357 | 400 358 | 110 359 | 40 360 | 361 | <<interface>> 362 | NoteCollection 363 | 364 | 365 | 366 | com.baselet.element.old.element.Relation 367 | 368 | 380 369 | 320 370 | 50 371 | 100 372 | 373 | lt=<<<<-> 374 | m2= * 375 | 30;30;30;80 376 | 377 | 378 | Relation 379 | 380 | 140 381 | 410 382 | 240 383 | 100 384 | 385 | lt=<<. 386 | 220.0;10.0;10.0;10.0;10.0;80.0 387 | 388 | 389 | Relation 390 | 391 | 400 392 | 430 393 | 30 394 | 80 395 | 396 | lt=<<. 397 | 10.0;10.0;10.0;60.0 398 | 399 | 400 | UMLClass 401 | 402 | 560 403 | 190 404 | 100 405 | 30 406 | 407 | Key 408 | 409 | 410 | 411 | com.baselet.element.old.element.Relation 412 | 413 | 550 414 | 190 415 | 50 416 | 150 417 | 418 | lt=<<<<-> 419 | m2= 1 420 | 30;30;30;130 421 | 422 | 423 | com.baselet.element.old.element.Relation 424 | 425 | 430 426 | 170 427 | 150 428 | 190 429 | 430 | lt=<<<<-> 431 | m2=1 432 | 30;170;60;170;60;80;100;80;100;30;130;30 433 | 434 | 435 | com.baselet.element.old.element.Relation 436 | 437 | 630 438 | 180 439 | 180 440 | 430 441 | 442 | lt=<<<<-> 443 | m2= 1 444 | 30;30;160;30;160;410;90;410 445 | 446 | 447 | UMLClass 448 | 449 | 680 450 | 130 451 | 100 452 | 40 453 | 454 | <<enum>> 455 | KeyType 456 | 457 | 458 | 459 | com.baselet.element.old.element.Relation 460 | 461 | 590 462 | 110 463 | 110 464 | 100 465 | 466 | lt=<<<<-> 467 | m2=1 468 | 30;80;30;30;90;30 469 | 470 | 471 | com.baselet.element.old.element.Relation 472 | 473 | 240 474 | 490 475 | 150 476 | 80 477 | 478 | lt=<<<<-> 479 | m2=1 480 | 130;30;130;60;30;60 481 | 482 | 483 | UMLClass 484 | 485 | 80 486 | 180 487 | 150 488 | 50 489 | 490 | <<interface>> 491 | ImmutableMeasure 492 | 493 | 494 | 495 | UMLClass 496 | 497 | 60 498 | 270 499 | 130 500 | 30 501 | 502 | RepeatedMeasure 503 | 504 | 505 | 506 | Relation 507 | 508 | 120 509 | 220 510 | 30 511 | 70 512 | 513 | lt=<<. 514 | 10.0;10.0;10.0;50.0 515 | 516 | 517 | Relation 518 | 519 | 190 520 | 220 521 | 190 522 | 130 523 | 524 | lt=<<. 525 | 10.0;10.0;10.0;110.0;170.0;110.0 526 | 527 | 528 | UMLNote 529 | 530 | 60 531 | 300 532 | 130 533 | 50 534 | 535 | MeasureRepeat 536 | can generate this 537 | bg=orange 538 | 539 | 540 | 541 | UMLClass 542 | 543 | 330 544 | 700 545 | 210 546 | 100 547 | 548 | <<enum>> 549 | MeasureDurationValidator 550 | 551 | 552 | 553 | UMLClass 554 | 555 | 370 556 | 750 557 | 130 558 | 40 559 | 560 | <<enum>> 561 | CompletionState 562 | 563 | 564 | 565 | -------------------------------------------------------------------------------- /docs/images/UMLClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/UMLClassDiagram.png -------------------------------------------------------------------------------- /docs/images/all-clefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/all-clefs.png -------------------------------------------------------------------------------- /docs/images/alto-clef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/alto-clef.png -------------------------------------------------------------------------------- /docs/images/bass-clef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/bass-clef.png -------------------------------------------------------------------------------- /docs/images/chord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/chord.png -------------------------------------------------------------------------------- /docs/images/g-clef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/g-clef.png -------------------------------------------------------------------------------- /docs/images/grand-staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/grand-staff.png -------------------------------------------------------------------------------- /docs/images/lyrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/lyrics.png -------------------------------------------------------------------------------- /docs/images/measure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/measure.png -------------------------------------------------------------------------------- /docs/images/multi-staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/multi-staff.png -------------------------------------------------------------------------------- /docs/images/multi-voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/multi-voice.png -------------------------------------------------------------------------------- /docs/images/pitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/pitch.png -------------------------------------------------------------------------------- /docs/images/rests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/rests.png -------------------------------------------------------------------------------- /docs/images/single-staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/single-staff.png -------------------------------------------------------------------------------- /docs/images/single-tab-stash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/single-tab-stash.png -------------------------------------------------------------------------------- /docs/images/staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/staff.png -------------------------------------------------------------------------------- /docs/images/symphony-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/music-notation-swift/music-notation/6b907c4974bf35e64d7f402919ae932a7387e2d7/docs/images/symphony-9.png --------------------------------------------------------------------------------