├── .gitignore ├── LICENSE ├── PlotAudio ├── PlotAudio.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── PlotAudio │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── PlotAudio-1024.jpg │ │ ├── PlotAudio-1024.png │ │ ├── PlotAudio-128.png │ │ ├── PlotAudio-16.png │ │ ├── PlotAudio-256 1.png │ │ ├── PlotAudio-256.png │ │ ├── PlotAudio-32 1.png │ │ ├── PlotAudio-32.png │ │ ├── PlotAudio-512 1.png │ │ ├── PlotAudio-512.png │ │ └── PlotAudio-64.png │ └── Contents.json │ ├── Audio Files │ ├── 2 Distinct Channels.aif │ ├── Accelerating.m4a │ ├── Before September - 01.01.m4a │ ├── Channels - 5.1 (C L R Ls Rs LFE).mp4 │ ├── Channels - 7.1 (L R C LFE Rls Rrs Ls Rs).wav │ ├── Clock.m4a │ ├── Crescendo.m4a │ ├── Distorted Guitar.wav │ ├── Fireworks.m4a │ ├── Guitar Solo.m4a │ ├── Heartbeat.m4a │ ├── I'm Afraid I Can't Do That.m4a │ ├── Noisy Signal With Break.mp4 │ ├── Piano.m4a │ ├── Pop.m4a │ ├── Ring Tone 5.m4a │ ├── Skidding.m4a │ ├── Spinning.m4a │ ├── Squeeze.m4a │ ├── Stomp Clap Percussion.m4a │ ├── Today's Sound.wav │ ├── Todays Sound 2.wav │ ├── Todays Sound 3.wav │ ├── Tone - 500 hz Single Channel.wav │ ├── Triangle.m4a │ ├── Tuba.m4a │ ├── Undulating Harp.m4a │ ├── Waves.m4a │ ├── Wavvy.m4a │ ├── Whoosh 1.m4a │ ├── Whoosh 2.m4a │ ├── Wind.m4a │ ├── Xylophone.m4a │ └── zzZ.m4a │ ├── AudioPlayer.swift │ ├── ControlView.swift │ ├── ControlViewObservable.swift │ ├── Credits.rtf │ ├── File.swift │ ├── FileTableObservable.swift │ ├── FileTableView.swift │ ├── Info.plist │ ├── PlotAudio.entitlements │ ├── PlotAudio │ ├── AVAsset-extensions.swift │ ├── Audio Samples │ │ ├── PlotAudio_ClockSample.m4a │ │ ├── PlotAudio_PianoSample.m4a │ │ └── PlotAudio_TubaSample.m4a │ ├── PlotAudio.swift │ ├── PlotAudioConstants.swift │ ├── PlotAudioObservable.swift │ └── PlotAudioWaveformView.swift │ ├── PlotAudioApp.swift │ ├── PlotAudioAppView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── Video Files │ ├── Drums 3.mov │ ├── Echo.mov │ ├── Heartbeat.mov │ ├── Ring Tone 11.mov │ ├── Ring Tone 5.mov │ ├── Ring Tone 7.mov │ ├── Ring Tone 9.mov │ ├── Squeeze.mov │ ├── Todays Sound 3.mov │ ├── Tuba.mov │ ├── Wavvy.mov │ ├── Wind.mov │ └── Xylophone.mov └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## User settings 2 | xcuserdata/ 3 | *.xcuserstate 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Limit Point 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 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CD1B549128E7B84300C4C666 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = CD1B549028E7B84300C4C666 /* Credits.rtf */; }; 11 | CD1B549428E7B92400C4C666 /* Video Files in Resources */ = {isa = PBXBuildFile; fileRef = CD1B549228E7B92400C4C666 /* Video Files */; }; 12 | CD1B549528E7B92400C4C666 /* Audio Files in Resources */ = {isa = PBXBuildFile; fileRef = CD1B549328E7B92400C4C666 /* Audio Files */; }; 13 | CD1B54A728E8681E00C4C666 /* PlotAudioConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54A628E8681E00C4C666 /* PlotAudioConstants.swift */; }; 14 | CD1B54A928E8685800C4C666 /* PlotAudioObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54A828E8685800C4C666 /* PlotAudioObservable.swift */; }; 15 | CD1B54AB28E868AF00C4C666 /* AVAsset-extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54AA28E868AF00C4C666 /* AVAsset-extensions.swift */; }; 16 | CD1B54AD28E8690D00C4C666 /* PlotAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54AC28E8690D00C4C666 /* PlotAudio.swift */; }; 17 | CD1B54AF28E8694000C4C666 /* PlotAudioWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54AE28E8694000C4C666 /* PlotAudioWaveformView.swift */; }; 18 | CD1B54B128E8698900C4C666 /* PlotAudioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54B028E8698900C4C666 /* PlotAudioApp.swift */; }; 19 | CD1B54B328E869FB00C4C666 /* PlotAudioAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54B228E869FB00C4C666 /* PlotAudioAppView.swift */; }; 20 | CD1B54B528E86A2600C4C666 /* FileTableObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54B428E86A2600C4C666 /* FileTableObservable.swift */; }; 21 | CD1B54B728E86A4800C4C666 /* FileTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54B628E86A4800C4C666 /* FileTableView.swift */; }; 22 | CD1B54B928E86A7200C4C666 /* ControlViewObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54B828E86A7200C4C666 /* ControlViewObservable.swift */; }; 23 | CD1B54BB28E86AD500C4C666 /* ControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54BA28E86AD500C4C666 /* ControlView.swift */; }; 24 | CD1B54BD28E86B0200C4C666 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54BC28E86B0200C4C666 /* AudioPlayer.swift */; }; 25 | CD1B54BF28E86B3500C4C666 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1B54BE28E86B3500C4C666 /* File.swift */; }; 26 | CD4FE6E728E7987B0078D691 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD4FE6E628E7987B0078D691 /* Assets.xcassets */; }; 27 | CD4FE6EB28E7987B0078D691 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD4FE6EA28E7987B0078D691 /* Preview Assets.xcassets */; }; 28 | CD4FE6FF28E799270078D691 /* PlotAudio_ClockSample.m4a in Resources */ = {isa = PBXBuildFile; fileRef = CD4FE6F728E799270078D691 /* PlotAudio_ClockSample.m4a */; }; 29 | CD4FE70028E799270078D691 /* PlotAudio_PianoSample.m4a in Resources */ = {isa = PBXBuildFile; fileRef = CD4FE6F828E799270078D691 /* PlotAudio_PianoSample.m4a */; }; 30 | CD4FE70128E799270078D691 /* PlotAudio_TubaSample.m4a in Resources */ = {isa = PBXBuildFile; fileRef = CD4FE6F928E799270078D691 /* PlotAudio_TubaSample.m4a */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | CD1B549028E7B84300C4C666 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 35 | CD1B549228E7B92400C4C666 /* Video Files */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Video Files"; sourceTree = ""; }; 36 | CD1B549328E7B92400C4C666 /* Audio Files */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Audio Files"; sourceTree = ""; }; 37 | CD1B54A628E8681E00C4C666 /* PlotAudioConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudioConstants.swift; sourceTree = ""; }; 38 | CD1B54A828E8685800C4C666 /* PlotAudioObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudioObservable.swift; sourceTree = ""; }; 39 | CD1B54AA28E868AF00C4C666 /* AVAsset-extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset-extensions.swift"; sourceTree = ""; }; 40 | CD1B54AC28E8690D00C4C666 /* PlotAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudio.swift; sourceTree = ""; }; 41 | CD1B54AE28E8694000C4C666 /* PlotAudioWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudioWaveformView.swift; sourceTree = ""; }; 42 | CD1B54B028E8698900C4C666 /* PlotAudioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudioApp.swift; sourceTree = ""; }; 43 | CD1B54B228E869FB00C4C666 /* PlotAudioAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlotAudioAppView.swift; sourceTree = ""; }; 44 | CD1B54B428E86A2600C4C666 /* FileTableObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTableObservable.swift; sourceTree = ""; }; 45 | CD1B54B628E86A4800C4C666 /* FileTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTableView.swift; sourceTree = ""; }; 46 | CD1B54B828E86A7200C4C666 /* ControlViewObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlViewObservable.swift; sourceTree = ""; }; 47 | CD1B54BA28E86AD500C4C666 /* ControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = ""; }; 48 | CD1B54BC28E86B0200C4C666 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 49 | CD1B54BE28E86B3500C4C666 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 50 | CD4FE6DF28E7987A0078D691 /* PlotAudio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PlotAudio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | CD4FE6E628E7987B0078D691 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | CD4FE6E828E7987B0078D691 /* PlotAudio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PlotAudio.entitlements; sourceTree = ""; }; 53 | CD4FE6EA28E7987B0078D691 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 54 | CD4FE6F728E799270078D691 /* PlotAudio_ClockSample.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = PlotAudio_ClockSample.m4a; sourceTree = ""; }; 55 | CD4FE6F828E799270078D691 /* PlotAudio_PianoSample.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = PlotAudio_PianoSample.m4a; sourceTree = ""; }; 56 | CD4FE6F928E799270078D691 /* PlotAudio_TubaSample.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = PlotAudio_TubaSample.m4a; sourceTree = ""; }; 57 | CD4FE70328E79FCF0078D691 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | CD4FE6DC28E7987A0078D691 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | CD4FE6D628E7987A0078D691 = { 72 | isa = PBXGroup; 73 | children = ( 74 | CD4FE6E128E7987A0078D691 /* PlotAudio */, 75 | CD4FE6E028E7987A0078D691 /* Products */, 76 | ); 77 | sourceTree = ""; 78 | }; 79 | CD4FE6E028E7987A0078D691 /* Products */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | CD4FE6DF28E7987A0078D691 /* PlotAudio.app */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | CD4FE6E128E7987A0078D691 /* PlotAudio */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | CD4FE70328E79FCF0078D691 /* Info.plist */, 91 | CD4FE6F128E799270078D691 /* PlotAudio */, 92 | CD1B54B028E8698900C4C666 /* PlotAudioApp.swift */, 93 | CD1B54B228E869FB00C4C666 /* PlotAudioAppView.swift */, 94 | CD1B54B428E86A2600C4C666 /* FileTableObservable.swift */, 95 | CD1B54B628E86A4800C4C666 /* FileTableView.swift */, 96 | CD1B54B828E86A7200C4C666 /* ControlViewObservable.swift */, 97 | CD1B54BA28E86AD500C4C666 /* ControlView.swift */, 98 | CD1B54BC28E86B0200C4C666 /* AudioPlayer.swift */, 99 | CD1B54BE28E86B3500C4C666 /* File.swift */, 100 | CD1B549328E7B92400C4C666 /* Audio Files */, 101 | CD1B549228E7B92400C4C666 /* Video Files */, 102 | CD4FE6E628E7987B0078D691 /* Assets.xcassets */, 103 | CD4FE6E828E7987B0078D691 /* PlotAudio.entitlements */, 104 | CD1B549028E7B84300C4C666 /* Credits.rtf */, 105 | CD4FE6E928E7987B0078D691 /* Preview Content */, 106 | ); 107 | path = PlotAudio; 108 | sourceTree = ""; 109 | }; 110 | CD4FE6E928E7987B0078D691 /* Preview Content */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | CD4FE6EA28E7987B0078D691 /* Preview Assets.xcassets */, 114 | ); 115 | path = "Preview Content"; 116 | sourceTree = ""; 117 | }; 118 | CD4FE6F128E799270078D691 /* PlotAudio */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | CD4FE6F628E799270078D691 /* Audio Samples */, 122 | CD1B54A628E8681E00C4C666 /* PlotAudioConstants.swift */, 123 | CD1B54A828E8685800C4C666 /* PlotAudioObservable.swift */, 124 | CD1B54AE28E8694000C4C666 /* PlotAudioWaveformView.swift */, 125 | CD1B54AC28E8690D00C4C666 /* PlotAudio.swift */, 126 | CD1B54AA28E868AF00C4C666 /* AVAsset-extensions.swift */, 127 | ); 128 | path = PlotAudio; 129 | sourceTree = ""; 130 | }; 131 | CD4FE6F628E799270078D691 /* Audio Samples */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | CD4FE6F728E799270078D691 /* PlotAudio_ClockSample.m4a */, 135 | CD4FE6F828E799270078D691 /* PlotAudio_PianoSample.m4a */, 136 | CD4FE6F928E799270078D691 /* PlotAudio_TubaSample.m4a */, 137 | ); 138 | path = "Audio Samples"; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | CD4FE6DE28E7987A0078D691 /* PlotAudio */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = CD4FE6EE28E7987B0078D691 /* Build configuration list for PBXNativeTarget "PlotAudio" */; 147 | buildPhases = ( 148 | CD4FE6DB28E7987A0078D691 /* Sources */, 149 | CD4FE6DC28E7987A0078D691 /* Frameworks */, 150 | CD4FE6DD28E7987A0078D691 /* Resources */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = PlotAudio; 157 | productName = PlotAudio; 158 | productReference = CD4FE6DF28E7987A0078D691 /* PlotAudio.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | /* End PBXNativeTarget section */ 162 | 163 | /* Begin PBXProject section */ 164 | CD4FE6D728E7987A0078D691 /* Project object */ = { 165 | isa = PBXProject; 166 | attributes = { 167 | BuildIndependentTargetsInParallel = 1; 168 | LastSwiftUpdateCheck = 1400; 169 | LastUpgradeCheck = 1400; 170 | ORGANIZATIONNAME = "Limit Point LLC"; 171 | TargetAttributes = { 172 | CD4FE6DE28E7987A0078D691 = { 173 | CreatedOnToolsVersion = 14.0.1; 174 | }; 175 | }; 176 | }; 177 | buildConfigurationList = CD4FE6DA28E7987A0078D691 /* Build configuration list for PBXProject "PlotAudio" */; 178 | compatibilityVersion = "Xcode 14.0"; 179 | developmentRegion = en; 180 | hasScannedForEncodings = 0; 181 | knownRegions = ( 182 | en, 183 | Base, 184 | ); 185 | mainGroup = CD4FE6D628E7987A0078D691; 186 | productRefGroup = CD4FE6E028E7987A0078D691 /* Products */; 187 | projectDirPath = ""; 188 | projectRoot = ""; 189 | targets = ( 190 | CD4FE6DE28E7987A0078D691 /* PlotAudio */, 191 | ); 192 | }; 193 | /* End PBXProject section */ 194 | 195 | /* Begin PBXResourcesBuildPhase section */ 196 | CD4FE6DD28E7987A0078D691 /* Resources */ = { 197 | isa = PBXResourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | CD1B549528E7B92400C4C666 /* Audio Files in Resources */, 201 | CD4FE6EB28E7987B0078D691 /* Preview Assets.xcassets in Resources */, 202 | CD1B549128E7B84300C4C666 /* Credits.rtf in Resources */, 203 | CD4FE70128E799270078D691 /* PlotAudio_TubaSample.m4a in Resources */, 204 | CD4FE6FF28E799270078D691 /* PlotAudio_ClockSample.m4a in Resources */, 205 | CD4FE70028E799270078D691 /* PlotAudio_PianoSample.m4a in Resources */, 206 | CD1B549428E7B92400C4C666 /* Video Files in Resources */, 207 | CD4FE6E728E7987B0078D691 /* Assets.xcassets in Resources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXResourcesBuildPhase section */ 212 | 213 | /* Begin PBXSourcesBuildPhase section */ 214 | CD4FE6DB28E7987A0078D691 /* Sources */ = { 215 | isa = PBXSourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | CD1B54B728E86A4800C4C666 /* FileTableView.swift in Sources */, 219 | CD1B54B528E86A2600C4C666 /* FileTableObservable.swift in Sources */, 220 | CD1B54B128E8698900C4C666 /* PlotAudioApp.swift in Sources */, 221 | CD1B54A928E8685800C4C666 /* PlotAudioObservable.swift in Sources */, 222 | CD1B54AD28E8690D00C4C666 /* PlotAudio.swift in Sources */, 223 | CD1B54AB28E868AF00C4C666 /* AVAsset-extensions.swift in Sources */, 224 | CD1B54B328E869FB00C4C666 /* PlotAudioAppView.swift in Sources */, 225 | CD1B54B928E86A7200C4C666 /* ControlViewObservable.swift in Sources */, 226 | CD1B54BB28E86AD500C4C666 /* ControlView.swift in Sources */, 227 | CD1B54A728E8681E00C4C666 /* PlotAudioConstants.swift in Sources */, 228 | CD1B54BF28E86B3500C4C666 /* File.swift in Sources */, 229 | CD1B54BD28E86B0200C4C666 /* AudioPlayer.swift in Sources */, 230 | CD1B54AF28E8694000C4C666 /* PlotAudioWaveformView.swift in Sources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXSourcesBuildPhase section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | CD4FE6EC28E7987B0078D691 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 288 | MTL_FAST_MATH = YES; 289 | ONLY_ACTIVE_ARCH = YES; 290 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 291 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 292 | }; 293 | name = Debug; 294 | }; 295 | CD4FE6ED28E7987B0078D691 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ANALYZER_NONNULL = YES; 300 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 301 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 302 | CLANG_ENABLE_MODULES = YES; 303 | CLANG_ENABLE_OBJC_ARC = YES; 304 | CLANG_ENABLE_OBJC_WEAK = YES; 305 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 306 | CLANG_WARN_BOOL_CONVERSION = YES; 307 | CLANG_WARN_COMMA = YES; 308 | CLANG_WARN_CONSTANT_CONVERSION = YES; 309 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 310 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 311 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INFINITE_RECURSION = YES; 315 | CLANG_WARN_INT_CONVERSION = YES; 316 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 317 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 318 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 319 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 320 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 325 | CLANG_WARN_UNREACHABLE_CODE = YES; 326 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 327 | COPY_PHASE_STRIP = NO; 328 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 329 | ENABLE_NS_ASSERTIONS = NO; 330 | ENABLE_STRICT_OBJC_MSGSEND = YES; 331 | GCC_C_LANGUAGE_STANDARD = gnu11; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 334 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 335 | GCC_WARN_UNDECLARED_SELECTOR = YES; 336 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 337 | GCC_WARN_UNUSED_FUNCTION = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | MTL_ENABLE_DEBUG_INFO = NO; 340 | MTL_FAST_MATH = YES; 341 | SWIFT_COMPILATION_MODE = wholemodule; 342 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 343 | }; 344 | name = Release; 345 | }; 346 | CD4FE6EF28E7987B0078D691 /* Debug */ = { 347 | isa = XCBuildConfiguration; 348 | buildSettings = { 349 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 350 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 351 | CODE_SIGN_ENTITLEMENTS = PlotAudio/PlotAudio.entitlements; 352 | CODE_SIGN_STYLE = Automatic; 353 | CURRENT_PROJECT_VERSION = 1; 354 | DEVELOPMENT_ASSET_PATHS = "\"PlotAudio/Preview Content\""; 355 | DEVELOPMENT_TEAM = GZABHNF58X; 356 | ENABLE_HARDENED_RUNTIME = YES; 357 | ENABLE_PREVIEWS = YES; 358 | GENERATE_INFOPLIST_FILE = YES; 359 | INFOPLIST_FILE = PlotAudio/Info.plist; 360 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 361 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 362 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 363 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 364 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 365 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 366 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 367 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 369 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 370 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 371 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 372 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 373 | MACOSX_DEPLOYMENT_TARGET = 12.3; 374 | MARKETING_VERSION = 1.0; 375 | PRODUCT_BUNDLE_IDENTIFIER = "com.limit-point.PlotAudio"; 376 | PRODUCT_NAME = "$(TARGET_NAME)"; 377 | SDKROOT = auto; 378 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 379 | SWIFT_EMIT_LOC_STRINGS = YES; 380 | SWIFT_VERSION = 5.0; 381 | TARGETED_DEVICE_FAMILY = "1,2"; 382 | }; 383 | name = Debug; 384 | }; 385 | CD4FE6F028E7987B0078D691 /* Release */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 390 | CODE_SIGN_ENTITLEMENTS = PlotAudio/PlotAudio.entitlements; 391 | CODE_SIGN_STYLE = Automatic; 392 | CURRENT_PROJECT_VERSION = 1; 393 | DEVELOPMENT_ASSET_PATHS = "\"PlotAudio/Preview Content\""; 394 | DEVELOPMENT_TEAM = GZABHNF58X; 395 | ENABLE_HARDENED_RUNTIME = YES; 396 | ENABLE_PREVIEWS = YES; 397 | GENERATE_INFOPLIST_FILE = YES; 398 | INFOPLIST_FILE = PlotAudio/Info.plist; 399 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 400 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 401 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 402 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 403 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 404 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 405 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 406 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 407 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 408 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 409 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 410 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 411 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 412 | MACOSX_DEPLOYMENT_TARGET = 12.3; 413 | MARKETING_VERSION = 1.0; 414 | PRODUCT_BUNDLE_IDENTIFIER = "com.limit-point.PlotAudio"; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | SDKROOT = auto; 417 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 418 | SWIFT_EMIT_LOC_STRINGS = YES; 419 | SWIFT_VERSION = 5.0; 420 | TARGETED_DEVICE_FAMILY = "1,2"; 421 | }; 422 | name = Release; 423 | }; 424 | /* End XCBuildConfiguration section */ 425 | 426 | /* Begin XCConfigurationList section */ 427 | CD4FE6DA28E7987A0078D691 /* Build configuration list for PBXProject "PlotAudio" */ = { 428 | isa = XCConfigurationList; 429 | buildConfigurations = ( 430 | CD4FE6EC28E7987B0078D691 /* Debug */, 431 | CD4FE6ED28E7987B0078D691 /* Release */, 432 | ); 433 | defaultConfigurationIsVisible = 0; 434 | defaultConfigurationName = Release; 435 | }; 436 | CD4FE6EE28E7987B0078D691 /* Build configuration list for PBXNativeTarget "PlotAudio" */ = { 437 | isa = XCConfigurationList; 438 | buildConfigurations = ( 439 | CD4FE6EF28E7987B0078D691 /* Debug */, 440 | CD4FE6F028E7987B0078D691 /* Release */, 441 | ); 442 | defaultConfigurationIsVisible = 0; 443 | defaultConfigurationName = Release; 444 | }; 445 | /* End XCConfigurationList section */ 446 | }; 447 | rootObject = CD4FE6D728E7987A0078D691 /* Project object */; 448 | } 449 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/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 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PlotAudio-1024.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "PlotAudio-16.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "PlotAudio-32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "PlotAudio-32.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "PlotAudio-64.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "PlotAudio-128.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "PlotAudio-256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "PlotAudio-256.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "PlotAudio-512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "PlotAudio-512.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "PlotAudio-1024.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-1024.jpg -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-1024.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-128.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-16.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-256 1.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-256.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-32 1.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-32.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-512 1.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-512.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Assets.xcassets/AppIcon.appiconset/PlotAudio-64.png -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/2 Distinct Channels.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/2 Distinct Channels.aif -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Accelerating.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Accelerating.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Before September - 01.01.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Before September - 01.01.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Channels - 5.1 (C L R Ls Rs LFE).mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Channels - 5.1 (C L R Ls Rs LFE).mp4 -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Channels - 7.1 (L R C LFE Rls Rrs Ls Rs).wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Channels - 7.1 (L R C LFE Rls Rrs Ls Rs).wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Clock.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Clock.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Crescendo.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Crescendo.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Distorted Guitar.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Distorted Guitar.wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Fireworks.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Fireworks.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Guitar Solo.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Guitar Solo.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Heartbeat.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Heartbeat.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/I'm Afraid I Can't Do That.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/I'm Afraid I Can't Do That.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Noisy Signal With Break.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Noisy Signal With Break.mp4 -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Piano.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Piano.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Pop.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Pop.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Ring Tone 5.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Ring Tone 5.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Skidding.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Skidding.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Spinning.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Spinning.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Squeeze.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Squeeze.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Stomp Clap Percussion.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Stomp Clap Percussion.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Today's Sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Today's Sound.wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Todays Sound 2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Todays Sound 2.wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Todays Sound 3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Todays Sound 3.wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Tone - 500 hz Single Channel.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Tone - 500 hz Single Channel.wav -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Triangle.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Triangle.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Tuba.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Tuba.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Undulating Harp.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Undulating Harp.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Waves.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Waves.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Wavvy.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Wavvy.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Whoosh 1.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Whoosh 1.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Whoosh 2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Whoosh 2.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Wind.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Wind.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/Xylophone.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/Xylophone.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Audio Files/zzZ.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Audio Files/zzZ.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/AudioPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioPlayer.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#AudioPlayer 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import AVFoundation 13 | 14 | protocol AudioPlayerDelegate: AnyObject { // AnyObject - required for AudioPlayer's weak reference for delegate (only reference types can have weak reference to prevent retain cycle) 15 | func audioPlayProgress(_ player:AudioPlayer?, percent: CGFloat) 16 | func audioPlayDone(_ player:AudioPlayer?, percent: CGFloat) 17 | } 18 | 19 | class AudioPlayer: NSObject, AVAudioPlayerDelegate { 20 | 21 | var audioPlayer: AVAudioPlayer? 22 | var timer:Timer? 23 | 24 | weak var delegate: AudioPlayerDelegate? 25 | 26 | deinit { 27 | stopTimer() 28 | } 29 | 30 | func isPlaying() -> Bool { 31 | 32 | guard let player = self.audioPlayer else { 33 | return false 34 | } 35 | 36 | return player.isPlaying 37 | } 38 | 39 | func stopPlayingAudio() { 40 | if let audioPlayer = audioPlayer, audioPlayer.isPlaying { 41 | audioPlayer.stop() // this won't invoke 'audioPlayerDidFinishPlaying' 42 | stopTimer() 43 | delegate?.audioPlayDone(self, percent: audioPlayer.currentTime / audioPlayer.duration) 44 | } 45 | } 46 | 47 | func pausePlayingAudio() { 48 | if let audioPlayer = audioPlayer, audioPlayer.isPlaying { 49 | stopTimer() 50 | audioPlayer.pause() 51 | } 52 | } 53 | 54 | func stopTimer() { 55 | timer?.invalidate() 56 | timer = nil 57 | } 58 | 59 | func startTimer() { 60 | let schedule = { 61 | 62 | self.timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in 63 | 64 | if let player = self?.audioPlayer { 65 | let percent = player.currentTime / player.duration 66 | 67 | self?.delegate?.audioPlayProgress(self, percent: CGFloat(percent)) 68 | } 69 | 70 | } 71 | } 72 | 73 | if Thread.isMainThread { 74 | schedule() 75 | } 76 | else { 77 | DispatchQueue.main.sync { 78 | schedule() 79 | } 80 | } 81 | } 82 | 83 | func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { 84 | stopTimer() 85 | delegate?.audioPlayDone(self, percent: 0) 86 | } 87 | 88 | func play(percent:Double) { 89 | 90 | guard let player = audioPlayer else { return } 91 | 92 | stopTimer() 93 | 94 | let delay:TimeInterval = 0.01 95 | let now = player.deviceCurrentTime 96 | let timeToPlay = now + delay 97 | 98 | player.currentTime = percent * player.duration 99 | 100 | startTimer() 101 | 102 | player.play(atTime: timeToPlay) 103 | } 104 | 105 | func playAudioURL(_ url:URL, percent:Double = 0) -> Bool { 106 | 107 | if FileManager.default.fileExists(atPath: url.path) == false { 108 | Swift.print("There is no audio file to play.") 109 | return false 110 | } 111 | 112 | do { 113 | stopTimer() 114 | 115 | audioPlayer = try AVAudioPlayer(contentsOf: url) 116 | guard let player = audioPlayer else { return false } 117 | 118 | player.delegate = self 119 | player.prepareToPlay() 120 | 121 | play(percent: percent) 122 | } 123 | catch let error { 124 | print(error.localizedDescription) 125 | return false 126 | } 127 | 128 | return true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/ControlView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControlView.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#ControlViewObservable 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import SwiftUI 13 | 14 | struct commonButtonModifier: ViewModifier { 15 | func body(content: Content) -> some View { 16 | content 17 | .buttonStyle(BorderlessButtonStyle()) 18 | .font(.system(size: 32, weight: .light)) 19 | .frame(width: 44, height: 44) 20 | } 21 | } 22 | 23 | struct PlayerControlsView: View { 24 | 25 | @ObservedObject var controlViewObservable: ControlViewObservable 26 | 27 | var body: some View { 28 | if let filename = controlViewObservable.fileTableObservable.currentFile?.url.lastPathComponent { 29 | Text(filename) 30 | } 31 | 32 | Text("\(controlViewObservable.plotAudioObservable.currentTimeString())") 33 | .monospacedDigit() 34 | 35 | HStack { 36 | 37 | if controlViewObservable.isPlaying { 38 | if controlViewObservable.isPaused { 39 | Button(action: { controlViewObservable.resumeMedia() }, label: { 40 | Label("Resume", systemImage: "play.rectangle.fill") 41 | }) 42 | .modifier(commonButtonModifier()) 43 | } 44 | else { 45 | Button(action: { controlViewObservable.pauseMedia() }, label: { 46 | Label("Pause", systemImage: "pause.circle.fill") 47 | }) 48 | .modifier(commonButtonModifier()) 49 | .disabled(controlViewObservable.isPlaying == false) 50 | } 51 | } 52 | else { 53 | Button(action: { controlViewObservable.playSelectedMedia() }, label: { 54 | Label("Play", systemImage: "play.rectangle") 55 | }) 56 | .modifier(commonButtonModifier()) 57 | } 58 | 59 | Button(action: { controlViewObservable.stopMedia() }, label: { 60 | Label("Stop", systemImage: "stop.circle.fill") 61 | }) 62 | .modifier(commonButtonModifier()) 63 | .disabled(controlViewObservable.isPlaying == false && controlViewObservable.isPaused == false) 64 | } 65 | } 66 | } 67 | 68 | struct AspectFactorView: View { 69 | 70 | @ObservedObject var controlViewObservable: ControlViewObservable 71 | 72 | @State private var editingChanged = false 73 | 74 | var body: some View { 75 | VStack { 76 | Text(String(format: "%.2f", controlViewObservable.aspectFactor)) 77 | .foregroundColor(editingChanged ? .red : .blue) 78 | 79 | Slider( 80 | value: $controlViewObservable.aspectFactor, 81 | in: 0.5...3.0 82 | ) { 83 | Text("Aspect Factor") 84 | } minimumValueLabel: { 85 | Text("0.5") 86 | } maximumValueLabel: { 87 | Text("3") 88 | } onEditingChanged: { editing in 89 | editingChanged = editing 90 | } 91 | 92 | Text("Aspect Factor") 93 | } 94 | .padding() 95 | } 96 | } 97 | 98 | struct PlotOptionsView: View { 99 | 100 | @ObservedObject var controlViewObservable: ControlViewObservable 101 | 102 | var body: some View { 103 | HStack { 104 | Toggle(isOn: $controlViewObservable.plotAudioObservable.pathGradient) { 105 | Text("Gradient") 106 | } 107 | .padding() 108 | 109 | Toggle(isOn: $controlViewObservable.plotAudioObservable.fillIndicator) { 110 | Text("Fill Indicator") 111 | } 112 | .padding() 113 | } 114 | .padding() 115 | 116 | AspectFactorView(controlViewObservable: controlViewObservable) 117 | 118 | Toggle(isOn: $controlViewObservable.plotAudioObservable.pathFrame) { 119 | Text("Frame Path") 120 | } 121 | .padding() 122 | 123 | BarsOptionsView(controlViewObservable: controlViewObservable) 124 | } 125 | } 126 | 127 | struct BarsOptionsView: View { 128 | 129 | @ObservedObject var controlViewObservable: ControlViewObservable 130 | 131 | var body: some View { 132 | HStack { 133 | VStack { 134 | HStack { 135 | Text("Bar Width") 136 | Picker(selection: $controlViewObservable.plotAudioObservable.barWidth, 137 | label: EmptyView(), 138 | content: { 139 | ForEach(kPlotAudioBarWidths, id: \.self) { 140 | Text("\($0)").tag($0) 141 | } 142 | }) 143 | .pickerStyle(.automatic) 144 | .frame(width: 100) 145 | } 146 | 147 | } 148 | 149 | VStack { 150 | HStack { 151 | Text("Spacing") 152 | Picker(selection: $controlViewObservable.plotAudioObservable.barSpacing, 153 | label: EmptyView(), 154 | content: { 155 | ForEach(kPlotAudioBarSpacings, id: \.self) { 156 | Text("\($0)").tag($0) 157 | } 158 | }) 159 | .pickerStyle(.automatic) 160 | .frame(width: 100) 161 | } 162 | 163 | } 164 | } 165 | } 166 | } 167 | 168 | struct PreprocessOptionsView: View { 169 | 170 | @ObservedObject var controlViewObservable: ControlViewObservable 171 | 172 | var body: some View { 173 | Picker("Noise Floor", selection: $controlViewObservable.plotAudioObservable.noiseFloor) { 174 | Text("-90").tag(-90) 175 | Text("-80").tag(-80) 176 | Text("-70").tag(-70) 177 | Text("-60").tag(-60) 178 | Text("-50").tag(-50) 179 | } 180 | .pickerStyle(.segmented) 181 | .frame(width: 300) 182 | 183 | #if os(iOS) 184 | Text("Noise Floor") 185 | .font(.footnote).frame(width: 300) 186 | .padding() 187 | #endif 188 | 189 | Toggle(isOn: $controlViewObservable.plotAudioObservable.pathAntialias) { 190 | Text("Decimate with averaging (anti-alias)") 191 | } 192 | .padding() 193 | 194 | // Changing downsampleRateSeconds will not affect the plot, only the memory required to process data 195 | Picker("Downsample Rate", selection: $controlViewObservable.plotAudioObservable.downsampleRateSeconds) { 196 | Text("1").tag(Int?.some(1)) 197 | Text("10").tag(Int?.some(10)) 198 | Text("60").tag(Int?.some(60)) 199 | Text("300").tag(Int?.some(300)) 200 | Text("Whole").tag(nil as Int?) 201 | } 202 | .pickerStyle(.segmented) 203 | .frame(width: 300) 204 | .padding() 205 | 206 | #if os(iOS) 207 | Text("Downsample Rate (seconds)") 208 | .font(.footnote).frame(width: 300) 209 | .padding() 210 | #endif 211 | } 212 | } 213 | 214 | struct ControlView: View { 215 | 216 | @ObservedObject var controlViewObservable: ControlViewObservable 217 | 218 | var body: some View { 219 | 220 | VStack { 221 | 222 | PlayerControlsView(controlViewObservable: controlViewObservable) 223 | 224 | PlotOptionsView(controlViewObservable: controlViewObservable) 225 | 226 | PreprocessOptionsView(controlViewObservable: controlViewObservable) 227 | } 228 | } 229 | } 230 | 231 | struct ControlView_Previews: PreviewProvider { 232 | static var previews: some View { 233 | ControlView(controlViewObservable: ControlViewObservable(plotAudioObservable: PlotAudioObservable(), fileTableObservable: FileTableObservable())) 234 | } 235 | } 236 | 237 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/ControlViewObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControlViewObservable.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#ControlViewObservable 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import AVFoundation 14 | import Combine 15 | 16 | let kPreferredTimeScale:Int32 = 64000 17 | 18 | class ControlViewObservable: ObservableObject, AudioPlayerDelegate, PlotAudioDelegate { 19 | 20 | @Published var mediaType:MediaType = .audio 21 | 22 | @Published var plotAudioObservable: PlotAudioObservable 23 | @Published var fileTableObservable: FileTableObservable 24 | 25 | var audioPlayer:AudioPlayer 26 | 27 | var videoPlayer:AVPlayer 28 | var videoPlayerPeriodicTimeObserver:Any? 29 | var videoPlayerItem:AVPlayerItem 30 | var videoDuration:Double = 0 31 | 32 | @Published var isPaused:Bool = false 33 | @Published var isPlaying:Bool = false 34 | 35 | @Published var aspectFactor:Double = 1.0 36 | var originalFrameSize:CGSize? 37 | 38 | var cancelBag = Set() 39 | 40 | init(plotAudioObservable:PlotAudioObservable, fileTableObservable: FileTableObservable) { 41 | 42 | self.plotAudioObservable = plotAudioObservable 43 | self.fileTableObservable = fileTableObservable 44 | 45 | audioPlayer = AudioPlayer() 46 | 47 | let asset = AVAsset(url: fileTableObservable.files[0].url) 48 | videoDuration = asset.duration.seconds 49 | videoPlayer = AVPlayer(playerItem: AVPlayerItem(asset: asset)) 50 | videoPlayerItem = videoPlayer.currentItem! // used in seek 51 | 52 | videoPlayerPeriodicTimeObserver = self.videoPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: nil) { [weak self] cmTime in 53 | if let duration = self?.videoDuration { 54 | self?.plotAudioObservable.indicatorPercent = cmTime.seconds / duration 55 | } 56 | } 57 | 58 | audioPlayer.delegate = self 59 | 60 | self.plotAudioObservable.plotAudioDelegate = self 61 | 62 | plotAudioObservable.objectWillChange.sink { [weak self] in 63 | self?.objectWillChange.send() 64 | }.store(in: &cancelBag) 65 | 66 | fileTableObservable.objectWillChange.sink { [weak self] in 67 | self?.objectWillChange.send() 68 | }.store(in: &cancelBag) 69 | 70 | $mediaType.sink { [weak self] newMediaType in 71 | if newMediaType != self?.mediaType { 72 | DispatchQueue.main.async { 73 | self?.plotAudioObservable.plotAudio() 74 | } 75 | } 76 | 77 | }.store(in: &cancelBag) 78 | 79 | $aspectFactor.sink { [weak self] newAspectFactor in 80 | if newAspectFactor != self?.aspectFactor { 81 | DispatchQueue.main.async { 82 | 83 | if self?.originalFrameSize == nil { 84 | self?.originalFrameSize = self?.plotAudioObservable.frameSize 85 | } 86 | 87 | if let originalFrameSize = self?.originalFrameSize, let aspectFactor = self?.aspectFactor { 88 | let newHeight = originalFrameSize.height * aspectFactor 89 | self?.plotAudioObservable.frameSize = CGSize(width: originalFrameSize.width, height: newHeight) 90 | } 91 | } 92 | } 93 | 94 | }.store(in: &cancelBag) 95 | } 96 | 97 | deinit { 98 | print("ControlViewObservable deinit") 99 | } 100 | 101 | func updatePlayer(_ url:URL) { 102 | let asset = AVAsset(url: url) 103 | videoDuration = asset.duration.seconds 104 | self.videoPlayerItem = AVPlayerItem(asset: asset) 105 | self.videoPlayer.replaceCurrentItem(with: self.videoPlayerItem) 106 | if let periodicTimeObserver = self.videoPlayerPeriodicTimeObserver { 107 | self.videoPlayer.removeTimeObserver(periodicTimeObserver) 108 | } 109 | self.videoPlayerPeriodicTimeObserver = self.videoPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: nil) { [weak self] cmTime in 110 | if let duration = self?.videoDuration { 111 | if cmTime.seconds == duration { 112 | self?.isPlaying = false 113 | self?.isPaused = false 114 | self?.plotAudioObservable.indicatorPercent = 0 115 | } 116 | else { 117 | self?.plotAudioObservable.indicatorPercent = cmTime.seconds / duration 118 | } 119 | 120 | } 121 | } 122 | } 123 | 124 | func playSelectedMedia() { 125 | isPlaying = true 126 | isPaused = false 127 | if let currentFile = fileTableObservable.currentFile { 128 | switch mediaType { 129 | case .audio: 130 | let _ = audioPlayer.playAudioURL(currentFile.url) 131 | case .video: 132 | updatePlayer(currentFile.url) 133 | videoPlayer.play() 134 | } 135 | } 136 | } 137 | 138 | // AudioPlayerDelegate 139 | // set delegate in init! 140 | func audioPlayDone(_ player: AudioPlayer?, percent: CGFloat) { 141 | if isPlaying == false { 142 | plotAudioObservable.indicatorPercent = 0 143 | } 144 | else { 145 | isPlaying = false 146 | isPaused = false 147 | plotAudioObservable.indicatorPercent = percent 148 | } 149 | } 150 | 151 | // AudioPlayerDelegate 152 | func audioPlayProgress(_ player: AudioPlayer?, percent: CGFloat) { 153 | plotAudioObservable.indicatorPercent = percent 154 | } 155 | 156 | func pauseMedia() { 157 | switch mediaType { 158 | case .audio: 159 | audioPlayer.pausePlayingAudio() 160 | case .video: 161 | videoPlayer.pause() 162 | } 163 | isPaused = true 164 | } 165 | 166 | func resumeMedia() { 167 | switch mediaType { 168 | case .audio: 169 | audioPlayer.play(percent: plotAudioObservable.indicatorPercent) 170 | case .video: 171 | videoPlayer.play() 172 | } 173 | isPaused = false 174 | } 175 | 176 | func stopMedia() { 177 | if isPaused { // prevents delay if audio player paused 178 | resumeMedia() 179 | } 180 | isPlaying = false 181 | isPaused = false 182 | switch mediaType { 183 | case .audio: 184 | audioPlayer.stopPlayingAudio() 185 | case .video: 186 | videoPlayer.pause() 187 | videoPlayer.seek(to: CMTime.zero, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) 188 | } 189 | } 190 | 191 | // PlotAudioDelegate 192 | // set delegate in init! 193 | func plotAudioDragChanged(_ value: CGFloat) { 194 | switch mediaType { 195 | case .audio: 196 | if isPlaying { 197 | audioPlayer.stopPlayingAudio() 198 | } 199 | isPlaying = false 200 | isPaused = false 201 | case .video: 202 | videoPlayer.pause() 203 | isPlaying = false 204 | isPaused = false 205 | videoPlayer.seek(to: CMTimeMakeWithSeconds(value * videoDuration, preferredTimescale: kPreferredTimeScale), toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) 206 | } 207 | 208 | } 209 | 210 | // PlotAudioDelegate 211 | func plotAudioDragEnded(_ value: CGFloat) { 212 | switch mediaType { 213 | case .audio: 214 | isPlaying = true 215 | if let currentFile = fileTableObservable.currentFile { 216 | let _ = audioPlayer.playAudioURL(currentFile.url, percent: value) 217 | } 218 | case .video: 219 | isPlaying = true 220 | videoPlayer.play() 221 | } 222 | 223 | } 224 | 225 | // PlotAudioDelegate 226 | func plotAudioDidFinishPlotting() { 227 | self.playSelectedMedia() 228 | } 229 | } 230 | 231 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf190 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \viewkind0 5 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\qc 6 | 7 | \f0\fs22 \cf0 Limit Point LLC} -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/ 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import AVFoundation 14 | 15 | struct File: Identifiable { 16 | var url:URL 17 | var id = UUID() 18 | var duration:String { 19 | let asset = AVAsset(url: url) 20 | return asset.durationText 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/FileTableObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileTableObservable.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#FileTableObservable 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import Combine 14 | 15 | enum MediaType: String, CaseIterable, Identifiable { 16 | case audio, video 17 | var id: Self { self } 18 | } 19 | 20 | let kAudioFilesSubdirectory = "Audio Files" 21 | let kVideoFilesSubdirectory = "Video Files" 22 | 23 | let kAudioExtensions: [String] = ["aac", "m4a", "aiff", "aif", "wav", "mp3", "caf", "m4r", "flac", "mp4"] 24 | let kVideoExtensions: [String] = ["mov"] 25 | 26 | class FileTableObservable: ObservableObject { 27 | 28 | @Published var mediaType:MediaType = .audio 29 | 30 | @Published var files:[File] = [] 31 | @Published var selectedFileID:UUID? 32 | @Published var currentFile:File? 33 | 34 | var cancelBag = Set() 35 | 36 | func selectIndex(_ index:Int) { 37 | currentFile = files[index] 38 | selectedFileID = files[index].id 39 | } 40 | 41 | func loadFiles(extensions:[String] = kAudioExtensions, subdirectory:String = kAudioFilesSubdirectory) { 42 | files = [] 43 | for ext in extensions { 44 | if let urls = Bundle.main.urls(forResourcesWithExtension: ext, subdirectory: subdirectory) { 45 | for url in urls { 46 | files.append(File(url: url)) 47 | } 48 | } 49 | } 50 | files.sort(by: { $0.url.lastPathComponent > $1.url.lastPathComponent }) 51 | } 52 | 53 | func loadFiles(mediaType:MediaType) { 54 | switch mediaType { 55 | case .audio: 56 | loadFiles(extensions:kAudioExtensions, subdirectory:kAudioFilesSubdirectory) 57 | case .video: 58 | loadFiles(extensions:kVideoExtensions, subdirectory:kVideoFilesSubdirectory) 59 | } 60 | } 61 | 62 | init() { 63 | loadFiles(mediaType: .audio) 64 | 65 | $selectedFileID.sink { [weak self] newSelectedFileID in 66 | if newSelectedFileID != self?.selectedFileID, let index = self?.files.firstIndex(where: {$0.id == newSelectedFileID}) { 67 | self?.currentFile = self?.files[index] 68 | } 69 | 70 | }.store(in: &cancelBag) 71 | 72 | $mediaType.sink { [weak self] newMediaType in 73 | if newMediaType != self?.mediaType { 74 | self?.loadFiles(mediaType: newMediaType) 75 | } 76 | 77 | }.store(in: &cancelBag) 78 | } 79 | 80 | deinit { 81 | print("FileTableObservable deinit") 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/FileTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileTableView.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#PlotAudioAppView 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import SwiftUI 14 | 15 | struct FileTableViewRowView: View { 16 | 17 | var file:File 18 | 19 | var body: some View { 20 | HStack { 21 | Text("\(file.url.lastPathComponent) [\(file.duration)]") 22 | } 23 | } 24 | } 25 | 26 | struct FileTableView: View { 27 | 28 | @ObservedObject var fileTableObservable: FileTableObservable 29 | 30 | func title() -> String { 31 | return ("\(fileTableObservable.mediaType.rawValue.capitalized) Files") 32 | } 33 | 34 | var body: some View { 35 | 36 | if fileTableObservable.files.count == 0 { 37 | Text("No Media Files") 38 | .padding() 39 | } 40 | else { 41 | #if os(macOS) 42 | VStack { 43 | Text(title()) 44 | .font(.title) 45 | List(fileTableObservable.files, selection: $fileTableObservable.selectedFileID) { 46 | FileTableViewRowView(file: $0) 47 | } 48 | } 49 | #else 50 | NavigationView { 51 | List(fileTableObservable.files, selection: $fileTableObservable.selectedFileID) { 52 | FileTableViewRowView(file: $0) 53 | } 54 | .navigationTitle(title()) 55 | .environment(\.editMode, .constant(.active)) 56 | 57 | } 58 | .navigationViewStyle(StackNavigationViewStyle()) 59 | #endif 60 | } 61 | 62 | } 63 | } 64 | 65 | struct FileTableView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | FileTableView(fileTableObservable: FileTableObservable()) 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIFileSharingEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio.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 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/AVAsset-extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVAsset-extensions.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/ 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import AVFoundation 14 | 15 | extension AVAsset { 16 | 17 | var durationText:String { 18 | let totalSeconds = CMTimeGetSeconds(self.duration) 19 | return AVAsset.secondsToString(secondsIn: totalSeconds) 20 | } 21 | 22 | class func secondsToString(secondsIn:Double, includeTicks:Bool = true) -> String { 23 | 24 | if CGFloat(secondsIn) > (CGFloat.greatestFiniteMagnitude / 2.0) { 25 | return "∞" 26 | } 27 | 28 | let hours:Int = Int(secondsIn / 3600) 29 | 30 | let minutes:Int = Int(secondsIn.truncatingRemainder(dividingBy: 3600) / 60) 31 | let seconds:Int = Int(secondsIn.truncatingRemainder(dividingBy: 60)) 32 | let ticks:Int = Int(100 * secondsIn.truncatingRemainder(dividingBy: 1)) 33 | 34 | if includeTicks { 35 | if hours > 0 { 36 | return String(format: "%i:%02i:%02i:%02i", hours, minutes, seconds, ticks) 37 | } else { 38 | return String(format: "%02i:%02i:%02i", minutes, seconds, ticks) 39 | } 40 | } 41 | else { 42 | if hours > 0 { 43 | return String(format: "%i:%02i:%02i", hours, minutes, seconds) 44 | } else { 45 | return String(format: "%02i:%02i", minutes, seconds) 46 | } 47 | } 48 | } 49 | 50 | func audioSampleBuffer(outputSettings: [String : Any]?) -> CMSampleBuffer? { 51 | 52 | var buffer:CMSampleBuffer? 53 | 54 | if let audioTrack = self.tracks(withMediaType: .audio).first, let audioReader = try? AVAssetReader(asset: self) { 55 | 56 | let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: outputSettings) 57 | 58 | if audioReader.canAdd(audioReaderOutput) { 59 | audioReader.add(audioReaderOutput) 60 | 61 | if audioReader.startReading() { 62 | buffer = audioReaderOutput.copyNextSampleBuffer() 63 | 64 | audioReader.cancelReading() 65 | } 66 | } 67 | } 68 | 69 | return buffer 70 | } 71 | 72 | // Note: the number of samples per buffer may be variable, resulting in different bufferCounts 73 | func audioBufferAndSampleCounts(_ outputSettings:[String : Any]) -> (bufferCount:Int, sampleCount:Int) { 74 | 75 | var sampleCount:Int = 0 76 | var bufferCount:Int = 0 77 | 78 | guard let audioTrack = self.tracks(withMediaType: .audio).first else { 79 | return (bufferCount, sampleCount) 80 | } 81 | 82 | if let audioReader = try? AVAssetReader(asset: self) { 83 | 84 | let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: outputSettings) 85 | audioReader.add(audioReaderOutput) 86 | 87 | if audioReader.startReading() { 88 | 89 | while audioReader.status == .reading { 90 | if let sampleBuffer = audioReaderOutput.copyNextSampleBuffer() { 91 | sampleCount += sampleBuffer.numSamples 92 | bufferCount += 1 93 | } 94 | else { 95 | audioReader.cancelReading() 96 | } 97 | } 98 | } 99 | } 100 | 101 | return (bufferCount, sampleCount) 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_ClockSample.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_ClockSample.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_PianoSample.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_PianoSample.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_TubaSample.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/PlotAudio/Audio Samples/PlotAudio_TubaSample.m4a -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/PlotAudio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudio.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#DownsampleAudio 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import AVFoundation 13 | import Accelerate 14 | import SwiftUI 15 | 16 | func getOSMajorVersion() -> Int { 17 | return ProcessInfo.processInfo.operatingSystemVersion.majorVersion 18 | } 19 | 20 | func getOSVersion() -> Float { 21 | let version = ProcessInfo.processInfo.operatingSystemVersion 22 | return Float(version.majorVersion) + Float(version.minorVersion) * 0.1 23 | } 24 | 25 | let kPAAudioReaderSettings = [ 26 | AVFormatIDKey: Int(kAudioFormatLinearPCM) as AnyObject, 27 | AVLinearPCMBitDepthKey: 16 as AnyObject, 28 | AVLinearPCMIsBigEndianKey: false as AnyObject, 29 | AVLinearPCMIsFloatKey: false as AnyObject, 30 | AVNumberOfChannelsKey: 1 as AnyObject, 31 | AVLinearPCMIsNonInterleaved: false as AnyObject] 32 | 33 | let downsampleAudioQueue: DispatchQueue = DispatchQueue(label: "com.limit-point.downsampleAudioQueue", autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.workItem) 34 | 35 | class DownsampleAudio { 36 | 37 | let running = DispatchSemaphore(value: 0) 38 | var noiseFloor:Double 39 | var antialias:Bool 40 | 41 | init(noiseFloor:Double, antialias:Bool) { 42 | self.noiseFloor = noiseFloor 43 | self.antialias = antialias 44 | } 45 | 46 | deinit { 47 | print("DownsampleAudio deinit") 48 | } 49 | 50 | func audioReader(asset:AVAsset, outputSettings: [String : Any]?) -> (audioTrack:AVAssetTrack?, audioReader:AVAssetReader?, audioReaderOutput:AVAssetReaderTrackOutput?) { 51 | 52 | if let audioTrack = asset.tracks(withMediaType: .audio).first { 53 | if let audioReader = try? AVAssetReader(asset: asset) { 54 | let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: outputSettings) 55 | return (audioTrack, audioReader, audioReaderOutput) 56 | } 57 | } 58 | 59 | return (nil, nil, nil) 60 | } 61 | 62 | func extractSamples(_ sampleBuffer:CMSampleBuffer) -> [Int16]? { 63 | 64 | if let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) { 65 | 66 | let sizeofInt16 = MemoryLayout.size 67 | 68 | let bufferLength = CMBlockBufferGetDataLength(dataBuffer) 69 | 70 | var data = [Int16](repeating: 0, count: bufferLength / sizeofInt16) 71 | 72 | CMBlockBufferCopyDataBytes(dataBuffer, atOffset: 0, dataLength: bufferLength, destination: &data) 73 | 74 | return data 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func downsample(_ audioSamples:[Int16], decimationFactor:Int) -> [Double]? { 81 | 82 | guard decimationFactor <= audioSamples.count else { 83 | return nil 84 | } 85 | 86 | // convert to decibels 87 | var audioSamplesD = [Double](repeating: 0, count: audioSamples.count) 88 | 89 | vDSP.convertElements(of: audioSamples, to: &audioSamplesD) 90 | 91 | vDSP.absolute(audioSamplesD, result: &audioSamplesD) 92 | 93 | vDSP.convert(amplitude: audioSamplesD, toDecibels: &audioSamplesD, zeroReference: Double(Int16.max)) 94 | 95 | audioSamplesD = vDSP.clip(audioSamplesD, to: noiseFloor...0) 96 | 97 | // downsample 98 | var filter = [Double](repeating: 1.0 / Double(decimationFactor), count:decimationFactor) 99 | 100 | if antialias == false { 101 | filter = [1.0] 102 | } 103 | 104 | let downsamplesLength = Int(audioSamplesD.count / decimationFactor) 105 | var downsamples = [Double](repeating: 0.0, count:downsamplesLength) 106 | 107 | #if os(macOS) 108 | vDSP_desampD(audioSamplesD, vDSP_Stride(decimationFactor), filter, &downsamples, vDSP_Length(downsamplesLength), vDSP_Length(filter.count)) 109 | #else 110 | if getOSMajorVersion() >= 17 { 111 | vDSP.downsample(audioSamplesD, decimationFactor: decimationFactor, filter: filter, result: &downsamples) 112 | } else { 113 | vDSP_desampD(audioSamplesD, vDSP_Stride(decimationFactor), filter, &downsamples, vDSP_Length(downsamplesLength), vDSP_Length(filter.count)) 114 | } 115 | #endif 116 | 117 | return downsamples 118 | } 119 | 120 | func readAndDownsampleAudioSamples(asset:AVAsset, downsampleCount:Int, completion: @escaping ([Double]?) -> ()) { 121 | 122 | let (_, reader, readerOutput) = self.audioReader(asset:asset, outputSettings: kPAAudioReaderSettings) 123 | 124 | guard let audioReader = reader, 125 | let audioReaderOutput = readerOutput 126 | else { 127 | return completion(nil) 128 | } 129 | 130 | if audioReader.canAdd(audioReaderOutput) { 131 | audioReader.add(audioReaderOutput) 132 | } 133 | else { 134 | return completion(nil) 135 | } 136 | 137 | var audioSamples:[Int16] = [] 138 | 139 | if audioReader.startReading() { 140 | 141 | while audioReader.status == .reading { 142 | 143 | autoreleasepool { [weak self] in 144 | 145 | if let sampleBuffer = audioReaderOutput.copyNextSampleBuffer(), let bufferSamples = self?.extractSamples(sampleBuffer) { 146 | audioSamples.append(contentsOf: bufferSamples) 147 | } 148 | else { 149 | audioReader.cancelReading() 150 | } 151 | } 152 | } 153 | } 154 | 155 | let totalSampleCount = asset.audioBufferAndSampleCounts(kPAAudioReaderSettings).sampleCount 156 | let decimationFactor = totalSampleCount / downsampleCount 157 | 158 | guard let downsamples = downsample(audioSamples, decimationFactor: decimationFactor) else { 159 | completion(nil) 160 | return 161 | } 162 | 163 | completion(downsamples) 164 | } 165 | 166 | func readAndDownsampleAudioSamples(asset:AVAsset, downsampleCount:Int, downsampleRateSeconds:Int, completion: @escaping ([Double]?) -> ()) { 167 | 168 | let (_, reader, readerOutput) = self.audioReader(asset:asset, outputSettings: kPAAudioReaderSettings) 169 | 170 | guard let audioReader = reader, 171 | let audioReaderOutput = readerOutput 172 | else { 173 | return completion(nil) 174 | } 175 | 176 | if audioReader.canAdd(audioReaderOutput) { 177 | audioReader.add(audioReaderOutput) 178 | } 179 | else { 180 | return completion(nil) 181 | } 182 | 183 | guard let sampleBuffer = asset.audioSampleBuffer(outputSettings:kPAAudioReaderSettings), 184 | let sampleBufferSourceFormat = CMSampleBufferGetFormatDescription(sampleBuffer), 185 | let audioStreamBasicDescription = sampleBufferSourceFormat.audioStreamBasicDescription else { 186 | return completion(nil) 187 | } 188 | 189 | let totalSampleCount = asset.audioBufferAndSampleCounts(kPAAudioReaderSettings).sampleCount 190 | let audioSampleRate = audioStreamBasicDescription.mSampleRate 191 | 192 | guard downsampleCount <= totalSampleCount else { 193 | return completion(nil) 194 | } 195 | 196 | let decimationFactor = totalSampleCount / downsampleCount 197 | 198 | var downsamples:[Double] = [] 199 | 200 | var audioSamples:[Int16] = [] 201 | let audioSampleSizeThreshold = Int(audioSampleRate) * downsampleRateSeconds 202 | 203 | if audioReader.startReading() { 204 | 205 | while audioReader.status == .reading { 206 | 207 | autoreleasepool { [weak self] in 208 | 209 | if let sampleBuffer = audioReaderOutput.copyNextSampleBuffer(), let bufferSamples = self?.extractSamples(sampleBuffer) { 210 | 211 | audioSamples.append(contentsOf: bufferSamples) 212 | 213 | if audioSamples.count > audioSampleSizeThreshold { 214 | 215 | if let audioSamplesDownsamples = self?.downsample(audioSamples, decimationFactor: decimationFactor) { 216 | downsamples.append(contentsOf: audioSamplesDownsamples) 217 | } 218 | 219 | // calculate number of leftover samples not processed in decimation 220 | let remainder = audioSamples.count.quotientAndRemainder(dividingBy: decimationFactor).remainder 221 | 222 | // save leftover 223 | var end:[Int16] = [] 224 | if audioSamples.count-1 >= audioSamples.count-remainder { 225 | end = Array(audioSamples[audioSamples.count-remainder...audioSamples.count-1]) 226 | } 227 | audioSamples.removeAll() 228 | // restore leftover 229 | audioSamples.append(contentsOf: end) 230 | } 231 | } 232 | else { 233 | audioReader.cancelReading() 234 | } 235 | } 236 | } 237 | 238 | if audioSamples.count > 0 { 239 | if let audioSamplesDownsamples = downsample(audioSamples, decimationFactor: decimationFactor) { 240 | downsamples.append(contentsOf: audioSamplesDownsamples) 241 | } 242 | } 243 | } 244 | 245 | completion(downsamples) 246 | } 247 | 248 | func run(asset:AVAsset, downsampleCount:Int, downsampleRateSeconds:Int?, completion: @escaping ([Double]?) -> ()) { 249 | downsampleAudioQueue.async { [weak self] in 250 | 251 | guard let self = self else { 252 | return 253 | } 254 | 255 | if let downsampleRateSeconds = downsampleRateSeconds { 256 | self.readAndDownsampleAudioSamples(asset: asset, downsampleCount: downsampleCount, downsampleRateSeconds: downsampleRateSeconds) { audioSamples in 257 | print("audio downsampled progressively") 258 | self.running.signal() 259 | completion(audioSamples) 260 | } 261 | } 262 | else { 263 | self.readAndDownsampleAudioSamples(asset: asset, downsampleCount: downsampleCount) { audioSamples in 264 | print("audio downsampled") 265 | self.running.signal() 266 | completion(audioSamples) 267 | } 268 | } 269 | 270 | self.running.wait() 271 | } 272 | } 273 | } 274 | 275 | func PlotAudio(audioSamples:[Double], frameSize:CGSize, noiseFloor:Double) -> Path? { 276 | 277 | let sampleCount = audioSamples.count 278 | 279 | guard sampleCount > 0 else { 280 | return nil 281 | } 282 | 283 | let audioSamplesMaximum = vDSP.maximum(audioSamples) 284 | let midPoint = frameSize.height / 2 285 | let deltaT = frameSize.width / Double(sampleCount) 286 | 287 | let audioPath = Path { path in 288 | 289 | for i in 0...sampleCount-1 { 290 | 291 | var scaledSample = midPoint 292 | 293 | if audioSamplesMaximum != noiseFloor { 294 | scaledSample *= (audioSamples[i] - noiseFloor) / (audioSamplesMaximum - noiseFloor) 295 | } 296 | 297 | path.move(to: CGPoint(x: deltaT * Double(i), y: midPoint - scaledSample)) 298 | path.addLine(to: CGPoint(x: deltaT * Double(i), y: midPoint + scaledSample)) 299 | 300 | } 301 | } 302 | 303 | return audioPath 304 | } 305 | 306 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/PlotAudioConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudioConstants.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/ 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import SwiftUI 14 | 15 | func ScreenSize() -> CGSize { 16 | var width:CGFloat = 0 17 | var height:CGFloat = 0 18 | 19 | #if os(macOS) 20 | if let screen = NSScreen.main { 21 | let rect = screen.frame 22 | height = rect.size.height * 0.9 23 | width = rect.size.width * 0.5 24 | } 25 | #elseif os(iOS) 26 | width = UIScreen.main.bounds.width 27 | height = UIScreen.main.bounds.height 28 | #endif 29 | return CGSize(width: width, height: height) 30 | } 31 | 32 | let kPlotAudioHeight = 35.0 33 | let kPlotAudioPathGradient = false 34 | let kPlotAudioPathFrame = false 35 | let kPlotAudioBarWidth = 1 36 | let kPlotAudioBarSpacing = 0 37 | 38 | let kPlotAudioPathAntialias = true 39 | 40 | let kPlotAudioBarWidths:[Int] = [1,2,3,4,5,6,7,8,9,10] 41 | let kPlotAudioBarSpacings:[Int] = [0,1,2,3,4,5,6,7,8,9] 42 | 43 | func DefaultPlotAudioFrameSize() -> CGSize { 44 | return CGSize(width: ScreenSize().width - CGFloat(kPlotAudioBarWidths.last!/2), height: kPlotAudioHeight) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/PlotAudioObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudioObservable.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#PlotAudioObservable 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | import AVFoundation 14 | import SwiftUI 15 | import Combine 16 | 17 | protocol PlotAudioDelegate : AnyObject { 18 | func plotAudioDragChanged(_ value: CGFloat) 19 | func plotAudioDragEnded(_ value: CGFloat) 20 | func plotAudioDidFinishPlotting() 21 | } 22 | 23 | class PlotAudioObservable: ObservableObject { 24 | 25 | weak var plotAudioDelegate:PlotAudioDelegate? 26 | var downsampleAudio:DownsampleAudio? 27 | 28 | @Published var asset:AVAsset? 29 | 30 | @Published var audioSamples:[Double]? 31 | @Published var noiseFloor:Int = -70 32 | @Published var downsampleRateSeconds:Int? = 10 33 | @Published var frameSize:CGSize 34 | @Published var indicatorPercent:Double = 0 35 | @Published var fillIndicator:Bool = true 36 | @Published var selectionRange:ClosedRange = 0.0...0.0 37 | @Published var audioPath = Path() 38 | 39 | var audioDuration:Double = 0 40 | 41 | @Published var isPlotting:Bool = false 42 | 43 | @Published var pathGradient:Bool 44 | @Published var pathFrame:Bool 45 | @Published var barWidth:Int 46 | @Published var barSpacing:Int 47 | 48 | @Published var pathAntialias:Bool 49 | 50 | var cancelBag = Set() 51 | 52 | func currentTimeString() -> String { 53 | return AVAsset.secondsToString(secondsIn: indicatorPercent * audioDuration) 54 | } 55 | 56 | func downsampleAudioToPlot(asset:AVAsset, downsampleRateSeconds:Int?) { 57 | 58 | isPlotting = true 59 | 60 | audioDuration = asset.duration.seconds 61 | 62 | downsampleAudio = DownsampleAudio(noiseFloor: Double(self.noiseFloor), antialias: pathAntialias) 63 | 64 | downsampleAudio?.run(asset: asset, downsampleCount: self.downsampleCount(), downsampleRateSeconds: downsampleRateSeconds) { audioSamples in 65 | DispatchQueue.main.async { [weak self] in 66 | self?.audioSamples = audioSamples 67 | self?.isPlotting = false 68 | 69 | self?.plotAudioDelegate?.plotAudioDidFinishPlotting() 70 | } 71 | } 72 | } 73 | 74 | func updatePaths() { 75 | if let audioSamples = self.audioSamples { 76 | if let audioPath = PlotAudio(audioSamples: audioSamples, frameSize: frameSize, noiseFloor: Double(self.noiseFloor)) { 77 | self.audioPath = audioPath 78 | } 79 | } 80 | } 81 | 82 | func plotAudio() { 83 | if let asset = asset { 84 | downsampleAudioToPlot(asset: asset, downsampleRateSeconds: self.downsampleRateSeconds) 85 | } 86 | 87 | } 88 | 89 | func lineWidth() -> Double { 90 | return Double(barWidth) 91 | } 92 | 93 | func downsampleCount() -> Int { 94 | return Int(self.frameSize.width / Double((self.barSpacing + self.barWidth))) 95 | } 96 | 97 | func handleDragChanged(value:CGFloat) { 98 | plotAudioDelegate?.plotAudioDragChanged(value) 99 | } 100 | 101 | func handleDragEnded(value:CGFloat) { 102 | plotAudioDelegate?.plotAudioDragEnded(value) 103 | } 104 | 105 | convenience init(url:URL, pathGradient:Bool = kPlotAudioPathGradient, pathFrame:Bool = kPlotAudioPathFrame, barWidth:Int = kPlotAudioBarWidth, barSpacing:Int = kPlotAudioBarSpacing, frameSize:CGSize = DefaultPlotAudioFrameSize(), pathAntialias:Bool = kPlotAudioPathAntialias) { 106 | 107 | self.init(asset: AVAsset(url: url), pathGradient:pathGradient, pathFrame:pathFrame, barWidth:barWidth, barSpacing:barSpacing, frameSize:frameSize, pathAntialias: pathAntialias) 108 | } 109 | 110 | init(asset:AVAsset? = nil, pathGradient:Bool = kPlotAudioPathGradient, pathFrame:Bool = kPlotAudioPathFrame, barWidth:Int = kPlotAudioBarWidth, barSpacing:Int = kPlotAudioBarSpacing, frameSize:CGSize = DefaultPlotAudioFrameSize(), pathAntialias:Bool = kPlotAudioPathAntialias) { 111 | 112 | if let asset = asset { 113 | self.asset = asset 114 | } 115 | 116 | self.frameSize = frameSize 117 | self.pathGradient = pathGradient 118 | self.pathFrame = pathFrame 119 | self.barWidth = barWidth 120 | self.barSpacing = barSpacing 121 | 122 | self.pathAntialias = pathAntialias 123 | 124 | $audioSamples.sink { [weak self] _ in 125 | DispatchQueue.main.async { 126 | self?.updatePaths() 127 | } 128 | } 129 | .store(in: &cancelBag) 130 | 131 | $noiseFloor.sink { [weak self] _ in 132 | DispatchQueue.main.async { 133 | self?.plotAudio() 134 | } 135 | } 136 | .store(in: &cancelBag) 137 | 138 | $barWidth.sink { [weak self] _ in 139 | DispatchQueue.main.async { 140 | self?.plotAudio() 141 | } 142 | } 143 | .store(in: &cancelBag) 144 | 145 | $barSpacing.sink { [weak self] _ in 146 | DispatchQueue.main.async { 147 | self?.plotAudio() 148 | } 149 | } 150 | .store(in: &cancelBag) 151 | 152 | $downsampleRateSeconds.sink { [weak self] _ in 153 | DispatchQueue.main.async { 154 | self?.plotAudio() 155 | } 156 | } 157 | .store(in: &cancelBag) 158 | 159 | $frameSize.sink { [weak self] _ in 160 | DispatchQueue.main.async { 161 | self?.updatePaths() 162 | } 163 | } 164 | .store(in: &cancelBag) 165 | 166 | $asset.sink { [weak self] _ in 167 | DispatchQueue.main.async { 168 | self?.plotAudio() 169 | } 170 | 171 | }.store(in: &cancelBag) 172 | 173 | $pathAntialias.sink { [weak self] newPathAntialias in 174 | if self?.pathAntialias != newPathAntialias { 175 | DispatchQueue.main.async { 176 | self?.plotAudio() 177 | } 178 | } 179 | }.store(in: &cancelBag) 180 | 181 | } 182 | 183 | deinit { 184 | print("PlotAudioObservable deinit") 185 | } 186 | } 187 | 188 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudio/PlotAudioWaveformView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudioWaveformView.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#PlotAudioWaveformView 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import AVFoundation 13 | import SwiftUI 14 | 15 | let gradient = LinearGradient(gradient: Gradient(colors: [.black, Color(red: 150.0/255.0, green: 226.0/255.0, blue: 248.0/255.0)]), startPoint: .topLeading, endPoint: .bottomTrailing) 16 | 17 | let paleRed = Color(Color.RGBColorSpace.sRGB, red: 1, green: 0, blue: 0, opacity: 0.5) 18 | 19 | struct PlotAudioWaveformView: View { 20 | 21 | @ObservedObject var plotAudioObservable: PlotAudioObservable 22 | 23 | var body: some View { 24 | ZStack { 25 | // audio plot 26 | if plotAudioObservable.pathGradient { 27 | plotAudioObservable.audioPath 28 | .stroke(Color.clear, style: StrokeStyle(lineWidth: plotAudioObservable.lineWidth(), lineCap: .round, lineJoin: .round)) 29 | .overlay( 30 | gradient.mask( 31 | plotAudioObservable.audioPath 32 | .stroke(Color.red, style: StrokeStyle(lineWidth: plotAudioObservable.lineWidth(), lineCap: .round, lineJoin: .round)) 33 | ) 34 | ) 35 | //.clipShape(Rectangle()) // clips to the view - first bar clipped 36 | } 37 | else { 38 | plotAudioObservable.audioPath 39 | .stroke(Color.blue, style: StrokeStyle(lineWidth: plotAudioObservable.lineWidth(), lineCap: .round, lineJoin: .round)) 40 | } 41 | 42 | // indicator (filled or stroked) 43 | if plotAudioObservable.indicatorPercent > 0 { 44 | if plotAudioObservable.fillIndicator { 45 | Path { path in 46 | path.addRect(CGRect(x: 0, y: 0, width: plotAudioObservable.indicatorPercent * plotAudioObservable.frameSize.width, height: plotAudioObservable.frameSize.height).insetBy(dx: 0, dy: -plotAudioObservable.lineWidth()/2)) 47 | } 48 | .fill(paleRed) 49 | } 50 | else { 51 | Path { path in 52 | path.move(to: CGPoint(x: plotAudioObservable.indicatorPercent * plotAudioObservable.frameSize.width, y: -plotAudioObservable.lineWidth()/2)) 53 | path.addLine(to: CGPoint(x: plotAudioObservable.indicatorPercent * plotAudioObservable.frameSize.width, y: plotAudioObservable.frameSize.height + plotAudioObservable.lineWidth()/2)) 54 | } 55 | .stroke(Color.red, lineWidth: 2) 56 | } 57 | } 58 | 59 | // selection range 60 | if plotAudioObservable.selectionRange.upperBound > plotAudioObservable.selectionRange.lowerBound { 61 | Path { path in 62 | path.addRect(CGRect(x: plotAudioObservable.selectionRange.lowerBound * plotAudioObservable.frameSize.width, y: 0, width: (plotAudioObservable.selectionRange.upperBound - plotAudioObservable.selectionRange.lowerBound) * plotAudioObservable.frameSize.width, height: plotAudioObservable.frameSize.height).insetBy(dx: 0, dy: -plotAudioObservable.lineWidth()/2)) 63 | } 64 | .stroke(.yellow, lineWidth: 1) 65 | } 66 | 67 | // optional frame 68 | if plotAudioObservable.pathFrame { 69 | Path { path in 70 | path.addRect(CGRect(origin: CGPoint(x: 0, y: 0), size: plotAudioObservable.frameSize).insetBy(dx: 0, dy: -plotAudioObservable.lineWidth()/2)) 71 | } 72 | .stroke(Color(red: 0.0, green: 0, blue: 0.0, opacity: 1), style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) 73 | } 74 | } 75 | .frame(width: plotAudioObservable.frameSize.width, height: plotAudioObservable.frameSize.height, alignment: Alignment.center) 76 | .background(Color(red: 1, green: 1, blue: 1, opacity: 0.1)) 77 | .gesture(DragGesture(minimumDistance: 0) 78 | .onChanged({ value in 79 | let percent = min(max(0, value.location.x / plotAudioObservable.frameSize.width), 1) 80 | plotAudioObservable.indicatorPercent = percent 81 | plotAudioObservable.handleDragChanged(value:percent) 82 | }) 83 | .onEnded({ value in 84 | let percent = min(max(0, value.location.x / plotAudioObservable.frameSize.width), 1) 85 | plotAudioObservable.indicatorPercent = percent 86 | plotAudioObservable.handleDragEnded(value: percent) 87 | }) 88 | ) 89 | .overlay( 90 | Group { 91 | if plotAudioObservable.isPlotting { 92 | ProgressView("") 93 | } 94 | } 95 | ) 96 | } 97 | } 98 | 99 | let kClockURL = Bundle.main.url(forResource: "PlotAudio_ClockSample", withExtension: "m4a")! 100 | let kPianoURL = Bundle.main.url(forResource: "PlotAudio_PianoSample", withExtension: "m4a")! 101 | let kTubaURL = Bundle.main.url(forResource: "PlotAudio_TubaSample", withExtension: "m4a")! 102 | 103 | struct PlotAudioWaveformView_Previews: PreviewProvider { 104 | static var previews: some View { 105 | 106 | VStack { 107 | PlotAudioWaveformView(plotAudioObservable: PlotAudioObservable(url: kClockURL, pathGradient: true, pathFrame: true, barWidth: 3, barSpacing: 2)) 108 | 109 | PlotAudioWaveformView(plotAudioObservable: PlotAudioObservable(url: kTubaURL, pathGradient: true, barWidth: 10, barSpacing: 2, frameSize: CGSize(width: 300, height: 200))) 110 | 111 | PlotAudioWaveformView(plotAudioObservable: PlotAudioObservable(asset: AVAsset(url: kPianoURL))) 112 | } 113 | 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudioApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudioApp.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#PlotAudioApp 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import SwiftUI 13 | import Accelerate 14 | 15 | func DecibelExample() { 16 | var bufferSamplesD:[Double] = [1.0, 3.0, 5.0] 17 | 18 | vDSP.convert(amplitude: bufferSamplesD, toDecibels: &bufferSamplesD, zeroReference: 5.0) 19 | 20 | print("vDSP.convert = \(bufferSamplesD)") 21 | // = [-13.979400086720375, -4.436974992327126, 0.0] 22 | 23 | // compare 24 | print("20 * log10 = \([20 * log10(1.0/5.0), 20 * log10(3.0/5.0), 20 * log10(5.0/5.0)])") 25 | // = [-13.979400086720375, -4.436974992327128, 0.0] 26 | 27 | print(Int16.max) 28 | // 32767 29 | 30 | print(20 * log10(1.0/Double(Int16.max))) 31 | // -90.30873362283398 32 | } 33 | 34 | func downsample(audioSamples:[Int16], decimationFactor:Int) -> [Double]? { 35 | 36 | guard decimationFactor <= audioSamples.count else { 37 | print("downsample, error : decimationFactor \(decimationFactor) > audioSamples.count \(audioSamples.count)") 38 | return nil 39 | } 40 | 41 | print("downsample, array: \(audioSamples)") 42 | print("decimationFactor \(decimationFactor)") 43 | 44 | var audioSamplesD = [Double](repeating: 0, count: audioSamples.count) 45 | 46 | vDSP.convertElements(of: audioSamples, to: &audioSamplesD) 47 | 48 | let filter = [Double](repeating: 1.0 / Double(decimationFactor), count:decimationFactor) 49 | 50 | let downsamplesLength = audioSamplesD.count / decimationFactor 51 | var downsamples = [Double](repeating: 0.0, count:downsamplesLength) 52 | 53 | vDSP_desampD(audioSamplesD, vDSP_Stride(decimationFactor), filter, &downsamples, vDSP_Length(downsamplesLength), vDSP_Length(decimationFactor)) 54 | 55 | print("downsample, result: \(downsamples)") 56 | 57 | return downsamples 58 | } 59 | 60 | func DecimationExample(audioSamples:[Int16], desiredSize:Int) { 61 | 62 | guard desiredSize <= audioSamples.count else { 63 | print("Can't decimate : desiredSize \(desiredSize) > audioSamples.count \(audioSamples.count)") 64 | return 65 | } 66 | 67 | let decimationFactor = audioSamples.count / desiredSize 68 | 69 | print("audioSamples \(audioSamples)") 70 | print("audioSamples.count = \(audioSamples.count)") 71 | print("desiredSize = \(desiredSize)") 72 | print("decimationFactor = \(decimationFactor)") 73 | 74 | print("\nWHOLE:\n") 75 | 76 | var downsamples_whole:[Double] = [] 77 | 78 | // downsample whole at once 79 | if let downsamples = downsample(audioSamples: audioSamples, decimationFactor: decimationFactor) { 80 | downsamples_whole.append(contentsOf: downsamples) 81 | } 82 | 83 | print("\nPROGRESSIVE:\n") 84 | 85 | // downsample in blocks of size greater than a threshold 86 | var downsamples_blocks:[Double] = [] 87 | 88 | var block:[Int16] = [] 89 | let blockSizeThreshold = 3 90 | var blockCount:Int = 0 91 | 92 | // read a sample at a time into a block 93 | for sample in audioSamples { 94 | 95 | block.append(sample) 96 | 97 | // process clock when threshold exceeded 98 | if block.count > blockSizeThreshold { 99 | 100 | blockCount += 1 101 | print("block \(blockCount) = \(block)") 102 | 103 | // downsample block 104 | if let blockDownsampled = downsample(audioSamples: block, decimationFactor: decimationFactor) { 105 | downsamples_blocks.append(contentsOf: blockDownsampled) 106 | } 107 | 108 | // calculate number of leftover samples not processed in decimation 109 | let remainder = block.count.quotientAndRemainder(dividingBy: decimationFactor).remainder 110 | 111 | // save leftover 112 | var end:[Int16] = [] 113 | if block.count-1 >= block.count-remainder { 114 | end = Array(block[block.count-remainder...block.count-1]) 115 | 116 | } 117 | 118 | print("end = \(end)\n") 119 | 120 | block.removeAll() 121 | // restore leftover 122 | block.append(contentsOf: end) 123 | } 124 | } 125 | 126 | // process anything left 127 | if block.count > 0 { 128 | print("block (final) = \(block)") 129 | if let blockDownsampled = downsample(audioSamples: block, decimationFactor: decimationFactor) { 130 | downsamples_blocks.append(contentsOf: blockDownsampled) 131 | print("final block downsamples = \(blockDownsampled)\n") 132 | } 133 | } 134 | 135 | print("downsamples:\(downsamples_whole) (whole)") 136 | print("downsamples:\(downsamples_blocks) (blocks)") 137 | } 138 | 139 | @main 140 | struct PlotAudioApp: App { 141 | 142 | init() { 143 | // blog examples 144 | DecimationExample(audioSamples:[3,2,1,2,2,1,7], desiredSize:2) 145 | DecibelExample() 146 | } 147 | 148 | var body: some Scene { 149 | WindowGroup { 150 | PlotAudioAppView(controlViewObservable: ControlViewObservable(plotAudioObservable: PlotAudioObservable(), fileTableObservable: FileTableObservable())) 151 | } 152 | 153 | } 154 | } 155 | 156 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/PlotAudioAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlotAudioAppView.swift 3 | // PlotAudio 4 | // 5 | // Read discussion at: 6 | // http://www.limit-point.com/blog/2022/plot-audio/#PlotAudioAppView 7 | // 8 | // Created by Joseph Pagliaro on 10/1/22. 9 | // Copyright © 2022 Limit Point LLC. All rights reserved. 10 | // 11 | 12 | import SwiftUI 13 | import AVFoundation 14 | import AVKit 15 | 16 | struct PlotAudioAppView: View { 17 | 18 | @StateObject var controlViewObservable: ControlViewObservable 19 | 20 | var body: some View { 21 | ScrollView { 22 | VStack { 23 | Picker("Media Type", selection: $controlViewObservable.fileTableObservable.mediaType) { 24 | ForEach(MediaType.allCases) { type in 25 | Text(type.rawValue.capitalized) 26 | } 27 | } 28 | .pickerStyle(.segmented) 29 | .padding() 30 | 31 | FileTableView(fileTableObservable: controlViewObservable.fileTableObservable) 32 | .onChange(of: controlViewObservable.fileTableObservable.selectedFileID) { newSelectedFileID in 33 | if newSelectedFileID != nil, let index = controlViewObservable.fileTableObservable.files.firstIndex(where: {$0.id == newSelectedFileID}) { 34 | let fileURL = controlViewObservable.fileTableObservable.files[index].url 35 | controlViewObservable.plotAudioObservable.asset = AVAsset(url: fileURL) 36 | } 37 | } 38 | .frame(minHeight: 300) 39 | 40 | if controlViewObservable.fileTableObservable.mediaType == .video { 41 | VideoPlayer(player: controlViewObservable.videoPlayer) 42 | .frame(minHeight: 300) 43 | } 44 | 45 | PlotAudioWaveformView(plotAudioObservable: controlViewObservable.plotAudioObservable) 46 | 47 | ControlView(controlViewObservable: controlViewObservable) 48 | } 49 | .onChange(of: controlViewObservable.fileTableObservable.mediaType) { newMediaType in 50 | controlViewObservable.stopMedia() 51 | controlViewObservable.mediaType = newMediaType 52 | controlViewObservable.fileTableObservable.selectIndex(3) 53 | } 54 | .onAppear { 55 | controlViewObservable.plotAudioObservable.pathGradient = true 56 | controlViewObservable.plotAudioObservable.barWidth = 3 57 | controlViewObservable.plotAudioObservable.barSpacing = 2 58 | 59 | controlViewObservable.fileTableObservable.selectIndex(3) 60 | } 61 | } 62 | 63 | } 64 | } 65 | 66 | struct PlotAudioAppView_Previews: PreviewProvider { 67 | static var previews: some View { 68 | PlotAudioAppView(controlViewObservable: ControlViewObservable(plotAudioObservable: PlotAudioObservable(), fileTableObservable: FileTableObservable())) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Drums 3.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Drums 3.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Echo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Echo.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Heartbeat.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Heartbeat.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Ring Tone 11.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Ring Tone 11.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Ring Tone 5.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Ring Tone 5.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Ring Tone 7.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Ring Tone 7.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Ring Tone 9.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Ring Tone 9.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Squeeze.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Squeeze.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Todays Sound 3.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Todays Sound 3.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Tuba.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Tuba.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Wavvy.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Wavvy.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Wind.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Wind.mov -------------------------------------------------------------------------------- /PlotAudio/PlotAudio/Video Files/Xylophone.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LimitPoint/PlotAudio/fea74f49373c7d85a9a6be1a5f8a924cf547ea15/PlotAudio/PlotAudio/Video Files/Xylophone.mov -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PlotAudio](https://www.limit-point.com/assets/images/PlotAudio.jpg) 2 | # PlotAudio 3 | ## Draws audio waveforms 4 | 5 | The associated Xcode project implements a SwiftUI app for macOS and iOS that processes audio samples in a file to plot their decibel levels over time. 6 | 7 | Learn more about plotting audio samples from our [in-depth blog post](https://www.limit-point.com/blog/2022/plot-audio). 8 | 9 | The subfolder *PlotAudio* contains a small set of independent files that prepare and display audio data in a specialized interactive view. The remaining files build an app around that functionality. 10 | 11 | A list of audio and video files for processing is included in the bundle resources subdirectories *Audio Files* and *Video Files*. 12 | 13 | Add your own audio and video files or use the sample set provided. 14 | 15 | Tap on a file in the list to plot its audio. 16 | 17 | Adjust controls for appearance and drag or tap on the audio plot to set play locations. 18 | 19 | 20 | 21 | The core files that implement preparing and plotting audio data in a view: 22 | 23 | * DownsampleAudio : The [AVFoundation], [Accelerate] and [SwiftUI] code that processes audio samples for plotting as a [Path] with the `PlotAudio` Function. 24 | * PlotAudioWaveformView : The [View] that draws the plot of the processed audio samples. 25 | * PlotAudioObservable : The [ObservableObject] that handles interacting with the plot such as dragging to set play location via its `PlotAudioDelegate`, or updating when necessary. 26 | 27 | The *PlotAudio* folder can be added to other projects to display audio plots of media [URL] or [AVAsset]. The Xcode preview of `PlotAudioWaveformView` is setup to display the audio plots of three included audio files. 28 | 29 | [AVFoundation]: https://developer.apple.com/documentation/avfoundation 30 | [Accelerate]: https://developer.apple.com/documentation/accelerate 31 | [SwiftUI]: https://developer.apple.com/tutorials/swiftui 32 | [Path]: https://developer.apple.com/documentation/swiftui/path 33 | [View]: https://developer.apple.com/documentation/swiftui/view 34 | [ObservableObject]: https://developer.apple.com/documentation/combine/observableobject 35 | [AVAsset]: https://developer.apple.com/documentation/avfoundation/avasset 36 | [URL]: https://developer.apple.com/documentation/foundation/url 37 | --------------------------------------------------------------------------------