CMTime Segment Finder (binary search) - kirseia/study GitHub Wiki
- 단, 아이템은 sorted CMTimeRange Array 라고 가정한다.
- 아래 ver.1 에 문제가 있어서 수정했음
let timeRange1 = CMTimeRange(start: .zero, end: 2.5.time)
let timeRange8 = CMTimeRange(start: 2.5.time, end: 10.0.time)
let timeRange2 = CMTimeRange(start: 3.0.time, end: 5.0.time)
let timeRange3 = CMTimeRange(start: 4.0.time, end: 5.5.time)
let timeRange4 = CMTimeRange(start: 4.5.time, end: 7.0.time)
let timeRange5 = CMTimeRange(start: 7.0.time, end: 7.5.time)
let timeRange6 = CMTimeRange(start: 7.7.time, end: 8.0.time)
let timeRange7 = CMTimeRange(start: 9.0.time, end: 15.0.time)
이런식으로 있다면 sort 가 start 기준으로만 되어있다고 할 때... end 를 찾을 때 문제가 발생한다. 따라서 binary search 로 lower/upper bound 를 찾는건 문제가 됨.
그래서 binary search 는 한쪽만 찾는 것으로 처리했다. 대신, 전체 시간 기준으로 앞쪽이면 앞부터, 뒤 쪽이면 뒤부터 찾는 방식을 도입했음.
import AVFoundation
extension Double {
var time: CMTime {
return CMTime(seconds: self, preferredTimescale: 1000)
}
}
extension CMTime {
public func isNearlyEqualTo(time: CMTime, _ tolerance: CMTime = CMTime(value: 1, timescale: 600)) -> Bool {
let delta = CMTimeAbsoluteValue(self - time)
return delta < tolerance
}
}
protocol TimeRange {
var range: CMTimeRange { get set }
}
// 비교용
class TimeRangeSearch<T> where T: TimeRange {
var array: [T] = []
func list(on time: CMTime) -> [T] {
return array.filter { $0.range.containsTime(time) }
}
}
// FastTimeRangeSearch 가 약 30%정도 빠름
// (단일 건으로는 엄청난 차이는 나지 않으나 매 frame 마다 호출될테니 효과는 있을 것으로 보임)
class FastTimeRangeSearch<T> where T: TimeRange {
var array: [T] = [] {
didSet {
startSortArray = array.sorted { (lhs, rhs) -> Bool in
return lhs.range.start < rhs.range.start
}
startTime = startSortArray.first?.range.start ?? .zero
endSortArray = array.sorted(by: { (lhs, rhs) -> Bool in
return lhs.range.end < rhs.range.end
})
endTime = endSortArray.last?.range.end ?? .zero
}
}
init() {
}
init(array: [T]) {
self.array = array
}
private var startSortArray: [T] = []
private var endSortArray: [T] = []
private var startTime: CMTime = .zero
private var endTime: CMTime = .zero
func list(on time: CMTime) -> [T] {
// 양방향으로 binarySearch 는 불가해서 그나마 범위를 줄이기 위해 앞/뒤 먼저 찾을 범위를 선택
let startSearch = abs((startTime - time).seconds) < abs((endTime - time).seconds)
var lowerIndex = 0
var upperIndex = array.count
if startSearch {
guard let _upperIndex = binarySearchDirection(time: time, isStart: true, range: 0 ..< self.array.count) else {
return []
}
upperIndex = _upperIndex
} else {
guard let _lowerIndex = binarySearchDirection(time: time, isStart: false, range: 0 ..< self.array.count) else {
return []
}
lowerIndex = _lowerIndex
}
var results: [T] = []
for index in (lowerIndex..<upperIndex) {
let item = startSortArray[index]
if item.range.containsTime(time) {
results.append(item)
}
}
return results
}
private func binarySearchDirection(time: CMTime, isStart: Bool, range: Range<Int>) -> Int? {
if range.lowerBound >= range.upperBound {
let index = range.lowerBound - 1
if index < 0 {
return nil
} else if index < array.count {
if isStart {
return startSortArray[index].range.containsTime(time) ? index + 1 : nil
} else {
return endSortArray[index].range.containsTime(time) ? index : nil
}
} else {
return nil
}
}
let midIndex:Int = range.lowerBound + (range.upperBound - range.lowerBound) / 2
let targetArray = isStart ? startSortArray : endSortArray
let item = targetArray[midIndex]
let compareValue = isStart ? item.range.start : item.range.end
if compareValue.isNearlyEqualTo(time: time, 0.01.time) {
return isStart ? midIndex + 1 : midIndex
} else if compareValue >= time {
return binarySearchDirection(time: time, isStart: isStart, range: range.lowerBound ..< midIndex)
} else {
return binarySearchDirection(time: time, isStart: isStart, range: midIndex + 1 ..< range.upperBound)
}
}
}
Test Code
import XCTest
import AVFoundation
@testable import testFramework
class testFrameworkTests: XCTestCase {
struct TimeData: TimeRange, Equatable {
var range: CMTimeRange
public static func == (lhs: TimeData, rhs: TimeData) -> Bool {
return CMTimeRangeEqual(lhs.range, rhs.range)
}
}
let timeRangeSearch = FastTimeRangeSearch<TimeData>()
override func setUp() {
let timeRange0 = TimeData(range: CMTimeRange.init(start: 0.time, end: 2.5.time))
let timeRange1 = TimeData(range: CMTimeRange.init(start: 2.0.time, end: 10.0.time))
let timeRange2 = TimeData(range: CMTimeRange.init(start: 3.0.time, end: 5.0.time))
let timeRange3 = TimeData(range: CMTimeRange.init(start: 4.0.time, end: 5.5.time))
let timeRange4 = TimeData(range: CMTimeRange.init(start: 4.5.time, end: 7.0.time))
let timeRange5 = TimeData(range: CMTimeRange.init(start: 7.0.time, end: 7.5.time))
let timeRange6 = TimeData(range: CMTimeRange.init(start: 7.7.time, end: 8.0.time))
let timeRange7 = TimeData(range: CMTimeRange.init(start: 9.0.time, end: 15.0.time))
timeRangeSearch.array = [timeRange0, timeRange1, timeRange2, timeRange3,
timeRange4, timeRange5, timeRange6, timeRange7]
}
func testBinarySearch1() {
let result1 = timeRangeSearch.list(on: 1.0.time)
XCTAssertEqual(result1[0], timeRangeSearch.array[0])
}
func testBinarySearch2() {
let result3 = timeRangeSearch.list(on: 100.0.time)
XCTAssertEqual(result3.count, 0)
}
func testBinarySearch3() {
let result4 = timeRangeSearch.list(on: 4.5.time)
let data = timeRangeSearch.array
XCTAssertEqual(result4, [data[1], data[2], data[3], data[4]])
}
func testBinarySearch4() {
let result2 = timeRangeSearch.list(on: 5.7.time)
let data = timeRangeSearch.array
XCTAssertEqual(result2, [data[1], data[4]])
}
}
import AVFoundation
extension Double {
var time: CMTime {
return CMTime(seconds: self, preferredTimescale: 600)
}
}
extension CMTime {
public func isNearlyEqualTo(time: CMTime, _ tolerance: CMTime = CMTime(value: 1, timescale: 600)) -> Bool {
let delta = CMTimeAbsoluteValue(self - time)
return delta < tolerance
}
}
extension Array {
func binarySsearch(time: CMTime) -> [Element] {
guard self.first is CMTimeRange else {
return []
}
guard let lowIndex = binarySearchDirection(time: time,
isStart: true,
range: 0 ..< self.count) else {
return []
}
guard let upperIndex = binarySearchDirection(time: time,
isStart: false,
range: 0 ..< self.count) else {
return []
}
if lowIndex > upperIndex {
return []
}
let range = lowIndex...upperIndex
print(range)
return self.enumerated().filter({ (item) -> Bool in
range.contains(item.offset)
}).map({ (item) -> Element in
return item.element
})
}
func binarySearchDirection(time: CMTime, isStart:Bool, range: Range<Int>) -> Int? {
guard self is [CMTimeRange] else {
return nil
}
if range.lowerBound >= range.upperBound {
let index = range.lowerBound + (isStart ? 0 : -1)
if index < self.count, let timeRange = self[index] as? CMTimeRange {
return timeRange.containsTime(time) ? index : nil
} else {
return nil
}
}
let midIndex:Int = range.lowerBound + (range.upperBound - range.lowerBound) / 2
guard let timeRange = self[midIndex] as? CMTimeRange else {
preconditionFailure("impossible")
}
let compareValue = isStart ? timeRange.end : timeRange.start
if compareValue.isNearlyEqualTo(time: time, 0.01.time) {
return midIndex
} else if compareValue >= time {
return binarySearchDirection(time: time, isStart: isStart, range: range.lowerBound ..< midIndex)
} else {
return binarySearchDirection(time: time, isStart: isStart, range: midIndex + 1 ..< range.upperBound)
}
}
}
var data:[CMTimeRange] = []
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
let timeRange1 = CMTimeRange(start: .zero, end: 2.5.time)
let timeRange2 = CMTimeRange(start: 3.0.time, end: 5.0.time)
let timeRange3 = CMTimeRange(start: 4.0.time, end: 5.5.time)
let timeRange4 = CMTimeRange(start: 4.5.time, end: 7.0.time)
let timeRange5 = CMTimeRange(start: 7.0.time, end: 7.5.time)
let timeRange6 = CMTimeRange(start: 7.7.time, end: 8.0.time)
let timeRange7 = CMTimeRange(start: 9.0.time, end: 15.0.time)
data = [timeRange1, timeRange2, timeRange3,
timeRange4, timeRange5, timeRange6, timeRange7]
data.sort { (lhs, rhs) -> Bool in
return lhs.start < rhs.start
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testBinarySearch1() {
let result1 = data.binarySsearch(time: 1.0.time)
XCTAssertEqual(result1[0], data[0])
let result2 = data.binarySsearch(time: 5.7.time)
XCTAssertEqual(result2.count, 1)
}
func testBinarySearch2() {
let result3 = data.binarySsearch(time: 100.0.time)
XCTAssertEqual(result3.count, 0)
}
func testBinarySearch3() {
let result4 = data.binarySsearch(time: 4.5.time)
XCTAssertEqual(result4, [data[1], data[2], data[3]])
}