├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .spi.yml ├── Demo ├── KeyboardDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── KeyboardDemo.xcscheme ├── Shared │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-1025.png │ │ │ ├── Icon-120.png │ │ │ ├── Icon-121.png │ │ │ ├── Icon-152.png │ │ │ ├── Icon-167.png │ │ │ ├── Icon-180.png │ │ │ ├── Icon-20.png │ │ │ ├── Icon-29.png │ │ │ ├── Icon-40.png │ │ │ ├── Icon-41.png │ │ │ ├── Icon-42.png │ │ │ ├── Icon-58.png │ │ │ ├── Icon-59.png │ │ │ ├── Icon-60.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon-80.png │ │ │ ├── Icon-81.png │ │ │ ├── Icon-87.png │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32-1.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ └── Contents.json │ ├── ContentView.swift │ └── KeyboardDemoApp.swift └── macOS │ └── macOS.entitlements ├── Keyboard.playground ├── Contents.swift └── contents.xcplayground ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Keyboard │ ├── KeyContainer.swift │ ├── Keyboard.docc │ ├── Keyboard.md │ └── Resources │ │ └── demo.png │ ├── Keyboard.swift │ ├── KeyboardKey.swift │ ├── KeyboardModel.swift │ ├── Layouts │ ├── Guitar.swift │ ├── Isomorphic.swift │ ├── KeyboardLayout.swift │ ├── Piano.swift │ ├── PianoSpacer.swift │ ├── VerticalIsomorphic.swift │ └── VerticalPiano.swift │ ├── MIDIMonitorKeyboard.swift │ ├── MultitouchView.swift │ └── PreferenceKeys.swift └── Tests └── KeyboardTests └── KeyboardTests.swift /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | swift_test: 12 | name: Test 13 | runs-on: macos-latest 14 | steps: 15 | - name: Check out Keyboard 16 | uses: actions/checkout@v4 17 | - name: Test Keyboard 18 | run: swift test -c release 19 | 20 | # Build the demo projects. 21 | build_demo: 22 | name: Build Demo 23 | runs-on: macos-latest 24 | needs: [swift_test] 25 | steps: 26 | - name: Check out Keyboard 27 | uses: actions/checkout@v4 28 | - name: Build Demo 29 | run: xcodebuild build -project Demo/KeyboardDemo.xcodeproj -scheme KeyboardDemo -destination "name=My Mac" 30 | 31 | # Send notification to Discord on failure. 32 | send_notification: 33 | name: Send Notification 34 | uses: AudioKit/ci/.github/workflows/send_notification.yml@main 35 | needs: [swift_test, build_demo] 36 | if: ${{ failure() && github.ref == 'refs/heads/main' }} 37 | secrets: inherit 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | 94 | Package.resolved 95 | .swiftpm -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Keyboard] -------------------------------------------------------------------------------- /Demo/KeyboardDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F1B7598E2848C56C0042846A /* KeyboardDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B7597E2848C56B0042846A /* KeyboardDemoApp.swift */; }; 11 | F1B759902848C56C0042846A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B7597F2848C56B0042846A /* ContentView.swift */; }; 12 | F1B759922848C56C0042846A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1B759802848C56C0042846A /* Assets.xcassets */; }; 13 | F1B759A22848C5FB0042846A /* Keyboard in Frameworks */ = {isa = PBXBuildFile; productRef = F1B759A12848C5FB0042846A /* Keyboard */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | F1B7597E2848C56B0042846A /* KeyboardDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDemoApp.swift; sourceTree = ""; }; 18 | F1B7597F2848C56B0042846A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 19 | F1B759802848C56C0042846A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 20 | F1B759852848C56C0042846A /* KeyboardDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeyboardDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | F1B7598D2848C56C0042846A /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 22 | F1B7599D2848C5B30042846A /* Keyboard */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Keyboard; path = ..; sourceTree = ""; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | F1B759822848C56C0042846A /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | F1B759A22848C5FB0042846A /* Keyboard in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | F1B759782848C56B0042846A = { 38 | isa = PBXGroup; 39 | children = ( 40 | F1B7599C2848C5B30042846A /* Packages */, 41 | F1B7597D2848C56B0042846A /* Shared */, 42 | F1B7598C2848C56C0042846A /* macOS */, 43 | F1B759862848C56C0042846A /* Products */, 44 | F1B7599E2848C5F20042846A /* Frameworks */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | F1B7597D2848C56B0042846A /* Shared */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | F1B7597E2848C56B0042846A /* KeyboardDemoApp.swift */, 52 | F1B7597F2848C56B0042846A /* ContentView.swift */, 53 | F1B759802848C56C0042846A /* Assets.xcassets */, 54 | ); 55 | path = Shared; 56 | sourceTree = ""; 57 | }; 58 | F1B759862848C56C0042846A /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | F1B759852848C56C0042846A /* KeyboardDemo.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | F1B7598C2848C56C0042846A /* macOS */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | F1B7598D2848C56C0042846A /* macOS.entitlements */, 70 | ); 71 | path = macOS; 72 | sourceTree = ""; 73 | }; 74 | F1B7599C2848C5B30042846A /* Packages */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | F1B7599D2848C5B30042846A /* Keyboard */, 78 | ); 79 | name = Packages; 80 | sourceTree = ""; 81 | }; 82 | F1B7599E2848C5F20042846A /* Frameworks */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | ); 86 | name = Frameworks; 87 | sourceTree = ""; 88 | }; 89 | /* End PBXGroup section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | F1B759842848C56C0042846A /* KeyboardDemo */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = F1B759962848C56C0042846A /* Build configuration list for PBXNativeTarget "KeyboardDemo" */; 95 | buildPhases = ( 96 | F1B759812848C56C0042846A /* Sources */, 97 | F1B759822848C56C0042846A /* Frameworks */, 98 | F1B759832848C56C0042846A /* Resources */, 99 | ); 100 | buildRules = ( 101 | ); 102 | dependencies = ( 103 | ); 104 | name = KeyboardDemo; 105 | packageProductDependencies = ( 106 | F1B759A12848C5FB0042846A /* Keyboard */, 107 | ); 108 | productName = "KeyboardDemo (iOS)"; 109 | productReference = F1B759852848C56C0042846A /* KeyboardDemo.app */; 110 | productType = "com.apple.product-type.application"; 111 | }; 112 | /* End PBXNativeTarget section */ 113 | 114 | /* Begin PBXProject section */ 115 | F1B759792848C56B0042846A /* Project object */ = { 116 | isa = PBXProject; 117 | attributes = { 118 | BuildIndependentTargetsInParallel = 1; 119 | LastSwiftUpdateCheck = 1340; 120 | LastUpgradeCheck = 1340; 121 | TargetAttributes = { 122 | F1B759842848C56C0042846A = { 123 | CreatedOnToolsVersion = 13.4; 124 | }; 125 | }; 126 | }; 127 | buildConfigurationList = F1B7597C2848C56B0042846A /* Build configuration list for PBXProject "KeyboardDemo" */; 128 | compatibilityVersion = "Xcode 13.0"; 129 | developmentRegion = en; 130 | hasScannedForEncodings = 0; 131 | knownRegions = ( 132 | en, 133 | Base, 134 | ); 135 | mainGroup = F1B759782848C56B0042846A; 136 | productRefGroup = F1B759862848C56C0042846A /* Products */; 137 | projectDirPath = ""; 138 | projectRoot = ""; 139 | targets = ( 140 | F1B759842848C56C0042846A /* KeyboardDemo */, 141 | ); 142 | }; 143 | /* End PBXProject section */ 144 | 145 | /* Begin PBXResourcesBuildPhase section */ 146 | F1B759832848C56C0042846A /* Resources */ = { 147 | isa = PBXResourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | F1B759922848C56C0042846A /* Assets.xcassets in Resources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXResourcesBuildPhase section */ 155 | 156 | /* Begin PBXSourcesBuildPhase section */ 157 | F1B759812848C56C0042846A /* Sources */ = { 158 | isa = PBXSourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | F1B759902848C56C0042846A /* ContentView.swift in Sources */, 162 | F1B7598E2848C56C0042846A /* KeyboardDemoApp.swift in Sources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXSourcesBuildPhase section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | F1B759942848C56C0042846A /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | CLANG_ANALYZER_NONNULL = YES; 174 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 175 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 176 | CLANG_ENABLE_MODULES = YES; 177 | CLANG_ENABLE_OBJC_ARC = YES; 178 | CLANG_ENABLE_OBJC_WEAK = YES; 179 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 180 | CLANG_WARN_BOOL_CONVERSION = YES; 181 | CLANG_WARN_COMMA = YES; 182 | CLANG_WARN_CONSTANT_CONVERSION = YES; 183 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 192 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 194 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 196 | CLANG_WARN_STRICT_PROTOTYPES = YES; 197 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 198 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = NO; 202 | DEBUG_INFORMATION_FORMAT = dwarf; 203 | ENABLE_STRICT_OBJC_MSGSEND = YES; 204 | ENABLE_TESTABILITY = YES; 205 | GCC_C_LANGUAGE_STANDARD = gnu11; 206 | GCC_DYNAMIC_NO_PIC = NO; 207 | GCC_NO_COMMON_BLOCKS = YES; 208 | GCC_OPTIMIZATION_LEVEL = 0; 209 | GCC_PREPROCESSOR_DEFINITIONS = ( 210 | "DEBUG=1", 211 | "$(inherited)", 212 | ); 213 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 214 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 215 | GCC_WARN_UNDECLARED_SELECTOR = YES; 216 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 217 | GCC_WARN_UNUSED_FUNCTION = YES; 218 | GCC_WARN_UNUSED_VARIABLE = YES; 219 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 220 | MTL_FAST_MATH = YES; 221 | ONLY_ACTIVE_ARCH = YES; 222 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 223 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 224 | }; 225 | name = Debug; 226 | }; 227 | F1B759952848C56C0042846A /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 234 | CLANG_ENABLE_MODULES = YES; 235 | CLANG_ENABLE_OBJC_ARC = YES; 236 | CLANG_ENABLE_OBJC_WEAK = YES; 237 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 238 | CLANG_WARN_BOOL_CONVERSION = YES; 239 | CLANG_WARN_COMMA = YES; 240 | CLANG_WARN_CONSTANT_CONVERSION = YES; 241 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 242 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 243 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 244 | CLANG_WARN_EMPTY_BODY = YES; 245 | CLANG_WARN_ENUM_CONVERSION = YES; 246 | CLANG_WARN_INFINITE_RECURSION = YES; 247 | CLANG_WARN_INT_CONVERSION = YES; 248 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 250 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 251 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 252 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 253 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 254 | CLANG_WARN_STRICT_PROTOTYPES = YES; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 257 | CLANG_WARN_UNREACHABLE_CODE = YES; 258 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 259 | COPY_PHASE_STRIP = NO; 260 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 261 | ENABLE_NS_ASSERTIONS = NO; 262 | ENABLE_STRICT_OBJC_MSGSEND = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu11; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | MTL_ENABLE_DEBUG_INFO = NO; 272 | MTL_FAST_MATH = YES; 273 | SWIFT_COMPILATION_MODE = wholemodule; 274 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 275 | }; 276 | name = Release; 277 | }; 278 | F1B759972848C56C0042846A /* Debug */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 282 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 283 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 284 | CODE_SIGN_STYLE = Automatic; 285 | CURRENT_PROJECT_VERSION = 1; 286 | DEVELOPMENT_TEAM = 9W69ZP8S5F; 287 | ENABLE_PREVIEWS = YES; 288 | GENERATE_INFOPLIST_FILE = YES; 289 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 290 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 291 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 292 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 293 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 294 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 295 | LD_RUNPATH_SEARCH_PATHS = ( 296 | "$(inherited)", 297 | "@executable_path/Frameworks", 298 | ); 299 | MACOSX_DEPLOYMENT_TARGET = 12.4; 300 | MARKETING_VERSION = 1.0; 301 | PRODUCT_BUNDLE_IDENTIFIER = io.audiokit.KeyboardDemo; 302 | PRODUCT_NAME = KeyboardDemo; 303 | SDKROOT = iphoneos; 304 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 305 | SUPPORTS_MACCATALYST = NO; 306 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 307 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 308 | SWIFT_EMIT_LOC_STRINGS = YES; 309 | SWIFT_VERSION = 5.0; 310 | TARGETED_DEVICE_FAMILY = "1,2,7"; 311 | }; 312 | name = Debug; 313 | }; 314 | F1B759982848C56C0042846A /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 319 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_TEAM = 9W69ZP8S5F; 323 | ENABLE_PREVIEWS = YES; 324 | GENERATE_INFOPLIST_FILE = YES; 325 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 326 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 327 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 328 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 329 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 330 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 331 | LD_RUNPATH_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "@executable_path/Frameworks", 334 | ); 335 | MACOSX_DEPLOYMENT_TARGET = 12.4; 336 | MARKETING_VERSION = 1.0; 337 | PRODUCT_BUNDLE_IDENTIFIER = io.audiokit.KeyboardDemo; 338 | PRODUCT_NAME = KeyboardDemo; 339 | SDKROOT = iphoneos; 340 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 341 | SUPPORTS_MACCATALYST = NO; 342 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 343 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 344 | SWIFT_EMIT_LOC_STRINGS = YES; 345 | SWIFT_VERSION = 5.0; 346 | TARGETED_DEVICE_FAMILY = "1,2,7"; 347 | VALIDATE_PRODUCT = YES; 348 | }; 349 | name = Release; 350 | }; 351 | /* End XCBuildConfiguration section */ 352 | 353 | /* Begin XCConfigurationList section */ 354 | F1B7597C2848C56B0042846A /* Build configuration list for PBXProject "KeyboardDemo" */ = { 355 | isa = XCConfigurationList; 356 | buildConfigurations = ( 357 | F1B759942848C56C0042846A /* Debug */, 358 | F1B759952848C56C0042846A /* Release */, 359 | ); 360 | defaultConfigurationIsVisible = 0; 361 | defaultConfigurationName = Release; 362 | }; 363 | F1B759962848C56C0042846A /* Build configuration list for PBXNativeTarget "KeyboardDemo" */ = { 364 | isa = XCConfigurationList; 365 | buildConfigurations = ( 366 | F1B759972848C56C0042846A /* Debug */, 367 | F1B759982848C56C0042846A /* Release */, 368 | ); 369 | defaultConfigurationIsVisible = 0; 370 | defaultConfigurationName = Release; 371 | }; 372 | /* End XCConfigurationList section */ 373 | 374 | /* Begin XCSwiftPackageProductDependency section */ 375 | F1B759A12848C5FB0042846A /* Keyboard */ = { 376 | isa = XCSwiftPackageProductDependency; 377 | productName = Keyboard; 378 | }; 379 | /* End XCSwiftPackageProductDependency section */ 380 | }; 381 | rootObject = F1B759792848C56B0042846A /* Project object */; 382 | } 383 | -------------------------------------------------------------------------------- /Demo/KeyboardDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/KeyboardDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/KeyboardDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "tonic", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/AudioKit/Tonic.git", 7 | "state" : { 8 | "revision" : "8f103c6765af93bc1d2c3e18eb35f02718b0613d", 9 | "version" : "2.0.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Demo/KeyboardDemo.xcodeproj/xcshareddata/xcschemes/KeyboardDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-42.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-59.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-81.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Icon-120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-121.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "Icon-180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "Icon-41.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "Icon-58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "Icon-80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "Icon-152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "Icon-1025.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "icon_16x16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "icon_32x32.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "icon_32x32-1.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "icon_32x32@2x.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "icon_128x128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "icon_128x128@2x.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "icon_256x256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "icon_256x256@2x.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "icon_512x512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "icon_512x512@2x.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-1025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-1025.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-120.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-121.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-152.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-180.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-41.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-42.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-58.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-59.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-80.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-81.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/Icon-87.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32-1.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Demo/Shared/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | import Keyboard 2 | import SwiftUI 3 | import Tonic 4 | 5 | let evenSpacingInitialSpacerRatio: [Letter: CGFloat] = [ 6 | .C: 0.0, 7 | .D: 2.0 / 12.0, 8 | .E: 4.0 / 12.0, 9 | .F: 0.0 / 12.0, 10 | .G: 1.0 / 12.0, 11 | .A: 3.0 / 12.0, 12 | .B: 5.0 / 12.0 13 | ] 14 | 15 | let evenSpacingSpacerRatio: [Letter: CGFloat] = [ 16 | .C: 7.0 / 12.0, 17 | .D: 7.0 / 12.0, 18 | .E: 7.0 / 12.0, 19 | .F: 7.0 / 12.0, 20 | .G: 7.0 / 12.0, 21 | .A: 7.0 / 12.0, 22 | .B: 7.0 / 12.0 23 | ] 24 | 25 | let evenSpacingRelativeBlackKeyWidth: CGFloat = 7.0 / 12.0 26 | 27 | struct ContentView: View { 28 | 29 | func noteOn(pitch: Pitch, point: CGPoint) { 30 | print("note on \(pitch)") 31 | } 32 | 33 | func noteOff(pitch: Pitch) { 34 | print("note off \(pitch)") 35 | } 36 | 37 | func noteOnWithVerticalVelocity(pitch: Pitch, point: CGPoint) { 38 | print("note on \(pitch), midiVelocity: \(Int(point.y * 127))") 39 | } 40 | 41 | func noteOnWithReversedVerticalVelocity(pitch: Pitch, point: CGPoint) { 42 | print("note on \(pitch), midiVelocity: \(Int((1.0 - point.y) * 127))") 43 | } 44 | 45 | var randomColors: [Color] = (0 ... 12).map { _ in 46 | Color(red: Double.random(in: 0 ... 1), 47 | green: Double.random(in: 0 ... 1), 48 | blue: Double.random(in: 0 ... 1), opacity: 1) 49 | } 50 | 51 | @State var lowNote = 24 52 | @State var highNote = 48 53 | 54 | @State var scaleIndex = Scale.allCases.firstIndex(of: .chromatic) ?? 0 { 55 | didSet { 56 | if scaleIndex >= Scale.allCases.count { scaleIndex = 0 } 57 | if scaleIndex < 0 { scaleIndex = Scale.allCases.count - 1 } 58 | scale = Scale.allCases[scaleIndex] 59 | } 60 | } 61 | 62 | @State var scale: Scale = .chromatic 63 | @State var root: NoteClass = .C 64 | @State var rootIndex = 0 65 | @Environment(\.colorScheme) var colorScheme 66 | 67 | var body: some View { 68 | HStack { 69 | Keyboard(layout: .verticalIsomorphic(pitchRange: Pitch(48) ... Pitch(77))).frame(width: 100) 70 | VStack { 71 | HStack { 72 | Stepper("Lowest Note: \(Pitch(intValue: lowNote).note(in: .C).description)", 73 | onIncrement: { 74 | if lowNote < 126, highNote > lowNote + 12 { 75 | lowNote += 1 76 | } 77 | }, 78 | onDecrement: { 79 | if lowNote > 0 { 80 | lowNote -= 1 81 | } 82 | }) 83 | Stepper("Highest Note: \(Pitch(intValue: highNote).note(in: .C).description)", 84 | onIncrement: { 85 | if highNote < 126 { 86 | highNote += 1 87 | } 88 | }, 89 | onDecrement: { 90 | if highNote > 1, highNote > lowNote + 12 { 91 | highNote -= 1 92 | } 93 | 94 | }) 95 | } 96 | Keyboard(layout: .piano(pitchRange: Pitch(intValue: lowNote) ... Pitch(intValue: highNote)), 97 | noteOn: noteOnWithVerticalVelocity(pitch:point:), noteOff: noteOff) 98 | .frame(minWidth: 100, minHeight: 100) 99 | 100 | HStack { 101 | Stepper("Root: \(root.description)", 102 | onIncrement: { 103 | let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass } 104 | var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0 105 | index += 1 106 | if index > 11 { index = 0} 107 | if index < 0 { index = 1} 108 | rootIndex = index 109 | root = allSharpNotes[index] 110 | }, 111 | onDecrement: { 112 | let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass } 113 | var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0 114 | index -= 1 115 | if index > 11 { index = 0} 116 | if index < 0 { index = 1} 117 | rootIndex = index 118 | root = allSharpNotes[index] 119 | }) 120 | 121 | Stepper("Scale: \(scale.description)", 122 | onIncrement: { scaleIndex += 1 }, 123 | onDecrement: { scaleIndex -= 1 }) 124 | } 125 | Keyboard(layout: .isomorphic(pitchRange: 126 | Pitch(intValue: 12 + rootIndex) ... Pitch(intValue: 84 + rootIndex), 127 | root: root, 128 | scale: scale), 129 | noteOn: noteOnWithReversedVerticalVelocity(pitch:point:), noteOff: noteOff) 130 | .frame(minWidth: 100, minHeight: 100) 131 | 132 | Keyboard(layout: .guitar(), 133 | noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in 134 | KeyboardKey(pitch: pitch, 135 | isActivated: isActivated, 136 | text: pitch.note(in: .F).description, 137 | pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)]), 138 | alignment: .center) 139 | } 140 | .frame(minWidth: 100, minHeight: 100) 141 | 142 | Keyboard(layout: .isomorphic(pitchRange: Pitch(48) ... Pitch(65))) { pitch, isActivated in 143 | KeyboardKey(pitch: pitch, 144 | isActivated: isActivated, 145 | text: pitch.note(in: .F).description, 146 | pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)])) 147 | } 148 | .frame(minWidth: 100, minHeight: 100) 149 | 150 | Keyboard(latching: true, noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in 151 | if isActivated { 152 | ZStack { 153 | Rectangle().foregroundColor(.black) 154 | VStack { 155 | Spacer() 156 | Text(pitch.note(in: .C).description).font(.largeTitle) 157 | }.padding() 158 | } 159 | 160 | } else { 161 | Rectangle().foregroundColor(randomColors[Int(pitch.intValue) % 12]) 162 | } 163 | } 164 | .frame(minWidth: 100, minHeight: 100) 165 | } 166 | Keyboard( 167 | layout: .verticalPiano(pitchRange: Pitch(48) ... Pitch(77), 168 | initialSpacerRatio: evenSpacingInitialSpacerRatio, 169 | spacerRatio: evenSpacingSpacerRatio, 170 | relativeBlackKeyWidth: evenSpacingRelativeBlackKeyWidth) 171 | ).frame(width: 100) 172 | } 173 | .background(colorScheme == .dark ? 174 | Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) 175 | } 176 | } 177 | 178 | struct ContentView_Previews: PreviewProvider { 179 | static var previews: some View { 180 | ContentView() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Demo/Shared/KeyboardDemoApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct KeyboardDemoApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Demo/macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Keyboard.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Keyboard 2 | import PlaygroundSupport 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct ContentView: View { 7 | var body: some View { 8 | Keyboard() 9 | Keyboard(layout: .isomorphic) 10 | Keyboard(pitchRange: Pitch(0) ... Pitch(60 + 37)) 11 | } 12 | } 13 | 14 | PlaygroundPage.current.setLiveView(ContentView()) 15 | -------------------------------------------------------------------------------- /Keyboard.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 AudioKit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Keyboard", 7 | platforms: [.macOS(.v12), .iOS(.v16), .visionOS(.v1)], 8 | products: [.library(name: "Keyboard", targets: ["Keyboard"])], 9 | dependencies: [.package(url: "https://github.com/AudioKit/Tonic.git", from: "2.0.0")], 10 | targets: [ 11 | .target(name: "Keyboard", dependencies: ["Tonic"]), 12 | .testTarget(name: "KeyboardTests", dependencies: ["Keyboard"]), 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keyboard 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FAudioKit%2FKeyboard%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/AudioKit/Keyboard) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FAudioKit%2FKeyboard%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/AudioKit/Keyboard) 5 | 6 | ![keyboard-demo](https://user-images.githubusercontent.com/13122/188524839-3864fe14-cc34-4bab-852d-6c8b565e0f05.png) 7 | 8 | Keyboard aims to be an easy-to-use musical keyboard with: 9 | 10 | - multi-touch interface 11 | - accurate note name labels on the piano keys given the musical key 12 | - stylized keys in any color 13 | - any number of notes, not just limited to octaves 14 | 15 | ## Goals 16 | 17 | - Good user interface 18 | - Good performance. We rely on SwiftUI's drag gestures 19 | 20 | ## Install 21 | 22 | Install using Swift Package Manager. 23 | -------------------------------------------------------------------------------- /Sources/Keyboard/KeyContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | /// This handles the interaction for key, so the user can provide their own 7 | /// visual representation. 8 | public struct KeyContainer: View { 9 | let content: (Pitch, Bool) -> Content 10 | 11 | var pitch: Pitch 12 | @ObservedObject var model: KeyboardModel 13 | 14 | var zIndex: Int 15 | 16 | /// Initialize the Container 17 | /// - Parameters: 18 | /// - model: KeyboardModel holding all the keys 19 | /// - pitch: Pitch of this key 20 | /// - zIndex: Layering in z-axis 21 | /// - content: View defining how to render a specific key 22 | init(model: KeyboardModel, 23 | pitch: Pitch, 24 | zIndex: Int = 0, 25 | @ViewBuilder content: @escaping (Pitch, Bool) -> Content) 26 | { 27 | self.model = model 28 | self.pitch = pitch 29 | self.zIndex = zIndex 30 | self.content = content 31 | } 32 | 33 | func rect(rect: CGRect) -> some View { 34 | content(pitch, model.touchedPitches.contains(pitch) || model.externallyActivatedPitches.contains(pitch)) 35 | .contentShape(Rectangle()) 36 | .gesture( 37 | TapGesture().onEnded { _ in 38 | if model.latching { 39 | if model.externallyActivatedPitches.contains(pitch) { 40 | model.externallyActivatedPitches.remove(pitch) 41 | } else { 42 | model.externallyActivatedPitches.add(pitch) 43 | } 44 | } 45 | } 46 | ) 47 | .preference(key: KeyRectsKey.self, 48 | value: [KeyRectInfo(rect: rect, 49 | pitch: pitch, 50 | zIndex: zIndex)]) 51 | } 52 | 53 | public var body: some View { 54 | GeometryReader { proxy in 55 | rect(rect: proxy.frame(in: .global)) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Keyboard/Keyboard.docc/Keyboard.md: -------------------------------------------------------------------------------- 1 | # ``Keyboard`` 2 | 3 | SwiftUI music keyboard 4 | 5 | ## Overview 6 | 7 | See the [AudioKit Cookbook](https://github.com/AudioKit/Cookbook/) for examples and see the source code for API of all Keyboard elements. This documentation gives a rudimentary overview but lacks most of the finer details. 8 | 9 | ![Demo Screenshot](demo) 10 | 11 | 12 | Keyboard aims to be an easy-to-use musical keyboard with: 13 | 14 | - multi-touch interface 15 | - accurate note name labels on the piano keys given the musical key 16 | - stylized keys in any color 17 | - any number of notes, not just limited to octaves 18 | 19 | ## Goals 20 | 21 | - Good user interface 22 | - Good performance. We rely on SwiftUI's drag gestures 23 | 24 | -------------------------------------------------------------------------------- /Sources/Keyboard/Keyboard.docc/Resources/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Keyboard/c0752677c602eec2f97ec50825581608bc56dac6/Sources/Keyboard/Keyboard.docc/Resources/demo.png -------------------------------------------------------------------------------- /Sources/Keyboard/Keyboard.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | @_exported import Tonic 5 | 6 | /// Touch-oriented musical keyboard 7 | public struct Keyboard: View where Content: View { 8 | let content: (Pitch, Bool) -> Content 9 | 10 | /// model contains the keys, their status and touches 11 | @StateObject public var model: KeyboardModel = .init() 12 | 13 | var latching: Bool 14 | var noteOn: (Pitch, CGPoint) -> Void 15 | var noteOff: (Pitch) -> Void 16 | var layout: KeyboardLayout 17 | 18 | /// Initialize the keyboard 19 | /// - Parameters: 20 | /// - layout: The geometry of the keys 21 | /// - latching: Latched keys stay on until they are pressed again 22 | /// - noteOn: Closure to perform when a key is pressed 23 | /// - noteOff: Closure to perform when a note ends 24 | /// - content: View defining how to render a specific key 25 | public init(layout: KeyboardLayout = .piano(pitchRange: Pitch(60) ... Pitch(72)), 26 | latching: Bool = false, 27 | noteOn: @escaping (Pitch, CGPoint) -> Void = { _, _ in }, 28 | noteOff: @escaping (Pitch) -> Void = { _ in }, 29 | @ViewBuilder content: @escaping (Pitch, Bool) -> Content) 30 | { 31 | self.latching = latching 32 | self.layout = layout 33 | self.noteOn = noteOn 34 | self.noteOff = noteOff 35 | self.content = content 36 | } 37 | 38 | /// Body enclosing the various layout views 39 | public var body: some View { 40 | ZStack { 41 | switch layout { 42 | case let .piano(pitchRange, initialSpacerRatio, spacerRatio, relativeBlackKeyWidth, relativeBlackKeyHeight): 43 | Piano(content: content, 44 | keyboard: model, 45 | spacer: PianoSpacer(pitchRange: pitchRange, 46 | initialSpacerRatio: initialSpacerRatio, 47 | spacerRatio: spacerRatio, 48 | relativeBlackKeyWidth: relativeBlackKeyWidth, 49 | relativeBlackKeyHeight: relativeBlackKeyHeight)) 50 | case let .isomorphic(pitchRange, root, scale): 51 | Isomorphic(content: content, 52 | model: model, 53 | pitchRange: pitchRange, 54 | root: root, 55 | scale: scale) 56 | case let .guitar(openPitches, fretCount): 57 | Guitar(content: content, model: model, openPitches: openPitches, fretCount: fretCount) 58 | case let .verticalIsomorphic(pitchRange, root, scale): 59 | VerticalIsomorphic(content: content, 60 | model: model, 61 | pitchRange: pitchRange, 62 | root: root, 63 | scale: scale) 64 | case let .verticalPiano(pitchRange, initialSpacerRatio, spacerRatio, relativeBlackKeyWidth): 65 | VerticalPiano(content: content, 66 | keyboard: model, 67 | spacer: PianoSpacer(pitchRange: pitchRange, 68 | initialSpacerRatio: initialSpacerRatio, 69 | spacerRatio: spacerRatio, 70 | relativeBlackKeyWidth: relativeBlackKeyWidth)) 71 | } 72 | 73 | if !latching { 74 | MultitouchView { touches in 75 | model.touchLocations = touches 76 | } 77 | } 78 | 79 | }.onPreferenceChange(KeyRectsKey.self) { keyRectInfos in 80 | model.keyRectInfos = keyRectInfos 81 | }.onAppear { 82 | model.noteOn = noteOn 83 | model.noteOff = noteOff 84 | model.latching = latching 85 | } 86 | } 87 | } 88 | 89 | public extension Keyboard where Content == KeyboardKey { 90 | /// Initialize the Keyboard with KeyboardKey as its content 91 | /// - Parameters: 92 | /// - layout: The geometry of the keys 93 | /// - latching: Latched keys stay on until they are pressed again 94 | /// - noteOn: Closure to perform when a key is pressed 95 | /// - noteOff: Closure to perform when a note ends 96 | init(layout: KeyboardLayout = .piano(pitchRange: Pitch(60) ... Pitch(72)), 97 | latching: Bool = false, 98 | noteOn: @escaping (Pitch, CGPoint) -> Void = { _, _ in }, 99 | noteOff: @escaping (Pitch) -> Void = { _ in }) 100 | { 101 | self.layout = layout 102 | self.latching = latching 103 | self.noteOn = noteOn 104 | self.noteOff = noteOff 105 | 106 | var alignment: Alignment = .bottom 107 | 108 | var flatTop = false 109 | switch layout { 110 | case .guitar: 111 | alignment = .center 112 | case .isomorphic: 113 | alignment = .bottom 114 | case .piano: 115 | flatTop = true 116 | case .verticalIsomorphic: 117 | alignment = .trailing 118 | case .verticalPiano: 119 | flatTop = true 120 | alignment = .trailing 121 | } 122 | content = { 123 | KeyboardKey( 124 | pitch: $0, 125 | isActivated: $1, 126 | flatTop: flatTop, 127 | alignment: alignment 128 | ) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Keyboard/KeyboardKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | /// A default visual representation for a key. 7 | public struct KeyboardKey: View { 8 | /// Initialize the keyboard key 9 | /// - Parameters: 10 | /// - pitch: Pitch assigned to the key 11 | /// - isActivated: Whether to represent this key in the "down" state 12 | /// - text: Label on the key 13 | /// - color: Color of the activated key 14 | /// - isActivatedExternally: Usually used for representing incoming MIDI 15 | public init(pitch: Pitch, 16 | isActivated: Bool, 17 | text: String = "unset", 18 | whiteKeyColor: Color = .white, 19 | blackKeyColor: Color = .black, 20 | pressedColor: Color = .red, 21 | flatTop: Bool = false, 22 | alignment: Alignment = .bottom, 23 | isActivatedExternally: Bool = false) 24 | { 25 | self.pitch = pitch 26 | self.isActivated = isActivated 27 | if text == "unset" { 28 | var newText = "" 29 | if pitch.note(in: .C).noteClass.description == "C" { 30 | newText = pitch.note(in: .C).description 31 | } else { 32 | newText = "" 33 | } 34 | self.text = newText 35 | } else { 36 | self.text = text 37 | } 38 | self.whiteKeyColor = whiteKeyColor 39 | self.blackKeyColor = blackKeyColor 40 | self.pressedColor = pressedColor 41 | self.flatTop = flatTop 42 | self.alignment = alignment 43 | self.isActivatedExternally = isActivatedExternally 44 | } 45 | 46 | var pitch: Pitch 47 | var isActivated: Bool 48 | var whiteKeyColor: Color 49 | var blackKeyColor: Color 50 | var pressedColor: Color 51 | var flatTop: Bool 52 | var alignment: Alignment 53 | var text: String 54 | var isActivatedExternally: Bool 55 | 56 | var keyColor: Color { 57 | if isActivatedExternally || isActivated { 58 | return pressedColor 59 | } 60 | return pitch.note(in: .C).accidental == .natural ? whiteKeyColor : blackKeyColor 61 | } 62 | 63 | var isWhite: Bool { 64 | pitch.note(in: .C).accidental == .natural 65 | } 66 | 67 | var textColor: Color { 68 | return pitch.note(in: .C).accidental == .natural ? blackKeyColor : whiteKeyColor 69 | } 70 | 71 | func minDimension(_ size: CGSize) -> CGFloat { 72 | return min(size.width, size.height) 73 | } 74 | 75 | func isTall(size: CGSize) -> Bool { 76 | size.height > size.width 77 | } 78 | 79 | // How much of the key height to take up with label 80 | func relativeFontSize(in containerSize: CGSize) -> CGFloat { 81 | minDimension(containerSize) * 0.333 82 | } 83 | 84 | let relativeTextPadding = 0.05 85 | 86 | func relativeCornerRadius(in containerSize: CGSize) -> CGFloat { 87 | minDimension(containerSize) * 0.125 88 | } 89 | 90 | func topPadding(_ size: CGSize) -> CGFloat { 91 | flatTop && alignment == .bottom ? relativeCornerRadius(in: size) : 0 92 | } 93 | 94 | func leadingPadding(_ size: CGSize) -> CGFloat { 95 | flatTop && alignment == .trailing ? relativeCornerRadius(in: size) : 0 96 | } 97 | 98 | func negativeTopPadding(_ size: CGSize) -> CGFloat { 99 | flatTop && alignment == .bottom ? -relativeCornerRadius(in: size) : 100 | isWhite ? 0.5 : 0 101 | } 102 | 103 | func negativeLeadingPadding(_ size: CGSize) -> CGFloat { 104 | flatTop && alignment == .trailing ? -relativeCornerRadius(in: size) : 105 | isWhite ? 0.5 : 0 106 | } 107 | 108 | public var body: some View { 109 | GeometryReader { proxy in 110 | ZStack(alignment: alignment) { 111 | Rectangle() 112 | .foregroundColor(keyColor) 113 | .padding(.top, topPadding(proxy.size)) 114 | .padding(.leading, leadingPadding(proxy.size)) 115 | .cornerRadius(relativeCornerRadius(in: proxy.size)) 116 | .padding(.top, negativeTopPadding(proxy.size)) 117 | .padding(.leading, negativeLeadingPadding(proxy.size)) 118 | .padding(.trailing, 0.5) 119 | Text(text) 120 | .font(Font(.init(.system, size: relativeFontSize(in: proxy.size)))) 121 | .foregroundColor(textColor) 122 | .padding(relativeFontSize(in: proxy.size) / 3.0) 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/Keyboard/KeyboardModel.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | /// Observable model calling back on noteOn and noteOff and storing the touch locations 7 | public class KeyboardModel: ObservableObject { 8 | var keyRectInfos: [KeyRectInfo] = [] 9 | var noteOn: (Pitch, CGPoint) -> Void = { _, _ in } 10 | var noteOff: (Pitch) -> Void = { _ in } 11 | var normalizedPoints = Array(repeating: CGPoint.zero, count: 128) 12 | var latching: Bool = false 13 | 14 | var touchLocations: [CGPoint] = [] { 15 | didSet { 16 | var newPitches = PitchSet() 17 | for location in touchLocations { 18 | var pitch: Pitch? 19 | var highestZindex = -1 20 | var normalizedPoint = CGPoint.zero 21 | for info in keyRectInfos where info.rect.contains(location) { 22 | if pitch == nil || info.zIndex > highestZindex { 23 | pitch = info.pitch 24 | highestZindex = info.zIndex 25 | normalizedPoint = CGPoint(x: (location.x - info.rect.minX) / info.rect.width, 26 | y: (location.y - info.rect.minY) / info.rect.height) 27 | } 28 | } 29 | if let p = pitch { 30 | newPitches.add(p) 31 | normalizedPoints[p.intValue] = normalizedPoint 32 | } 33 | } 34 | if touchedPitches.array != newPitches.array { 35 | touchedPitches = newPitches 36 | } 37 | } 38 | } 39 | 40 | /// all touched notes 41 | @Published public var touchedPitches = PitchSet() { 42 | willSet { triggerEvents(from: touchedPitches, to: newValue) } 43 | } 44 | 45 | /// Either latched keys or keys active due to external MIDI events. 46 | @Published public var externallyActivatedPitches = PitchSet() { 47 | willSet { triggerEvents(from: externallyActivatedPitches, to: newValue) } 48 | } 49 | 50 | func triggerEvents(from oldValue: PitchSet, to newValue: PitchSet) { 51 | let newPitches = newValue.subtracting(oldValue) 52 | let removedPitches = oldValue.subtracting(newValue) 53 | 54 | for pitch in removedPitches.array { 55 | noteOff(pitch) 56 | } 57 | 58 | for pitch in newPitches.array { 59 | noteOn(pitch, normalizedPoints[pitch.intValue]) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/Guitar.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct Guitar: View where Content: View { 7 | let content: (Pitch, Bool) -> Content 8 | var model: KeyboardModel 9 | var openPitches: [Pitch] 10 | var fretCount: Int 11 | 12 | var body: some View { 13 | // Loop through the keys and add rows (strings) 14 | // Each row has a 5 note offset tuning them to 4ths 15 | // The pitchRange is for the lowest row (string) 16 | VStack(spacing: 0) { 17 | ForEach(0 ..< openPitches.count, id: \.self) { string in 18 | HStack(spacing: 0) { 19 | ForEach(0 ..< fretCount + 1, id: \.self) { fret in 20 | KeyContainer(model: model, 21 | pitch: Pitch(intValue: openPitches[string].intValue + fret), 22 | content: content) 23 | } 24 | } 25 | } 26 | } 27 | .clipShape(Rectangle()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/Isomorphic.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct Isomorphic: View where Content: View { 7 | let content: (Pitch, Bool) -> Content 8 | var model: KeyboardModel 9 | var pitchRange: ClosedRange 10 | var root: NoteClass 11 | var scale: Scale 12 | 13 | var pitchesToShow: [Pitch] { 14 | var pitchArray: [Pitch] = [] 15 | let key = Key(root: root, scale: scale) 16 | for pitch in pitchRange where pitch.existsNaturally(in: key) { 17 | pitchArray.append(pitch) 18 | } 19 | return Array(pitchArray) 20 | } 21 | 22 | var body: some View { 23 | HStack(spacing: 0) { 24 | ForEach(pitchesToShow, id: \.self) { pitch in 25 | KeyContainer(model: model, 26 | pitch: pitch, 27 | content: content) 28 | } 29 | } 30 | .clipShape(Rectangle()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/KeyboardLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | /// Types of keyboards we can generate 7 | public enum KeyboardLayout: Equatable, Hashable { 8 | /// Guitar in arbitrary tuning, from first string (highest) to loweset string 9 | case guitar(openPitches: [Pitch] = [Pitch(64), Pitch(59), Pitch(55), Pitch(50), Pitch(45), Pitch(40)], fretcount: Int = 22) 10 | 11 | /// All notes linearly right after one another 12 | case isomorphic(pitchRange: ClosedRange, 13 | root: NoteClass = .C, 14 | scale: Scale = .chromatic) 15 | 16 | /// Traditional Piano layout with raised black keys over white keys 17 | case piano(pitchRange: ClosedRange, 18 | initialSpacerRatio: [Letter: CGFloat] = PianoSpacer.defaultInitialSpacerRatio, 19 | spacerRatio: [Letter: CGFloat] = PianoSpacer.defaultSpacerRatio, 20 | relativeBlackKeyWidth: CGFloat = PianoSpacer.defaultRelativeBlackKeyWidth, 21 | relativeBlackKeyHeight: CGFloat = PianoSpacer.defaultRelativeBlackKeyHeight) 22 | 23 | /// For piano roll, jam strip type views 24 | case verticalIsomorphic(pitchRange: ClosedRange, 25 | root: NoteClass = .C, 26 | scale: Scale = .chromatic) 27 | 28 | /// Traditional Piano vertical layout with raised black keys over white keys 29 | case verticalPiano(pitchRange: ClosedRange, 30 | initialSpacerRatio: [Letter: CGFloat] = PianoSpacer.defaultInitialSpacerRatio, 31 | spacerRatio: [Letter: CGFloat] = PianoSpacer.defaultSpacerRatio, 32 | relativeBlackKeyWidth: CGFloat = PianoSpacer.defaultRelativeBlackKeyWidth) 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/Piano.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct Piano: View where Content: View { 7 | let content: (Pitch, Bool) -> Content 8 | let keyboard: KeyboardModel 9 | let spacer: PianoSpacer 10 | 11 | var body: some View { 12 | GeometryReader { geo in 13 | ZStack(alignment: .topLeading) { 14 | HStack(spacing: 0) { 15 | ForEach(spacer.whiteKeys, id: \.self) { pitch in 16 | KeyContainer(model: keyboard, pitch: pitch, content: content) 17 | .frame(width: spacer.whiteKeyWidth(geo.size.width)) 18 | } 19 | } 20 | 21 | // Black keys. 22 | VStack(alignment: .leading) { 23 | HStack(spacing: 0) { 24 | Rectangle().opacity(0) 25 | .frame(width: spacer.initialSpacerWidth(geo.size.width)) 26 | if spacer.pitchRange.lowerBound != spacer.pitchRangeBoundedByNaturals.lowerBound { 27 | Rectangle().opacity(0).frame(width: spacer.lowerBoundSpacerWidth(geo.size.width)) 28 | } 29 | ForEach(spacer.pitchRange, id: \.self) { pitch in 30 | if spacer.isBlackKey(Pitch(intValue: pitch.intValue)) { 31 | KeyContainer(model: keyboard, 32 | pitch: Pitch(intValue: pitch.intValue), 33 | zIndex: 1, 34 | content: content) 35 | .frame(width: spacer.blackKeyWidth(geo.size.width)) 36 | } else { 37 | Rectangle().opacity(0) 38 | .frame(width: spacer.blackKeySpacerWidth(geo.size.width, pitch: pitch)) 39 | } 40 | } 41 | } 42 | Spacer().frame(height: geo.size.height * (1 - spacer.relativeBlackKeyHeight)) 43 | } 44 | } 45 | } 46 | .clipShape(Rectangle()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/PianoSpacer.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | public struct PianoSpacer { 7 | public static let defaultInitialSpacerRatio: [Letter: CGFloat] = [ 8 | .C: 0.0, 9 | .D: 3.0 / 16.0, 10 | .E: 6.0 / 16.0, 11 | .F: 0.0 / 16.0, 12 | .G: 3.0 / 16.0, 13 | .A: 4.5 / 16.0, 14 | .B: 6.0 / 16.0 15 | ] 16 | public static let defaultSpacerRatio: [Letter: CGFloat] = [ 17 | .C: 10.0 / 16.0, 18 | .D: 10.0 / 16.0, 19 | .E: 10.0 / 16.0, 20 | .F: 10.0 / 16.0, 21 | .G: 8.5 / 16.0, 22 | .A: 8.5 / 16.0, 23 | .B: 10.0 / 16.0 24 | ] 25 | public static let defaultRelativeBlackKeyWidth: CGFloat = 9.0 / 16.0 26 | 27 | /// Default value for Black Key Height 28 | public static let defaultRelativeBlackKeyHeight: CGFloat = 0.53 29 | 30 | public var pitchRange: ClosedRange 31 | public var initialSpacerRatio: [Letter: CGFloat] 32 | public var spacerRatio: [Letter: CGFloat] 33 | public var relativeBlackKeyWidth: CGFloat = PianoSpacer.defaultRelativeBlackKeyWidth 34 | /// The smaller the number, the shorter the black keys appear. A value of 1 approximates an isomorphic keyboard 35 | public var relativeBlackKeyHeight: CGFloat = PianoSpacer.defaultRelativeBlackKeyHeight 36 | } 37 | 38 | extension PianoSpacer { 39 | public var whiteKeys: [Pitch] { 40 | var returnValue: [Pitch] = [] 41 | for pitch in pitchRangeBoundedByNaturals where pitch.note(in: .C).accidental == .natural { 42 | returnValue.append(pitch) 43 | } 44 | return returnValue 45 | } 46 | 47 | public func isBlackKey(_ pitch: Pitch) -> Bool { 48 | pitch.note(in: .C).accidental != .natural 49 | } 50 | 51 | // NOTE: The magic numbers here come from the canonical piano layout 52 | // Probably instead of using HStacks we should just lay things out on a canvas 53 | public var initialSpacer: CGFloat { 54 | let note = pitchRangeBoundedByNaturals.lowerBound.note(in: .C) 55 | return initialSpacerRatio[note.letter] ?? 0 56 | } 57 | 58 | public func space(pitch: Pitch) -> CGFloat { 59 | let note = pitch.note(in: .C) 60 | return spacerRatio[note.letter] ?? 0 61 | } 62 | 63 | public func whiteKeyWidth(_ width: CGFloat) -> CGFloat { 64 | width / CGFloat(whiteKeys.count) 65 | } 66 | 67 | public func blackKeyWidth(_ width: CGFloat) -> CGFloat { 68 | whiteKeyWidth(width) * relativeBlackKeyWidth 69 | } 70 | 71 | public var pitchRangeBoundedByNaturals: ClosedRange { 72 | var lowerBound = pitchRange.lowerBound 73 | if lowerBound.note(in: .C).accidental != .natural { 74 | lowerBound = Pitch(intValue: lowerBound.intValue - 1) 75 | } 76 | var upperBound = pitchRange.upperBound 77 | if upperBound.note(in: .C).accidental != .natural { 78 | upperBound = Pitch(intValue: upperBound.intValue + 1) 79 | } 80 | return lowerBound ... upperBound 81 | } 82 | 83 | public func initialSpacerWidth(_ width: CGFloat) -> CGFloat { 84 | whiteKeyWidth(width) * initialSpacer 85 | } 86 | 87 | public func lowerBoundSpacerWidth(_ width: CGFloat) -> CGFloat { 88 | whiteKeyWidth(width) * space(pitch: pitchRange.lowerBound) 89 | } 90 | 91 | public func blackKeySpacerWidth(_ width: CGFloat, pitch: Pitch) -> CGFloat { 92 | whiteKeyWidth(width) * space(pitch: pitch) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/VerticalIsomorphic.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct VerticalIsomorphic: View where Content: View { 7 | let content: (Pitch, Bool) -> Content 8 | var model: KeyboardModel 9 | var pitchRange: ClosedRange 10 | var root: NoteClass 11 | var scale: Scale 12 | 13 | var pitchesToShow: [Pitch] { 14 | var pitchArray: [Pitch] = [] 15 | let key = Key(root: root, scale: scale) 16 | for pitch in pitchRange where pitch.existsNaturally(in: key) { 17 | pitchArray.append(pitch) 18 | } 19 | return Array(pitchArray) 20 | } 21 | 22 | var body: some View { 23 | VStack(spacing: 0) { 24 | ForEach(pitchesToShow.reversed(), id: \.self) { pitch in 25 | KeyContainer(model: model, 26 | pitch: pitch, 27 | content: content) 28 | } 29 | } 30 | .clipShape(Rectangle()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Keyboard/Layouts/VerticalPiano.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | struct VerticalPiano: View where Content: View { 7 | let content: (Pitch, Bool) -> Content 8 | let keyboard: KeyboardModel 9 | let spacer: PianoSpacer 10 | 11 | var body: some View { 12 | GeometryReader { geo in 13 | ZStack(alignment: .topLeading) { 14 | VStack(spacing: 0) { 15 | ForEach(spacer.whiteKeys.reversed(), id: \.self) { pitch in 16 | KeyContainer(model: keyboard, 17 | pitch: pitch, 18 | content: content) 19 | .frame(height: spacer.whiteKeyWidth(geo.size.height)) 20 | } 21 | } 22 | 23 | // Black keys. 24 | HStack(alignment: .bottom) { 25 | VStack(spacing: 0) { 26 | Spacer() 27 | ForEach(spacer.pitchRange.reversed(), id: \.self) { pitch in 28 | if spacer.isBlackKey(Pitch(intValue: pitch.intValue)) { 29 | KeyContainer(model: keyboard, 30 | pitch: Pitch(intValue: pitch.intValue), 31 | zIndex: 1, 32 | content: content) 33 | .frame(height: spacer.blackKeyWidth(geo.size.height)) 34 | } else { 35 | Rectangle().opacity(0) 36 | .frame(height: spacer.blackKeySpacerWidth(geo.size.height, pitch: pitch)) 37 | } 38 | } 39 | if spacer.pitchRange.lowerBound != spacer.pitchRangeBoundedByNaturals.lowerBound { 40 | Rectangle().opacity(0) 41 | .frame(height: spacer.lowerBoundSpacerWidth(geo.size.height)) 42 | } 43 | Rectangle().opacity(0).frame(height: spacer.initialSpacerWidth(geo.size.height)) 44 | } 45 | 46 | // This space pushes the black keys left. 47 | // XXX: perhaps we should give the user control of 48 | // the spacing. 49 | Spacer().frame(height: geo.size.height * 0.47) 50 | } 51 | } 52 | } 53 | .clipShape(Rectangle()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Keyboard/MIDIMonitorKeyboard.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | import Tonic 4 | 5 | @available(iOS 15, macOS 12, *) 6 | extension GraphicsContext { 7 | func fill(rect: CGRect, with color: Color) { 8 | fill(Path(roundedRect: rect, cornerRadius: 0), with: GraphicsContext.Shading.color(color)) 9 | } 10 | } 11 | 12 | /// MIDIMonitorKeyboard 13 | @available(iOS 15, macOS 12, *) 14 | public struct MIDIMonitorKeyboard: View { 15 | 16 | var layout: KeyboardLayout 17 | var activatedPitches: PitchSet 18 | var colorFunction: (Pitch)->Color 19 | var spacer: PianoSpacer = PianoSpacer(pitchRange: Pitch(60) ... Pitch(72), initialSpacerRatio: PianoSpacer.defaultInitialSpacerRatio, spacerRatio: PianoSpacer.defaultSpacerRatio) 20 | 21 | public init(layout: KeyboardLayout = .piano(pitchRange: Pitch(60) ... Pitch(72)), 22 | activatedPitches: PitchSet = PitchSet(), 23 | colorFunction: @escaping (Pitch)->Color = { _ in Color.red } 24 | ) 25 | { 26 | self.layout = layout 27 | self.activatedPitches = activatedPitches 28 | self.colorFunction = colorFunction 29 | 30 | switch layout { 31 | case let .piano(pitchRange, initialSpacerRatio, spacerRatio, relativeBlackKeyWidth, relativeBlackKeyHeight): 32 | spacer = PianoSpacer(pitchRange: pitchRange, 33 | initialSpacerRatio: initialSpacerRatio, 34 | spacerRatio: spacerRatio, 35 | relativeBlackKeyWidth: relativeBlackKeyWidth, 36 | relativeBlackKeyHeight: relativeBlackKeyHeight) 37 | default: 38 | print("Unimplimented") 39 | } 40 | } 41 | 42 | public var body: some View { 43 | switch layout { 44 | case .piano: 45 | 46 | Canvas { cx, size in 47 | cx.fill(rect: CGRect(origin: .zero, size: size), with: .black) 48 | var color = Color.white 49 | for (i, pitch) in spacer.whiteKeys.enumerated() { 50 | color = Color.white 51 | if activatedPitches.contains(pitch) { 52 | color = colorFunction(pitch) 53 | } 54 | let r = CGRect(x: CGFloat(i) * spacer.whiteKeyWidth(size.width), 55 | y: 0, 56 | width: spacer.whiteKeyWidth(size.width) - 1, 57 | height: size.height) 58 | cx.fill(rect: r, with: color) 59 | } 60 | 61 | var x: CGFloat = spacer.initialSpacerWidth(size.width) 62 | if spacer.pitchRange.lowerBound != spacer.pitchRangeBoundedByNaturals.lowerBound { 63 | x += spacer.lowerBoundSpacerWidth(size.width) 64 | } 65 | for pitch in spacer.pitchRange { 66 | color = Color.black 67 | 68 | let r = CGRect(x: x, 69 | y: 0, 70 | width: spacer.blackKeyWidth(size.width), 71 | height: size.height * spacer.relativeBlackKeyHeight) 72 | 73 | if activatedPitches.contains(pitch) { 74 | color = colorFunction(pitch) 75 | } 76 | 77 | 78 | if spacer.isBlackKey(pitch) { 79 | cx.fill(rect: r, with: color) 80 | x += spacer.blackKeyWidth(size.width) 81 | } else { 82 | x += spacer.blackKeySpacerWidth(size.width, pitch: pitch) 83 | } 84 | } 85 | } 86 | default: 87 | EmptyView() 88 | } 89 | } 90 | } 91 | 92 | var p = PitchSet(pitches: [Pitch(65), Pitch(68), Pitch(71), Pitch(74)]) 93 | 94 | // Removing Preview macro until Xcode 15 is released 95 | /* 96 | #Preview { 97 | MIDIMonitorKeyboard(layout: .piano(pitchRange: Pitch(61)...Pitch(88)), 98 | activatedPitches: p, 99 | colorFunction: { x in Color(cgColor: PitchColor.helmholtz[Int(x.pitchClass)])} 100 | ).frame(width: 600, height: 100) 101 | } 102 | */ 103 | -------------------------------------------------------------------------------- /Sources/Keyboard/MultitouchView.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | 5 | public typealias TouchCallback = ([CGPoint]) -> Void 6 | 7 | #if !os(macOS) 8 | 9 | import UIKit 10 | 11 | class MultitouchViewIOS: UIView { 12 | 13 | var callback: TouchCallback = { _ in } 14 | var touches = Set() 15 | 16 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 17 | self.touches.formUnion(touches) 18 | callback(self.touches.map { $0.location(in: nil)}) 19 | } 20 | 21 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 22 | callback(self.touches.map { $0.location(in: nil)}) 23 | } 24 | 25 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 26 | self.touches.subtract(touches) 27 | callback(self.touches.map { $0.location(in: nil)}) 28 | } 29 | 30 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 31 | self.touches.subtract(touches) 32 | callback(self.touches.map { $0.location(in: nil)}) 33 | } 34 | } 35 | 36 | struct MultitouchView: UIViewRepresentable { 37 | 38 | var callback: TouchCallback = { _ in } 39 | 40 | func makeUIView(context: Context) -> MultitouchViewIOS { 41 | let view = MultitouchViewIOS() 42 | view.callback = callback 43 | view.isMultipleTouchEnabled = true 44 | return view 45 | } 46 | 47 | func updateUIView(_ uiView: MultitouchViewIOS, context: Context) { 48 | uiView.callback = callback 49 | } 50 | } 51 | 52 | #else 53 | 54 | import AppKit 55 | 56 | class MultitouchViewMacOS: NSView { 57 | 58 | var callback: TouchCallback = { _ in } 59 | 60 | func flip(_ p: CGPoint) -> CGPoint { 61 | CGPoint(x: p.x, y: window!.frame.size.height - p.y) 62 | } 63 | 64 | override func mouseDown(with event: NSEvent) { 65 | callback([flip(event.locationInWindow)]) 66 | } 67 | 68 | override func mouseDragged(with event: NSEvent) { 69 | callback([flip(event.locationInWindow)]) 70 | } 71 | 72 | override func mouseUp(with event: NSEvent) { 73 | callback([]) 74 | } 75 | 76 | } 77 | 78 | struct MultitouchView: NSViewRepresentable { 79 | 80 | var callback: TouchCallback = { _ in } 81 | 82 | func makeNSView(context: Context) -> MultitouchViewMacOS { 83 | let view = MultitouchViewMacOS() 84 | view.callback = callback 85 | return view 86 | } 87 | 88 | func updateNSView(_ uiView: MultitouchViewMacOS, context: Context) { 89 | uiView.callback = callback 90 | } 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /Sources/Keyboard/PreferenceKeys.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | import SwiftUI 4 | import Tonic 5 | 6 | /// For accumulating key rects. 7 | struct KeyRectsKey: PreferenceKey { 8 | static var defaultValue: [KeyRectInfo] = [] 9 | 10 | static func reduce(value: inout [KeyRectInfo], nextValue: () -> [KeyRectInfo]) { 11 | value.append(contentsOf: nextValue()) 12 | } 13 | } 14 | 15 | struct KeyRectInfo: Equatable { 16 | var rect: CGRect 17 | var pitch: Pitch 18 | var zIndex: Int = 0 19 | } 20 | -------------------------------------------------------------------------------- /Tests/KeyboardTests/KeyboardTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Keyboard/ 2 | 3 | @testable import Keyboard 4 | import Tonic 5 | import XCTest 6 | 7 | final class KeyboardTests: XCTestCase { 8 | func testKeyboardModel() throws { 9 | let model = KeyboardModel() 10 | 11 | model.keyRectInfos = [KeyRectInfo(rect: CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100)), pitch: Pitch(60))] 12 | 13 | var testPitch: Pitch? 14 | 15 | var noteOnReceived = false 16 | model.noteOn = { pitch, _ in 17 | testPitch = pitch 18 | noteOnReceived = true 19 | } 20 | 21 | var noteOffReceived = false 22 | model.noteOff = { pitch in 23 | testPitch = pitch 24 | noteOffReceived = true 25 | } 26 | 27 | model.touchLocations = [CGPoint(x: 10, y: 10)] 28 | 29 | XCTAssertEqual(testPitch, Pitch(60)) 30 | XCTAssertTrue(noteOnReceived) 31 | 32 | model.touchLocations = [] 33 | 34 | XCTAssertEqual(testPitch, Pitch(60)) 35 | XCTAssertTrue(noteOffReceived) 36 | } 37 | 38 | func testKeyboardModelZIndex() throws { 39 | let model = KeyboardModel() 40 | 41 | model.keyRectInfos = [ 42 | KeyRectInfo(rect: CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100)), pitch: Pitch(60)), 43 | KeyRectInfo(rect: CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100)), pitch: Pitch(61), zIndex: 1), 44 | ] 45 | 46 | var testPitch: Pitch? 47 | var noteOnReceived = false 48 | model.noteOn = { pitch, _ in 49 | testPitch = pitch 50 | noteOnReceived = true 51 | } 52 | 53 | var noteOffReceived = false 54 | model.noteOff = { pitch in 55 | testPitch = pitch 56 | noteOffReceived = true 57 | } 58 | 59 | model.touchLocations = [CGPoint(x: 10, y: 10)] 60 | 61 | XCTAssertEqual(testPitch, Pitch(61)) 62 | XCTAssertTrue(noteOnReceived) 63 | 64 | model.touchLocations = [] 65 | 66 | XCTAssertEqual(testPitch, Pitch(61)) 67 | XCTAssertTrue(noteOffReceived) 68 | } 69 | } 70 | --------------------------------------------------------------------------------