├── RecordProject ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Editor │ ├── TimeCollectionViewCell.swift │ ├── WaveWithMeterCollectionViewCell.swift │ ├── TimeCollectionViewCell.xib │ ├── WaveWithMeterCollectionViewCell.xib │ ├── AudioEditorViewController.swift │ └── AudioEditorViewController.xib ├── Record │ ├── WaveCollectionViewCell.swift │ ├── WaveCollectionViewCell.xib │ ├── AudioRecordViewController.swift │ └── Base.lproj │ │ └── Main.storyboard ├── Handler │ ├── RecorderSessionHandler.swift │ ├── AudioPlayer.swift │ ├── RecorderFileHandler.swift │ ├── RecorderHandler.swift │ └── AudioEditorHandler.swift ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Manager │ ├── RecorderManager.swift │ └── AudioEditorManager.swift ├── Extension │ └── Extensions.swift ├── Info.plist └── SceneDelegate.swift ├── RecordProject.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── yoseiyamagishi.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── RecordProject.xcscheme └── project.pbxproj ├── README.md └── .gitignore /RecordProject/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/xcuserdata/yoseiyamagishi.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RecordProject/Editor/TimeCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeCollectionViewCell.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/30. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TimeCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet weak var timeLabel: UILabel! 14 | 15 | func draw(time: String) { 16 | timeLabel.text = time 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioEditor 2 | Demo of audio recorder and editor with a audio waveform UI. 3 | 4 | (音声波形表示UI付きの収録と音声編集のデモです。) 5 | 6 | ![Videotogif](https://user-images.githubusercontent.com/22518469/102613880-a4194d00-4176-11eb-9e5f-e5cf46d1fa8b.gif) 7 | 8 | ## 実装 9 | - Record: AVAudioRecorder 10 | - Player: AVAudioPlayer 11 | - Editor: AVAssetExportSession 12 | 13 | ## その他 14 | - Demo 15 | 16 | https://twitter.com/fairy_engineer/status/1307851911710691328?s=20 17 | 18 | - iOSDC2020 19 | 20 | 「実装したくなる音声編集」というタイトルでiOSDC2020で発表した資料です。 21 | 22 | https://speakerdeck.com/yoseiyamagishi/shi-zhuang-sitakunaruyin-sheng-bian-ji?slide=130 23 | -------------------------------------------------------------------------------- /RecordProject/Record/WaveCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaveCollectionViewCell.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/05. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WaveCollectionViewCell: UICollectionViewCell { 12 | @IBOutlet var waveHeight: NSLayoutConstraint! 13 | @IBOutlet var waveView: UIView! 14 | 15 | func draw(height: CGFloat, index: Int) { 16 | // 振幅の高さ 17 | waveHeight.constant = height 18 | // セル間のスペースを取るために偶数の波形は表示する 19 | waveView.isHidden = index % 2 != 0 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/xcuserdata/yoseiyamagishi.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RecordProject.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | CA2FB2EA24FA042800BB6ABB 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /RecordProject/Handler/RecorderSessionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecorderSessionHandler.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/18. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | class RecorderSessionHandler { 12 | let session = AVAudioSession.sharedInstance() 13 | 14 | func requestPermission(completion: @escaping (Bool) -> Void) { 15 | session.requestRecordPermission { granted in 16 | completion(granted) 17 | } 18 | } 19 | 20 | func setActive() { 21 | try! session.setCategory(.playAndRecord) 22 | try! session.setActive(true, options: []) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /RecordProject/Handler/AudioPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioPlayer.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/05. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | class AudioPlayer { 11 | var audioPlayer: AVAudioPlayer? 12 | var isPlaying: Bool { audioPlayer?.isPlaying ?? false } 13 | func setupPlayer(with url: URL) { // プレイヤーを作成 14 | audioPlayer = try! AVAudioPlayer(contentsOf: url) 15 | audioPlayer?.prepareToPlay() 16 | } 17 | func play(currentTime: Double) { // 再生位置を決めて再生 18 | audioPlayer?.currentTime = currentTime 19 | audioPlayer?.play() 20 | } 21 | func pause() { // 再生を一時停止 22 | audioPlayer?.pause() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /RecordProject/Editor/WaveWithMeterCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaveWithMeterCollectionViewCell.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WaveWithMeterCollectionViewCell: UICollectionViewCell { 12 | @IBOutlet var waveHeight: NSLayoutConstraint! 13 | @IBOutlet var waveView: UIView! 14 | @IBOutlet weak var meterHeight: NSLayoutConstraint! 15 | @IBOutlet weak var meterView: UIView! 16 | 17 | func draw(height: CGFloat, index: Int) { 18 | waveHeight.constant = height 19 | waveView.isHidden = index % 2 != 0 20 | // メーター表示 21 | meterView.isHidden = index % 2 != 0 22 | // 5秒おきに高さを変更 23 | meterHeight.constant = index % 50 == 0 ? 10 : 5 24 | // 1秒おきに色を変更 25 | meterView.backgroundColor = index % 10 == 0 ? .lightGray : .black 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RecordProject/Handler/RecorderFileHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecorderFileHandler.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/18. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RecorderFileHandler { 12 | 13 | let fileManager: FileManager = .default 14 | 15 | func fileUrl(fileName: String) -> URL? { 16 | fileManager.urls( 17 | for: .documentDirectory, 18 | in: .userDomainMask 19 | ).first?.appendingPathComponent(fileName) 20 | } 21 | 22 | func removeFile(fileName: String) { 23 | do { 24 | guard 25 | let url = fileUrl(fileName: fileName), 26 | fileManager.fileExists(atPath: url.path) 27 | else { 28 | return 29 | } 30 | try FileManager.default.removeItem(at: url) 31 | } catch { 32 | print("ファイルの削除に失敗しました。", error) 33 | } 34 | } 35 | 36 | func copy(atUrl: URL, toUrl: URL) { 37 | do { 38 | // atUrlをtoUrlにファイルをコピー 39 | try FileManager.default.copyItem(at: atUrl, to: toUrl) 40 | } catch { 41 | print("ファイルのコピーに失敗しました", error) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RecordProject/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /RecordProject/Handler/RecorderHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecorderHandler.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/18. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | class RecorderHandler { 12 | private var recorder: AVAudioRecorder? 13 | // 収録中かどうかのフラグ 14 | var isRecording: Bool { recorder?.isRecording ?? false } 15 | // 現在時刻 16 | var currentTime: Float { Float(self.recorder?.currentTime ?? 0) } 17 | // 収録開始 18 | func record() { recorder?.record() } 19 | // 収録一時停止 20 | func pause() { recorder?.pause() } 21 | // 収録停止 22 | func stop() { recorder?.stop() } 23 | 24 | 25 | func setup(url: URL) { 26 | self.recorder = try! AVAudioRecorder(url: url, settings: settings) 27 | recorder?.isMeteringEnabled = true // デシベルを抽出を有効にする 28 | recorder?.prepareToRecord() // レコーダーに収録準備させる 29 | } 30 | 31 | func amplitude() -> Float { 32 | self.recorder?.updateMeters() 33 | // 指定されたチャネルの平均パワーをデシベル単位で返却 34 | // averagePowerは 0dB:最大電力 -160dB:最小電力(ほぼ無音) 35 | let decibel = recorder?.averagePower(forChannel: 0) ?? 0 36 | // デシベルから振幅を取得する 37 | let amp = pow(10, decibel / 20) 38 | return max(0, min(amp, 1)) // 0...1の間の値 39 | } 40 | 41 | private let settings: [String: Any] = [ 42 | // MPEG-4 AACコーデックを指定するキー 43 | AVFormatIDKey: kAudioFormatMPEG4AAC, 44 | // サンプルレート変換品質 45 | AVEncoderAudioQualityKey: AVAudioQuality.medium.rawValue, 46 | // モノラル 47 | AVNumberOfChannelsKey: 1, 48 | // サンプルレート 49 | AVSampleRateKey: 44100 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | *.DS_Store 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | Package.resolved 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | 45 | Pods/ 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 53 | 54 | fastlane/report.xml 55 | fastlane/Preview.html 56 | fastlane/screenshots 57 | fastlane/test_output 58 | 59 | ## For this project 60 | vendor/ 61 | 62 | ## Xcode Patch 63 | *.xcodeproj/* 64 | !*.xcodeproj/project.pbxproj 65 | !*.xcodeproj/xcshareddata/ 66 | !*.xcworkspace/contents.xcworkspacedata 67 | /*.gcno 68 | RadioTalk.xcodeproj/* 69 | 70 | ### Xcode Patch ### 71 | **/xcshareddata/WorkspaceSettings.xcsettings -------------------------------------------------------------------------------- /RecordProject/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RecordProject/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /RecordProject/Handler/AudioEditorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioEditorHandler.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/09/05. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | class AudioEditorHandler { 11 | // オリジナルから指定した時間を編集して、編集後のURLを返却 12 | func edit( 13 | originUrl: URL, // 収録したファイル 14 | exportFileUrl: URL, // 編集後のエクスポートするファイル 15 | timeRanges: [CMTimeRange], // 削除部分以外の時間範囲 16 | fileType: AVFileType = .m4a, // ファイルタイプ 17 | completion: @escaping (Result) -> Void 18 | ) { 19 | 20 | // 収録ファイルのTrackを取得 21 | let originAsset = AVURLAsset(url: originUrl) 22 | let originTrack = originAsset.tracks(withMediaType: .audio).first! 23 | 24 | // 新しいTrackを用意 25 | let composition = AVMutableComposition() 26 | let editTrack = composition.addMutableTrack( 27 | withMediaType: .audio, 28 | preferredTrackID: kCMPersistentTrackID_Invalid 29 | )! 30 | 31 | // 収録トラックから削除部分以外を新しいトラックに追加 32 | var nextStartTime: CMTime = .zero 33 | timeRanges.forEach { timeRange in 34 | try! editTrack.insertTimeRange( 35 | timeRange, of: originTrack, at: nextStartTime 36 | ) 37 | nextStartTime = CMTimeAdd(nextStartTime, timeRange.duration) 38 | } 39 | 40 | // exportSessionを用意して編集後のファイルをエクスポート 41 | let exportSession = AVAssetExportSession( 42 | asset: composition, 43 | presetName: AVAssetExportPresetAppleM4A 44 | )! 45 | exportSession.outputURL = exportFileUrl 46 | exportSession.outputFileType = fileType 47 | exportSession.exportAsynchronously { 48 | switch exportSession.status { 49 | case .completed: 50 | completion(.success(exportFileUrl)) 51 | default: 52 | completion(.failure(exportSession.error!)) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /RecordProject/Manager/RecorderManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecorderManager.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | class RecorderManager { 11 | var amplitudes: [Float] = [] 12 | let recordFileName: String = "originalAudio.m4a" 13 | var originalFileUrl: URL? 14 | var recorderHandler = RecorderHandler() 15 | var fileHandler = RecorderFileHandler() 16 | var sessionHandler = RecorderSessionHandler() 17 | 18 | var isRecording: Bool { recorderHandler.isRecording } 19 | var currentTime: (minute: String, second: String, millisecond: String) { 20 | let time = recorderHandler.currentTime 21 | let minute = Int(time / 60) 22 | let second = Int(time.truncatingRemainder(dividingBy: 60)) 23 | let millisecond = Int( 24 | (time - Float(minute * 60) - Float(second)) * 100.0 25 | ) 26 | return ( 27 | minute: String(format: "%02d:", minute), 28 | second: String(format: "%02d.", second), 29 | millisecond: String(format: "%02d", millisecond) 30 | ) 31 | } 32 | 33 | func setup() { 34 | // マイクの許可を取る 35 | sessionHandler.requestPermission { granted in 36 | guard granted else { return } 37 | // 音声ファイルを用意する 38 | let fileUrl = self.fileHandler.fileUrl(fileName: self.recordFileName)! 39 | self.originalFileUrl = fileUrl 40 | // レコーダーをセットアップ 41 | self.recorderHandler.setup(url: fileUrl) 42 | } 43 | } 44 | 45 | func record() { 46 | // セッションをアクティブ 47 | sessionHandler.setActive() 48 | // 収録を開始 49 | recorderHandler.record() 50 | } 51 | 52 | // 音声波形の更新 53 | func updateAmpliude() { 54 | let amplitude = recorderHandler.amplitude() 55 | amplitudes.append(amplitude) 56 | } 57 | 58 | func pause() { 59 | recorderHandler.pause() 60 | } 61 | 62 | func stop() { 63 | recorderHandler.stop() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /RecordProject/Extension/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSObject { 12 | @nonobjc static var className: String { 13 | String(describing: self) 14 | } 15 | 16 | var className: String { 17 | type(of: self).className 18 | } 19 | } 20 | 21 | extension UICollectionView { 22 | func register(cellType: T.Type) { 23 | let className = cellType.className 24 | register(cellType, forCellWithReuseIdentifier: className) 25 | } 26 | 27 | func register(cellTypes: [T.Type]) { 28 | cellTypes.forEach { register(cellType: $0) } 29 | } 30 | 31 | func registerNib(cellType: T.Type) { 32 | let className = cellType.className 33 | let nib = UINib(nibName: className, bundle: nil) 34 | register(nib, forCellWithReuseIdentifier: className) 35 | } 36 | 37 | func registerNib(cellTypes: [T.Type]) { 38 | cellTypes.forEach { registerNib(cellType: $0) } 39 | } 40 | 41 | func dequeueReusableCell(for indexPath: IndexPath) -> T { 42 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.className, for: indexPath) as? T else { 43 | return T() 44 | } 45 | return cell 46 | } 47 | 48 | func dequeueReusableView(for indexPath: IndexPath, of kind: String = UICollectionView.elementKindSectionHeader) -> T { 49 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.className, for: indexPath) as? T else { 50 | return T() 51 | } 52 | return view 53 | } 54 | 55 | public func scrollToTop(animated: Bool = true) { 56 | setContentOffset(CGPoint.zero, animated: animated) 57 | } 58 | } 59 | 60 | extension UIView { 61 | // 角丸にする 62 | func allMaskCorner() { 63 | layer.cornerRadius = frame.height / 2 64 | layer.masksToBounds = true 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /RecordProject/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | マイクを許可しますか? 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /RecordProject/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/xcshareddata/xcschemes/RecordProject.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /RecordProject/Editor/TimeCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /RecordProject/Editor/WaveWithMeterCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /RecordProject/Record/WaveCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /RecordProject/Manager/AudioEditorManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioEditorManager.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/31. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | class AudioEditorManager { 11 | let editFileName = "editedAudio.m4a" // 編集後のファイル名 12 | var editFileUrl: URL? // 編集後のファイルURLを保持するため 13 | let originalUrl: URL // 収録したオリジナルファイル 14 | var amplitudes: [Float] // 取得した波形 15 | 16 | var audioPlayer = AudioPlayer() 17 | var fileManager = RecorderFileHandler() 18 | var editHandler = AudioEditorHandler() 19 | 20 | // 取得した振幅のタイムインターバル 21 | let timeInterval: Double = 0.1 22 | // 1秒の横幅 23 | let oneSecoundWidth: CGFloat = 20 24 | // 収録時間 25 | var totalTime: Double { 26 | Double(amplitudes.count) * timeInterval 27 | } 28 | // 収録時間のtext 29 | var totalTimeText: String { 30 | let time = timeText(time: totalTime) 31 | return "/ " + time.minute + time.second + time.millisecond 32 | } 33 | 34 | var isPlaying: Bool { audioPlayer.isPlaying } 35 | 36 | init(amplitudes: [Float], originalUrl: URL) { 37 | self.originalUrl = originalUrl 38 | self.amplitudes = amplitudes 39 | audioPlayer.setupPlayer(with: originalUrl) 40 | } 41 | 42 | // 編集バーで指定した時間を編集する 43 | func edit( 44 | leftTime: Double, // 左バーの編集時間 45 | rightime: Double, // 右バーの編集時間 46 | completion: @escaping (Result, Error>) -> Void 47 | ) { 48 | 49 | // 0.1秒ごとに波形を取得してるのでまるめる 50 | // ex) leftTime: 15.175 → 15.2 51 | // rightTime: 18.20 → 18.2 52 | let roundedLeft = round(leftTime * 10) / 10 53 | let roundedRight = round(rightime * 10) / 10 54 | 55 | // 時間範囲(左) 56 | // ex) 0 から 152 / 10 57 | let leftTimeRange = CMTimeRangeFromTimeToTime( 58 | start: CMTime(value: Int64(0*10), timescale: 10), 59 | end: CMTime(value: Int64(roundedLeft*10), timescale: 10) 60 | ) 61 | // 時間範囲(右) 62 | // ex) 182 / 10 から 収録時間 63 | let rightTimeRange = CMTimeRangeFromTimeToTime( 64 | start: CMTime(value: Int64(roundedRight*10), timescale: 10), 65 | end: CMTime(value: Int64(totalTime*10), timescale: 10) 66 | ) 67 | 68 | // エクスポートするファイルを用意する 69 | fileManager.removeFile(fileName: editFileName) 70 | let exportFileUrl = fileManager.fileUrl(fileName: editFileName)! 71 | 72 | // 指定された時間範囲を編集する 73 | editHandler.edit( 74 | originUrl: originalUrl, exportFileUrl: exportFileUrl, 75 | timeRanges: [leftTimeRange, rightTimeRange] 76 | ) { result in 77 | switch result { 78 | case let .success(url): 79 | self.editFileUrl = url 80 | // 編集後のファイルをプレイヤーに再設定 81 | self.audioPlayer.setupPlayer(with: url) 82 | // 編集範囲 83 | let removeRange = Int(roundedLeft * 10).. String { 104 | let time = Float(index * 5) 105 | let minute = Int(time / 60) 106 | let second = Int(time.truncatingRemainder(dividingBy: 60)) 107 | return String(format: "%02d:%02d", minute, second) 108 | } 109 | 110 | // 現在時刻 111 | func timeText(time: Double) -> (minute: String, second: String, millisecond: String) { 112 | let minute = Int(time / 60) 113 | let second = Int(time.truncatingRemainder(dividingBy: 60)) 114 | let millisecond = Int((time - Double(minute * 60) - Double(second)) * 100.0) 115 | return ( 116 | minute: String(format: "%02d:", minute), 117 | second: String(format: "%02d.", second), 118 | millisecond: String(format: "%02d", millisecond) 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /RecordProject/Record/AudioRecordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioRecordViewController.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/29. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AudioRecordViewController: UIViewController { 12 | @IBOutlet weak var minuteTimeLabel: UILabel! 13 | @IBOutlet weak var secondTimeLabel: UILabel! 14 | @IBOutlet weak var millisecondTimeLabel: UILabel! 15 | 16 | @IBOutlet weak var collectionView: UICollectionView! { 17 | didSet { 18 | collectionView.dataSource = self 19 | collectionView.delegate = self 20 | collectionView.registerNib( 21 | cellType: WaveCollectionViewCell.self 22 | ) 23 | } 24 | } 25 | 26 | @IBOutlet weak var recordButton: UIButton! { 27 | didSet { 28 | recordButton.allMaskCorner() 29 | recordButton.addTarget(self, action: #selector(switchRecord), for: .touchUpInside) 30 | } 31 | } 32 | 33 | @IBOutlet weak var stopButton: UIButton! { 34 | didSet { 35 | stopButton.allMaskCorner() 36 | stopButton.addTarget(self, action: #selector(stop), for: .touchUpInside) 37 | } 38 | } 39 | 40 | var recorderManager = RecorderManager() 41 | var timer: Timer? 42 | var collectionViewHeight: CGFloat { 43 | collectionView.frame.height 44 | } 45 | var screenHalfWidth: CGFloat { 46 | UIScreen.main.bounds.width / 2 47 | } 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | 52 | 53 | // ナビゲーションの背景色を変更 54 | navigationController?.navigationBar.barTintColor = UIColor.darkGray 55 | // ナビゲーションバーのアイテムの色(戻るとか読み込みゲージとか) 56 | navigationController?.navigationBar.tintColor = UIColor.white 57 | // レコーダーのセットアップ 58 | recorderManager.setup() 59 | } 60 | 61 | // 現在時刻を設定 62 | private func setupCurrentTime() { 63 | let currentTime = self.recorderManager.currentTime 64 | self.minuteTimeLabel.text = currentTime.minute 65 | self.secondTimeLabel.text = currentTime.second 66 | self.millisecondTimeLabel.text = currentTime.millisecond 67 | } 68 | 69 | @objc func switchRecord() { 70 | let isRecording = recorderManager.isRecording 71 | // 収録ボタンの制御 72 | recordButton.setTitle(isRecording ? "収録開始" : "一時停止", for: .normal) 73 | // 収録制御 74 | isRecording ? recorderManager.pause() : recorderManager.record() 75 | // タイマー制御 76 | setupTimer(isRecording: isRecording) 77 | } 78 | 79 | func setupTimer(isRecording: Bool) { 80 | if isRecording { 81 | timer?.invalidate() // タイマー停止 82 | timer = nil 83 | } else { 84 | timer = Timer.scheduledTimer( 85 | withTimeInterval: 0.1, // タイマーインターバル 86 | repeats: true 87 | ) { timer in 88 | self.setupCurrentTime() // 現在時刻を更新 89 | self.recorderManager.updateAmpliude() // デシベルの取得 90 | self.insertCollectionView() // 取得した波形の表示 91 | } 92 | } 93 | } 94 | 95 | private func insertCollectionView() { 96 | // 取得した波形のIndexPath 97 | let endIndex = self.recorderManager.amplitudes.count - 1 98 | let lastIndexPath = IndexPath(row: endIndex, section: 0) 99 | UIView.performWithoutAnimation { // アニメーションOFF 100 | // 取得した波形をCollectionViewにinsert 101 | self.collectionView.performBatchUpdates( 102 | { self.collectionView.insertItems(at: [lastIndexPath]) }, 103 | completion: { _ in 104 | // insert完了後にスクロールする 105 | self.collectionView.scrollToItem( 106 | at: lastIndexPath, at: .left, animated: false 107 | ) 108 | } 109 | ) 110 | } 111 | } 112 | 113 | 114 | @objc private func stop() { 115 | setupTimer(isRecording: true) 116 | 117 | recorderManager.stop() // 収録停止 118 | let audioEditorViewController = AudioEditorViewController( 119 | amplitudes: recorderManager.amplitudes, 120 | originalUrl: recorderManager.originalFileUrl! 121 | ) 122 | navigationController?.pushViewController( 123 | audioEditorViewController, 124 | animated: true 125 | ) 126 | } 127 | 128 | // アニメーションさせないでinsertする場合 129 | func memo() { 130 | let amplitudeCount = self.recorderManager.amplitudes.count - 1 131 | let lastIndexPath = IndexPath(row: amplitudeCount, section: 0) 132 | UIView.performWithoutAnimation { 133 | self.collectionView.performBatchUpdates( 134 | { self.collectionView.insertItems(at: [lastIndexPath]) }, 135 | completion: { _ in 136 | self.collectionView.scrollToItem( 137 | at: lastIndexPath, 138 | at: .left, 139 | animated: false 140 | ) 141 | } 142 | ) 143 | } 144 | } 145 | } 146 | 147 | extension AudioRecordViewController: UICollectionViewDataSource { 148 | 149 | 150 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 151 | // 取得した波形の数 152 | recorderManager.amplitudes.count 153 | } 154 | 155 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 156 | let cell = collectionView.dequeueReusableCell(for: indexPath) as WaveCollectionViewCell 157 | let amplitudes = recorderManager.amplitudes[indexPath.row] 158 | // 振幅(0..1) * collectionViewの高さ = 波形の高さ 159 | let waveHeight = CGFloat(amplitudes) * collectionViewHeight 160 | cell.draw(height: waveHeight, index: indexPath.row) 161 | return cell 162 | } 163 | 164 | } 165 | 166 | extension AudioRecordViewController: UICollectionViewDelegateFlowLayout { 167 | 168 | // セルのサイズを設定 169 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 170 | // タイムインターバルの横幅(0.1秒の横幅) 171 | let meterWidth: CGFloat = 2 172 | return CGSize(width: meterWidth, height: collectionViewHeight) 173 | } 174 | 175 | // 垂直方向におけるセル間のマージン 176 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 177 | 0 178 | } 179 | // 水平方向におけるセル間のマージン 180 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 181 | 0 182 | } 183 | // collectionViewのEdgeInsetsは0にする 184 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 185 | .zero 186 | } 187 | 188 | // ヘッダーのサイズ設定 189 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 190 | CGSize(width: screenHalfWidth, height: collectionViewHeight) 191 | } 192 | // フッターのサイズ設定 193 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 194 | CGSize(width: screenHalfWidth, height: collectionViewHeight) 195 | } 196 | 197 | 198 | } 199 | -------------------------------------------------------------------------------- /RecordProject/Record/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 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 104 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /RecordProject/Editor/AudioEditorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioEditorViewController.swift 3 | // RecordProject 4 | // 5 | // Created by Yosei Yamagishi on 2020/08/30. 6 | // Copyright © 2020 Yosei Yamagishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AudioEditorViewController: UIViewController { 12 | enum CollectionType: Int, CaseIterable { 13 | case decibel 14 | case time 15 | } 16 | var collectionTypes: [CollectionType] = CollectionType.allCases 17 | 18 | // MARK: 時間 19 | @IBOutlet weak var minuteTimeLabel: UILabel! 20 | @IBOutlet weak var secondTimeLabel: UILabel! 21 | @IBOutlet weak var millisecondTimeLabel: UILabel! 22 | @IBOutlet weak var totalTimeLabel: UILabel! 23 | 24 | // MARK: デシベル 25 | 26 | @IBOutlet weak var waveCollectionView: UICollectionView! { 27 | didSet { 28 | waveCollectionView.tag = CollectionType.decibel.rawValue 29 | waveCollectionView.dataSource = self 30 | waveCollectionView.delegate = self 31 | waveCollectionView.showsHorizontalScrollIndicator = false 32 | waveCollectionView.registerNib(cellType: WaveWithMeterCollectionViewCell.self) 33 | } 34 | } 35 | 36 | // MARK: 時間表記 37 | 38 | @IBOutlet weak var timeCollectionView: UICollectionView! { 39 | didSet { 40 | timeCollectionView.tag = CollectionType.time.rawValue 41 | timeCollectionView.dataSource = self 42 | timeCollectionView.delegate = self 43 | timeCollectionView.showsHorizontalScrollIndicator = false 44 | timeCollectionView.isScrollEnabled = false 45 | timeCollectionView.registerNib(cellType: TimeCollectionViewCell.self) 46 | } 47 | } 48 | 49 | // MARK: 最前面の範囲のバーを表示するスクロールビュー 50 | 51 | @IBOutlet weak var editBarScrollView: UIScrollView! { 52 | didSet { 53 | editBarScrollView.delegate = self 54 | editBarScrollView.showsHorizontalScrollIndicator = false 55 | } 56 | } 57 | @IBOutlet weak var frontScroollContentView: UIView! 58 | @IBOutlet weak var frontScrollContentViewWidth: NSLayoutConstraint! { 59 | didSet { 60 | // 波形のコンテンツの横幅 + ヘッダー + フッター 61 | frontScrollContentViewWidth.constant = waveContentViewWidth + spaceWidth * 2 62 | } 63 | } 64 | 65 | // 中心 66 | @IBOutlet weak var centerLineView: UIView! 67 | 68 | // 左のバー 69 | @IBOutlet weak var leftEditBarLeading: NSLayoutConstraint! { 70 | didSet { 71 | leftEditBarLeading.constant = spaceWidth 72 | } 73 | } 74 | // 右のバー 75 | @IBOutlet weak var rightEditBarTrailing: NSLayoutConstraint! { 76 | didSet { 77 | rightEditBarTrailing.constant = spaceWidth - meterWidth 78 | } 79 | } 80 | 81 | // MARK: 最背面の範囲のViewを表示するスクロールビュー 82 | 83 | @IBOutlet weak var editEreaScrollView: UIScrollView! { 84 | didSet { 85 | editEreaScrollView.delegate = self 86 | editEreaScrollView.showsHorizontalScrollIndicator = false 87 | } 88 | } 89 | @IBOutlet weak var backScroollContentView: UIView! 90 | @IBOutlet weak var backScrollContentViewWidth: NSLayoutConstraint! { 91 | didSet { 92 | // 波形のコンテンツの横幅 + ヘッダー + フッター 93 | backScrollContentViewWidth.constant = waveContentViewWidth + spaceWidth * 2 94 | } 95 | } 96 | 97 | // 波形表示部分の背景色をつけるためのViewの調整 98 | @IBOutlet weak var backContentViewLeading: NSLayoutConstraint! { 99 | didSet { 100 | backContentViewLeading.constant = spaceWidth 101 | } 102 | } 103 | // 波形表示部分の背景色をつけるためのViewの調整 104 | @IBOutlet weak var backContentViewTrailing: NSLayoutConstraint! { 105 | didSet { 106 | backContentViewTrailing.constant = spaceWidth 107 | } 108 | } 109 | 110 | // 編集エリアビューのLeadingの制約 111 | @IBOutlet weak var editAreaViewLeading: NSLayoutConstraint! { 112 | didSet { 113 | editAreaViewLeading.constant = spaceWidth 114 | } 115 | } 116 | // 編集エリアビューのTrailingの制約 117 | @IBOutlet weak var editAreaViewTrailing: NSLayoutConstraint! { 118 | didSet { 119 | editAreaViewTrailing.constant = spaceWidth - meterWidth 120 | } 121 | } 122 | 123 | // MARK: 編集エリアの設定 124 | 125 | @IBOutlet weak var leftBarButton: UIButton! { 126 | didSet { 127 | leftBarButton.allMaskCorner() 128 | leftBarButton.addTarget(self, action: #selector(moveLeftArea), for: .touchUpInside) 129 | } 130 | } 131 | 132 | @IBOutlet weak var rightBarButton: UIButton! { 133 | didSet { 134 | rightBarButton.allMaskCorner() 135 | rightBarButton.addTarget(self, action: #selector(moveRightArea), for: .touchUpInside) 136 | } 137 | } 138 | 139 | // 左の編集バーを移動させる 140 | // guard rightEditBarTrailing.constant < currentRightEditBarTrailing else { return } 141 | 142 | // 左の編集バーを移動させる 143 | @objc func moveLeftArea() { 144 | // 現在のスクロール位置 145 | let currentOffset = editBarScrollView.contentOffset.x 146 | // 現在のスクロール位置 + ヘッダー幅 147 | let editBarLeading = currentOffset + spaceWidth 148 | // 左の編集バーのLeadingを現在のスクロール位置に設定 149 | leftEditBarLeading.constant = editBarLeading 150 | // 編集エリアのLeadingを現在のスクロール位置に設定 151 | editAreaViewLeading.constant = editBarLeading 152 | UIView.animate(withDuration: 0.2) { 153 | self.view.layoutIfNeeded() 154 | } 155 | } 156 | 157 | // 右の編集バーを移動させる 158 | // 右の編集バーが左の編集バーを越えないように制御 159 | // guard leftEditBarLeading.constant < currentLeftEditBarLeading else { return } 160 | 161 | // 右の編集バーを移動させる 162 | @objc func moveRightArea() { 163 | // 現在のスクロール位置 164 | let currentOffset = editBarScrollView.contentOffset.x 165 | // ContentViewの横幅 - (現在スクロール位置 + フッター幅 + メーター幅) 166 | let editBarTrailing = contentViewWidth - (currentOffset + spaceWidth + 2) 167 | // 右の編集バーのTrailingを現在のスクロール位置に設定 168 | rightEditBarTrailing.constant = editBarTrailing 169 | // 編集エリアのTrailingを現在のスクロール位置に設定 170 | editAreaViewTrailing.constant = editBarTrailing 171 | // アニメーションさせて移動させる 172 | UIView.animate(withDuration: 0.2) { 173 | self.view.layoutIfNeeded() 174 | } 175 | } 176 | 177 | // MARK: 再生 178 | 179 | @IBOutlet weak var playButton: UIButton! { 180 | didSet { 181 | playButton.allMaskCorner() 182 | playButton.addTarget(self, action: #selector(play), for: .touchUpInside) 183 | } 184 | } 185 | 186 | @objc private func play() { 187 | let isPlaying = editorManager.isPlaying 188 | // 再生ボタンUI(iOS13以降使用可能なSF Symbolsを使って画像切替) 189 | let playImage = isPlaying 190 | ? UIImage(systemName: "play.fill") 191 | : UIImage(systemName: "pause.fill") 192 | playButton.setImage(playImage, for: .normal) 193 | // 現在スクロール位置 / 20px(1秒の横幅) 194 | let currentTime = editBarScrollView.contentOffset.x / oneSecoundWidth 195 | let time = max(0, min(Double(currentTime), totalTime)) 196 | // AVAudioPlayerの現在位置から再生と一時停止 197 | isPlaying 198 | ? editorManager.pause() 199 | : editorManager.play(currentTime: time) 200 | // タイマーをセット 201 | setupTimer(isPlaying: editorManager.isPlaying) 202 | } 203 | 204 | private func setupTimer(isPlaying: Bool) { 205 | if !isPlaying { 206 | playerTimer?.invalidate() 207 | playerTimer = nil 208 | } else { 209 | playerTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in 210 | guard self.editorManager.isPlaying else { 211 | timer.invalidate() 212 | self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal) 213 | return 214 | } 215 | // 0.1秒ごとに現在位置にメーター幅足して上げる 216 | var movePoint = self.editBarScrollView.contentOffset 217 | movePoint.x += 2 218 | self.editBarScrollView.setContentOffset(movePoint, animated: false) 219 | } 220 | } 221 | } 222 | 223 | // MARK: 編集 224 | 225 | @IBOutlet weak var editButton: UIButton! { 226 | didSet { 227 | editButton.allMaskCorner() 228 | editButton.addTarget(self, action: #selector(edit), for: .touchUpInside) 229 | } 230 | } 231 | 232 | // 音声を編集する 233 | @objc private func edit() { 234 | editorManager.edit(leftTime: leftEditBarTime, rightime: rightEditBarTime) { result in 235 | 236 | switch result { 237 | case let .success(removeRange): 238 | DispatchQueue.main.async { 239 | self.waveCollectionView.performBatchUpdates({ 240 | var removeIndexs: [IndexPath] = [] 241 | for index in removeRange { 242 | removeIndexs.append(IndexPath(item: index, section: 0)) 243 | } 244 | self.waveCollectionView.deleteItems(at: removeIndexs) 245 | self.initView() 246 | }) 247 | } 248 | case let .failure(error): 249 | print(error) 250 | } 251 | } 252 | } 253 | 254 | private func initView() { 255 | // フロントのScrollViewを初期状態に戻す 256 | frontScrollContentViewWidth.constant = waveContentViewWidth + spaceWidth * 2 257 | leftEditBarLeading.constant = spaceWidth 258 | rightEditBarTrailing.constant = spaceWidth - meterWidth 259 | // バックのScrollViewを初期状態に戻す 260 | backScrollContentViewWidth.constant = waveContentViewWidth + spaceWidth * 2 261 | editAreaViewTrailing.constant = spaceWidth - meterWidth 262 | editAreaViewLeading.constant = spaceWidth 263 | // トータル時間を修正 264 | totalTimeLabel.text = editorManager.totalTimeText 265 | 266 | // カーソル 267 | self.editEreaScrollView.setContentOffset(.zero, animated: false) 268 | self.editBarScrollView.setContentOffset(.zero, animated: false) 269 | self.waveCollectionView.setContentOffset(.zero, animated: false) 270 | self.waveCollectionView.reloadData() 271 | self.timeCollectionView.reloadData() 272 | } 273 | 274 | @available(*, unavailable) 275 | required init?(coder: NSCoder) { 276 | fatalError("init(coder:) has not been implemented") 277 | } 278 | 279 | // 音声編集のマネージャー 280 | let editorManager: AudioEditorManager 281 | // 振幅 282 | var amplitudes: [Float] { editorManager.amplitudes } 283 | // タイムインターバルの横幅(0.1秒の横幅) 284 | let meterWidth: CGFloat = 2 285 | // 1秒の横幅 286 | let oneSecoundWidth: CGFloat = 20 287 | // 5秒 288 | let fiveSecond = 5 289 | 290 | // 波形表示のコンテンツの高さ 291 | var collectionViewHeight: CGFloat { waveCollectionView.frame.height } 292 | // ヘッダー・フッターの横幅(画面幅の半分) 293 | var spaceWidth: CGFloat { 294 | UIScreen.main.bounds.width / 2 295 | } 296 | // 収録した波形を表示するトータルの横幅 297 | var waveContentViewWidth: CGFloat { 298 | CGFloat(amplitudes.count) * meterWidth 299 | } 300 | // スクロールする全体のwidth 301 | var contentViewWidth: CGFloat { 302 | waveContentViewWidth + spaceWidth * 2 303 | } 304 | // 収録時間 305 | var totalTime: Double { 306 | editorManager.totalTime 307 | } 308 | // 左の編集バーの時間 309 | var leftEditBarTime: Double { 310 | let leftTime = (leftEditBarLeading.constant - spaceWidth) / oneSecoundWidth 311 | return max(0, min(Double(leftTime), totalTime)) 312 | } 313 | // 右の編集バーの時間 314 | var rightEditBarTime: Double { 315 | let rightTime = (contentViewWidth - rightEditBarTrailing.constant - spaceWidth - meterWidth) / oneSecoundWidth 316 | return max(0, min(Double(rightTime), totalTime)) 317 | } 318 | 319 | // 再生のタイマー 320 | var playerTimer: Timer? 321 | 322 | init(amplitudes: [Float], originalUrl: URL) { 323 | self.editorManager = AudioEditorManager( 324 | amplitudes: amplitudes, 325 | originalUrl: originalUrl 326 | ) 327 | 328 | super.init(nibName: nil, bundle: nil) 329 | } 330 | 331 | override func viewDidLoad() { 332 | super.viewDidLoad() 333 | totalTimeLabel.text = editorManager.totalTimeText 334 | } 335 | 336 | func decibelHeight(index: Int) -> CGFloat { 337 | let height = CGFloat(amplitudes[index]) * collectionViewHeight 338 | return height < collectionViewHeight ? height : collectionViewHeight 339 | } 340 | 341 | func setupCurrentTime() { 342 | // 現在スクロール位置 / 20px(1秒の横幅) 343 | let currentTime = editBarScrollView.contentOffset.x / oneSecoundWidth 344 | // 現在時間が収録時間を超えないように制御 345 | let time = max(0, min(Double(currentTime), totalTime)) 346 | let timeText = editorManager.timeText(time: time) 347 | minuteTimeLabel.text = timeText.minute 348 | secondTimeLabel.text = timeText.second 349 | millisecondTimeLabel.text = timeText.millisecond 350 | } 351 | } 352 | 353 | extension AudioEditorViewController: UICollectionViewDataSource { 354 | 355 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 356 | switch collectionTypes[collectionView.tag] { 357 | case .decibel: return amplitudes.count 358 | // ex) 収録時間:11秒 / 5秒 + 1 = 3 359 | // 表示時間:0:00、00:05、00:10 360 | case .time: return Int(totalTime / 5.0) + 1 361 | } 362 | } 363 | 364 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 365 | switch collectionTypes[collectionView.tag] { 366 | case .decibel: 367 | let cell = collectionView.dequeueReusableCell(for: indexPath) as WaveWithMeterCollectionViewCell 368 | cell.draw( 369 | height: decibelHeight(index: indexPath.row), 370 | index: indexPath.row 371 | ) 372 | return cell 373 | case .time: 374 | let cell = collectionView.dequeueReusableCell(for: indexPath) as TimeCollectionViewCell 375 | cell.draw(time: editorManager.meterTime(index: indexPath.row)) 376 | return cell 377 | } 378 | } 379 | } 380 | 381 | extension AudioEditorViewController: UICollectionViewDelegateFlowLayout { 382 | 383 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 384 | switch collectionTypes[collectionView.tag] { 385 | case .decibel: 386 | return CGSize(width: meterWidth, height: collectionViewHeight) 387 | case .time: 388 | // Cellサイズは0.1秒で2pxなので5秒だと100px 389 | return CGSize(width: 20 * 5, height: 10) 390 | } 391 | } 392 | 393 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 394 | .zero 395 | } 396 | 397 | // 水平方向におけるセル間のマージン 398 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 399 | 0 400 | } 401 | 402 | // 垂直方向におけるセル間のマージン 403 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 404 | 0 405 | } 406 | 407 | // ヘッダーを追加 408 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 409 | CGSize(width: spaceWidth, height: collectionViewHeight) 410 | } 411 | 412 | // フッターを追加 413 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 414 | CGSize(width: spaceWidth, height: collectionViewHeight) 415 | } 416 | } 417 | 418 | extension AudioEditorViewController: UIScrollViewDelegate { 419 | // 編集バーScrollViewのスクロールイベント 420 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 421 | // 編集バーのスクロールされた距離 422 | let contentOffset = scrollView.contentOffset 423 | // 各ScrollViewと連結 424 | waveCollectionView.setContentOffset(contentOffset, animated: false) 425 | timeCollectionView.setContentOffset(contentOffset, animated: false) 426 | editEreaScrollView.setContentOffset(contentOffset, animated: false) 427 | // スクロール量に応じて、現在時刻の更新 428 | setupCurrentTime() 429 | } 430 | } 431 | 432 | -------------------------------------------------------------------------------- /RecordProject.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CA13B12A24FC7C6100746FDF /* AudioEditorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA13B12924FC7C6100746FDF /* AudioEditorManager.swift */; }; 11 | CA1D5EDE2514C7B300F98DEB /* RecorderSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1D5EDD2514C7B300F98DEB /* RecorderSessionHandler.swift */; }; 12 | CA1D5EE02514C7C800F98DEB /* RecorderFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1D5EDF2514C7C800F98DEB /* RecorderFileHandler.swift */; }; 13 | CA1D5EE22514C7EF00F98DEB /* RecorderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1D5EE12514C7EF00F98DEB /* RecorderHandler.swift */; }; 14 | CA2FB2EF24FA042800BB6ABB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB2EE24FA042800BB6ABB /* AppDelegate.swift */; }; 15 | CA2FB2F124FA042800BB6ABB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB2F024FA042800BB6ABB /* SceneDelegate.swift */; }; 16 | CA2FB2F324FA042800BB6ABB /* AudioRecordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB2F224FA042800BB6ABB /* AudioRecordViewController.swift */; }; 17 | CA2FB2F624FA042800BB6ABB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB2F424FA042800BB6ABB /* Main.storyboard */; }; 18 | CA2FB2F824FA042A00BB6ABB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB2F724FA042A00BB6ABB /* Assets.xcassets */; }; 19 | CA2FB2FB24FA042A00BB6ABB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB2F924FA042A00BB6ABB /* LaunchScreen.storyboard */; }; 20 | CA2FB30524FA28F000BB6ABB /* RecorderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB30424FA28F000BB6ABB /* RecorderManager.swift */; }; 21 | CA2FB30724FA30D700BB6ABB /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB30624FA30D700BB6ABB /* Extensions.swift */; }; 22 | CA2FB30A24FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB30824FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.swift */; }; 23 | CA2FB30B24FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB30924FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.xib */; }; 24 | CA2FB31C24FBB19C00BB6ABB /* AudioEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB31A24FBB19C00BB6ABB /* AudioEditorViewController.swift */; }; 25 | CA2FB31D24FBB19C00BB6ABB /* AudioEditorViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB31B24FBB19C00BB6ABB /* AudioEditorViewController.xib */; }; 26 | CA2FB32424FBCF3500BB6ABB /* TimeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2FB32224FBCF3500BB6ABB /* TimeCollectionViewCell.swift */; }; 27 | CA2FB32524FBCF3500BB6ABB /* TimeCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CA2FB32324FBCF3500BB6ABB /* TimeCollectionViewCell.xib */; }; 28 | CAAD09352503489D00AF891E /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAD09342503489D00AF891E /* AudioPlayer.swift */; }; 29 | CAAD0937250348CD00AF891E /* AudioEditorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAD0936250348CD00AF891E /* AudioEditorHandler.swift */; }; 30 | CAAD09402503606F00AF891E /* WaveCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAD093E2503606F00AF891E /* WaveCollectionViewCell.swift */; }; 31 | CAAD09412503606F00AF891E /* WaveCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CAAD093F2503606F00AF891E /* WaveCollectionViewCell.xib */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | CA13B12924FC7C6100746FDF /* AudioEditorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEditorManager.swift; sourceTree = ""; }; 36 | CA1D5EDD2514C7B300F98DEB /* RecorderSessionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderSessionHandler.swift; sourceTree = ""; }; 37 | CA1D5EDF2514C7C800F98DEB /* RecorderFileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderFileHandler.swift; sourceTree = ""; }; 38 | CA1D5EE12514C7EF00F98DEB /* RecorderHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderHandler.swift; sourceTree = ""; }; 39 | CA2FB2EB24FA042800BB6ABB /* RecordProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RecordProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | CA2FB2EE24FA042800BB6ABB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | CA2FB2F024FA042800BB6ABB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 42 | CA2FB2F224FA042800BB6ABB /* AudioRecordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordViewController.swift; sourceTree = ""; }; 43 | CA2FB2F524FA042800BB6ABB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | CA2FB2F724FA042A00BB6ABB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 45 | CA2FB2FA24FA042A00BB6ABB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | CA2FB2FC24FA042A00BB6ABB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | CA2FB30424FA28F000BB6ABB /* RecorderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderManager.swift; sourceTree = ""; }; 48 | CA2FB30624FA30D700BB6ABB /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 49 | CA2FB30824FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveWithMeterCollectionViewCell.swift; sourceTree = ""; }; 50 | CA2FB30924FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WaveWithMeterCollectionViewCell.xib; sourceTree = ""; }; 51 | CA2FB31A24FBB19C00BB6ABB /* AudioEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEditorViewController.swift; sourceTree = ""; tabWidth = 5; }; 52 | CA2FB31B24FBB19C00BB6ABB /* AudioEditorViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AudioEditorViewController.xib; sourceTree = ""; }; 53 | CA2FB32224FBCF3500BB6ABB /* TimeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeCollectionViewCell.swift; sourceTree = ""; }; 54 | CA2FB32324FBCF3500BB6ABB /* TimeCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimeCollectionViewCell.xib; sourceTree = ""; }; 55 | CAAD09342503489D00AF891E /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 56 | CAAD0936250348CD00AF891E /* AudioEditorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEditorHandler.swift; sourceTree = ""; }; 57 | CAAD093E2503606F00AF891E /* WaveCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveCollectionViewCell.swift; sourceTree = ""; }; 58 | CAAD093F2503606F00AF891E /* WaveCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WaveCollectionViewCell.xib; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | CA2FB2E824FA042800BB6ABB /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | CA1D5EDC2514C7A500F98DEB /* Handler */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | CA1D5EDD2514C7B300F98DEB /* RecorderSessionHandler.swift */, 76 | CA1D5EDF2514C7C800F98DEB /* RecorderFileHandler.swift */, 77 | CA1D5EE12514C7EF00F98DEB /* RecorderHandler.swift */, 78 | CAAD0936250348CD00AF891E /* AudioEditorHandler.swift */, 79 | CAAD09342503489D00AF891E /* AudioPlayer.swift */, 80 | ); 81 | path = Handler; 82 | sourceTree = ""; 83 | }; 84 | CA1D5EE32514C82500F98DEB /* Manager */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | CA13B12924FC7C6100746FDF /* AudioEditorManager.swift */, 88 | CA2FB30424FA28F000BB6ABB /* RecorderManager.swift */, 89 | ); 90 | path = Manager; 91 | sourceTree = ""; 92 | }; 93 | CA1D5EE525182F7F00F98DEB /* Extension */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | CA2FB30624FA30D700BB6ABB /* Extensions.swift */, 97 | ); 98 | path = Extension; 99 | sourceTree = ""; 100 | }; 101 | CA2FB2E224FA042800BB6ABB = { 102 | isa = PBXGroup; 103 | children = ( 104 | CA2FB2ED24FA042800BB6ABB /* RecordProject */, 105 | CA2FB2EC24FA042800BB6ABB /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | CA2FB2EC24FA042800BB6ABB /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | CA2FB2EB24FA042800BB6ABB /* RecordProject.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | CA2FB2ED24FA042800BB6ABB /* RecordProject */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | CA1D5EE525182F7F00F98DEB /* Extension */, 121 | CA1D5EE32514C82500F98DEB /* Manager */, 122 | CA1D5EDC2514C7A500F98DEB /* Handler */, 123 | CA2FB31524FB6F2D00BB6ABB /* Editor */, 124 | CA2FB31424FB6DFA00BB6ABB /* Record */, 125 | CA2FB2EE24FA042800BB6ABB /* AppDelegate.swift */, 126 | CA2FB2F024FA042800BB6ABB /* SceneDelegate.swift */, 127 | CA2FB2F724FA042A00BB6ABB /* Assets.xcassets */, 128 | CA2FB2F924FA042A00BB6ABB /* LaunchScreen.storyboard */, 129 | CA2FB2FC24FA042A00BB6ABB /* Info.plist */, 130 | ); 131 | path = RecordProject; 132 | sourceTree = ""; 133 | }; 134 | CA2FB31424FB6DFA00BB6ABB /* Record */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | CA2FB2F224FA042800BB6ABB /* AudioRecordViewController.swift */, 138 | CA2FB2F424FA042800BB6ABB /* Main.storyboard */, 139 | CAAD093E2503606F00AF891E /* WaveCollectionViewCell.swift */, 140 | CAAD093F2503606F00AF891E /* WaveCollectionViewCell.xib */, 141 | ); 142 | path = Record; 143 | sourceTree = ""; 144 | }; 145 | CA2FB31524FB6F2D00BB6ABB /* Editor */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | CA2FB31A24FBB19C00BB6ABB /* AudioEditorViewController.swift */, 149 | CA2FB31B24FBB19C00BB6ABB /* AudioEditorViewController.xib */, 150 | CA2FB32224FBCF3500BB6ABB /* TimeCollectionViewCell.swift */, 151 | CA2FB32324FBCF3500BB6ABB /* TimeCollectionViewCell.xib */, 152 | CA2FB30824FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.swift */, 153 | CA2FB30924FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.xib */, 154 | ); 155 | path = Editor; 156 | sourceTree = ""; 157 | }; 158 | /* End PBXGroup section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | CA2FB2EA24FA042800BB6ABB /* RecordProject */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = CA2FB2FF24FA042A00BB6ABB /* Build configuration list for PBXNativeTarget "RecordProject" */; 164 | buildPhases = ( 165 | CA2FB2E724FA042800BB6ABB /* Sources */, 166 | CA2FB2E824FA042800BB6ABB /* Frameworks */, 167 | CA2FB2E924FA042800BB6ABB /* Resources */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | ); 173 | name = RecordProject; 174 | productName = RecordProject; 175 | productReference = CA2FB2EB24FA042800BB6ABB /* RecordProject.app */; 176 | productType = "com.apple.product-type.application"; 177 | }; 178 | /* End PBXNativeTarget section */ 179 | 180 | /* Begin PBXProject section */ 181 | CA2FB2E324FA042800BB6ABB /* Project object */ = { 182 | isa = PBXProject; 183 | attributes = { 184 | LastSwiftUpdateCheck = 1160; 185 | LastUpgradeCheck = 1160; 186 | ORGANIZATIONNAME = "Yosei Yamagishi"; 187 | TargetAttributes = { 188 | CA2FB2EA24FA042800BB6ABB = { 189 | CreatedOnToolsVersion = 11.6; 190 | }; 191 | }; 192 | }; 193 | buildConfigurationList = CA2FB2E624FA042800BB6ABB /* Build configuration list for PBXProject "RecordProject" */; 194 | compatibilityVersion = "Xcode 9.3"; 195 | developmentRegion = en; 196 | hasScannedForEncodings = 0; 197 | knownRegions = ( 198 | en, 199 | Base, 200 | ); 201 | mainGroup = CA2FB2E224FA042800BB6ABB; 202 | productRefGroup = CA2FB2EC24FA042800BB6ABB /* Products */; 203 | projectDirPath = ""; 204 | projectRoot = ""; 205 | targets = ( 206 | CA2FB2EA24FA042800BB6ABB /* RecordProject */, 207 | ); 208 | }; 209 | /* End PBXProject section */ 210 | 211 | /* Begin PBXResourcesBuildPhase section */ 212 | CA2FB2E924FA042800BB6ABB /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | CA2FB2FB24FA042A00BB6ABB /* LaunchScreen.storyboard in Resources */, 217 | CA2FB2F824FA042A00BB6ABB /* Assets.xcassets in Resources */, 218 | CA2FB2F624FA042800BB6ABB /* Main.storyboard in Resources */, 219 | CA2FB30B24FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.xib in Resources */, 220 | CAAD09412503606F00AF891E /* WaveCollectionViewCell.xib in Resources */, 221 | CA2FB31D24FBB19C00BB6ABB /* AudioEditorViewController.xib in Resources */, 222 | CA2FB32524FBCF3500BB6ABB /* TimeCollectionViewCell.xib in Resources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | /* End PBXResourcesBuildPhase section */ 227 | 228 | /* Begin PBXSourcesBuildPhase section */ 229 | CA2FB2E724FA042800BB6ABB /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | CAAD09402503606F00AF891E /* WaveCollectionViewCell.swift in Sources */, 234 | CA2FB2F324FA042800BB6ABB /* AudioRecordViewController.swift in Sources */, 235 | CA2FB30A24FA38FE00BB6ABB /* WaveWithMeterCollectionViewCell.swift in Sources */, 236 | CAAD0937250348CD00AF891E /* AudioEditorHandler.swift in Sources */, 237 | CAAD09352503489D00AF891E /* AudioPlayer.swift in Sources */, 238 | CA2FB31C24FBB19C00BB6ABB /* AudioEditorViewController.swift in Sources */, 239 | CA2FB30724FA30D700BB6ABB /* Extensions.swift in Sources */, 240 | CA1D5EDE2514C7B300F98DEB /* RecorderSessionHandler.swift in Sources */, 241 | CA1D5EE02514C7C800F98DEB /* RecorderFileHandler.swift in Sources */, 242 | CA1D5EE22514C7EF00F98DEB /* RecorderHandler.swift in Sources */, 243 | CA2FB30524FA28F000BB6ABB /* RecorderManager.swift in Sources */, 244 | CA2FB2EF24FA042800BB6ABB /* AppDelegate.swift in Sources */, 245 | CA2FB2F124FA042800BB6ABB /* SceneDelegate.swift in Sources */, 246 | CA2FB32424FBCF3500BB6ABB /* TimeCollectionViewCell.swift in Sources */, 247 | CA13B12A24FC7C6100746FDF /* AudioEditorManager.swift in Sources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | /* End PBXSourcesBuildPhase section */ 252 | 253 | /* Begin PBXVariantGroup section */ 254 | CA2FB2F424FA042800BB6ABB /* Main.storyboard */ = { 255 | isa = PBXVariantGroup; 256 | children = ( 257 | CA2FB2F524FA042800BB6ABB /* Base */, 258 | ); 259 | name = Main.storyboard; 260 | sourceTree = ""; 261 | }; 262 | CA2FB2F924FA042A00BB6ABB /* LaunchScreen.storyboard */ = { 263 | isa = PBXVariantGroup; 264 | children = ( 265 | CA2FB2FA24FA042A00BB6ABB /* Base */, 266 | ); 267 | name = LaunchScreen.storyboard; 268 | sourceTree = ""; 269 | }; 270 | /* End PBXVariantGroup section */ 271 | 272 | /* Begin XCBuildConfiguration section */ 273 | CA2FB2FD24FA042A00BB6ABB /* Debug */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ALWAYS_SEARCH_USER_PATHS = NO; 277 | CLANG_ANALYZER_NONNULL = YES; 278 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 279 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 280 | CLANG_CXX_LIBRARY = "libc++"; 281 | CLANG_ENABLE_MODULES = YES; 282 | CLANG_ENABLE_OBJC_ARC = YES; 283 | CLANG_ENABLE_OBJC_WEAK = YES; 284 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 285 | CLANG_WARN_BOOL_CONVERSION = YES; 286 | CLANG_WARN_COMMA = YES; 287 | CLANG_WARN_CONSTANT_CONVERSION = YES; 288 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 289 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 290 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 291 | CLANG_WARN_EMPTY_BODY = YES; 292 | CLANG_WARN_ENUM_CONVERSION = YES; 293 | CLANG_WARN_INFINITE_RECURSION = YES; 294 | CLANG_WARN_INT_CONVERSION = YES; 295 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 297 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 299 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 300 | CLANG_WARN_STRICT_PROTOTYPES = YES; 301 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 302 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | COPY_PHASE_STRIP = NO; 306 | DEBUG_INFORMATION_FORMAT = dwarf; 307 | ENABLE_STRICT_OBJC_MSGSEND = YES; 308 | ENABLE_TESTABILITY = YES; 309 | GCC_C_LANGUAGE_STANDARD = gnu11; 310 | GCC_DYNAMIC_NO_PIC = NO; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_OPTIMIZATION_LEVEL = 0; 313 | GCC_PREPROCESSOR_DEFINITIONS = ( 314 | "DEBUG=1", 315 | "$(inherited)", 316 | ); 317 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 318 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 319 | GCC_WARN_UNDECLARED_SELECTOR = YES; 320 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 321 | GCC_WARN_UNUSED_FUNCTION = YES; 322 | GCC_WARN_UNUSED_VARIABLE = YES; 323 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 324 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 325 | MTL_FAST_MATH = YES; 326 | ONLY_ACTIVE_ARCH = YES; 327 | SDKROOT = iphoneos; 328 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 329 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 330 | }; 331 | name = Debug; 332 | }; 333 | CA2FB2FE24FA042A00BB6ABB /* Release */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ALWAYS_SEARCH_USER_PATHS = NO; 337 | CLANG_ANALYZER_NONNULL = YES; 338 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 339 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 340 | CLANG_CXX_LIBRARY = "libc++"; 341 | CLANG_ENABLE_MODULES = YES; 342 | CLANG_ENABLE_OBJC_ARC = YES; 343 | CLANG_ENABLE_OBJC_WEAK = YES; 344 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 345 | CLANG_WARN_BOOL_CONVERSION = YES; 346 | CLANG_WARN_COMMA = YES; 347 | CLANG_WARN_CONSTANT_CONVERSION = YES; 348 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 349 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 350 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 351 | CLANG_WARN_EMPTY_BODY = YES; 352 | CLANG_WARN_ENUM_CONVERSION = YES; 353 | CLANG_WARN_INFINITE_RECURSION = YES; 354 | CLANG_WARN_INT_CONVERSION = YES; 355 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 356 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 357 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 359 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 360 | CLANG_WARN_STRICT_PROTOTYPES = YES; 361 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 362 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 363 | CLANG_WARN_UNREACHABLE_CODE = YES; 364 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 365 | COPY_PHASE_STRIP = NO; 366 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 367 | ENABLE_NS_ASSERTIONS = NO; 368 | ENABLE_STRICT_OBJC_MSGSEND = YES; 369 | GCC_C_LANGUAGE_STANDARD = gnu11; 370 | GCC_NO_COMMON_BLOCKS = YES; 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | GCC_WARN_UNUSED_VARIABLE = YES; 377 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 378 | MTL_ENABLE_DEBUG_INFO = NO; 379 | MTL_FAST_MATH = YES; 380 | SDKROOT = iphoneos; 381 | SWIFT_COMPILATION_MODE = wholemodule; 382 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 383 | VALIDATE_PRODUCT = YES; 384 | }; 385 | name = Release; 386 | }; 387 | CA2FB30024FA042A00BB6ABB /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 391 | CODE_SIGN_STYLE = Automatic; 392 | DEVELOPMENT_TEAM = Q22XAKJMSL; 393 | INFOPLIST_FILE = RecordProject/Info.plist; 394 | LD_RUNPATH_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "@executable_path/Frameworks", 397 | ); 398 | PRODUCT_BUNDLE_IDENTIFIER = FairyLand.RecordProject; 399 | PRODUCT_NAME = "$(TARGET_NAME)"; 400 | SWIFT_VERSION = 5.0; 401 | TARGETED_DEVICE_FAMILY = "1,2"; 402 | }; 403 | name = Debug; 404 | }; 405 | CA2FB30124FA042A00BB6ABB /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | CODE_SIGN_STYLE = Automatic; 410 | DEVELOPMENT_TEAM = Q22XAKJMSL; 411 | INFOPLIST_FILE = RecordProject/Info.plist; 412 | LD_RUNPATH_SEARCH_PATHS = ( 413 | "$(inherited)", 414 | "@executable_path/Frameworks", 415 | ); 416 | PRODUCT_BUNDLE_IDENTIFIER = FairyLand.RecordProject; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | SWIFT_VERSION = 5.0; 419 | TARGETED_DEVICE_FAMILY = "1,2"; 420 | }; 421 | name = Release; 422 | }; 423 | /* End XCBuildConfiguration section */ 424 | 425 | /* Begin XCConfigurationList section */ 426 | CA2FB2E624FA042800BB6ABB /* Build configuration list for PBXProject "RecordProject" */ = { 427 | isa = XCConfigurationList; 428 | buildConfigurations = ( 429 | CA2FB2FD24FA042A00BB6ABB /* Debug */, 430 | CA2FB2FE24FA042A00BB6ABB /* Release */, 431 | ); 432 | defaultConfigurationIsVisible = 0; 433 | defaultConfigurationName = Release; 434 | }; 435 | CA2FB2FF24FA042A00BB6ABB /* Build configuration list for PBXNativeTarget "RecordProject" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | CA2FB30024FA042A00BB6ABB /* Debug */, 439 | CA2FB30124FA042A00BB6ABB /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | /* End XCConfigurationList section */ 445 | }; 446 | rootObject = CA2FB2E324FA042800BB6ABB /* Project object */; 447 | } 448 | -------------------------------------------------------------------------------- /RecordProject/Editor/AudioEditorViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 58 | 67 | 76 | 77 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 245 | 255 | 267 | 268 | 269 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | --------------------------------------------------------------------------------