├── 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 | 
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 |
--------------------------------------------------------------------------------