├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Example ├── .gitignore ├── .ruby-version ├── Podfile ├── Podfile.lock ├── Tests │ ├── LSPAudioPlayerSpec.m │ ├── LSPAudioViewSpec.m │ ├── LSPCMTimeHelperSpec.m │ ├── LSPConfigurationSpec.m │ ├── LSPProgressViewSpec.m │ ├── Nyan-Cat-Test.mp3 │ ├── Tests-Info.plist │ ├── Tests-Prefix.pch │ └── en.lproj │ │ └── InfoPlist.strings ├── loudspeaker.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── Tests.xcscheme │ │ └── loudspeaker.xcscheme ├── loudspeaker.xcworkspace │ └── contents.xcworkspacedata └── loudspeaker │ ├── Base.lproj │ └── Main.storyboard │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 60@2x.png │ │ ├── 60@3x.png │ │ ├── 76@1x.png │ │ ├── 76@3x.png │ │ ├── 83.5@2x.png │ │ └── Contents.json │ └── Contents.json │ ├── LSPAppDelegate.h │ ├── LSPAppDelegate.m │ ├── LSPViewController.h │ ├── LSPViewController.m │ ├── Launch.storyboard │ ├── Nyan-Cat.mp3 │ ├── en.lproj │ └── InfoPlist.strings │ ├── loudspeaker-Info.plist │ ├── loudspeaker-Prefix.pch │ └── main.m ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Pod ├── Assets │ ├── .gitkeep │ ├── Localizable.strings │ ├── audio_close_icon.png │ ├── audio_close_icon@2x.png │ ├── audio_close_icon@3x.png │ ├── audio_pause_icon.png │ ├── audio_pause_icon@2x.png │ ├── audio_pause_icon@3x.png │ ├── audio_play_icon.png │ ├── audio_play_icon@2x.png │ └── audio_play_icon@3x.png └── Classes │ ├── .gitkeep │ ├── LSPAudioPlayer.h │ ├── LSPAudioPlayer.m │ ├── LSPAudioPlayerModel.h │ ├── LSPAudioPlayerModel.m │ ├── LSPAudioView.h │ ├── LSPAudioView.m │ ├── LSPAudioViewController.h │ ├── LSPAudioViewController.m │ ├── LSPCMTimeHelper.h │ ├── LSPCMTimeHelper.m │ ├── LSPConfiguration.h │ ├── LSPConfiguration.m │ ├── LSPKit.h │ ├── LSPKit.m │ ├── LSPProgressView.h │ ├── LSPProgressView.m │ └── loudspeaker.h ├── README.md └── loudspeaker.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # CocoaPods 26 | Pods/ 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode9 2 | language: objective-c 3 | podfile: Example/Podfile 4 | bundler_args: "--without development --deployment" 5 | cache: 6 | - bundler 7 | - cocoapods 8 | script: 9 | - set -o pipefail && xcodebuild -workspace Example/loudspeaker.xcworkspace -scheme Tests -configuration Debug clean build test -sdk iphonesimulator -destination platform="iOS Simulator,OS=latest,name=iPhone X" | xcpretty -c 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 1.0.0 4 | 5 | This release contains breaking changes so read the changes carefully to see how you may be effected. 6 | 7 | ##### Added 8 | 9 | * 3x assets 10 | * Added iPhone X/safe area layout support 11 | * Added icons and loading screen to example project 12 | * Added configuration 13 | * Allows configuration of volume, interval to update progress bar and elapsed string, and accuracy for taps on progress to jump playback 14 | * All `LSPAudioViewController` instances must be initialized with a `LSPConfiguration` instance 15 | * A default configuration is available from `deafultConfiguration` in `LSPConfigurationBuilder` or you can build your own 16 | * Player frame is now determined by `LSPAudioViewController`'s frame 17 | * Added `show` and `hide:` to `LSPAudioViewController` to toggle the default presentation animations 18 | * Added accessibility to interactive elements 19 | * Accessibility elements are localized to English, localization strings available to be overridden by other languages 20 | * Added scrubbing gesture to progress bar 21 | * Added `jumpToProgress:` in `LSPAudioViewController` to move playback to an exact progress point 22 | * Added `setVolume:` in `LSPAudioPlayer` to set player volume during playback 23 | * More tests 24 | 25 | ##### Removed 26 | 27 | * Removed iOS 8 support 28 | * Removed static `sharedInstance` and `player` references from `LSPAudioPlayer` 29 | * Removed `bottomConstraint`, `observationInterval`, `seekTolerance`, `title`, and `URL` properties from `LSPAudioViewController` 30 | * These were moved into configurations or completely removed as they were unnecessary 31 | 32 | ##### Changed 33 | 34 | * Changed `audioViewController:didClosePlayer:` to be called after close animation is finished 35 | * `LSPAudioViewController` no longer overrides `view`; instead, the player UI is located in `playerView` 36 | 37 | ##### Fixed 38 | 39 | * Fixed timestamp not updating when playback paused but timeline tapped to change position 40 | * Fixed timestamp displaying inaccurate times when starting or stopping playback 41 | 42 | ### 0.2.0 43 | 44 | * Dropped iOS 7 support 45 | * Updated Masonry to 1.1 46 | -------------------------------------------------------------------------------- /Example/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | *.xcuserstate 4 | *.xcbkptlist 5 | *.xcuserstate 6 | */build/* 7 | Build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | *.xcworkspace 17 | !default.xcworkspace 18 | xcuserdata 19 | profile 20 | *.moved-aside 21 | DerivedData 22 | .idea/ 23 | *.hmap 24 | *.xcuserstate 25 | *.xcbkptlist 26 | .LSOverride 27 | 28 | # Cocoapods 29 | Pods 30 | 31 | -------------------------------------------------------------------------------- /Example/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.2 2 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '9.0' 4 | use_frameworks! 5 | inhibit_all_warnings! 6 | 7 | target 'loudspeaker' do 8 | pod 'loudspeaker', :path => '../' 9 | 10 | target 'Tests' do 11 | inherit! :search_paths 12 | 13 | pod 'Specta' 14 | pod 'Expecta' 15 | pod 'OCMock' 16 | end 17 | end 18 | 19 | post_install do |installer| 20 | installer.pods_project.targets.each do |target| 21 | target.build_configurations.each do |config| 22 | config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = "" 23 | config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" 24 | config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Expecta (1.0.6) 3 | - loudspeaker (1.0.0): 4 | - Masonry (~> 1.1.0) 5 | - Masonry (1.1.0) 6 | - OCMock (3.4) 7 | - Specta (1.0.7) 8 | 9 | DEPENDENCIES: 10 | - Expecta 11 | - loudspeaker (from `../`) 12 | - OCMock 13 | - Specta 14 | 15 | EXTERNAL SOURCES: 16 | loudspeaker: 17 | :path: ../ 18 | 19 | SPEC CHECKSUMS: 20 | Expecta: 3b6bd90a64b9a1dcb0b70aa0e10a7f8f631667d5 21 | loudspeaker: a09f6b0f5af84d056c86c3ad063ded0f90cdb3fc 22 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 23 | OCMock: 35ae71d6a8fcc1b59434d561d1520b9dd4f15765 24 | Specta: 3e1bd89c3517421982dc4d1c992503e48bd5fe66 25 | 26 | PODFILE CHECKSUM: 6cfa779b26a19b93894470338981de29e1c1fb06 27 | 28 | COCOAPODS: 1.3.1 29 | -------------------------------------------------------------------------------- /Example/Tests/LSPAudioPlayerSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayerSpec.m 3 | // LSPAudioPlayerSpec 4 | // 5 | // Created by Adam Yanalunas on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | 14 | 15 | #define SETUP_AND_RESET_PLAYER \ 16 | __block LSPAudioPlayer* player = LSPAudioPlayer.new;\ 17 | \ 18 | afterEach(^{\ 19 | [player resetPlayer];\ 20 | });\ 21 | \ 22 | afterAll(^{\ 23 | player = nil;\ 24 | }); 25 | 26 | 27 | SpecBegin(LSPAudioPlayerSpec) 28 | 29 | context(@"LSPAudioPlayer", ^{ 30 | __block NSURL* audioFile1 = nil; 31 | __block NSURL* audioFile2 = nil; 32 | 33 | beforeAll(^{ 34 | audioFile1 = [NSBundle.mainBundle URLForResource:@"Nyan-Cat" withExtension:@"mp3"]; 35 | audioFile2 = [[NSBundle bundleForClass:self.class] URLForResource:@"Nyan-Cat-Test" withExtension:@"mp3"]; 36 | }); 37 | 38 | describe(@"playFromURLString:", ^{ 39 | __block LSPAudioPlayer* player = LSPAudioPlayer.new; 40 | __block OCMockObject *playerMock; 41 | __block NSString* file = @"~/Documents/banana.mp3"; 42 | 43 | beforeEach(^{ 44 | playerMock = OCMPartialMock(player); 45 | }); 46 | 47 | afterEach(^{ 48 | [playerMock stopMocking]; 49 | [player resetPlayer]; 50 | }); 51 | 52 | afterAll(^{ 53 | player = nil; 54 | }); 55 | 56 | it(@"transforms a URL-like string into a NSURL, calling configureWithURL", ^{ 57 | NSString* tildeString = [file stringByExpandingTildeInPath]; 58 | NSURL* expectedUrl = [NSURL fileURLWithPath:[tildeString stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet] isDirectory:NO]; 59 | 60 | [[playerMock expect] playFromURL:expectedUrl]; 61 | 62 | [player playFromURLString:file]; 63 | }); 64 | }); 65 | 66 | describe(@"playFromURL:", ^{ 67 | __block LSPAudioPlayer* player = LSPAudioPlayer.new; 68 | 69 | afterEach(^{ 70 | [player resetPlayer]; 71 | }); 72 | 73 | afterAll(^{ 74 | player = nil; 75 | }); 76 | 77 | it(@"immediately starts playing file if it is not the previously-called file", ^{ 78 | AVQueuePlayer* queuePlayer = player.player; 79 | 80 | expect(queuePlayer.items).to.beEmpty(); 81 | 82 | [player playFromURL:audioFile1]; 83 | 84 | expect(queuePlayer.items).to.haveCountOf(1); 85 | expect([(AVURLAsset *)[queuePlayer.items[0] asset] URL]).to.equal(audioFile1); 86 | }); 87 | 88 | it(@"doesn't create a new item when called again with the previous file", ^{ 89 | AVQueuePlayer* queuePlayer = player.player; 90 | 91 | [player playFromURL:audioFile1]; 92 | expect(queuePlayer.items).to.haveCountOf(1); 93 | 94 | [player playFromURL:audioFile1]; 95 | expect(queuePlayer.items).to.haveCountOf(1); 96 | }); 97 | 98 | it(@"should toggle playback", ^{ 99 | [player playFromURL:audioFile1]; 100 | 101 | expect(player.isPlaying).to.beTruthy(); 102 | }); 103 | 104 | it(@"sets up a new file to play if it is different than the last", ^{ 105 | AVQueuePlayer* queuePlayer = player.player; 106 | 107 | [player playFromURL:audioFile1]; 108 | NSURL* oldPath = player.previousPath; 109 | 110 | [queuePlayer pause]; 111 | 112 | [player playFromURL:audioFile2]; 113 | NSURL* newPath = audioFile2; 114 | 115 | AVPlayerItem* track = [queuePlayer.items objectAtIndex:0]; 116 | expect(track.asset.tracks).to.haveCountOf(1); 117 | expect(player.previousPath).to.equal(newPath); 118 | expect(player.previousPath).toNot.equal(oldPath); 119 | }); 120 | }); 121 | 122 | 123 | describe(@"play", ^{ 124 | SETUP_AND_RESET_PLAYER 125 | 126 | it(@"should play the current track", ^{ 127 | [player play]; 128 | expect(player.isPlaying).to.beTruthy(); 129 | }); 130 | }); 131 | 132 | 133 | describe(@"pause", ^{ 134 | SETUP_AND_RESET_PLAYER 135 | 136 | it(@"should pause the current track", ^{ 137 | [player pause]; 138 | expect(player.isPlaying).to.beFalsy(); 139 | }); 140 | }); 141 | 142 | 143 | describe(@"stop", ^{ 144 | SETUP_AND_RESET_PLAYER 145 | 146 | __block AVQueuePlayer* queuePlayer = player.player; 147 | 148 | beforeAll(^{ 149 | queuePlayer = player.player; 150 | }); 151 | 152 | beforeEach(^{ 153 | [player playFromURL:audioFile1]; 154 | }); 155 | 156 | afterAll(^{ 157 | queuePlayer = nil; 158 | }); 159 | 160 | it(@"should remove any queued items", ^{ 161 | expect(queuePlayer.items).to.haveCountOf(1); 162 | 163 | [player stop]; 164 | 165 | expect(queuePlayer.items).to.haveCountOf(0); 166 | }); 167 | 168 | it(@"should forget the last track played", ^{ 169 | expect(player.previousPath).to.equal(audioFile1); 170 | 171 | [player stop]; 172 | 173 | expect(player.previousPath).to.beNil(); 174 | }); 175 | }); 176 | 177 | 178 | describe(@"togglePlayback", ^{ 179 | SETUP_AND_RESET_PLAYER 180 | 181 | it(@"should pause playback if currently playing", ^{ 182 | [player pause]; 183 | [player togglePlayback]; 184 | expect(player.isPlaying).to.beTruthy(); 185 | }); 186 | 187 | it(@"should resume playback if currently paused", ^{ 188 | [player play]; 189 | [player togglePlayback]; 190 | expect(player.isPlaying).to.beFalsy(); 191 | }); 192 | }); 193 | 194 | 195 | describe(@"currentTime", ^{ 196 | __block CMTime destinationTime; 197 | 198 | SETUP_AND_RESET_PLAYER 199 | 200 | beforeEach(^{ 201 | [player playFromURL:audioFile1]; 202 | 203 | AVQueuePlayer *queuePlayer = player.player; 204 | destinationTime = [player timeFromProgress:.5]; 205 | CMTime seekTolerance = CMTimeMake(1, 100); 206 | [queuePlayer seekToTime:destinationTime toleranceBefore:seekTolerance toleranceAfter:seekTolerance]; 207 | }); 208 | 209 | it(@"should return the elapsed time of the current track", ^{ 210 | expect(CMTIME_COMPARE_INLINE(player.currentTime, ==, destinationTime)).to.beTruthy(); 211 | }); 212 | }); 213 | 214 | 215 | describe(@"duration", ^{ 216 | __block CMTime durationTime; 217 | 218 | SETUP_AND_RESET_PLAYER 219 | 220 | beforeEach(^{ 221 | [player playFromURL:audioFile1]; 222 | AVQueuePlayer *queuePlayer = player.player; 223 | durationTime = queuePlayer.currentItem.duration; 224 | }); 225 | 226 | it(@"should return the duration of the current track", ^{ 227 | expect(CMTIME_COMPARE_INLINE(player.duration, ==, durationTime)).to.beTruthy(); 228 | }); 229 | }); 230 | 231 | 232 | describe(@"status", ^{ 233 | SETUP_AND_RESET_PLAYER 234 | 235 | beforeEach(^{ 236 | [player playFromURL:audioFile1]; 237 | }); 238 | 239 | it(@"should return the player's current playback status", ^{ 240 | AVQueuePlayer *queuePlayer = player.player; 241 | expect(player.status).to.equal(queuePlayer.status); 242 | }); 243 | }); 244 | 245 | 246 | describe(@"isPlaying", ^{ 247 | SETUP_AND_RESET_PLAYER 248 | 249 | beforeEach(^{ 250 | [player playFromURL:audioFile1]; 251 | }); 252 | 253 | it(@"should return YES if audio is determined to be playing", ^{ 254 | [player play]; 255 | expect(player.isPlaying).to.beTruthy(); 256 | }); 257 | 258 | it(@"should return NO if audio is determined to be paused or stopped", ^{ 259 | [player pause]; 260 | expect(player.isPlaying).to.beFalsy(); 261 | 262 | [player play]; 263 | expect(player.isPlaying).to.beTruthy(); 264 | 265 | [player stop]; 266 | expect(player.isPlaying).to.beFalsy(); 267 | }); 268 | }); 269 | 270 | 271 | describe(@"timeForProgress:", ^{ 272 | SETUP_AND_RESET_PLAYER 273 | 274 | beforeEach(^{ 275 | // 30 seconds 276 | CMTime duration = CMTimeMake(30, 1); 277 | player = OCMPartialMock(player); 278 | OCMStub(player.duration).andReturn(duration); 279 | }); 280 | 281 | afterEach(^{ 282 | [(OCMockObject *)player stopMocking]; 283 | }); 284 | 285 | it(@"should return a time for the current track based on a 0-1 progress through the track", ^{ 286 | CMTime halfway = [player timeFromProgress:.5]; 287 | Float64 halfwaySeconds = CMTimeGetSeconds(halfway); 288 | expect(halfwaySeconds).to.equal(15.); 289 | 290 | CMTime threeQuarters = [player timeFromProgress:.75]; 291 | Float64 threeQuartersSeconds = CMTimeGetSeconds(threeQuarters); 292 | expect(threeQuartersSeconds).to.equal(22.5); 293 | }); 294 | }); 295 | 296 | 297 | describe(@"resetPlayer", ^{ 298 | __block OCMockObject *ncStub; 299 | __block LSPAudioPlayer* player = LSPAudioPlayer.new; 300 | 301 | beforeEach(^{ 302 | ncStub = OCMPartialMock(NSNotificationCenter.defaultCenter); 303 | }); 304 | 305 | afterAll(^{ 306 | player = nil; 307 | }); 308 | 309 | afterEach(^{ 310 | [player resetPlayer]; 311 | [ncStub stopMocking]; 312 | }); 313 | 314 | it(@"removes observers if previousPath is not nil", ^{ 315 | player.previousPath = audioFile1; 316 | 317 | [[ncStub expect] removeObserver:player]; 318 | [player resetPlayer]; 319 | [ncStub verify]; 320 | }); 321 | 322 | it(@"doesn't remove observers if previousPath is nil", ^{ 323 | [[ncStub reject] removeObserver:player]; 324 | [player resetPlayer]; 325 | [ncStub verify]; 326 | }); 327 | 328 | it(@"removes items from queue if previousPath is not nil", ^{ 329 | player.previousPath = audioFile1; 330 | 331 | AVPlayerItem* item = [AVPlayerItem playerItemWithURL:audioFile1]; 332 | 333 | AVQueuePlayer* queuePlayer = player.player; 334 | [queuePlayer insertItem:item afterItem:nil]; 335 | 336 | expect(queuePlayer.items).to.haveCountOf(1); 337 | [player resetPlayer]; 338 | expect(queuePlayer.items).to.haveCountOf(0); 339 | }); 340 | 341 | // TODO: Check that this is the test causing Travis to fail tests 342 | // it(@"does not remove items from queue if previousPath is nil", ^{ 343 | // player.previousPath = audioFile1; 344 | // 345 | // AVPlayerItem* item = [AVPlayerItem playerItemWithURL:audioFile1]; 346 | // 347 | // AVQueuePlayer* queuePlayer = LSPAudioPlayer.player; 348 | // [queuePlayer insertItem:item afterItem:nil]; 349 | // [queuePlayer pause]; 350 | // 351 | // expect(queuePlayer.items).to.haveCountOf(1); 352 | // player.previousPath = nil; 353 | // [player resetPlayer]; 354 | // expect(queuePlayer.items).to.haveCountOf(1); 355 | // }); 356 | 357 | it(@"nils out the previousPath", ^{ 358 | player.previousPath = [NSURL URLWithString:@"http://foo.com"]; 359 | 360 | [player resetPlayer]; 361 | 362 | expect(player.previousPath).to.beNil(); 363 | }); 364 | }); 365 | }); 366 | 367 | 368 | SpecEnd 369 | -------------------------------------------------------------------------------- /Example/Tests/LSPAudioViewSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioViewSpec.m 3 | // Tests 4 | // 5 | // Created by Adam Yanalunas on 11/20/17. 6 | // Copyright © 2017 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | SpecBegin(LSPAudioView) 14 | 15 | 16 | describe(@"initForAutoLayout", ^{ 17 | __block LSPAudioView *view; 18 | 19 | beforeEach(^{ 20 | view = [LSPAudioView.alloc initForAutoLayout]; 21 | }); 22 | 23 | it(@"sets view to disable autoresize mask", ^{ 24 | expect(view.translatesAutoresizingMaskIntoConstraints).to.beFalsy(); 25 | }); 26 | 27 | it(@"provides a default background color", ^{ 28 | UIColor *bgColor = [UIColor colorWithWhite:238/255. alpha:1]; 29 | expect(view.backgroundColor).to.equal(bgColor); 30 | }); 31 | 32 | it(@"configures the view", ^{ 33 | expect(view.closeButton.superview).to.beIdenticalTo(view); 34 | expect(view.playbackTimeLabel.superview).to.beIdenticalTo(view); 35 | expect(view.playPauseButton.superview).to.beIdenticalTo(view); 36 | expect(view.progressView.superview).to.beIdenticalTo(view); 37 | expect(view.titleLabel.superview).to.beIdenticalTo(view); 38 | }); 39 | }); 40 | 41 | describe(@"setCurrentProgress:forDuration:", ^{ 42 | __block LSPAudioView *view; 43 | 44 | beforeEach(^{ 45 | view = LSPAudioView.newAutoLayoutView; 46 | [view setCurrentProgress:@"123" forDuration:@"456"]; 47 | }); 48 | 49 | it(@"formats the progress text", ^{ 50 | expect(view.playbackTimeLabel.text).to.equal(@"123 / 456"); 51 | }); 52 | 53 | it(@"applies an updated acessibility label", ^{ 54 | expect(view.playbackTimeLabel.accessibilityLabel).to.equal(@"123 seconds elapsed"); 55 | }); 56 | }); 57 | 58 | describe(@"showLayoutForPlaying:", ^{ 59 | __block LSPAudioView *view; 60 | 61 | beforeEach(^{ 62 | view = LSPAudioView.newAutoLayoutView; 63 | }); 64 | 65 | context(@"play", ^{ 66 | beforeEach(^{ 67 | [view showLayoutForPlaying:NO]; 68 | }); 69 | 70 | it(@"shows the play button", ^{ 71 | UIImage *actual = [view.playPauseButton imageForState:UIControlStateNormal]; 72 | expect(actual).to.equal(LSPKit.playIcon); 73 | }); 74 | 75 | it(@"applies accessibility information", ^{ 76 | expect(view.playPauseButton.accessibilityLabel).to.equal(@"Play"); 77 | expect(view.playPauseButton.accessibilityHint).to.equal(@"Tap to resume playback"); 78 | }); 79 | }); 80 | 81 | context(@"pause", ^{ 82 | beforeEach(^{ 83 | [view showLayoutForPlaying:YES]; 84 | }); 85 | 86 | it(@"shows the pause button", ^{ 87 | UIImage *actual = [view.playPauseButton imageForState:UIControlStateNormal]; 88 | expect(actual).to.equal(LSPKit.pauseIcon); 89 | }); 90 | 91 | it(@"applies accessibility information", ^{ 92 | expect(view.playPauseButton.accessibilityLabel).to.equal(@"Pause"); 93 | expect(view.playPauseButton.accessibilityHint).to.equal(@"Tap to pause playback"); 94 | }); 95 | }); 96 | }); 97 | 98 | 99 | SpecEnd 100 | -------------------------------------------------------------------------------- /Example/Tests/LSPCMTimeHelperSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPCMTimeHelperSpec.m 3 | // Tests 4 | // 5 | // Created by Adam Yanalunas on 11/17/17. 6 | // Copyright © 2017 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | SpecBegin(LSPCMTimeHelperSpec) 14 | 15 | describe(@"readableCMTime:", ^{ 16 | it(@"formats a time hours long", ^{ 17 | CMTime time = CMTimeMake(60 * 90, 1); 18 | NSString *formatted = [LSPCMTimeHelper readableCMTime:time]; 19 | 20 | expect(formatted).to.equal(@"1:30:00"); 21 | }); 22 | 23 | it(@"formats a time minutes long", ^{ 24 | CMTime time = CMTimeMake(60 * 45, 1); 25 | NSString *formatted = [LSPCMTimeHelper readableCMTime:time]; 26 | 27 | expect(formatted).to.equal(@"45:00"); 28 | }); 29 | 30 | it(@"formats a time seconds long", ^{ 31 | CMTime time = CMTimeMake(42, 1); 32 | NSString *formatted = [LSPCMTimeHelper readableCMTime:time]; 33 | 34 | expect(formatted).to.equal(@"00:42"); 35 | }); 36 | 37 | it(@"ignores invalid time formats", ^{ 38 | CMTime time = CMTimeMake(-1, 1); 39 | NSString *formatted = [LSPCMTimeHelper readableCMTime:time]; 40 | 41 | expect(formatted).to.equal(@"00:00"); 42 | }); 43 | }); 44 | 45 | SpecEnd 46 | -------------------------------------------------------------------------------- /Example/Tests/LSPConfigurationSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPConfigurationSpec.m 3 | // Tests 4 | // 5 | // Created by Adam Yanalunas on 11/20/17. 6 | // Copyright © 2017 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | 13 | SpecBegin(LSPConfigurationBuild) 14 | 15 | 16 | describe(@"defaultConfiguration", ^{ 17 | __block LSPConfiguration *config; 18 | 19 | beforeEach(^{ 20 | config = LSPConfigurationBuilder.defaultConfiguration; 21 | }); 22 | 23 | it(@"contains the default configuration values", ^{ 24 | expect(config.seekTolerance.value).to.equal(1); 25 | expect(config.seekTolerance.timescale).to.equal(100); 26 | expect(config.observationInterval.value).to.equal(1); 27 | expect(config.observationInterval.timescale).to.equal(35); 28 | expect(config.volume).to.equal(1); 29 | }); 30 | }); 31 | 32 | describe(@"configurationWithBuilder:", ^{ 33 | __block LSPConfiguration *config; 34 | 35 | beforeEach(^{ 36 | config = [LSPConfigurationBuilder configurationWithBuilder:^(LSPConfigurationBuilder *builder) { 37 | builder.seekTolerance = CMTimeMake(1, 2); 38 | builder.observationInterval = CMTimeMake(3, 4); 39 | builder.volume = .2; 40 | }]; 41 | }); 42 | 43 | it(@"uses the given values to build a configuration", ^{ 44 | expect(config.seekTolerance.value).to.equal(1); 45 | expect(config.seekTolerance.timescale).to.equal(2); 46 | expect(config.observationInterval.value).to.equal(3); 47 | expect(config.observationInterval.timescale).to.equal(4); 48 | expect(config.volume).to.equal(.2); 49 | }); 50 | }); 51 | 52 | 53 | SpecEnd 54 | -------------------------------------------------------------------------------- /Example/Tests/LSPProgressViewSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPProgressViewSpec.m 3 | // Tests 4 | // 5 | // Created by Adam Yanalunas on 11/20/17. 6 | // Copyright © 2017 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | 14 | SpecBegin(LSPProgressView) 15 | 16 | 17 | describe(@"initForAutoLayout", ^{ 18 | __block LSPProgressView *view; 19 | __block OCMockObject *viewMock; 20 | 21 | beforeEach(^{ 22 | view = [LSPProgressView.alloc initForAutoLayout]; 23 | viewMock = OCMPartialMock(view); 24 | }); 25 | 26 | afterEach(^{ 27 | [viewMock stopMocking]; 28 | }); 29 | 30 | it(@"sets view to disable autoresize mask", ^{ 31 | expect(view.translatesAutoresizingMaskIntoConstraints).to.beFalsy(); 32 | }); 33 | 34 | it(@"sets up the view", ^{ 35 | [[viewMock expect] setup]; 36 | }); 37 | }); 38 | 39 | describe(@"newAutoLayoutView", ^{ 40 | __block LSPProgressView *view; 41 | __block OCMockObject *viewMock; 42 | 43 | beforeEach(^{ 44 | view = LSPProgressView.newAutoLayoutView; 45 | viewMock = OCMPartialMock(view); 46 | }); 47 | 48 | afterEach(^{ 49 | [viewMock stopMocking]; 50 | }); 51 | 52 | it(@"sets view to disable autoresize mask", ^{ 53 | expect(view.translatesAutoresizingMaskIntoConstraints).to.beFalsy(); 54 | }); 55 | 56 | it(@"sets up the view", ^{ 57 | [[viewMock expect] setup]; 58 | }); 59 | }); 60 | 61 | describe(@"setup", ^{ 62 | __block LSPProgressView *view; 63 | 64 | beforeEach(^{ 65 | view = LSPProgressView.newAutoLayoutView; 66 | [view setup]; 67 | }); 68 | 69 | it(@"sets up subviews", ^{ 70 | expect(view.progressBackground.superview).to.beIdenticalTo(view); 71 | expect(view.progressBar.superview).to.beIdenticalTo(view); 72 | }); 73 | 74 | it(@"provides default background colors", ^{ 75 | UIColor *backgroundColor = [UIColor colorWithWhite:207/255. alpha:1]; 76 | UIColor *foregroundColor = [UIColor colorWithRed:88/255. green:199/255. blue:226/255. alpha:1]; 77 | 78 | expect(view.progressBackground.backgroundColor).to.equal(backgroundColor); 79 | expect(view.progressBar.backgroundColor).to.equal(foregroundColor); 80 | }); 81 | }); 82 | 83 | describe(@"setProgress:", ^{ 84 | __block LSPProgressView *view; 85 | 86 | beforeEach(^{ 87 | UIView *parent = [UIView.alloc initWithFrame:CGRectMake(0, 0, 400, 50)]; 88 | view = LSPProgressView.newAutoLayoutView; 89 | [parent addSubview:view]; 90 | 91 | NSDictionary *viewBindings = NSDictionaryOfVariableBindings(view); 92 | [parent addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:viewBindings]]; 93 | [parent addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:viewBindings]]; 94 | 95 | [view layoutIfNeeded]; 96 | 97 | [view setProgress:.5]; 98 | }); 99 | 100 | it(@"moves the progress bar to the new progress amount", ^{ 101 | CGFloat width = CGRectGetWidth(view.progressBar.frame); 102 | expect(width).to.equal(200); 103 | }); 104 | 105 | it(@"updates the progress bar's accessiblity label", ^{ 106 | expect(view.progressBar.accessibilityLabel).to.equal(@"50%"); 107 | }); 108 | }); 109 | 110 | describe(@"progressBar", ^{ 111 | __block LSPProgressView *view; 112 | 113 | beforeEach(^{ 114 | view = LSPProgressView.newAutoLayoutView; 115 | }); 116 | 117 | it(@"is configured to tell iOS its accessibility information updates frequently", ^{ 118 | BOOL hasTraits = (view.progressBar.accessibilityTraits & UIAccessibilityTraitUpdatesFrequently) != 0; 119 | expect(hasTraits).to.beTruthy(); 120 | }); 121 | }); 122 | 123 | 124 | SpecEnd 125 | -------------------------------------------------------------------------------- /Example/Tests/Nyan-Cat-Test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/Tests/Nyan-Cat-Test.mp3 -------------------------------------------------------------------------------- /Example/Tests/Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/Tests/Tests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every test case source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | 9 | #define EXP_SHORTHAND 10 | #import 11 | #import 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /Example/Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/loudspeaker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4CB019164D0B45776C000F6E /* Pods_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A1340D735A19B0195BF9D95 /* Pods_Tests.framework */; }; 11 | 5999A69D261A1BE94A8873A0 /* Pods_loudspeaker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A96B09DA14504E865B964CDD /* Pods_loudspeaker.framework */; }; 12 | 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 13 | 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; 14 | 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 15 | 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; }; 16 | 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; }; 17 | 6003F59E195388D20070C39A /* LSPAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* LSPAppDelegate.m */; }; 18 | 6003F5A1195388D20070C39A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6003F59F195388D20070C39A /* Main.storyboard */; }; 19 | 6003F5A7195388D20070C39A /* LSPViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* LSPViewController.m */; }; 20 | 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; }; 21 | 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; 22 | 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 23 | 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 24 | 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 25 | 6003F5BC195388D20070C39A /* LSPAudioPlayerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5BB195388D20070C39A /* LSPAudioPlayerSpec.m */; }; 26 | 66275C8C19BA808800E74C05 /* Nyan-Cat-Test.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66275C8B19BA808800E74C05 /* Nyan-Cat-Test.mp3 */; }; 27 | 6644D01519B1B06A006521EC /* Nyan-Cat.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6644D01419B1B06A006521EC /* Nyan-Cat.mp3 */; }; 28 | 6649A3DF1FBF545F00C4A1DD /* LSPCMTimeHelperSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 6649A3DE1FBF545F00C4A1DD /* LSPCMTimeHelperSpec.m */; }; 29 | 6697C9DB1FC388CF003311FC /* LSPAudioViewSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 6697C9DA1FC388CF003311FC /* LSPAudioViewSpec.m */; }; 30 | 6697C9DD1FC38DF1003311FC /* LSPProgressViewSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 6697C9DC1FC38DF1003311FC /* LSPProgressViewSpec.m */; }; 31 | 6697C9DF1FC395F4003311FC /* LSPConfigurationSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 6697C9DE1FC395F4003311FC /* LSPConfigurationSpec.m */; }; 32 | 66A27D3D1FBE678B00BAA835 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 66A27D3C1FBE678B00BAA835 /* Launch.storyboard */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 6003F582195388D10070C39A /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 6003F589195388D20070C39A; 41 | remoteInfo = loudspeaker; 42 | }; 43 | /* End PBXContainerItemProxy section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 04CE74406D184CFBBBA1F083 /* loudspeaker.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = loudspeaker.podspec; path = ../loudspeaker.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 47 | 120B8E2BB30D71BE484C407B /* Pods-loudspeaker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-loudspeaker.release.xcconfig"; path = "Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker.release.xcconfig"; sourceTree = ""; }; 48 | 277C080A6A344991E9CCDE66 /* Pods-loudspeaker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-loudspeaker.debug.xcconfig"; path = "Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker.debug.xcconfig"; sourceTree = ""; }; 49 | 5A1340D735A19B0195BF9D95 /* Pods_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 6003F58A195388D20070C39A /* loudspeaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = loudspeaker.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 52 | 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 53 | 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 54 | 6003F595195388D20070C39A /* loudspeaker-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "loudspeaker-Info.plist"; sourceTree = ""; }; 55 | 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 56 | 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 57 | 6003F59B195388D20070C39A /* loudspeaker-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "loudspeaker-Prefix.pch"; sourceTree = ""; }; 58 | 6003F59C195388D20070C39A /* LSPAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSPAppDelegate.h; sourceTree = ""; }; 59 | 6003F59D195388D20070C39A /* LSPAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPAppDelegate.m; sourceTree = ""; }; 60 | 6003F5A0195388D20070C39A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 61 | 6003F5A5195388D20070C39A /* LSPViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSPViewController.h; sourceTree = ""; }; 62 | 6003F5A6195388D20070C39A /* LSPViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPViewController.m; sourceTree = ""; }; 63 | 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 64 | 6003F5AE195388D20070C39A /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 66 | 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 67 | 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 68 | 6003F5BB195388D20070C39A /* LSPAudioPlayerSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPAudioPlayerSpec.m; sourceTree = ""; }; 69 | 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = ""; }; 70 | 66275C8B19BA808800E74C05 /* Nyan-Cat-Test.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Nyan-Cat-Test.mp3"; sourceTree = ""; }; 71 | 6644D01419B1B06A006521EC /* Nyan-Cat.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "Nyan-Cat.mp3"; sourceTree = ""; }; 72 | 6649A3DE1FBF545F00C4A1DD /* LSPCMTimeHelperSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPCMTimeHelperSpec.m; sourceTree = ""; }; 73 | 6697C9DA1FC388CF003311FC /* LSPAudioViewSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPAudioViewSpec.m; sourceTree = ""; }; 74 | 6697C9DC1FC38DF1003311FC /* LSPProgressViewSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPProgressViewSpec.m; sourceTree = ""; }; 75 | 6697C9DE1FC395F4003311FC /* LSPConfigurationSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSPConfigurationSpec.m; sourceTree = ""; }; 76 | 66A27D3C1FBE678B00BAA835 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; 77 | A96B09DA14504E865B964CDD /* Pods_loudspeaker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_loudspeaker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | AF755EAFAF0B404E8A8F022C /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 79 | BBDDD2935A556DB5BD8B2D97 /* Pods-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig"; sourceTree = ""; }; 80 | CC4EB31AA02B580F6A5F7C00 /* Pods-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.release.xcconfig"; sourceTree = ""; }; 81 | DE8FBD6FCCE74BAE960063B5 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 6003F587195388D20070C39A /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, 90 | 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, 91 | 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, 92 | 5999A69D261A1BE94A8873A0 /* Pods_loudspeaker.framework in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | 6003F5AB195388D20070C39A /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, 101 | 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, 102 | 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, 103 | 4CB019164D0B45776C000F6E /* Pods_Tests.framework in Frameworks */, 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | 6003F581195388D10070C39A = { 111 | isa = PBXGroup; 112 | children = ( 113 | 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, 114 | 6003F593195388D20070C39A /* loudspeaker */, 115 | 6003F5B5195388D20070C39A /* Tests */, 116 | 6003F58C195388D20070C39A /* Frameworks */, 117 | 6003F58B195388D20070C39A /* Products */, 118 | A4137025745AAC23586B180F /* Pods */, 119 | ); 120 | sourceTree = ""; 121 | }; 122 | 6003F58B195388D20070C39A /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 6003F58A195388D20070C39A /* loudspeaker.app */, 126 | 6003F5AE195388D20070C39A /* Tests.xctest */, 127 | ); 128 | name = Products; 129 | sourceTree = ""; 130 | }; 131 | 6003F58C195388D20070C39A /* Frameworks */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 6003F58D195388D20070C39A /* Foundation.framework */, 135 | 6003F58F195388D20070C39A /* CoreGraphics.framework */, 136 | 6003F591195388D20070C39A /* UIKit.framework */, 137 | 6003F5AF195388D20070C39A /* XCTest.framework */, 138 | 5A1340D735A19B0195BF9D95 /* Pods_Tests.framework */, 139 | A96B09DA14504E865B964CDD /* Pods_loudspeaker.framework */, 140 | ); 141 | name = Frameworks; 142 | sourceTree = ""; 143 | }; 144 | 6003F593195388D20070C39A /* loudspeaker */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 6003F5A8195388D20070C39A /* Images.xcassets */, 148 | 66A27D3C1FBE678B00BAA835 /* Launch.storyboard */, 149 | 6003F59C195388D20070C39A /* LSPAppDelegate.h */, 150 | 6003F59D195388D20070C39A /* LSPAppDelegate.m */, 151 | 6003F5A5195388D20070C39A /* LSPViewController.h */, 152 | 6003F5A6195388D20070C39A /* LSPViewController.m */, 153 | 6003F59F195388D20070C39A /* Main.storyboard */, 154 | 6003F594195388D20070C39A /* Supporting Files */, 155 | ); 156 | path = loudspeaker; 157 | sourceTree = ""; 158 | }; 159 | 6003F594195388D20070C39A /* Supporting Files */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 6644D01419B1B06A006521EC /* Nyan-Cat.mp3 */, 163 | 6003F595195388D20070C39A /* loudspeaker-Info.plist */, 164 | 6003F596195388D20070C39A /* InfoPlist.strings */, 165 | 6003F599195388D20070C39A /* main.m */, 166 | 6003F59B195388D20070C39A /* loudspeaker-Prefix.pch */, 167 | ); 168 | name = "Supporting Files"; 169 | sourceTree = ""; 170 | }; 171 | 6003F5B5195388D20070C39A /* Tests */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 6003F5BB195388D20070C39A /* LSPAudioPlayerSpec.m */, 175 | 6697C9DA1FC388CF003311FC /* LSPAudioViewSpec.m */, 176 | 6649A3DE1FBF545F00C4A1DD /* LSPCMTimeHelperSpec.m */, 177 | 6697C9DE1FC395F4003311FC /* LSPConfigurationSpec.m */, 178 | 6697C9DC1FC38DF1003311FC /* LSPProgressViewSpec.m */, 179 | 6003F5B6195388D20070C39A /* Supporting Files */, 180 | ); 181 | path = Tests; 182 | sourceTree = ""; 183 | }; 184 | 6003F5B6195388D20070C39A /* Supporting Files */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 66275C8B19BA808800E74C05 /* Nyan-Cat-Test.mp3 */, 188 | 6003F5B7195388D20070C39A /* Tests-Info.plist */, 189 | 6003F5B8195388D20070C39A /* InfoPlist.strings */, 190 | 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */, 191 | ); 192 | name = "Supporting Files"; 193 | sourceTree = ""; 194 | }; 195 | 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 04CE74406D184CFBBBA1F083 /* loudspeaker.podspec */, 199 | DE8FBD6FCCE74BAE960063B5 /* README.md */, 200 | AF755EAFAF0B404E8A8F022C /* LICENSE */, 201 | ); 202 | name = "Podspec Metadata"; 203 | sourceTree = ""; 204 | }; 205 | A4137025745AAC23586B180F /* Pods */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | BBDDD2935A556DB5BD8B2D97 /* Pods-Tests.debug.xcconfig */, 209 | CC4EB31AA02B580F6A5F7C00 /* Pods-Tests.release.xcconfig */, 210 | 277C080A6A344991E9CCDE66 /* Pods-loudspeaker.debug.xcconfig */, 211 | 120B8E2BB30D71BE484C407B /* Pods-loudspeaker.release.xcconfig */, 212 | ); 213 | name = Pods; 214 | sourceTree = ""; 215 | }; 216 | /* End PBXGroup section */ 217 | 218 | /* Begin PBXNativeTarget section */ 219 | 6003F589195388D20070C39A /* loudspeaker */ = { 220 | isa = PBXNativeTarget; 221 | buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "loudspeaker" */; 222 | buildPhases = ( 223 | 6A78E2D99612904378871230 /* [CP] Check Pods Manifest.lock */, 224 | 6003F586195388D20070C39A /* Sources */, 225 | 6003F587195388D20070C39A /* Frameworks */, 226 | 6003F588195388D20070C39A /* Resources */, 227 | D29F17EAAAA416CD1A65A5D5 /* [CP] Embed Pods Frameworks */, 228 | 5FB7ABD54F6F6AB3BC115B1A /* [CP] Copy Pods Resources */, 229 | ); 230 | buildRules = ( 231 | ); 232 | dependencies = ( 233 | ); 234 | name = loudspeaker; 235 | productName = loudspeaker; 236 | productReference = 6003F58A195388D20070C39A /* loudspeaker.app */; 237 | productType = "com.apple.product-type.application"; 238 | }; 239 | 6003F5AD195388D20070C39A /* Tests */ = { 240 | isa = PBXNativeTarget; 241 | buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Tests" */; 242 | buildPhases = ( 243 | C2D6CA83C0B94A0BD33CD7DC /* [CP] Check Pods Manifest.lock */, 244 | 6003F5AA195388D20070C39A /* Sources */, 245 | 6003F5AB195388D20070C39A /* Frameworks */, 246 | 6003F5AC195388D20070C39A /* Resources */, 247 | 700189DE91770B50B11F4E3B /* [CP] Embed Pods Frameworks */, 248 | 5D3ACD3DAD153EA9B7AE1489 /* [CP] Copy Pods Resources */, 249 | ); 250 | buildRules = ( 251 | ); 252 | dependencies = ( 253 | 6003F5B4195388D20070C39A /* PBXTargetDependency */, 254 | ); 255 | name = Tests; 256 | productName = loudspeakerTests; 257 | productReference = 6003F5AE195388D20070C39A /* Tests.xctest */; 258 | productType = "com.apple.product-type.bundle.unit-test"; 259 | }; 260 | /* End PBXNativeTarget section */ 261 | 262 | /* Begin PBXProject section */ 263 | 6003F582195388D10070C39A /* Project object */ = { 264 | isa = PBXProject; 265 | attributes = { 266 | CLASSPREFIX = LSP; 267 | LastUpgradeCheck = 0900; 268 | ORGANIZATIONNAME = "Adam Yanalunas"; 269 | TargetAttributes = { 270 | 6003F589195388D20070C39A = { 271 | ProvisioningStyle = Automatic; 272 | }; 273 | 6003F5AD195388D20070C39A = { 274 | TestTargetID = 6003F589195388D20070C39A; 275 | }; 276 | }; 277 | }; 278 | buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "loudspeaker" */; 279 | compatibilityVersion = "Xcode 3.2"; 280 | developmentRegion = English; 281 | hasScannedForEncodings = 0; 282 | knownRegions = ( 283 | en, 284 | Base, 285 | ); 286 | mainGroup = 6003F581195388D10070C39A; 287 | productRefGroup = 6003F58B195388D20070C39A /* Products */; 288 | projectDirPath = ""; 289 | projectRoot = ""; 290 | targets = ( 291 | 6003F589195388D20070C39A /* loudspeaker */, 292 | 6003F5AD195388D20070C39A /* Tests */, 293 | ); 294 | }; 295 | /* End PBXProject section */ 296 | 297 | /* Begin PBXResourcesBuildPhase section */ 298 | 6003F588195388D20070C39A /* Resources */ = { 299 | isa = PBXResourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 6003F5A9195388D20070C39A /* Images.xcassets in Resources */, 303 | 66A27D3D1FBE678B00BAA835 /* Launch.storyboard in Resources */, 304 | 6003F5A1195388D20070C39A /* Main.storyboard in Resources */, 305 | 6644D01519B1B06A006521EC /* Nyan-Cat.mp3 in Resources */, 306 | 6003F598195388D20070C39A /* InfoPlist.strings in Resources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | 6003F5AC195388D20070C39A /* Resources */ = { 311 | isa = PBXResourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */, 315 | 66275C8C19BA808800E74C05 /* Nyan-Cat-Test.mp3 in Resources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXResourcesBuildPhase section */ 320 | 321 | /* Begin PBXShellScriptBuildPhase section */ 322 | 5D3ACD3DAD153EA9B7AE1489 /* [CP] Copy Pods Resources */ = { 323 | isa = PBXShellScriptBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | ); 327 | inputPaths = ( 328 | ); 329 | name = "[CP] Copy Pods Resources"; 330 | outputPaths = ( 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | shellPath = /bin/sh; 334 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Tests/Pods-Tests-resources.sh\"\n"; 335 | showEnvVarsInLog = 0; 336 | }; 337 | 5FB7ABD54F6F6AB3BC115B1A /* [CP] Copy Pods Resources */ = { 338 | isa = PBXShellScriptBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | ); 342 | inputPaths = ( 343 | "${SRCROOT}/Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker-resources.sh", 344 | $PODS_CONFIGURATION_BUILD_DIR/loudspeaker/loudspeaker.bundle, 345 | ); 346 | name = "[CP] Copy Pods Resources"; 347 | outputPaths = ( 348 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | shellPath = /bin/sh; 352 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker-resources.sh\"\n"; 353 | showEnvVarsInLog = 0; 354 | }; 355 | 6A78E2D99612904378871230 /* [CP] Check Pods Manifest.lock */ = { 356 | isa = PBXShellScriptBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | ); 360 | inputPaths = ( 361 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 362 | "${PODS_ROOT}/Manifest.lock", 363 | ); 364 | name = "[CP] Check Pods Manifest.lock"; 365 | outputPaths = ( 366 | "$(DERIVED_FILE_DIR)/Pods-loudspeaker-checkManifestLockResult.txt", 367 | ); 368 | runOnlyForDeploymentPostprocessing = 0; 369 | shellPath = /bin/sh; 370 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 371 | showEnvVarsInLog = 0; 372 | }; 373 | 700189DE91770B50B11F4E3B /* [CP] Embed Pods Frameworks */ = { 374 | isa = PBXShellScriptBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | inputPaths = ( 379 | "${SRCROOT}/Pods/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh", 380 | "${BUILT_PRODUCTS_DIR}/Expecta/Expecta.framework", 381 | "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", 382 | "${BUILT_PRODUCTS_DIR}/Specta/Specta.framework", 383 | ); 384 | name = "[CP] Embed Pods Frameworks"; 385 | outputPaths = ( 386 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Expecta.framework", 387 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", 388 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Specta.framework", 389 | ); 390 | runOnlyForDeploymentPostprocessing = 0; 391 | shellPath = /bin/sh; 392 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh\"\n"; 393 | showEnvVarsInLog = 0; 394 | }; 395 | C2D6CA83C0B94A0BD33CD7DC /* [CP] Check Pods Manifest.lock */ = { 396 | isa = PBXShellScriptBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | ); 400 | inputPaths = ( 401 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 402 | "${PODS_ROOT}/Manifest.lock", 403 | ); 404 | name = "[CP] Check Pods Manifest.lock"; 405 | outputPaths = ( 406 | "$(DERIVED_FILE_DIR)/Pods-Tests-checkManifestLockResult.txt", 407 | ); 408 | runOnlyForDeploymentPostprocessing = 0; 409 | shellPath = /bin/sh; 410 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 411 | showEnvVarsInLog = 0; 412 | }; 413 | D29F17EAAAA416CD1A65A5D5 /* [CP] Embed Pods Frameworks */ = { 414 | isa = PBXShellScriptBuildPhase; 415 | buildActionMask = 2147483647; 416 | files = ( 417 | ); 418 | inputPaths = ( 419 | "${SRCROOT}/Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker-frameworks.sh", 420 | "${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework", 421 | "${BUILT_PRODUCTS_DIR}/loudspeaker/loudspeaker.framework", 422 | ); 423 | name = "[CP] Embed Pods Frameworks"; 424 | outputPaths = ( 425 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Masonry.framework", 426 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/loudspeaker.framework", 427 | ); 428 | runOnlyForDeploymentPostprocessing = 0; 429 | shellPath = /bin/sh; 430 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-loudspeaker/Pods-loudspeaker-frameworks.sh\"\n"; 431 | showEnvVarsInLog = 0; 432 | }; 433 | /* End PBXShellScriptBuildPhase section */ 434 | 435 | /* Begin PBXSourcesBuildPhase section */ 436 | 6003F586195388D20070C39A /* Sources */ = { 437 | isa = PBXSourcesBuildPhase; 438 | buildActionMask = 2147483647; 439 | files = ( 440 | 6003F59E195388D20070C39A /* LSPAppDelegate.m in Sources */, 441 | 6003F5A7195388D20070C39A /* LSPViewController.m in Sources */, 442 | 6003F59A195388D20070C39A /* main.m in Sources */, 443 | ); 444 | runOnlyForDeploymentPostprocessing = 0; 445 | }; 446 | 6003F5AA195388D20070C39A /* Sources */ = { 447 | isa = PBXSourcesBuildPhase; 448 | buildActionMask = 2147483647; 449 | files = ( 450 | 6003F5BC195388D20070C39A /* LSPAudioPlayerSpec.m in Sources */, 451 | 6697C9DF1FC395F4003311FC /* LSPConfigurationSpec.m in Sources */, 452 | 6697C9DB1FC388CF003311FC /* LSPAudioViewSpec.m in Sources */, 453 | 6697C9DD1FC38DF1003311FC /* LSPProgressViewSpec.m in Sources */, 454 | 6649A3DF1FBF545F00C4A1DD /* LSPCMTimeHelperSpec.m in Sources */, 455 | ); 456 | runOnlyForDeploymentPostprocessing = 0; 457 | }; 458 | /* End PBXSourcesBuildPhase section */ 459 | 460 | /* Begin PBXTargetDependency section */ 461 | 6003F5B4195388D20070C39A /* PBXTargetDependency */ = { 462 | isa = PBXTargetDependency; 463 | target = 6003F589195388D20070C39A /* loudspeaker */; 464 | targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */; 465 | }; 466 | /* End PBXTargetDependency section */ 467 | 468 | /* Begin PBXVariantGroup section */ 469 | 6003F596195388D20070C39A /* InfoPlist.strings */ = { 470 | isa = PBXVariantGroup; 471 | children = ( 472 | 6003F597195388D20070C39A /* en */, 473 | ); 474 | name = InfoPlist.strings; 475 | sourceTree = ""; 476 | }; 477 | 6003F59F195388D20070C39A /* Main.storyboard */ = { 478 | isa = PBXVariantGroup; 479 | children = ( 480 | 6003F5A0195388D20070C39A /* Base */, 481 | ); 482 | name = Main.storyboard; 483 | sourceTree = ""; 484 | }; 485 | 6003F5B8195388D20070C39A /* InfoPlist.strings */ = { 486 | isa = PBXVariantGroup; 487 | children = ( 488 | 6003F5B9195388D20070C39A /* en */, 489 | ); 490 | name = InfoPlist.strings; 491 | sourceTree = ""; 492 | }; 493 | /* End PBXVariantGroup section */ 494 | 495 | /* Begin XCBuildConfiguration section */ 496 | 6003F5BD195388D20070C39A /* Debug */ = { 497 | isa = XCBuildConfiguration; 498 | buildSettings = { 499 | ALWAYS_SEARCH_USER_PATHS = NO; 500 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 501 | CLANG_CXX_LIBRARY = "libc++"; 502 | CLANG_ENABLE_MODULES = YES; 503 | CLANG_ENABLE_OBJC_ARC = YES; 504 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 505 | CLANG_WARN_BOOL_CONVERSION = YES; 506 | CLANG_WARN_COMMA = YES; 507 | CLANG_WARN_CONSTANT_CONVERSION = YES; 508 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 509 | CLANG_WARN_EMPTY_BODY = YES; 510 | CLANG_WARN_ENUM_CONVERSION = YES; 511 | CLANG_WARN_INFINITE_RECURSION = YES; 512 | CLANG_WARN_INT_CONVERSION = YES; 513 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 514 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 515 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 517 | CLANG_WARN_STRICT_PROTOTYPES = YES; 518 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 519 | CLANG_WARN_UNREACHABLE_CODE = YES; 520 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 521 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 522 | COPY_PHASE_STRIP = NO; 523 | ENABLE_STRICT_OBJC_MSGSEND = YES; 524 | ENABLE_TESTABILITY = YES; 525 | GCC_C_LANGUAGE_STANDARD = gnu99; 526 | GCC_DYNAMIC_NO_PIC = NO; 527 | GCC_NO_COMMON_BLOCKS = YES; 528 | GCC_OPTIMIZATION_LEVEL = 0; 529 | GCC_PREPROCESSOR_DEFINITIONS = ( 530 | "DEBUG=1", 531 | "$(inherited)", 532 | ); 533 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 534 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 535 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 536 | GCC_WARN_UNDECLARED_SELECTOR = YES; 537 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 538 | GCC_WARN_UNUSED_FUNCTION = YES; 539 | GCC_WARN_UNUSED_VARIABLE = YES; 540 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 541 | ONLY_ACTIVE_ARCH = YES; 542 | SDKROOT = iphoneos; 543 | TARGETED_DEVICE_FAMILY = "1,2"; 544 | }; 545 | name = Debug; 546 | }; 547 | 6003F5BE195388D20070C39A /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | ALWAYS_SEARCH_USER_PATHS = NO; 551 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 552 | CLANG_CXX_LIBRARY = "libc++"; 553 | CLANG_ENABLE_MODULES = YES; 554 | CLANG_ENABLE_OBJC_ARC = YES; 555 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 556 | CLANG_WARN_BOOL_CONVERSION = YES; 557 | CLANG_WARN_COMMA = YES; 558 | CLANG_WARN_CONSTANT_CONVERSION = YES; 559 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 560 | CLANG_WARN_EMPTY_BODY = YES; 561 | CLANG_WARN_ENUM_CONVERSION = YES; 562 | CLANG_WARN_INFINITE_RECURSION = YES; 563 | CLANG_WARN_INT_CONVERSION = YES; 564 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 565 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 566 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 567 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 568 | CLANG_WARN_STRICT_PROTOTYPES = YES; 569 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 570 | CLANG_WARN_UNREACHABLE_CODE = YES; 571 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 572 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 573 | COPY_PHASE_STRIP = YES; 574 | ENABLE_NS_ASSERTIONS = NO; 575 | ENABLE_STRICT_OBJC_MSGSEND = YES; 576 | GCC_C_LANGUAGE_STANDARD = gnu99; 577 | GCC_NO_COMMON_BLOCKS = YES; 578 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 579 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 580 | GCC_WARN_UNDECLARED_SELECTOR = YES; 581 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 582 | GCC_WARN_UNUSED_FUNCTION = YES; 583 | GCC_WARN_UNUSED_VARIABLE = YES; 584 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 585 | SDKROOT = iphoneos; 586 | TARGETED_DEVICE_FAMILY = "1,2"; 587 | VALIDATE_PRODUCT = YES; 588 | }; 589 | name = Release; 590 | }; 591 | 6003F5C0195388D20070C39A /* Debug */ = { 592 | isa = XCBuildConfiguration; 593 | baseConfigurationReference = 277C080A6A344991E9CCDE66 /* Pods-loudspeaker.debug.xcconfig */; 594 | buildSettings = { 595 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 596 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 597 | CODE_SIGN_STYLE = Automatic; 598 | DEVELOPMENT_TEAM = ""; 599 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 600 | GCC_PREFIX_HEADER = "loudspeaker/loudspeaker-Prefix.pch"; 601 | INFOPLIST_FILE = "loudspeaker/loudspeaker-Info.plist"; 602 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 603 | PRODUCT_NAME = "$(TARGET_NAME)"; 604 | PROVISIONING_PROFILE = ""; 605 | PROVISIONING_PROFILE_SPECIFIER = ""; 606 | WRAPPER_EXTENSION = app; 607 | }; 608 | name = Debug; 609 | }; 610 | 6003F5C1195388D20070C39A /* Release */ = { 611 | isa = XCBuildConfiguration; 612 | baseConfigurationReference = 120B8E2BB30D71BE484C407B /* Pods-loudspeaker.release.xcconfig */; 613 | buildSettings = { 614 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 615 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 616 | CODE_SIGN_STYLE = Automatic; 617 | DEVELOPMENT_TEAM = ""; 618 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 619 | GCC_PREFIX_HEADER = "loudspeaker/loudspeaker-Prefix.pch"; 620 | INFOPLIST_FILE = "loudspeaker/loudspeaker-Info.plist"; 621 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 622 | PRODUCT_NAME = "$(TARGET_NAME)"; 623 | PROVISIONING_PROFILE_SPECIFIER = ""; 624 | WRAPPER_EXTENSION = app; 625 | }; 626 | name = Release; 627 | }; 628 | 6003F5C3195388D20070C39A /* Debug */ = { 629 | isa = XCBuildConfiguration; 630 | baseConfigurationReference = BBDDD2935A556DB5BD8B2D97 /* Pods-Tests.debug.xcconfig */; 631 | buildSettings = { 632 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/loudspeaker.app/loudspeaker"; 633 | FRAMEWORK_SEARCH_PATHS = ( 634 | "$(inherited)", 635 | "$(DEVELOPER_FRAMEWORKS_DIR)", 636 | ); 637 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 638 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 639 | GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; 640 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 641 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 642 | PRODUCT_NAME = "$(TARGET_NAME)"; 643 | TEST_HOST = "$(BUNDLE_LOADER)"; 644 | WRAPPER_EXTENSION = xctest; 645 | }; 646 | name = Debug; 647 | }; 648 | 6003F5C4195388D20070C39A /* Release */ = { 649 | isa = XCBuildConfiguration; 650 | baseConfigurationReference = CC4EB31AA02B580F6A5F7C00 /* Pods-Tests.release.xcconfig */; 651 | buildSettings = { 652 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/loudspeaker.app/loudspeaker"; 653 | FRAMEWORK_SEARCH_PATHS = ( 654 | "$(inherited)", 655 | "$(DEVELOPER_FRAMEWORKS_DIR)", 656 | ); 657 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 658 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 659 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 660 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 661 | PRODUCT_NAME = "$(TARGET_NAME)"; 662 | TEST_HOST = "$(BUNDLE_LOADER)"; 663 | WRAPPER_EXTENSION = xctest; 664 | }; 665 | name = Release; 666 | }; 667 | /* End XCBuildConfiguration section */ 668 | 669 | /* Begin XCConfigurationList section */ 670 | 6003F585195388D10070C39A /* Build configuration list for PBXProject "loudspeaker" */ = { 671 | isa = XCConfigurationList; 672 | buildConfigurations = ( 673 | 6003F5BD195388D20070C39A /* Debug */, 674 | 6003F5BE195388D20070C39A /* Release */, 675 | ); 676 | defaultConfigurationIsVisible = 0; 677 | defaultConfigurationName = Release; 678 | }; 679 | 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "loudspeaker" */ = { 680 | isa = XCConfigurationList; 681 | buildConfigurations = ( 682 | 6003F5C0195388D20070C39A /* Debug */, 683 | 6003F5C1195388D20070C39A /* Release */, 684 | ); 685 | defaultConfigurationIsVisible = 0; 686 | defaultConfigurationName = Release; 687 | }; 688 | 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Tests" */ = { 689 | isa = XCConfigurationList; 690 | buildConfigurations = ( 691 | 6003F5C3195388D20070C39A /* Debug */, 692 | 6003F5C4195388D20070C39A /* Release */, 693 | ); 694 | defaultConfigurationIsVisible = 0; 695 | defaultConfigurationName = Release; 696 | }; 697 | /* End XCConfigurationList section */ 698 | }; 699 | rootObject = 6003F582195388D10070C39A /* Project object */; 700 | } 701 | -------------------------------------------------------------------------------- /Example/loudspeaker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/loudspeaker.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 78 | 79 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Example/loudspeaker.xcodeproj/xcshareddata/xcschemes/loudspeaker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Example/loudspeaker.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Example/loudspeaker/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/60@2x.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/60@3x.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/76@1x.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/76@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/76@3x.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Images.xcassets/AppIcon.appiconset/83.5@2x.png -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "60@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "60@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "size" : "20x20", 48 | "scale" : "1x" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "size" : "20x20", 53 | "scale" : "2x" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "size" : "29x29", 58 | "scale" : "1x" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "size" : "29x29", 63 | "scale" : "2x" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "size" : "40x40", 68 | "scale" : "1x" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "size" : "40x40", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "76x76", 77 | "idiom" : "ipad", 78 | "filename" : "76@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "76x76", 83 | "idiom" : "ipad", 84 | "filename" : "76@3x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "83.5x83.5", 89 | "idiom" : "ipad", 90 | "filename" : "83.5@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "1024x1024", 95 | "idiom" : "ios-marketing", 96 | "filename" : "1024.png", 97 | "scale" : "1x" 98 | } 99 | ], 100 | "info" : { 101 | "version" : 1, 102 | "author" : "xcode" 103 | } 104 | } -------------------------------------------------------------------------------- /Example/loudspeaker/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/loudspeaker/LSPAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAppDelegate.h 3 | // loudspeaker 4 | // 5 | // Created by CocoaPods on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LSPAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Example/loudspeaker/LSPAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAppDelegate.m 3 | // loudspeaker 4 | // 5 | // Created by CocoaPods on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import "LSPAppDelegate.h" 10 | 11 | @implementation LSPAppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 14 | { 15 | // Override point for customization after application launch. 16 | return YES; 17 | } 18 | 19 | - (void)applicationWillResignActive:(UIApplication *)application 20 | { 21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 22 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 23 | } 24 | 25 | - (void)applicationDidEnterBackground:(UIApplication *)application 26 | { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | - (void)applicationWillEnterForeground:(UIApplication *)application 32 | { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | - (void)applicationDidBecomeActive:(UIApplication *)application 37 | { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application 42 | { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Example/loudspeaker/LSPViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPViewController.h 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface LSPViewController : UIViewController 13 | 14 | @property (weak, nonatomic) IBOutlet UIButton *demoButton; 15 | @property (weak, nonatomic) IBOutlet UIButton *harlequinButton; 16 | 17 | - (void)resetDemoButtons; 18 | - (IBAction)launchDemo:(id)sender; 19 | - (IBAction)launchHarlequinDemo:(id)sender; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Example/loudspeaker/LSPViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPViewController.m 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import "LSPViewController.h" 10 | #import 11 | 12 | 13 | @interface LSPViewController () 14 | 15 | @property (nonatomic) LSPAudioViewController *audioVC; 16 | @property (nonatomic) LSPConfiguration *configuration; 17 | 18 | - (void)audioViewControllerSetup; 19 | - (void)configureDemo:(id)sender; 20 | - (void)playAudioWithURL:(NSURL *)audioURL; 21 | 22 | @end 23 | 24 | 25 | @implementation LSPViewController 26 | 27 | 28 | #pragma mark - Demo 29 | - (IBAction)launchDemo:(id)sender 30 | { 31 | self.demoButton.enabled = NO; 32 | self.harlequinButton.enabled = NO; 33 | 34 | [self configureDemo:sender]; 35 | 36 | NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"Nyan-Cat" withExtension:@"mp3"]; 37 | [self playAudioWithURL:audioURL]; 38 | } 39 | 40 | 41 | - (void)resetDemoButtons 42 | { 43 | self.demoButton.enabled = YES; 44 | self.harlequinButton.enabled = YES; 45 | } 46 | 47 | 48 | - (IBAction)launchHarlequinDemo:(id)sender 49 | { 50 | [self launchDemo:sender]; 51 | } 52 | 53 | 54 | - (void)configureDemo:(id)sender 55 | { 56 | if (sender == self.harlequinButton) 57 | { 58 | self.configuration = [LSPConfigurationBuilder configurationWithBuilder:^(LSPConfigurationBuilder *builder) { 59 | builder.volume = 0.1; 60 | }]; 61 | self.audioVC = [LSPAudioViewController.alloc initWithConfiguration:self.configuration]; 62 | CGFloat playerHeight = 120; 63 | self.audioVC.view.frame = CGRectMake(0, CGRectGetHeight(self.view.frame) - playerHeight, CGRectGetWidth(self.view.frame), playerHeight); 64 | 65 | self.audioVC.playerView.progressView.foregroundColor = UIColor.redColor; 66 | self.audioVC.playerView.progressView.backgroundColor = UIColor.blueColor; 67 | self.audioVC.playerView.backgroundColor = UIColor.greenColor; 68 | self.audioVC.playerView.playbackTimeLabel.textColor = UIColor.brownColor; 69 | self.audioVC.playerView.titleLabel.textColor = UIColor.yellowColor; 70 | } 71 | else 72 | { 73 | self.configuration = [LSPConfigurationBuilder.defaultConfiguration build]; 74 | self.audioVC = [LSPAudioViewController.alloc initWithConfiguration:self.configuration]; 75 | CGFloat playerHeight = 60; 76 | self.audioVC.view.frame = CGRectMake(0, CGRectGetHeight(self.view.frame) - playerHeight, CGRectGetWidth(self.view.frame), playerHeight); 77 | 78 | self.audioVC.playerView.progressView.foregroundColor = [UIColor colorWithRed:88/255. green:199/255. blue:226/255. alpha:1]; 79 | self.audioVC.playerView.progressView.backgroundColor = [UIColor colorWithWhite:207/255. alpha:1]; 80 | self.audioVC.playerView.backgroundColor = [UIColor colorWithWhite:238/255. alpha:1]; 81 | self.audioVC.playerView.playbackTimeLabel.textColor = [UIColor colorWithWhite:102/255. alpha:1]; 82 | self.audioVC.playerView.titleLabel.textColor = [UIColor colorWithWhite:102/255. alpha:1]; 83 | } 84 | } 85 | 86 | 87 | #pragma mark - Player 88 | - (void)playAudioWithURL:(NSURL *)audioURL 89 | { 90 | [self audioViewControllerSetup]; 91 | [self.audioVC playAudioWithURL:audioURL]; 92 | [self.audioVC show]; 93 | } 94 | 95 | 96 | - (void)audioViewControllerSetup 97 | { 98 | self.audioVC.delegate = self; 99 | [self.view addSubview:self.audioVC.view]; 100 | 101 | [self.audioVC willMoveToParentViewController:self]; 102 | [self addChildViewController:self.audioVC]; 103 | [self.audioVC didMoveToParentViewController:self]; 104 | } 105 | 106 | 107 | - (void)audioViewController:(LSPAudioViewController *)viewController didClosePlayer:(LSPAudioPlayer *)player 108 | { 109 | [self resetDemoButtons]; 110 | } 111 | 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /Example/loudspeaker/Launch.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/loudspeaker/Nyan-Cat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Example/loudspeaker/Nyan-Cat.mp3 -------------------------------------------------------------------------------- /Example/loudspeaker/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/loudspeaker/loudspeaker-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | Launch 29 | UIMainStoryboardFile 30 | Main 31 | UIMainStoryboardFile~ipad 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Example/loudspeaker/loudspeaker-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /Example/loudspeaker/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 08/29/2014. 6 | // Copyright (c) 2014 Adam Yanalunas. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "LSPAppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([LSPAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'xcpretty' 4 | gem 'cocoapods' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.5) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | claide (1.0.2) 11 | cocoapods (1.3.1) 12 | activesupport (>= 4.0.2, < 5) 13 | claide (>= 1.0.2, < 2.0) 14 | cocoapods-core (= 1.3.1) 15 | cocoapods-deintegrate (>= 1.0.1, < 2.0) 16 | cocoapods-downloader (>= 1.1.3, < 2.0) 17 | cocoapods-plugins (>= 1.0.0, < 2.0) 18 | cocoapods-search (>= 1.0.0, < 2.0) 19 | cocoapods-stats (>= 1.0.0, < 2.0) 20 | cocoapods-trunk (>= 1.2.0, < 2.0) 21 | cocoapods-try (>= 1.1.0, < 2.0) 22 | colored2 (~> 3.1) 23 | escape (~> 0.0.4) 24 | fourflusher (~> 2.0.1) 25 | gh_inspector (~> 1.0) 26 | molinillo (~> 0.5.7) 27 | nap (~> 1.0) 28 | ruby-macho (~> 1.1) 29 | xcodeproj (>= 1.5.1, < 2.0) 30 | cocoapods-core (1.3.1) 31 | activesupport (>= 4.0.2, < 6) 32 | fuzzy_match (~> 2.0.4) 33 | nap (~> 1.0) 34 | cocoapods-deintegrate (1.0.1) 35 | cocoapods-downloader (1.1.3) 36 | cocoapods-plugins (1.0.0) 37 | nap 38 | cocoapods-search (1.0.0) 39 | cocoapods-stats (1.0.0) 40 | cocoapods-trunk (1.3.0) 41 | nap (>= 0.8, < 2.0) 42 | netrc (~> 0.11) 43 | cocoapods-try (1.1.0) 44 | colored2 (3.1.2) 45 | concurrent-ruby (1.0.5) 46 | escape (0.0.4) 47 | fourflusher (2.0.1) 48 | fuzzy_match (2.0.4) 49 | gh_inspector (1.0.3) 50 | i18n (0.9.0) 51 | concurrent-ruby (~> 1.0) 52 | minitest (5.10.3) 53 | molinillo (0.5.7) 54 | nanaimo (0.2.3) 55 | nap (1.1.0) 56 | netrc (0.11.0) 57 | rouge (1.10.1) 58 | ruby-macho (1.1.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.4) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.5.3) 63 | CFPropertyList (~> 2.3.3) 64 | claide (>= 1.0.2, < 2.0) 65 | colored2 (~> 3.1) 66 | nanaimo (~> 0.2.3) 67 | xcpretty (0.2.2) 68 | rouge (~> 1.8) 69 | 70 | PLATFORMS 71 | ruby 72 | 73 | DEPENDENCIES 74 | cocoapods 75 | xcpretty 76 | 77 | BUNDLED WITH 78 | 1.15.4 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Adam Yanalunas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Pod/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/.gitkeep -------------------------------------------------------------------------------- /Pod/Assets/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Pods 4 | 5 | Created by Adam Yanalunas on 11/19/17. 6 | 7 | */ 8 | 9 | "loudspeaker.close" = "Close"; 10 | "loudspeaker.pause" = "Pause"; 11 | "loudspeaker.play" = "Play"; 12 | "loudspeaker.seconds_elapsed_format" = "%@ seconds elapsed"; 13 | "loudspeaker.tap_to_pause_playback" = "Tap to pause playback"; 14 | "loudspeaker.tap_to_resume_playback" = "Tap to resume playback"; 15 | -------------------------------------------------------------------------------- /Pod/Assets/audio_close_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_close_icon.png -------------------------------------------------------------------------------- /Pod/Assets/audio_close_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_close_icon@2x.png -------------------------------------------------------------------------------- /Pod/Assets/audio_close_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_close_icon@3x.png -------------------------------------------------------------------------------- /Pod/Assets/audio_pause_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_pause_icon.png -------------------------------------------------------------------------------- /Pod/Assets/audio_pause_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_pause_icon@2x.png -------------------------------------------------------------------------------- /Pod/Assets/audio_pause_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_pause_icon@3x.png -------------------------------------------------------------------------------- /Pod/Assets/audio_play_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_play_icon.png -------------------------------------------------------------------------------- /Pod/Assets/audio_play_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_play_icon@2x.png -------------------------------------------------------------------------------- /Pod/Assets/audio_play_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Assets/audio_play_icon@3x.png -------------------------------------------------------------------------------- /Pod/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amco/loudspeaker/22a29ae14f7d4baf5c03f4e63f8649798a9b4ff1/Pod/Classes/.gitkeep -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioPlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayer.h 3 | // 4 | // Created by Adam Yanalunas on 3/12/13. 5 | // Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | @interface LSPAudioPlayer : NSObject 16 | 17 | @property (nonatomic, readonly) AVQueuePlayer *player; 18 | @property (nonatomic, nullable) NSURL* previousPath; 19 | @property (nonatomic, readonly, getter = isPlaying) BOOL playing; 20 | 21 | - (id)addProgressObserver:(void (^)(CMTime time))observer; 22 | - (CMTime)currentTime; 23 | - (CMTime)duration; 24 | - (void)jumpToProgress:(Float64)progress; 25 | - (void)play; 26 | - (void)playFromURL:(NSURL*)url; 27 | - (void)playFromURLString:(NSString*)url; 28 | - (void)pause; 29 | - (void)removeProgressObserver:(id)observer; 30 | - (void)resetPlayer; 31 | - (void)setVolume:(float)volume; 32 | - (AVPlayerStatus)status; 33 | - (void)stop; 34 | - (CMTime)timeFromProgress:(Float64)progress; 35 | - (void)togglePlayback; 36 | 37 | @end 38 | 39 | 40 | NS_ASSUME_NONNULL_END 41 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioPlayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayer.m 3 | // 4 | // Created by Adam Yanalunas on 3/12/13. 5 | // Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "LSPAudioPlayer.h" 10 | 11 | @interface LSPAudioPlayer () 12 | 13 | @property (nonatomic) CMTime seekTolerance; 14 | @property (nonatomic) CMTime observationInterval; 15 | 16 | - (void)listenForEndOfAudio:(AVPlayerItem*)audio; 17 | 18 | @end 19 | 20 | @implementation LSPAudioPlayer 21 | 22 | 23 | - (instancetype)init 24 | { 25 | self = [super init]; 26 | if (!self) return nil; 27 | 28 | _player = AVQueuePlayer.new; 29 | _observationInterval = CMTimeMake(1, 35); 30 | _seekTolerance = CMTimeMake(1, 100); 31 | 32 | return self; 33 | } 34 | 35 | 36 | #pragma mark - Playback 37 | - (void)playFromURLString:(NSString*)urlString 38 | { 39 | NSString *tildeString = [urlString stringByExpandingTildeInPath]; 40 | NSURL *url = [NSURL fileURLWithPath:[tildeString stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet] isDirectory:NO]; 41 | [self playFromURL:url]; 42 | } 43 | 44 | - (void)playFromURL:(NSURL*)path 45 | { 46 | if (![path.absoluteString isEqualToString:self.previousPath.absoluteString]) 47 | { 48 | [self pause]; 49 | [self.player removeAllItems]; 50 | 51 | AVPlayerItem *audioItem = [AVPlayerItem playerItemWithURL:path]; 52 | 53 | [self listenForEndOfAudio:audioItem]; 54 | [self.player insertItem:audioItem afterItem:nil]; 55 | 56 | self.previousPath = path; 57 | } 58 | 59 | [self togglePlayback]; 60 | } 61 | 62 | 63 | - (void)play 64 | { 65 | [self.player play]; 66 | } 67 | 68 | 69 | - (void)pause 70 | { 71 | [self.player pause]; 72 | } 73 | 74 | 75 | - (void)stop 76 | { 77 | [self pause]; 78 | [self.player removeAllItems]; 79 | self.previousPath = nil; 80 | } 81 | 82 | 83 | - (void)setVolume:(float)volume 84 | { 85 | self.player.volume = volume; 86 | } 87 | 88 | 89 | - (void)jumpToProgress:(Float64)progress 90 | { 91 | CMTime time = [self timeFromProgress:progress]; 92 | [self.player seekToTime:time toleranceBefore:self.seekTolerance toleranceAfter:self.seekTolerance]; 93 | } 94 | 95 | 96 | - (void)togglePlayback 97 | { 98 | if (self.isPlaying) 99 | { 100 | [self pause]; 101 | } 102 | else 103 | { 104 | [self play]; 105 | } 106 | } 107 | 108 | 109 | - (id)addProgressObserver:(void (^)(CMTime time))observer 110 | { 111 | return [self.player addPeriodicTimeObserverForInterval:self.observationInterval queue:NULL usingBlock:^(CMTime time) { 112 | observer(time); 113 | }]; 114 | } 115 | 116 | 117 | - (void)removeProgressObserver:(id)observer 118 | { 119 | [self.player removeTimeObserver:observer]; 120 | } 121 | 122 | 123 | #pragma mark - Helpers 124 | - (CMTime)currentTime 125 | { 126 | return self.player.currentItem.currentTime; 127 | } 128 | 129 | 130 | - (CMTime)duration 131 | { 132 | return self.player.currentItem.duration; 133 | } 134 | 135 | 136 | - (AVPlayerStatus)status 137 | { 138 | return self.player.status; 139 | } 140 | 141 | 142 | - (BOOL)isPlaying 143 | { 144 | return (self.player && self.player.rate); 145 | } 146 | 147 | 148 | - (CMTime)timeFromProgress:(Float64)progress 149 | { 150 | return CMTimeMakeWithSeconds(CMTimeGetSeconds(self.duration) * progress, 100); 151 | } 152 | 153 | 154 | - (void)listenForEndOfAudio:(AVPlayerItem*)audio 155 | { 156 | NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 157 | [center removeObserver:self]; 158 | [center addObserver:self selector:@selector(resetPlayer) name:AVPlayerItemDidPlayToEndTimeNotification object:audio]; 159 | } 160 | 161 | - (void)resetPlayer 162 | { 163 | if (self.previousPath) 164 | { 165 | [NSNotificationCenter.defaultCenter removeObserver:self]; 166 | [self.player removeAllItems]; 167 | self.previousPath = nil; 168 | } 169 | } 170 | 171 | @end 172 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioPlayerModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayerModel.h 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 11/16/17. 6 | // 7 | 8 | 9 | #import 10 | 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | @interface LSPAudioPlayerModel : NSObject 16 | 17 | - (nullable NSURL *)destination; 18 | - (void)setDestination:(NSURL *)url; 19 | - (nullable NSString *)title; 20 | 21 | @end 22 | 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioPlayerModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayerModel.m 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 11/16/17. 6 | // 7 | 8 | #import "LSPAudioPlayerModel.h" 9 | 10 | 11 | @interface LSPAudioPlayerModel () 12 | 13 | @property (nonatomic) NSURL *url; 14 | 15 | @end 16 | 17 | 18 | @implementation LSPAudioPlayerModel 19 | 20 | 21 | - (NSString *)title 22 | { 23 | return self.url.lastPathComponent; 24 | } 25 | 26 | 27 | - (NSURL *)destination 28 | { 29 | return self.destination; 30 | } 31 | 32 | 33 | - (void)setDestination:(NSURL *)url 34 | { 35 | self.url = url; 36 | } 37 | 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayer.h 3 | // 4 | // Created by Adam Yanalunas on 2/6/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | 9 | #import "LSPProgressView.h" 10 | 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | @interface LSPAudioView : UIView 16 | 17 | - (instancetype)initForAutoLayout NS_DESIGNATED_INITIALIZER; 18 | - (instancetype)init NS_UNAVAILABLE; 19 | - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; 20 | + (instancetype)new NS_UNAVAILABLE; 21 | + (instancetype)newAutoLayoutView; 22 | 23 | @property (nonatomic) UIButton *closeButton; 24 | @property (nonatomic) UILabel *playbackTimeLabel; 25 | @property (nonatomic) UIButton *playPauseButton; 26 | @property (nonatomic) LSPProgressView *progressView; 27 | @property (nonatomic) UILabel *titleLabel; 28 | 29 | - (void)setCurrentProgress:(NSString *)progress forDuration:(NSString *)duration; 30 | - (void)showLayoutForPlaying:(BOOL)isPlaying; 31 | 32 | @end 33 | 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioView.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioPlayer.m 3 | // 4 | // Created by Adam Yanalunas on 2/6/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import "LSPAudioView.h" 9 | #import "LSPKit.h" 10 | #import "LSPProgressView.h" 11 | #import 12 | 13 | 14 | @interface LSPAudioView () 15 | 16 | - (void)applyConstraints; 17 | - (void)attachSubviews; 18 | - (void)reset; 19 | - (void)setup; 20 | 21 | @end 22 | 23 | 24 | @implementation LSPAudioView 25 | 26 | 27 | #pragma mark - Lifespan 28 | - (instancetype)initForAutoLayout 29 | { 30 | self = [super initWithFrame:CGRectZero]; 31 | if (!self) return nil; 32 | 33 | self.translatesAutoresizingMaskIntoConstraints = NO; 34 | [self setup]; 35 | 36 | return self; 37 | } 38 | 39 | 40 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 41 | { 42 | return [self initForAutoLayout]; 43 | } 44 | 45 | 46 | + (instancetype)newAutoLayoutView 47 | { 48 | LSPAudioView *view = [super new]; 49 | if (!view) return nil; 50 | 51 | view.translatesAutoresizingMaskIntoConstraints = NO; 52 | [view setup]; 53 | 54 | return view; 55 | } 56 | 57 | 58 | - (void)setup 59 | { 60 | [self reset]; 61 | 62 | self.backgroundColor = [UIColor colorWithWhite:238/255. alpha:1]; 63 | 64 | [self attachSubviews]; 65 | [self applyConstraints]; 66 | [self showLayoutForPlaying:NO]; 67 | } 68 | 69 | 70 | - (void)reset 71 | { 72 | [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; 73 | } 74 | 75 | 76 | #pragma mark - Layout 77 | - (void)applyConstraints 78 | { 79 | float horizontalPadding = 12.f; 80 | float verticalPadding = 12.f; 81 | 82 | [self.progressView mas_makeConstraints:^(MASConstraintMaker *make) { 83 | make.left.equalTo(_playPauseButton.mas_right).offset(horizontalPadding); 84 | make.right.greaterThanOrEqualTo(_closeButton.mas_left).offset(-horizontalPadding); 85 | make.top.equalTo(_titleLabel.mas_bottom).offset(6); 86 | make.height.equalTo(@9); 87 | }]; 88 | 89 | [_playPauseButton mas_makeConstraints:^(MASConstraintMaker *make) { 90 | make.left.equalTo(self.mas_left); 91 | make.width.and.height.equalTo(@60); 92 | make.centerY.equalTo(self.mas_centerY); 93 | }]; 94 | 95 | [_closeButton mas_makeConstraints:^(MASConstraintMaker *make) { 96 | make.centerY.equalTo(self.mas_centerY); 97 | make.right.equalTo(self.mas_right); 98 | make.width.and.height.equalTo(@60); 99 | }]; 100 | 101 | [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { 102 | make.height.equalTo(@20); 103 | make.top.equalTo(self.mas_top).offset(verticalPadding); 104 | make.right.greaterThanOrEqualTo(_playbackTimeLabel.mas_left).offset(horizontalPadding); 105 | make.left.equalTo(_playPauseButton.mas_right).offset(horizontalPadding); 106 | }]; 107 | 108 | [_playbackTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) { 109 | make.width.equalTo(@100); 110 | make.right.greaterThanOrEqualTo(_closeButton.mas_left).offset(-horizontalPadding); 111 | make.top.greaterThanOrEqualTo(self.mas_top).offset(verticalPadding); 112 | }]; 113 | } 114 | 115 | 116 | - (void)attachSubviews 117 | { 118 | [self addSubview:self.closeButton]; 119 | [self addSubview:self.playbackTimeLabel]; 120 | [self addSubview:self.progressView]; 121 | [self addSubview:self.titleLabel]; 122 | [self addSubview:self.playPauseButton]; 123 | } 124 | 125 | 126 | #pragma mark - Playback information 127 | - (void)showLayoutForPlaying:(BOOL)isPlaying 128 | { 129 | UIImage *icon = (isPlaying ? LSPKit.pauseIcon : LSPKit.playIcon); 130 | [_playPauseButton setImage:icon forState:UIControlStateNormal]; 131 | [self setAccessibilityForPlayback:isPlaying]; 132 | } 133 | 134 | 135 | - (void)setCurrentProgress:(NSString *)progress forDuration:(NSString *)duration 136 | { 137 | NSString *labelText = [NSString stringWithFormat:@"%@ / %@", progress, duration]; 138 | 139 | self.playbackTimeLabel.text = labelText; 140 | self.playbackTimeLabel.accessibilityLabel = [NSString stringWithFormat:[LSPKit localizedString:@"loudspeaker.seconds_elapsed_format"], progress]; 141 | } 142 | 143 | 144 | #pragma mark - Helpers 145 | - (void)setAccessibilityForPlayback:(BOOL)isPlaying 146 | { 147 | if (isPlaying) 148 | { 149 | _playPauseButton.accessibilityLabel = [LSPKit localizedString:@"loudspeaker.pause"]; 150 | _playPauseButton.accessibilityHint = [LSPKit localizedString:@"loudspeaker.tap_to_pause_playback"]; 151 | } 152 | else 153 | { 154 | _playPauseButton.accessibilityLabel = [LSPKit localizedString:@"loudspeaker.play"]; 155 | _playPauseButton.accessibilityHint = [LSPKit localizedString:@"loudspeaker.tap_to_resume_playback"]; 156 | } 157 | } 158 | 159 | 160 | #pragma mark - Properties 161 | - (UIButton *)closeButton 162 | { 163 | if (!_closeButton) 164 | { 165 | _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; 166 | _closeButton.translatesAutoresizingMaskIntoConstraints = NO; 167 | [_closeButton setImage:LSPKit.closeIcon forState:UIControlStateNormal]; 168 | 169 | _closeButton.isAccessibilityElement = YES; 170 | _closeButton.accessibilityLabel = [LSPKit localizedString:@"loudspeaker.close"]; 171 | } 172 | 173 | return _closeButton; 174 | } 175 | 176 | 177 | - (UILabel *)playbackTimeLabel 178 | { 179 | if (!_playbackTimeLabel) 180 | { 181 | _playbackTimeLabel = [UILabel.alloc initWithFrame:CGRectZero]; 182 | _playbackTimeLabel.adjustsFontSizeToFitWidth = YES; 183 | _playbackTimeLabel.font = [UIFont fontWithName:LSPKit.fontName size:14.]; 184 | _playbackTimeLabel.textColor = [UIColor colorWithWhite:102/255. alpha:1]; 185 | _playbackTimeLabel.textAlignment = NSTextAlignmentRight; 186 | _playbackTimeLabel.translatesAutoresizingMaskIntoConstraints = NO; 187 | 188 | _playbackTimeLabel.isAccessibilityElement = YES; 189 | _playbackTimeLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; 190 | } 191 | 192 | return _playbackTimeLabel; 193 | } 194 | 195 | 196 | - (UIButton *)playPauseButton 197 | { 198 | if (!_playPauseButton) 199 | { 200 | _playPauseButton = [UIButton buttonWithType:UIButtonTypeCustom]; 201 | _playPauseButton.translatesAutoresizingMaskIntoConstraints = NO; 202 | [_playPauseButton setImage:LSPKit.playIcon forState:UIControlStateNormal]; 203 | 204 | _playPauseButton.isAccessibilityElement = YES; 205 | [self setAccessibilityForPlayback:NO]; 206 | } 207 | 208 | return _playPauseButton; 209 | } 210 | 211 | 212 | - (LSPProgressView *)progressView 213 | { 214 | if (!_progressView) 215 | { 216 | _progressView = [LSPProgressView newAutoLayoutView]; 217 | } 218 | 219 | return _progressView; 220 | } 221 | 222 | 223 | - (UILabel *)titleLabel 224 | { 225 | if (!_titleLabel) 226 | { 227 | _titleLabel = [UILabel.alloc initWithFrame:CGRectZero]; 228 | _titleLabel.font = [UIFont fontWithName:LSPKit.fontName size:14.]; 229 | _titleLabel.textAlignment = NSTextAlignmentLeft; 230 | _titleLabel.textColor = [UIColor colorWithWhite:102/255. alpha:1]; 231 | _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; 232 | 233 | _titleLabel.isAccessibilityElement = YES; 234 | } 235 | 236 | return _titleLabel; 237 | } 238 | 239 | @end 240 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioViewController.h 3 | // 4 | // Created by Adam Yanalunas on 2/6/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import "LSPAudioView.h" 9 | #import "LSPAudioPlayerModel.h" 10 | 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | @class LSPAudioPlayer, LSPAudioViewController, LSPConfiguration; 16 | 17 | @protocol LSPAudioViewControllerDelegate 18 | 19 | @optional 20 | - (void)audioViewController:(LSPAudioViewController *)viewController didClosePlayer:(LSPAudioPlayer *)player; 21 | - (void)audioViewController:(LSPAudioViewController *)viewController didPausePlayer:(LSPAudioPlayer *)player; 22 | - (void)audioViewController:(LSPAudioViewController *)viewController didPlayPlayer:(LSPAudioPlayer *)player; 23 | - (void)audioViewController:(LSPAudioViewController *)viewController didStopPlayer:(LSPAudioPlayer *)player; 24 | 25 | - (void)audioViewController:(LSPAudioViewController *)viewController willClosePlayer:(LSPAudioPlayer *)player; 26 | - (void)audioViewController:(LSPAudioViewController *)viewController willPausePlayer:(LSPAudioPlayer *)player; 27 | - (void)audioViewController:(LSPAudioViewController *)viewController willPlayPlayer:(LSPAudioPlayer *)player; 28 | - (void)audioViewController:(LSPAudioViewController *)viewController willStopPlayer:(LSPAudioPlayer *)player; 29 | 30 | @end 31 | 32 | 33 | @interface LSPAudioViewController : UIViewController 34 | 35 | @property (nonatomic, readonly) LSPConfiguration *configuration; 36 | @property (nonatomic, weak) id delegate; 37 | @property (nonatomic) LSPAudioPlayerModel *model; 38 | @property (nonatomic, readonly) LSPAudioView *playerView; 39 | 40 | + (instancetype)new NS_UNAVAILABLE; 41 | - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; 42 | - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; 43 | - (instancetype)initWithConfiguration:(LSPConfiguration *)configuration NS_DESIGNATED_INITIALIZER; 44 | 45 | - (void)close; 46 | - (void)hide:(nullable void (^)(void))completion; 47 | - (BOOL)isPlaying; 48 | - (void)jumpToProgress:(Float64)progress; 49 | - (void)pause; 50 | - (void)play; 51 | - (void)playAudioWithURL:(NSURL *)url; 52 | - (void)reset; 53 | - (void)show; 54 | - (void)stop; 55 | 56 | @end 57 | 58 | 59 | NS_ASSUME_NONNULL_END 60 | -------------------------------------------------------------------------------- /Pod/Classes/LSPAudioViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPAudioViewController.m 3 | // 4 | // Created by Adam Yanalunas on 2/6/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import "LSPAudioPlayer.h" 9 | #import "LSPAudioView.h" 10 | #import "LSPAudioViewController.h" 11 | #import "LSPCMTimeHelper.h" 12 | #import "LSPConfiguration.h" 13 | #import "LSPKit.h" 14 | #import "LSPProgressView.h" 15 | #import 16 | 17 | 18 | static void * LSPAudioViewControllerContext = &LSPAudioViewControllerContext; 19 | 20 | 21 | @interface LSPAudioViewController () 22 | 23 | @property (nonatomic) LSPAudioPlayer *player; 24 | @property (nonatomic) MASConstraint *bottomConstraint; 25 | @property (nonatomic) LSPConfiguration *configuration; 26 | @property (nonatomic) LSPAudioView *playerView; 27 | @property (nonatomic) BOOL playing; 28 | @property (nonatomic) id progressObserver; 29 | 30 | - (void)addTimeObserver; 31 | - (MASViewAttribute *)bottomLayout; 32 | - (CGFloat)bottomOffset; 33 | - (void)closeButtonPressed:(UIButton *)button; 34 | - (void)handleTimelineTap:(UITapGestureRecognizer *)gesture; 35 | - (void)playedUntilEnd:(NSNotification *)notificaiton; 36 | - (Float64)progressFromGesture:(UIGestureRecognizer *)gesture; 37 | - (void)removeTimeObserver; 38 | - (void)showProgress; 39 | - (void)togglePlayPause:(UIButton *)button; 40 | - (void)updateProgress; 41 | - (void)updateTimeLabel; 42 | 43 | @end 44 | 45 | 46 | @implementation LSPAudioViewController 47 | 48 | @dynamic view; 49 | 50 | #pragma mark - Lifecycle 51 | - (instancetype)initWithConfiguration:(LSPConfiguration *)configuration 52 | { 53 | self = [super initWithNibName:nil bundle:nil]; 54 | if (!self) return nil; 55 | 56 | _configuration = configuration; 57 | _model = LSPAudioPlayerModel.new; 58 | _player = LSPAudioPlayer.new; 59 | _playerView = [LSPAudioView newAutoLayoutView]; 60 | [_player setVolume:_configuration.volume]; 61 | 62 | return self; 63 | } 64 | 65 | 66 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 67 | { 68 | return nil; 69 | } 70 | 71 | 72 | - (void)viewDidLoad 73 | { 74 | [super viewDidLoad]; 75 | 76 | [self.view addSubview:self.playerView]; 77 | [self assignConstraintsToView]; 78 | 79 | [self.playerView.closeButton addTarget:self action:@selector(closeButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; 80 | [self.playerView.playPauseButton addTarget:self action:@selector(togglePlayPause:) forControlEvents:UIControlEventTouchUpInside]; 81 | 82 | UITapGestureRecognizer *tapGesture = [UITapGestureRecognizer.alloc initWithTarget:self action:@selector(handleTimelineTap:)]; 83 | [self.playerView.progressView addGestureRecognizer:tapGesture]; 84 | 85 | UIPanGestureRecognizer *panGesture = [UIPanGestureRecognizer.alloc initWithTarget:self action:@selector(handleTimelinePan:)]; 86 | [self.playerView.progressView addGestureRecognizer:panGesture]; 87 | 88 | [self addObserver:self forKeyPath:@"playing" options:0 context:&LSPAudioViewControllerContext]; 89 | } 90 | 91 | 92 | - (void)reset 93 | { 94 | [self stop]; 95 | [self removeTimeObserver]; 96 | 97 | @try 98 | { 99 | [self removeObserver:self forKeyPath:@"playing" context:&LSPAudioViewControllerContext]; 100 | } 101 | @catch (NSException * __unused exception) {} 102 | 103 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 104 | 105 | if (self.view.superview) 106 | { 107 | [self.view removeFromSuperview]; 108 | } 109 | 110 | if (self.parentViewController) 111 | { 112 | [self removeFromParentViewController]; 113 | } 114 | 115 | _delegate = nil; 116 | _playing = NO; 117 | } 118 | 119 | 120 | #pragma mark - KVO 121 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 122 | { 123 | if (context == LSPAudioViewControllerContext) 124 | { 125 | if ([keyPath isEqualToString:NSStringFromSelector(@selector(playing))]) 126 | { 127 | [self.playerView showLayoutForPlaying:self.player.isPlaying]; 128 | } 129 | } 130 | } 131 | 132 | 133 | #pragma mark - Layout 134 | - (void)assignConstraintsToView 135 | { 136 | [self.playerView mas_makeConstraints:^(MASConstraintMaker *make) { 137 | make.left.and.right.equalTo(self.view); 138 | make.height.equalTo(self.view); 139 | make.bottom.equalTo(self.bottomLayout); 140 | }]; 141 | [self.view layoutIfNeeded]; 142 | } 143 | 144 | 145 | - (void)show 146 | { 147 | CGFloat offset = [self bottomOffset]; 148 | [self.playerView mas_updateConstraints:^(MASConstraintMaker *make) { 149 | make.bottom.equalTo(self.bottomLayout).with.offset(offset); 150 | }]; 151 | [self.view layoutIfNeeded]; 152 | 153 | NSTimeInterval duration = .666f; 154 | NSTimeInterval delay = 0; 155 | UIViewAnimationOptions options = (UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut); 156 | 157 | [UIView animateWithDuration:duration delay:delay options:options animations:^{ 158 | [self.playerView mas_updateConstraints:^(MASConstraintMaker *make) { 159 | make.bottom.equalTo(self.bottomLayout).with.offset(0); 160 | }]; 161 | [self.view layoutIfNeeded]; 162 | } completion:nil]; 163 | } 164 | 165 | 166 | - (void)hide:(void (^)(void))completion 167 | { 168 | [self.view layoutIfNeeded]; 169 | 170 | NSTimeInterval duration = .666f; 171 | NSTimeInterval delay = 0; 172 | UIViewAnimationOptions options = (UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut); 173 | 174 | CGFloat offset = [self bottomOffset]; 175 | [UIView animateWithDuration:duration delay:delay options:options animations:^{ 176 | [self.playerView mas_updateConstraints:^(MASConstraintMaker *make) { 177 | make.bottom.equalTo(self.bottomLayout).with.offset(offset); 178 | }]; 179 | [self.view layoutIfNeeded]; 180 | } completion:^(BOOL finished) { 181 | if (completion) 182 | { 183 | dispatch_async(dispatch_get_main_queue(), ^{ 184 | completion(); 185 | }); 186 | } 187 | }]; 188 | } 189 | 190 | 191 | - (void)viewWillLayoutSubviews 192 | { 193 | [self showProgress]; 194 | } 195 | 196 | 197 | #pragma mark - Playback controls 198 | - (void)play 199 | { 200 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:willPlayPlayer:), [self.delegate audioViewController:self willPlayPlayer:self.player];) 201 | 202 | [self removeTimeObserver]; 203 | [self.player play]; 204 | [self addTimeObserver]; 205 | 206 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:didPlayPlayer:), [self.delegate audioViewController:self didPlayPlayer:self.player];) 207 | } 208 | 209 | 210 | - (void)pause 211 | { 212 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:willPausePlayer:), [self.delegate audioViewController:self willPausePlayer:self.player];) 213 | [self removeTimeObserver]; 214 | [self.player pause]; 215 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:didPausePlayer:), [self.delegate audioViewController:self didPausePlayer:self.player];) 216 | } 217 | 218 | 219 | - (void)stop 220 | { 221 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:willStopPlayer:), [self.delegate audioViewController:self willStopPlayer:self.player];) 222 | [[NSNotificationCenter defaultCenter] postNotificationName:LSPAudioPlayerStop object:self]; 223 | [self.player stop]; 224 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:didStopPlayer:), [self.delegate audioViewController:self didStopPlayer:self.player];) 225 | } 226 | 227 | 228 | - (void)playedUntilEnd:(NSNotification *)notificaiton 229 | { 230 | self.playing = NO; 231 | [self.playerView showLayoutForPlaying:NO]; 232 | [self.playerView.progressView setProgress:0]; 233 | [self close]; 234 | } 235 | 236 | 237 | #pragma mark - Actions 238 | 239 | - (void)playAudioWithURL:(NSURL *)url 240 | { 241 | NSDictionary *userInfo = @{ @"url": url }; 242 | [[NSNotificationCenter defaultCenter] postNotificationName:LSPAudioPlayerStart object:self userInfo:userInfo]; 243 | [self.player playFromURL:url]; 244 | [self playAudioSetupWithURL:url]; 245 | } 246 | 247 | - (void)playAudioSetupWithURL:(NSURL *)url 248 | { 249 | [self removeTimeObserver]; 250 | [self.model setDestination:url]; 251 | [self addTimeObserver]; 252 | self.playerView.titleLabel.text = self.model.title; 253 | 254 | self.playing = self.player.isPlaying; 255 | 256 | [self.playerView.progressView setProgress:0]; 257 | [self showProgress]; 258 | 259 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 260 | [nc removeObserver:self]; 261 | [nc addObserver:self selector:@selector(playedUntilEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; 262 | } 263 | 264 | - (void)close 265 | { 266 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:willClosePlayer:), [self.delegate audioViewController:self willClosePlayer:self.player];) 267 | [self.player stop]; 268 | [self hide:^{ 269 | DELEGATE_SAFELY(self.delegate, @selector(audioViewController:didClosePlayer:), [self.delegate audioViewController:self didClosePlayer:self.player];) 270 | [self reset]; 271 | }]; 272 | } 273 | 274 | 275 | - (void)closeButtonPressed:(UIButton *)button 276 | { 277 | [self close]; 278 | } 279 | 280 | 281 | - (void)togglePlayPause:(UIButton *)button 282 | { 283 | if (self.player.isPlaying) 284 | { 285 | [self pause]; 286 | } 287 | else 288 | { 289 | [self play]; 290 | } 291 | 292 | self.playing = self.player.isPlaying; 293 | } 294 | 295 | 296 | #pragma mark - Progress 297 | - (void)showProgress 298 | { 299 | if (self.player.status != AVPlayerStatusReadyToPlay) return; 300 | 301 | [self updateProgress]; 302 | [self updateTimeLabel]; 303 | } 304 | 305 | 306 | - (void)updateProgress 307 | { 308 | Float64 duration = CMTimeGetSeconds(self.player.duration); 309 | Float64 current = CMTimeGetSeconds(self.player.currentTime); 310 | 311 | if (!isnormal(duration)) return; 312 | 313 | Float64 progress = (current / duration); 314 | [self.playerView.progressView setProgress:progress]; 315 | } 316 | 317 | 318 | - (void)updateTimeLabel 319 | { 320 | NSString *duration = [LSPCMTimeHelper readableCMTime:self.player.duration]; 321 | NSString *current = [LSPCMTimeHelper readableCMTime:self.player.currentTime]; 322 | [self.playerView setCurrentProgress:current forDuration:duration]; 323 | } 324 | 325 | 326 | - (void)jumpToProgress:(Float64)progress 327 | { 328 | [self.player jumpToProgress:progress]; 329 | [self showProgress]; 330 | } 331 | 332 | 333 | #pragma mark - Helpers 334 | - (void)addTimeObserver 335 | { 336 | __weak __typeof(self)weakself = self; 337 | [self.player addProgressObserver:^(CMTime time) { 338 | [weakself showProgress]; 339 | }]; 340 | } 341 | 342 | 343 | - (void)removeTimeObserver 344 | { 345 | if (!_progressObserver) return; 346 | 347 | [self.player removeProgressObserver:_progressObserver]; 348 | _progressObserver = nil; 349 | } 350 | 351 | 352 | - (BOOL)isPlaying 353 | { 354 | return self.player.isPlaying; 355 | } 356 | 357 | 358 | - (CGFloat)bottomOffset 359 | { 360 | CGFloat safeAreaInset = 0; 361 | if (@available(iOS 11, *)) 362 | { 363 | safeAreaInset = self.view.safeAreaInsets.bottom; 364 | } 365 | return safeAreaInset + CGRectGetHeight(self.playerView.frame); 366 | } 367 | 368 | 369 | - (MASViewAttribute *)bottomLayout 370 | { 371 | MASViewAttribute *bottom = self.view.mas_bottom; 372 | if (@available(iOS 11, *)) 373 | { 374 | bottom = self.view.mas_safeAreaLayoutGuideBottom; 375 | } 376 | 377 | return bottom; 378 | } 379 | 380 | 381 | #pragma mark - Gestures 382 | - (void)handleTimelineTap:(UITapGestureRecognizer *)gesture 383 | { 384 | BOOL validTap = (gesture.state == UIGestureRecognizerStateEnded && self.playerView.progressView.hidden == NO); 385 | if (validTap) 386 | { 387 | Float64 progress = [self progressFromGesture:gesture]; 388 | [self jumpToProgress:progress]; 389 | } 390 | } 391 | 392 | 393 | // TODO: Pause while scrubbing, play when gesture is done 394 | - (void)handleTimelinePan:(UIPanGestureRecognizer *)gesture 395 | { 396 | BOOL validPan = (gesture.state == UIGestureRecognizerStateChanged && self.playerView.progressView.hidden == NO); 397 | if (validPan) 398 | { 399 | Float64 progress = [self progressFromGesture:gesture]; 400 | [self jumpToProgress:progress]; 401 | } 402 | } 403 | 404 | 405 | - (Float64)progressFromGesture:(UIGestureRecognizer *)gesture 406 | { 407 | CGPoint location = [gesture locationOfTouch:0 inView:self.playerView.progressView]; 408 | CGFloat xPos = location.x; 409 | return (Float64)xPos/CGRectGetWidth(self.playerView.progressView.frame); 410 | } 411 | 412 | 413 | @end 414 | -------------------------------------------------------------------------------- /Pod/Classes/LSPCMTimeHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPCMTimeHelper.h 3 | // 4 | // Created by Adam Yanalunas on 12/11/13. 5 | // Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface LSPCMTimeHelper : NSObject 11 | 12 | + (NSString *)readableCMTime:(CMTime)time; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Pod/Classes/LSPCMTimeHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPCMTimeHelper.m 3 | // 4 | // Created by Adam Yanalunas on 12/11/13. 5 | // Copyright (c) 2013 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import "LSPCMTimeHelper.h" 9 | 10 | @implementation LSPCMTimeHelper 11 | 12 | + (NSString *)readableCMTime:(CMTime)time 13 | { 14 | NSInteger totalSeconds = CMTimeGetSeconds(time); 15 | if (totalSeconds < 0) totalSeconds = 0; 16 | 17 | NSInteger minutes = floor(totalSeconds % 3600 / 60); 18 | NSInteger seconds = floor(totalSeconds % 60); 19 | 20 | NSString *formattedTime; 21 | if (totalSeconds > 3600) 22 | { 23 | NSInteger hours = floor(totalSeconds / 3600); 24 | formattedTime = [NSString stringWithFormat:@"%ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds]; 25 | } 26 | else if (totalSeconds > 60) 27 | { 28 | formattedTime = [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds]; 29 | } 30 | else 31 | { 32 | formattedTime = [NSString stringWithFormat:@"00:%02ld", (long)seconds]; 33 | } 34 | 35 | return formattedTime; 36 | } 37 | 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Pod/Classes/LSPConfiguration.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPConfiguration.h 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 11/17/17. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | @class LSPConfiguration; 16 | 17 | 18 | @interface LSPConfigurationBuilder : NSObject 19 | 20 | /** 21 | How often the progress UI updates. Uses `CMTime` which has a format of interval/base. 22 | Defaults to 1/35th of a second, i.e. `CMTimeMake(1, 35)`. 23 | */ 24 | @property (nonatomic) CMTime observationInterval; 25 | 26 | /** 27 | Defines accuracy of jump position when scrubbing or tapping the playback bar. 28 | The smaller the tolerance, the more accurate and more costly. 29 | Defaults to 1/100th of a second, i.e. `CMTimeMake(1, 100)`. 30 | */ 31 | @property (nonatomic) CMTime seekTolerance; 32 | 33 | /** 34 | A 0-1 float for playback volume relative to system volume. Does not control 35 | volume of the OS. 36 | Defaults to 1 37 | */ 38 | @property (nonatomic) CGFloat volume; 39 | 40 | + (nullable LSPConfiguration *)configurationWithBuilder:(void (^)(LSPConfigurationBuilder *builder))builderBlock; 41 | + (LSPConfigurationBuilder *)defaultConfiguration; 42 | 43 | - (nullable LSPConfiguration *)build; 44 | 45 | @end 46 | 47 | 48 | @interface LSPConfiguration : NSObject 49 | 50 | @property (nonatomic, readonly) CMTime observationInterval; 51 | @property (nonatomic, readonly) CMTime seekTolerance; 52 | @property (nonatomic, readonly) CGFloat volume; 53 | 54 | 55 | - (instancetype)init NS_UNAVAILABLE; 56 | + (instancetype)new NS_UNAVAILABLE; 57 | 58 | - (nullable instancetype)initWithBuilder:(LSPConfigurationBuilder *)builder NS_DESIGNATED_INITIALIZER; 59 | 60 | @end 61 | 62 | 63 | NS_ASSUME_NONNULL_END 64 | -------------------------------------------------------------------------------- /Pod/Classes/LSPConfiguration.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPConfiguration.m 3 | // loudspeaker 4 | // 5 | // Created by Adam Yanalunas on 11/17/17. 6 | // 7 | 8 | #import "LSPConfiguration.h" 9 | 10 | 11 | @implementation LSPConfigurationBuilder 12 | 13 | + (LSPConfigurationBuilder *)defaultConfiguration 14 | { 15 | LSPConfigurationBuilder *config = LSPConfigurationBuilder.new; 16 | config.seekTolerance = CMTimeMake(1, 100); 17 | config.observationInterval = CMTimeMake(1, 35); 18 | config.volume = 1; 19 | 20 | return config; 21 | } 22 | 23 | 24 | + (LSPConfiguration *)configurationWithBuilder:(void (^)(LSPConfigurationBuilder *))builderBlock 25 | { 26 | LSPConfigurationBuilder *builder = LSPConfigurationBuilder.defaultConfiguration; 27 | builderBlock(builder); 28 | 29 | return [builder build]; 30 | } 31 | 32 | 33 | - (LSPConfiguration *)build 34 | { 35 | return [LSPConfiguration.alloc initWithBuilder:self]; 36 | } 37 | 38 | @end 39 | 40 | 41 | @implementation LSPConfiguration 42 | 43 | - (instancetype)initWithBuilder:(LSPConfigurationBuilder *)builder 44 | { 45 | self = [super init]; 46 | if (!self) return nil; 47 | 48 | _seekTolerance = builder.seekTolerance; 49 | _observationInterval = builder.observationInterval; 50 | _volume = builder.volume; 51 | 52 | return self; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /Pod/Classes/LSPKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPKit.h 3 | // 4 | // Created by Adam Yanalunas on 8/29/14. 5 | // 6 | // 7 | 8 | #import 9 | 10 | 11 | #define DELEGATE_SAFELY(obj, selector, ...) \ 12 | if ([obj respondsToSelector:selector]) { __VA_ARGS__ } 13 | 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | 18 | extern NSString *const LSPAudioPlayerStart; 19 | extern NSString *const LSPAudioPlayerStop; 20 | 21 | 22 | @interface LSPKit : NSObject 23 | 24 | + (UIImage *)closeIcon; 25 | + (NSString *)fontName; 26 | + (nullable UIImage *)imageNamed:(NSString *)name type:(NSString *)type; 27 | + (nullable NSString *)localizedString:(NSString *)string; 28 | + (UIImage *)pauseIcon; 29 | + (UIImage *)playIcon; 30 | 31 | @end 32 | 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /Pod/Classes/LSPKit.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPKit.m 3 | // 4 | // Created by Adam Yanalunas on 8/29/14. 5 | // 6 | // 7 | 8 | #import "LSPKit.h" 9 | 10 | 11 | NSString *const LSPAudioPlayerStart = @"loudspeaker.audio.start"; 12 | NSString *const LSPAudioPlayerStop = @"loudspeaker.audio.stop"; 13 | 14 | 15 | @interface LSPKit () 16 | 17 | + (nullable NSBundle *)loudspeakerBundle; 18 | 19 | @end 20 | 21 | 22 | @implementation LSPKit 23 | 24 | 25 | + (NSBundle *)loudspeakerBundle 26 | { 27 | return [NSBundle bundleWithURL:[[NSBundle bundleForClass:self.class] URLForResource:@"loudspeaker" withExtension:@"bundle"]]; 28 | } 29 | 30 | 31 | + (UIImage *)imageNamed:(NSString *)name type:(NSString *)type 32 | { 33 | NSBundle *bundle = self.class.loudspeakerBundle; 34 | NSString *imageFileName = [NSString stringWithFormat:@"%@.%@", name, type]; 35 | 36 | return [UIImage imageNamed:imageFileName inBundle:bundle compatibleWithTraitCollection:nil]; 37 | } 38 | 39 | + (UIImage *)closeIcon 40 | { 41 | return [self.class imageNamed:@"audio_close_icon" type:@"png"]; 42 | } 43 | 44 | 45 | + (NSString *)fontName 46 | { 47 | return @"Helvetica-Neue"; 48 | } 49 | 50 | 51 | + (UIImage *)pauseIcon 52 | { 53 | return [self.class imageNamed:@"audio_pause_icon" type:@"png"]; 54 | } 55 | 56 | 57 | + (UIImage *)playIcon 58 | { 59 | return [self.class imageNamed:@"audio_play_icon" type:@"png"]; 60 | } 61 | 62 | 63 | + (NSString *)localizedString:(NSString *)string 64 | { 65 | return [self.class.loudspeakerBundle localizedStringForKey:string value:nil table:nil]; 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /Pod/Classes/LSPProgressView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSPProgressView.h 3 | // 4 | // Created by Adam Yanalunas on 2/12/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | 12 | @interface LSPProgressView : UIView 13 | 14 | @property (nonatomic) UIColor *backgroundColor; 15 | @property (nonatomic) UIColor *foregroundColor; 16 | @property (nonatomic) UIView *progressBackground; 17 | @property (nonatomic) UIView *progressBar; 18 | 19 | - (instancetype)initForAutoLayout NS_DESIGNATED_INITIALIZER; 20 | - (instancetype)init NS_UNAVAILABLE; 21 | - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; 22 | + (instancetype)new NS_UNAVAILABLE; 23 | + (instancetype)newAutoLayoutView; 24 | 25 | - (void)setProgress:(Float64)amount; 26 | - (void)setup; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /Pod/Classes/LSPProgressView.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSPProgressView.m 3 | // 4 | // Created by Adam Yanalunas on 2/12/14. 5 | // Copyright (c) 2014 Amco International Education Services, LLC. All rights reserved. 6 | // 7 | 8 | #import "LSPProgressView.h" 9 | #import 10 | 11 | 12 | @interface LSPProgressView () 13 | 14 | - (void)setupViews; 15 | 16 | @end 17 | 18 | 19 | @implementation LSPProgressView 20 | 21 | 22 | #pragma mark - Lifecycle 23 | - (instancetype)initForAutoLayout 24 | { 25 | self = [super initWithFrame:CGRectZero]; 26 | if (!self) return nil; 27 | 28 | self.translatesAutoresizingMaskIntoConstraints = NO; 29 | [self setup]; 30 | 31 | return self; 32 | } 33 | 34 | 35 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 36 | { 37 | return [self initForAutoLayout]; 38 | } 39 | 40 | 41 | + (instancetype)newAutoLayoutView 42 | { 43 | LSPProgressView *view = [super new]; 44 | if (!view) return nil; 45 | 46 | view.translatesAutoresizingMaskIntoConstraints = NO; 47 | [view setup]; 48 | 49 | return view; 50 | } 51 | 52 | 53 | - (void)setup 54 | { 55 | [self setupViews]; 56 | [self setForegroundColor:[UIColor colorWithRed:88/255. green:199/255. blue:226/255. alpha:1]]; 57 | [self setBackgroundColor:[UIColor colorWithWhite:207/255. alpha:1]]; 58 | } 59 | 60 | 61 | #pragma mark - Layout 62 | - (void)setupViews 63 | { 64 | [_progressBackground removeFromSuperview]; 65 | [_progressBar removeFromSuperview]; 66 | 67 | _progressBackground = nil; 68 | _progressBar = nil; 69 | 70 | [self addSubview:self.progressBackground]; 71 | [self addSubview:self.progressBar]; 72 | 73 | [self.progressBackground mas_makeConstraints:^(MASConstraintMaker *make) { 74 | make.edges.equalTo(self); 75 | }]; 76 | } 77 | 78 | 79 | - (void)setProgress:(Float64)amount 80 | { 81 | [self.progressBackground layoutIfNeeded]; 82 | CGRect updatedFrame = self.progressBackground.frame; 83 | updatedFrame.size.width = MIN(CGRectGetWidth(self.progressBackground.frame), CGRectGetWidth(updatedFrame) * amount); 84 | self.progressBar.frame = updatedFrame; 85 | 86 | self.progressBar.accessibilityLabel = [NSString stringWithFormat:@"%i%%", (int) round(updatedFrame.size.width / CGRectGetWidth(self.progressBackground.frame) * 100)]; 87 | } 88 | 89 | 90 | #pragma mark - Properties 91 | - (void)setBackgroundColor:(UIColor *)backgroundColor 92 | { 93 | _backgroundColor = backgroundColor; 94 | _progressBackground.backgroundColor = _backgroundColor; 95 | } 96 | 97 | 98 | - (void)setForegroundColor:(UIColor *)foregroundColor 99 | { 100 | _foregroundColor = foregroundColor; 101 | _progressBar.backgroundColor = foregroundColor; 102 | } 103 | 104 | 105 | - (UIView *)progressBackground 106 | { 107 | if (!_progressBackground) 108 | { 109 | _progressBackground = UIView.new; 110 | _progressBackground.translatesAutoresizingMaskIntoConstraints = NO; 111 | } 112 | 113 | return _progressBackground; 114 | } 115 | 116 | 117 | - (UIView *)progressBar 118 | { 119 | if (!_progressBar) 120 | { 121 | _progressBar = UIView.new; 122 | _progressBar.translatesAutoresizingMaskIntoConstraints = NO; 123 | 124 | _progressBar.isAccessibilityElement = YES; 125 | _progressBar.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; 126 | } 127 | 128 | return _progressBar; 129 | } 130 | 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /Pod/Classes/loudspeaker.h: -------------------------------------------------------------------------------- 1 | // 2 | // loudspeaker.h 3 | // Pods 4 | // 5 | // Created by Adam Yanalunas on 11/17/17. 6 | // 7 | 8 | #ifndef loudspeaker_h 9 | #define loudspeaker_h 10 | 11 | #import "LSPAudioPlayer.h" 12 | #import "LSPAudioPlayerModel.h" 13 | #import "LSPAudioView.h" 14 | #import "LSPAudioViewController.h" 15 | #import "LSPCMTimeHelper.h" 16 | #import "LSPConfiguration.h" 17 | #import "LSPKit.h" 18 | #import "LSPProgressView.h" 19 | 20 | #endif /* loudspeaker_h */ 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loudspeaker 2 | 3 | [![CI Status](http://img.shields.io/travis/amco/loudspeaker.svg?style=flat)](https://travis-ci.org/amco/loudspeaker) 4 | [![Version](https://img.shields.io/cocoapods/v/loudspeaker.svg?style=flat)](http://cocoadocs.org/docsets/loudspeaker) 5 | [![License](https://img.shields.io/cocoapods/l/loudspeaker.svg?style=flat)](http://cocoadocs.org/docsets/loudspeaker) 6 | [![Platform](https://img.shields.io/cocoapods/p/loudspeaker.svg?style=flat)](http://cocoadocs.org/docsets/loudspeaker) 7 | 8 | Loudspeaker is a simple, efficient, gorgeous, customizable audio player for iOS, backed by [AVQueuePlayer](https://developer.apple.com/library/ios/documentation/avfoundation/reference/avqueueplayer_class/Reference/Reference.html). 9 | 10 | ![Sample Screenshot](http://i.imgur.com/IOACIpO.png) 11 | 12 | ## Installation 13 | 14 | loudspeaker is available through [CocoaPods](http://cocoapods.org). To install 15 | it, simply add the following line to your Podfile: 16 | 17 | pod "loudspeaker" 18 | 19 | ## Demo 20 | 21 | You can either run `pod try loudspeaker` from your command line or clone the repo and run `pod install` from the Example directory followed by loading up the Example workspace. 22 | 23 | ## Contribution 24 | 25 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 26 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 27 | * Fork the project and submit a pull request from a feature or bugfix branch. 28 | * Please include tests. This is important so we don't break your changes unintentionally in a future version. 29 | * Please don't modify the podspec, version, or changelog. If you do change these files, please isolate a separate commit so we can cherry-pick around it. 30 | 31 | ## Author 32 | 33 | Adam Yanalunas, adamy@amcoonline.net 34 | 35 | ## License 36 | 37 | loudspeaker is available under the MIT license. See the LICENSE file for more info. 38 | 39 | -------------------------------------------------------------------------------- /loudspeaker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "loudspeaker" 3 | s.version = "1.0.0" 4 | s.summary = "An AVQueuePlayer-backed audio player with a modern, minimal UI" 5 | s.description = <<-DESC 6 | An audio player with a modern, minimal UI. Powered by AVQueuePlayer. 7 | 8 | * Plays all file formats supported by iOS 9 | * Auto Layout keeps it looking fiiiine however you jam it into your app 10 | * Jump forward or backward with gestures 11 | DESC 12 | s.homepage = "https://github.com/amco/loudspeaker" 13 | s.screenshot = "http://i.imgur.com/IOACIpO.png" 14 | s.license = 'MIT' 15 | s.author = { "Adam Yanalunas" => "adamy@yanalunas.com" } 16 | s.source = { :git => "https://github.com/amco/loudspeaker.git", :tag => s.version.to_s } 17 | s.social_media_url = 'https://twitter.com/adamyanalunas' 18 | 19 | s.platform = :ios, '9.0' 20 | s.requires_arc = true 21 | 22 | s.source_files = 'Pod/Classes' 23 | s.resource_bundles = { 24 | 'loudspeaker' => ['Pod/Assets/**/*'] 25 | } 26 | s.dependency 'Masonry', '~> 1.1.0' 27 | 28 | s.public_header_files = 'Pod/Classes/**/*.h' 29 | s.frameworks = 'UIKit', 'AVFoundation' 30 | end 31 | --------------------------------------------------------------------------------