Best Online Casino Sites

If you’re looking for the top online casino to play at, Bovada is a great place to start. It is simple to navigate, has slots and table games, and provides generous bonuses. The interface is simple and straightforward. It also offers secure payment options and mobile compatibility. While Bovada is not perfect, it is one of the be queen of the nile slot gametter casinos available online. They should be focusing on adding more games to their lobby.

Caesars is another casino that accepts a variety of deposit methods. The site has a variety of jackpot slots and has several locations across the United States. This site has stunning graphics, a variety of games, and an array of promotions for customers currently running. If you’re looking for the best online casino that is real money, it’s at Caesars. Chat live with them, so you won’t miss any of the excitement.

Caesars is another well-known name in the field of gambling. They have casinos that are physically located across the country and provide an array of games. Apart from offering an extensive selection of games, the site also offers a variety of benefits for existing customers. For instance, they provide a loyalty program, which rewards you for playing on their site. The top online casino sites will offer you a variety of payment options, such as Bitcoin. The fees will vary however you’ll be happy that you have found a safe and reputable online gambling website with a high withdrawal limit.

The best online casinos will provide many ways to reach the customer service team. The most reliable ones will have the option of a live chat, but if you prefer email, you can reach the casino’s management via email. If you’re dissatisfied with your experience, they’ll do their best to resolve the issue as quickly as possible. The top casinos offer a variety of customer support options to meet your needs. They’ll be delighted to answer any questions you might have or help you out.

Some of the top casinos online have live dealers and video chats. While they may not have all the games, they must include at least some of the most popular. They might not have the most unique games but slots are the most frequently played casino game. These games require no strategy, and have the highest chance of winning. They’re easy to navigate, as well, and are an excellent way to test out the casino you’ve never played at before making a deposit.

Online casinos that offer an array of games will be the best. There are hundreds of games and options to choose from. It is crucial to find the one that best suits your needs and budget. You can also check the history of payouts at a casino by examining the percentages of payouts. This will provide you with an idea of the amount of money a casino will pay out in the long run. You can also avail of many bonus offers that are free.

When selecting an online casino, it’s crucial to select a reliable website. Most online casinos publish payout statistics. The average percentage payout is an excellent indicator of how fair a casino is.eCOGRA is the body that monitors each casino’s payout ratio. These statistics are typically posted on the casino’s website. Although the average payout may not be the sole factor, it is an important one. High payout rates are a good option for players who are new to the game.

In addition to being legally regulated and trustworthy an online casino that is reputable will be able to provide you with statistics on payouts. Independent experts like eCOGRA will keep track of the payout percentages at different casinos. This information can be used to determine the credibility of a website. If a casino is accredited by eCOGRA it is very likely that it is fair and transparent.

PlayTech is play thunderstruck 2 free a leading software developer. They are the only one to offer live dealer coverage and webcam coverage. They also offer high-quality games and have short payout times. The most played casino games are slot machines. They are simple to learn and play, and have a high chance of winning. If you’re looking for a brand new online casino, Super Slots is a excellent option. Its games are easy to understand and are intuitive.

Live Streaming on iOS

В нашей работе нередко возникает необходимость реализовать отправку видеопотока с iOS-устройства в реальном – или близком к реальному – времени. Самый частый пример — использование iOS-устройства в качестве камеры слежения или создание стриминговых приложений наподобие Periscope. Как правило, при возникновении подобной задачи ставятся дополнительные условия — например, возможность проигрывания потока на другом устройстве (или в другом приложении) без возникновения лишних проблем в браузере или VLC-плеере, малые задержки (видеопоток должен транслироваться практически в режиме реального времени), низкая нагрузка на устройство (возможность длительной работы от батареи), отсутствие необходимости в специализированном медиасервере для обслуживания передачи потока, и т.п.

Continue reading

Оптимизация отрисовки waveform на iOS

В статье пойдёт речь о том, как визуализировать вейвформу оптимальным образом, что для этого подходит лучше – CAShapeLayer или drawRect(), а также о некоторых тонкостях работы со Swift. Информация будет полезна не только тем, кто разрабатывает сложные кастомные UI компоненты и работает со звуком, но и любым iOS-разработчикам для расширения кругозора.

Постановка задачи

Разработчики DENIVIP создали приложение для фильтрации шумов в аудиозаписи – Denoise. Для пользователя это выглядит примерно так: он выбирает видео из своего альбома, приложение строит вейвформу звуковой дорожки и пользователь может выбрать на ней область с шумом, который нужно подавить. После небольшой магии пользователь получает видео с отфильтрованным звуком.

Алгоритм подавления шумов в данной статье подробно не обсуждается, но по большому счету он не выходит за рамки университетской программы по цифровой обработке сигналов. Теоретическую базу можно найти в удобном виде на сайте MITOpenCourseWare. Если говорить очень кратко, когда пользователь выбирает участок с шумом, мы, используя прямое преобразование Фурье, получаем частотный спектр «шумного участка». Запомнив эти частоты как «плохие» и требующие подавления, алгоритм проходит по всей аудиодорожке, на каждом участке получает частотный спектр и применяет подавляющие коэффициенты к «плохим» частотам. Затем, после применения обратного преобразования Фурье, получается новая вейвформа (дискретные звуки), в которой шумы существенно подавлены. Вся сложность состоит лишь в том, как качественно выбрать «шумный» участок. При этом чем длиннее видео, тем важнее становится интерактивность вейвформы, т.к. пользователю может потребоваться выделить несколько секунд аудиодорожки в середине двухчасового видео.

В первую очередь нам необходимо отобразить звуковую дорожку в понятной для пользователя форме и при этом дать ему возможность манипулировать ею (изменять масштаб, перемещаться по ней, выделять фрагмент). Сразу замечу, что если мы попытаемся отобразить все сэмплы нашей аудиодорожки, то может оказаться, что, во-первых, нам не хватит памяти, а, во-вторых, процессор (а заодно и GPU) “захлебнется” от такого количества точек.

Пример. Для 10-минутного видео 16-битная стерео аудиодорожка с частотой дискретизации 44,1 кГц будет «весить» 10 * 60 * 2 * 44100 * 2 ~ 100 MB. При этом большая нагрузка ложится и на GPU.

Очевидным решением будет разбить сигнал на равные интервалы, посчитать, например, средние значения на этих интервалах и полученный набор (средних) использовать для отображения вейвформы. На деле будем использовать средние и максимальные значения на интервалах — для каждой аудиодорожки будем строить 2 вейвформы одна над другой. Плюс может потребоваться добавлять вейвформы «на лету” (для отображения отфильтрованного сигнала поверх оригинального). Существенным усложнением данного подхода является необходимость изменять масштаб вейформы, а также «перемещаться» по оси времени для выбора нужной области.

Замечу, что чтение сэмплов из аудиофайла и построение вейвформы может занимать продолжительное время. Для примера, на iPhone 5s вейвформа для 10-минутной записи строится порядка 10 секунд. Как известно, хуже ожидания для пользователя может быть только отсутствие информации о том, сколько еще нужно ждать. Поэтому мы решили сделать отрисовку загружающейся вейвформы анимированной, чтобы пользователь видел прогресс в интуитивном формате.

Итого для решения задачи нужно:

  1. Уметь читать одиночные сэмплы аудиодорожки и строить по ним данные для отображения вейвформы

  2. Уметь рисовать несколько вейвформ (с возможностью добавлять вейфвмормы в любой момент).

  3. Иметь возможность рисовать вейвформу “анимированно” по ходу чтения сэмплов.

  4. Добавить возможность масштабировать вейвформу и смещать её по времени.

Существующие решения

Лучшее из найденных решений: EZAudio. В целом интересный фреймворк, но он не совсем подходит для нашей задачи. Например, в нем нет возможности масштабирования графика.

Для отрисовки графика можно было использовать CorePlot. Но в целом это решение показалось громоздким для нашей задачи (также, возможно, потребовалось бы отдельно поработать над оптимизацией отрисовки большого набора значений).

Разработка

Архитектура waveform-модуля

По задаче необходимо уметь отображать не только конечные данные, но и данные в процессе построения. Также напомню о необходимости реализовать зум и перемещение по оси времени. Исходя из этого, было решено сильно отвязать слой UI от данных, которые ему предстоит рендерить.

Smooth waveform UI architecture

Примерно так упрощенно выглядят связи между классами, занятыми в отрисовке вейвформ.

Здесь

  1. Diagram View — контейнер одиночных вейвформ. Он также реализует синхронизацию отрисовки вейвформ, для чего использует CADisplayLink.

  2. Plot View —  класс, занимающийся отрисовкой. Этот класс должен знать только ту информацию, которую необходимо отобразить в данный момент времени, и получать её он будет из соответствующего Plot ViewModel.

  3. Diagram ViewModel — контейнер вью-моделей отдельных вейвформ.

  4. Plot ViewModel — источник данных для одиночной вейвформы. Этот класс будет решать, какие семплы и в каком количестве отдавать на отрисовку, когда это необходимо

Здесь опущены протоколы, через которые на самом деле реализована связь от View к ViewModel. Замечу только, что архитектурой не предусмотрено обратных связей от ViewModel к View.

Благодаря такому подходу мы получили возможность оптимизировать построение данных для вейвформы и отрисовку вейвформы независимо друг от друга. Также это дало возможность внести серьезные изменения в построение данных без воздействия на слой View. Но обо всем по порядку.

Чтение сэмплов

Для построения вейвформы необходимо получить несжатые семплы аудиодорожки. Сделать это можно с помощью инструментов, предоставляемых фреймворками AVFoundation и CoreMedia.

Чтение данных целиком представлено в классе:

import Foundation
import AVFoundation
import CoreMedia

protocol AudioSamplesHandler: class {
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int)
    func didStopReadSamples(count: Int)
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int)
}

class AudioSamplesReader: NSObject {
    var asset: AVAsset
    init(asset: AVAsset) {
        self.asset = asset
    }
    
    weak var samplesHandler: AudioSamplesHandler?
    
    func defaultAudioFormat() -> AudioStreamBasicDescription {
        var audioFormat               = AudioStreamBasicDescription()
        audioFormat.mSampleRate       = 44100
        audioFormat.mChannelsPerFrame = 2
        audioFormat.mBitsPerChannel   = 16
        return audioFormat
    }
    
    func readAudioFormat() -> AudioStreamBasicDescription? {
        guard let sound = self.asset.tracksWithMediaType(AVMediaTypeAudio).first else {
            /* handle error */
            return nil
        }
        
        guard let formatDescription = sound.formatDescriptions.first else {
            /* handle error */
            return nil
        }

        let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription as! CMAudioFormatDescription).memory

        var audioFormat               = AudioStreamBasicDescription()
        audioFormat.mSampleRate       = asbd.mSampleRate
        audioFormat.mChannelsPerFrame = asbd.mChannelsPerFrame
        audioFormat.mBitsPerChannel   = asbd.mBitsPerChannel
        return audioFormat
    }

    var readSamplesCount = 0
    
    func readAudioSamples(format: AudioStreamBasicDescription? = nil, sampleBlock: (UnsafePointer<Int16>!, length: Int) -> (Bool)) {
        
        readSamplesCount = 0
        
        let timerange = CMTimeRangeMake(kCMTimeZero, self.asset.duration)
        
        guard let sound = self.asset.tracksWithMediaType(AVMediaTypeAudio).first else {
            /* handle error */
            return
        }
        
        let assetReader: AVAssetReader
        
        do {
            assetReader = try AVAssetReader(asset: self.asset)
        } catch {
            /* handle error */
            return
        }
        
        let format = format ?? self.defaultAudioFormat()

        let audioReadSettings: [String: AnyObject] = [
            AVFormatIDKey : NSNumber(unsignedInt: kAudioFormatLinearPCM),
            AVSampleRateKey : format.mSampleRate,
            AVNumberOfChannelsKey : Int(format.mChannelsPerFrame),
            AVLinearPCMBitDepthKey : Int(format.mBitsPerChannel),
            AVLinearPCMIsBigEndianKey : false,
            AVLinearPCMIsFloatKey : false,
            AVLinearPCMIsNonInterleaved : false
        ]
        
        let readerOutput = AVAssetReaderTrackOutput.init(track: sound, outputSettings: audioReadSettings)
        
        assetReader.addOutput(readerOutput)
        assetReader.timeRange = timerange
        
        let started = assetReader.startReading()
        
        if !started { /* handle error */ }
        
        let estimatedSamplesCount = asset.duration.seconds * Double(format.mSampleRate)
        self.samplesHandler?.willStartReadSamples(estimatedSampleCount: Int(estimatedSamplesCount))

        while assetReader.status == .Reading {
            
            guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else {
                break
            }
            
            // Get buffer
            guard let buffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
                /* handle */
                break
            }
            
            let length = CMBlockBufferGetDataLength(buffer)
            
            // Append new data
            let tempBytes = UnsafeMutablePointer<Void>.alloc(length)
            var returnedPointer: UnsafeMutablePointer<Int8> = nil
            CMBlockBufferAccessDataBytes(buffer, 0, length, tempBytes, &returnedPointer)
            
            tempBytes.destroy()
            tempBytes.dealloc(length)
        
            samplesHandler?.handleSamples(UnsafePointer<Int16>(returnedPointer), length: length / 2, channelsCount: Int(format.mChannelsPerFrame))
            readSamplesCount += length / 2 / Int(format.mChannelsPerFrame)
        }
        
        switch assetReader.status {
        case .Unknown, .Failed, .Reading:
            /* handle error */ ()
        case .Cancelled, .Completed:
            samplesHandler?.didStopReadSamples(readSamplesCount)
            return
        }
    }
}

Не буду очень подробно останавливаться на этом коде. Скажу только, что перед началом чтения сэмплов можно узнать параметры аудиозаписи, такие как частота дискретизации и количество каналов (функция readAudioFormat), и использовать их при чтении. Но можно и пропустить этот момент и при настройке чтения использовать частоту, равную 44100 Гц, и читать по двум каналам. Глубину кодирования придется задать вручную (16 бит).

Также отмечу, что чтение сэмплов может занимать продолжительное время. Для видео в несколько минут время чтения будет порядка нескольких секунд. Поэтому алгоритм нужно использовать в фоновом потоке.

Обработка сэмплов

Следует помнить, что алгоритм чтения сэмплов, вообще говоря, выдает блоки разных размеров. В этом случае нам нужно последовательно обходить каждый новый блок сэмплов, и по ходу рассчитывать требуемые для построения вейвформ значения.

import Foundation
import AVFoundation

protocol AudioSamplesHandler: class {
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int)
    func didStopReadSamples(count: Int)
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int)
}

class SamplesHandler: AudioSamplesHandler {
    
    var asset: AVAsset
    init(asset: AVAsset) {
        self.asset = asset
    }
    
    var neededSamplesCount = 0

    var estimatedSampleCount: Int = 0
    var sampleBlockLength: Int    = 0
    var currentAvgDouble: Double  = 0
    var currentMax: Int16         = 0
    var globalMax: Int16          = 0
    var globalBlockIndex: Int     = 0
    
    var avgPulsesBuffer: [Int16] = []
    var maxPulsesBuffer: [Int16] = []
    
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) {
        
        self.estimatedSampleCount = estimatedSampleCount
        
        sampleBlockLength = Int(Double(estimatedSampleCount) / Double(neededSamplesCount))
        
        currentAvgDouble = 0
        currentMax       = 0
        globalMax        = 0
        globalBlockIndex = 0
    }
    
    func didStopReadSamples(count: Int) {
        //
    }
    
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int) {
        var currentBlock = globalBlockIndex / sampleBlockLength
        
        for index in 0..<length {
            
            let sample = samples[channelsCount * index]
            currentAvgDouble += fabs(Double(sample) / Double(sampleBlockLength))
            
            if currentMax < sample {
                currentMax = sample
            }
            
            if (globalMax < sample) {
                globalMax = sample
            }
            
            globalBlockIndex += 1
            
            let oldBlock = currentBlock
            currentBlock = globalBlockIndex / sampleBlockLength
            
            if currentBlock > oldBlock {
                self.avgPulsesBuffer[oldBlock] = Int16(currentAvgDouble)
                self.maxPulsesBuffer[oldBlock] = currentMax
                currentAvgDouble = 0
                currentMax       = 0
            }
        }
    }
}

Swift vs ObjC. Что лучше?

Поскольку приложение уже было написано на Objective-C, начинать писать новый модуль на Swift было неочевидным решением. Например, что касается построения данных для графиков, требовалось убедиться, что Swift не создаст проблем с производительностью. Мы провели проверку аналогичных алгоритмов, написанных на обоих языках и использующих аналогичные типы данных (например, если в Swift использовался тип UnsafePointer<Double>, то в алгоритме на Objective-C — массив double*).

Результаты были следующие:

Данные по работе алгоритмов чтения аудиосемплов и построения данных для графика.

iPhone 5

Swift (Whole-Module-Optimization):  20.1 c

Objective-C (-Ofast):  41.4 c

iPhone 6s

Swift (Whole-Module-Optimization):  5.1 c

Objective-C (-Ofast):  4.1 c

Тесты проводились и на других моделях iPhone. Результаты показали сильные расхождения на моделях с 32-разрядными процессорами и небольшие — на 64-разрядных моделях. (Для дальнейшей работы над оптимизацией в основном использовался iPhone 5s).

Как видим, большой разницы нет. При этом некоторые особенности Swift очень помогли в решении этой задачи. Про это чуть ниже.

Масштабирование вейвформы

В первоначальном варианте для зума использовалась возможность чтения сэмплов из произвольного отрезка аудиодорожки. См. свойство объекта AVAssetReader

assetReader.timeRange = CMTimeRange(start: kCMTimeZero, duration: asset.duration)

И при каждом зуме графика чтение начиналось снова.

pre_mov_crop

Вариант вполне рабочий, однако пересчет данных для отрезка требует примерно того же времени, что и расчет для всего аудиофайла: время порядка нескольких секунд (см. данные выше).

Рефакторинг обработки сэмплов

Для реализации плавного зума было решено попробовать другой подход. При чтении сэмплов можно строить данные для графика сразу для нескольких масштабов, выдавать на начальном этапе данные для исходного масштаба, а остальные наборы данных хранить в локальном хранилище (или в оперативной памяти) и отдавать их в UI по мере необходимости (а при зуме подменять один набор данных другим). Как мы уже упомянули, наша архитектура позволяет сделать это без необходимости изменений в UI.

Всю обработку блоков сэмплов мы обернули в отдельный класс, в котором реализовали хранилище для обработанных данных и возможность гибко менять логику обработки.

В качестве массива обработанных данных был использован UnsafeMutablePointer<T>, который по своей сути является аналогом массива на языке C (см. документацию (англ.): Структура указателя UnsafeMutablePointer, Взаимодействие с API языка C). Пример реализации брался отсюда.

import Foundation

class Channel<T> {
    init(){}
    var buffer: UnsafeMutablePointer<T> = nil
    
    var blockSize: Int = 1
    var count: Int = 0
    var space: Int = 0
    var totalCount: Int = 0
    
    var currentBlockSize: Int = 0
    
//    subscript(index: Int) -> Double {
//        get {
//            return buffer[index].double
//        }
//    }
    
    func handleValue(value: T) {
        if currentBlockSize == blockSize {
            self.clear()
            currentBlockSize = 0
        }
        currentBlockSize += 1
    }
    
    func appendValue(value: Double) {

        if space == count {
            let newSpace = max(space * 2, 16)
            self.moveSpaceTo(newSpace)
        }
//        (buffer + count).initialize(T(value))
        count += 1
    }
    
    func moveSpaceTo(newSpace: Int) {
        let newPtr = UnsafeMutablePointer<T>.alloc(newSpace)
        
        newPtr.moveInitializeFrom(buffer, count: count)
        
        buffer.dealloc(count)
        
        buffer = newPtr
        space = newSpace
    }
    
    func clear() {
        //
    }
    
    func complete() {
        
        self.totalCount = self.count
        print(self.blockSize, self.count, self.totalCount)
        self.clear()
    }
    
    deinit {
        buffer.destroy(space)
        buffer.dealloc(space)
    }
}

Примечание. Здесь опущен очень важный момент: этот класс будет использоваться параллельно в разных потоках (запись в бэкграунде, чтение в главном потоке). В Swift встроенных средств для работы с многопоточностью нет, так что атомарное поведение буфера придется реализовать вручную (например, вот так). В нашем случае, при использовании Array<T> вместо UnsafeMutablePointer<T> и без синхронизации мы периодически сталкивались с выходом за границы массива. С UnsafeMutablePointer<T> такая проблема не возникала.

Про числовые типы

Пару слов скажу про ассоциированный тип T в классе Channel<T>.

Напомним, что для реализации зума было решено обрабатывать сэмплы и строить данные сразу для нескольких масштабов вейвформы. Поэтому нужно было добиться не только высокой производительности, но и эффективности при работе с памятью. Для нашей задачи для экономии памяти достаточно хранить сэмплы в Int16, а там, где нужно — явно приводить типы (благо стандартные числовые типы в Swift легко приводятся один к другому). Однако задача была решена в чуть более общем виде — то есть, с возможностью выбирать числовой тип для хранения данных. Для нашей задачи мы создали протокол для работы с числами любого типа (в Swift нет единого числового типа).

protocol NumberType {
    init(_ v: Int16)
    init(_ v: Double)
    var int16: Int16 { get }
    var double: Double { get }
}

extension Int16: NumberType {
    public var int16: Int16 { return Int16(self) }
    public var double: Double { return Double(self) }
}

extension Double: NumberType {
    public var int16: Int16 { return Int16(self) }
    public var double: Double { return Double(self) }
}

Теперь в коде класса Channel можно указать следующее:

class Channel<T: NumberType> {
    ...
    subscript(index: Int) -> Double {
        get {
            return buffer[index].double
        }
    }
    
    func appendValue(value: Double) {
        ...
        (buffer + count).initialize(T(value))
        ...
    }
    ...
}

Параллельные расчеты для нескольких масштабов

Теперь можно в класс Channel добавить обработку сэмплов (например, нахождение максимумов).

class Channel<T:...> {
    ...
    private var _max = -Double.infinity
    
    public func _handleValue(value: Double) {
        if value > _max {
            _max = value
        }
    }
    public func handleValue(value: Double) {
        if currentBlockSize == blockSize {
            self.clear()
            currentBlockSize = 0
        }
        self._handleValue(value)
    }
    public func _clear() {
        self.appendValueToBuffer(_max)
        _max = -Double.infinity
    }
    ...
}

Обработка (сразу по нескольким каналам, для нескольких масштабов) будет выглядеть примерно так.

import Foundation

protocol ChannelSource {
    var channelsCount: Int { get }
    var onChannelsChanged: () -> () { get set }
    func channelAtIndex(index: Int) -> Channel<Int16>
}

class SamplesHandler: ChannelSource, AudioSamplesHandler {
    
    var scaleInterLevelFactor = 2
    var numberOfScaleLevels   = 10
    var neededSamplesCount    = 2000
    
    var channels = [Channel<Int16>]()
    
    var scaleIndex = 0
    var onChannelsChanged: () -> () = {_ in}
    
    var channelsCount: Int = 1
    
    func channelAtIndex(index: Int) -> Channel<Int16> {
        return channels[index + scaleIndex * channelsCount]
    }
    
    func createChannelsForDefaultLogicTypes() {
        
        var channels = [Channel<Int16>]()
        
        for _ in 0..<numberOfScaleLevels {
            let channel = Channel<Int16>()
            channels.append(channel)
        }
    }
    
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) {
        for index in 0..<numberOfScaleLevels {
            
            var totalCount = Int(Double(neededSamplesCount) * pow(Double(scaleInterLevelFactor), Double(index)))
            let blockSize  = Int(ceil(Double(estimatedSampleCount)/Double(totalCount)))
            totalCount = Int(Double(estimatedSampleCount)/Double(blockSize))
            
            let channel = channels[index]
            
            channel.totalCount = totalCount
            channel.blockSize  = blockSize
        }
    }
    
    func reset(scale: Double) {
    
        var scaleIndex = Int(floor(log(scale)/log(Double(scaleInterLevelFactor))))
        scaleIndex     = min(self.numberOfScaleLevels - 1, scaleIndex)
        if scaleIndex != self.scaleIndex {
            self.scaleIndex = scaleIndex
            self.onChannelsChanged()
        }
    }
    
    func didStopReadSamples(count: Int) {
        for channel in channels {
            channel.complete()
        }
    }
    
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int) {
    
        for channelIndex in 0..<numberOfScaleLevels {
            
            let channel = channels[channelIndex * channelsCount]

            for sampleIndex in 0..<length {
                let sample = samples[channelsCount * sampleIndex]
                channel.handleValue(sample)
            }
        }
    }
}

Для получения конкретных данных будем запрашивать нужный канал. При зуме будем вызывать метод reset, что будет приводить к подмене текущего канала (при определенном масштабе).

По такому же принципу можно добавить вычисление средних значений. А чтобы все это работало вместе, можно создать один абстрактный класс Channel и 2 наследника MaxChannel и AvgChannel (для максимумов и средних значений, соответственно).

После всех манипуляций зум выглядит так:

post_mov_crop

Оптимизация

Построение набора данных для вейвформы

В общем, этого достаточно для решения задачи, однако мы попробовали оптимизировать алгоритм. Что касается Swift, если вам требуется высокая производительность того или иного алгоритма, то одними из главных препятствий станут ARC и динамическая диспетчеризация. Вообще говоря, в Swift есть ряд возможностей помочь компилятору опустить некоторые проверки. Здесь мы рассмотрим один из подходов.

Для оптимизации было сделано следующее:

  1. Создали базовый класс, который инкапсулирует логику обработки сэмплов.

  2. В классе-канале создали свойство этого типа.

  3. В классе-логике создали слабую ссылку на класс-канал.

  4. Отнаследовали от этого типа конкретные обработчики (для средних и для максимумов).

  5. Сам класс-канал сделали ненаследуемым (с помощью ключевого слова final).

  6. Включили оптимизацию Whole-Module-Optimization.

Про 2 последних пункта читайте здесь.

Режим Whole-Module-Optimization включается в настройках проекта.

whole-module-optimization

 

class Channel<T: NumberType> {
    
    let logicProvider: LogicProvider
    init(logicProvider: LogicProvider) {
        
        self.logicProvider = logicProvider
        self.logicProvider.channel = self
    }
    
    func handleValue(value: Double) {
        ...
        self.logicProvider.handleValue(value)
    }
    
    func clear() {
        self.logicProvider.clear()
    }
}

protocol LogicUser: class {
    func appendValueToBuffer(value: Double)
    var blockSize: Int { get }
}

extension Channel: LogicUser {}

class LogicProvider {
    weak var channel: LogicUser?
    
    func handleValue(value: Double) {}
    func clear() {}
}

class AudioMaxValueLogicProvider: LogicProvider {
    var max = Double(Int16.min)
    
    override func handleValue(value: Double) {
        let value = abs(value)
        if value > max {
            max = value
        }
    }
    
    override func clear() {
        self.channel?.appendValueToBuffer(min(max, Double(Int16.max)))
        max = Double(Int16.min)
    }
}

class AudioAverageValueLogicProvider: LogicProvider {
    var summ = 0.0
    var count = 0
    
    override func handleValue(value: Double) {
        summ = summ + abs(value)
        count += 1
    }
    
    override func clear() {
        self.channel?.appendValueToBuffer(summ/Double(count))
        summ = 0.0
        count = 0
    }
}

Результаты сравнения скорости работы алгоритма (продолжительность видеоролика 10 мин):

В случае с наследованием каналов и логикой обработки внутри каналов время построения данных: 27.95 с

Для варианта с “внешней” логикой — 6.66 с

В конечном итоге, буфер также решено было спрятать внутрь отдельного класса для того, чтобы подружить класс Channel с ObjC (ObjC не умеет работать с обобщенными типами Swift).

Графика

Немного расскажем про работу с графикой.

По задаче требуется уметь рисовать большое количество вертикальных линий.

Исходный вариант

Изначальный вариант базировался на использовании CAShapeLayer и отрисовки с помощью линии обводки (stroke).

import UIKit

protocol PlotDataSource: class {
    var dataSourceFrame: CGRect { get }
    var pointsCount: Int { get }
    func pointAtIndex(index: Int) -> CGPoint
}

class Plot: UIView {
    weak var dataSource: PlotDataSource?
    var lineColor = UIColor.blackColor()
    var pathLayer = CAShapeLayer()
    
    convenience init(){
        self.init(frame: CGRectZero)
        self.setupPathLayer()
    }

    func setupPathLayer() {
        self.pathLayer.strokeColor = UIColor.blackColor().CGColor
        self.pathLayer.lineWidth   = 1.0
        self.layer.addSublayer(self.pathLayer)
        
        self.pathLayer.drawsAsynchronously = true
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.pathLayer.frame = self.bounds
        self.redraw()
    }
    
    func redraw() {
        self.pathLayer.path = self.newPathPart()
    }
    
    func newPathPart() -> CGPathRef {
        
        guard let dataSource = self.dataSource else {
            return CGPathCreateMutable()
        }
        
        let currentCount = dataSource.pointsCount
        let sourceBounds = dataSource.dataSourceFrame.size
        
        let mPath        = CGPathCreateMutable()
        
        CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY)
        
        let wProportion = self.bounds.size.width / sourceBounds.width
        let hPropostion = self.bounds.size.height / sourceBounds.height
        
        for index in 0..<currentCount {
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathMoveToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY - adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY + adjustedPoint.y)
            CGPathMoveToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
        }
        CGPathMoveToPoint(mPath, nil, CGPathGetCurrentPoint(mPath).x, self.bounds.midY)
        
        return mPath
    }
}

Время отрисовки для 2000 точек на каждую из 2 вейвформ, на iPhone 5s: 18 с

Вообще говоря, это рабочий вариант, однако для большого количества точек (порядка 1000), мы получали задержки при отрисовке кадров.

Вариант с оптимизацией №1  (построение CGPath без CGPathMoveToPoint)

Для начала избавились от использования функции CGPathMoveToPoint и там, где нужно, стали просто добавлять лишнюю линию вместо перемещения между точками.

import UIKit
class _Plot: Plot {
    
    func newPathPart() -> CGPathRef {
        
        guard let dataSource = self.dataSource else {
            return CGPathCreateMutable()
        }
        
        let currentCount = dataSource.pointsCount
        let sourceBounds = dataSource.dataSourceFrame.size
        
        let mPath        = CGPathCreateMutable()
        
        CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY)
        
        let wProportion = self.bounds.size.width / sourceBounds.width
        let hPropostion = self.bounds.size.height / sourceBounds.height
        
        for index in 0..<currentCount {
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY - adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY + adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
        }
        CGPathMoveToPoint(mPath, nil, CGPathGetCurrentPoint(mPath).x, self.bounds.midY)
        
        return mPath
    }
}

Время отрисовки для тех же исходных данных: 14.8 с. 

Вариант с оптимизацией №2 (stroke против fill)

Вейвформы у нас довольно простые: строим линию от середины вверх в соответствии с уровнем сэмпла, повторяем симметрично вниз, возвращаемся на середину, переходим к следующему значению. Это позволило совсем отказаться от stroke и строить линии, закрашивая нужные области (fill). При этом по прежнему рисуем замкнутую фигуру.

class _Plot: Plot {
   func setupPathLayer() {
        
        self.pathLayer             = CAShapeLayer()
        self.pathLayer.fillColor   = UIColor.blackColor().CGColor
        self.layer.addSublayer(self.pathLayer)
        
        self.pathLayer.drawsAsynchronously = true
    }
    func newPathPart() -> CGPathRef {
        
        let lineWidth: CGFloat = 1
        
        guard let dataSource = self.dataSource else {
            return CGPathCreateMutable()
        }
        
        let currentCount = dataSource.pointsCount
        let sourceBounds = dataSource.dataSourceFrame.size
        
        let mPath        = CGPathCreateMutable()
        CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY - lineWidth/2)
        
        let wProportion = self.bounds.size.width / sourceBounds.width
        let hPropostion = self.bounds.size.height / sourceBounds.height
        
        for index in 0..<currentCount {
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x,               self.bounds.midY - lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x,               self.bounds.midY - adjustedPoint.y - lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x + lineWidth,   self.bounds.midY - adjustedPoint.y - lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x + lineWidth,   self.bounds.midY - lineWidth/2)
        }
        CGPathAddLineToPoint(mPath, nil, CGPathGetCurrentPoint(mPath).x, self.bounds.midY)
        for index in 0..<currentCount {
            let index = currentCount - index - 1
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x,               self.bounds.midY + lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x,               self.bounds.midY + adjustedPoint.y + lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x + lineWidth,   self.bounds.midY + adjustedPoint.y + lineWidth/2)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x + lineWidth,   self.bounds.midY + lineWidth/2)
        }
        
        CGPathAddLineToPoint(mPath, nil, 0.0, self.bounds.midY)
        CGPathCloseSubpath(mPath)
        return mPath
    }
}

В этом случае получаем:  12.4 с.

Прирост скорости небольшой и заметен только на относительно старых устройствах (до iPhone 5s включительно).

Вариант с оптимизацией №3 (drawRect против CAShapeLayer)

Также попробовали вместо CAShapeLayer использовать отрисовку того же самого CGPath в drawRect (алгоритм построения CGPath как варианте № 1)

class _Plot: Plot {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.opaque = false
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.opaque = false
    }
    
    func redraw() {
        self.setNeedsDisplay()
    }
    
    override func drawRect(rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        
        CGContextSetLineWidth(context, 1/UIScreen.mainScreen().scale)
        CGContextAddPath(context, self.newPathPart())
        CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor)
        CGContextSetInterpolationQuality(context, .None);
        CGContextSetAllowsAntialiasing(context, false);
        CGContextSetShouldAntialias(context, false);
        CGContextStrokePath(context)
    }
}

Обращаем ваше внимание на то, что необходимо правильно выставить толщину линии, исходя из параметров экрана. Иначе преимущества в скорости вы не получите — отрисовка получится прерывистой и медленной. Также заметим, что здесь отключено ненужное сглаживание.

Результат теста: 9.46 c. 

В итоге, в результате оптимизации построения CGPath и переходе на отрисовку в drawRect(), можно ускорить построение вейвформы почти в 2 раза. Полученного результата уже достаточно для плавной отрисовки нашей вейвформы.

Вместо заключения

  1. Как видим, Swift вполне подходит для задач подобного уровня.
  2. Оптимизация алгоритмов на Swift – это одна большая тема, которую мы еще постараемся осветить.
  3. Для достижения оптимальных параметров отрисовки вашей графики не обязательно переходить на OpenGL или Metal.
  4. MVVM-подобный подход к построению архитектуры с разделением ответственностей View и ViewModel хорошо подходит для реализации  сложного интерактивного интерфейса.

Целиком проект с демоприложением можно найти на GitHub по ссылке.

Синхронизация данных в iOS приложениях, использующих Core Data

the-cloud-rainbow-unicorn
В этой статье мы хотим рассказать о том, с какими проблемами вы можете столкнуться при реализации синхронизации данных в iOS приложениях и какие существуют подходы и средства для их решения.Мы постараемся осветить этот вопрос с разных сторон, дав наиболее общий обзор этой темы, но все же основной интерес для нас будут представлять приложения, модель данных которых достаточно сложна для того, чтобы использовать структурированную базу данных и фреймворк Core Data. Continue reading

Использование возможностей UIDynamicAnimator в приложениях под iOS 7

После выхода iOS 7 у разработчиков появилось очень много новых инструментов для создания интересных и увлекательных эффектов и анимаций. Но уместность использования многих из них по-прежнему остаётся под вопросом. Изучив некоторые из них, мы решили в новом нашем проекте сделать анимацию, аналогичную реализованной Apple в приложении «Сообщения».

Для реализации такого поведения нам пришлось заменить используемый сначала экземпляр класса UITableView на UICollectionView, так как только последний был адаптирован Apple для работы с объектом-аниматором UIDynamicAnimator

 

После этого мы переопределили стандартный UICollectionViewFlowLayout:

#import <UIKit/UIKit.h>
 
@interface DVCollectionViewFlowLayout : UICollectionViewFlowLayout
 
@end

в котором определили  объект UIDynamicAnimator и переопределили необходимые для реализации функции:

#import "DVCollectionViewFlowLayout.h"

@interface DVCollectionViewFlowLayout()

@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;

@end

@implementation DVCollectionViewFlowLayout

@synthesize dynamicAnimator = _dynamicAnimator;

-(id)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    if (self){
        _dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
    }
    return self;
}

- (void)prepareLayout{
    
    [super prepareLayout];
    
    CGSize contentSize = [self collectionViewContentSize];
    NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];
    
    if (items.count != self.dynamicAnimator.behaviors.count) {
        [self.dynamicAnimator removeAllBehaviors];
        
        for (UICollectionViewLayoutAttributes *item in items) {
            UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
            springBehavior.length = 0.f;
            springBehavior.damping = 1.f;
            springBehavior.frequency = 6.8f;
            
            [self.dynamicAnimator addBehavior:springBehavior];
        }
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    return [self.dynamicAnimator itemsInRect:rect];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    CGFloat scrollDelta = newBounds.origin.y - self.collectionView.bounds.origin.y;
    CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
    
    for (UIAttachmentBehavior *springBehavior in self.dynamicAnimator.behaviors) {
        CGPoint anchorPoint = springBehavior.anchorPoint;
        CGFloat touchDistance = fabsf(touchLocation.y - anchorPoint.y);
        CGFloat resistanceFactor = 0.002;
        
        UICollectionViewLayoutAttributes *attributes = springBehavior.items.firstObject;
       
        CGPoint center = attributes.center;
        
        float resistedScroll = scrollDelta * touchDistance * resistanceFactor;
        float simpleScroll = scrollDelta;
        
        float actualScroll = MIN(abs(simpleScroll), abs(resistedScroll));
        if(simpleScroll < 0){
            actualScroll *= -1;
        }
        
        center.y += actualScroll;
        attributes.center = center;
        
        [self.dynamicAnimator updateItemUsingCurrentState:attributes];
    }
    
    return NO;
}

-(void)dealloc{
    [self.dynamicAnimator removeAllBehaviors];
    self.dynamicAnimator = nil;
}

@end

Динамическое поведение становится активным, когда вы его добавляете к объекту- аниматору, который является экземпляром UIDynamicAnimator. Аниматор определяет контекст в котором динамическое поведение выполняется.

Объект UIAttachmentBehavior определяет связь между динамическим элементом класса UICollectionViewLayoutAttributes и точкой item.center. Когда точка перемещается, присоединенный элемент также перемещается. C помощью свойств length, damping и frequency можно настроить поведение нужным нам образом.

UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
springBehavior.length = 0.f;
springBehavior.damping = 1.f;
springBehavior.frequency = 6.8f;

Также важно правильно подобрать resistanceFactor

CGFloat resistanceFactor = 0.002;

Другие статьи по теме:

  1. Эффект параллакса в iOS приложениях
  2. Осваиваем Core Motion в iOS

 

Создание кастомного UIActivity для публикации фото и текста в социальной сети ВКонтакте

Во время работы над очередной версией приложения PhotoSuerte возникла задача сделать публикацию фото в социальной сети ВКонтакте через стандартный контроллер UIActivityViewController.

activity

Continue reading

Настройка Jenkins CI на Mac OS X для сборки Android- и iOS-приложений Phonegap/Cordova и размещения их в TestFlight/HockeyApp

Разработка мобильных приложений в большинстве случаев является весьма увлекательным занятием, особенно если вам самим нравится то, что вы создаете.Работая с приличным количеством внешних проектов (мы разрабатываем приложения и сайты на заказ), а также с растущим числом своих внутренних приложений, мы задумались о минимизации времени и сил затрачиваемых на подготовку тестовых и релизных сборок приложений.При наличии нескольких разработчиков, тестировщиков и приличного числа проектов, затраты на подготовку сборок становятся весьма существенными.Поэтому чтобы не тратить силы зря и заниматься тем, что действительно важно для ваших сервисов, предлагаем инструкцию по созданию системы автоматической сборки приложений.Описанный далее подход мы используем, в первую очередь, в своих приложениях, с которыми предлагаем вам познакомиться: Together, PhotoSuerte, Routes.Tips. Continue reading

Node.JS & MongoDB: молниеносная быстрота создания прототипов

nodejs

Те, кто разрабатывает мобильные приложения, обычно начинают думать о рабочем прототипе сразу же после создания предварительного макета. В контексте методологии Lean Startup и концепции минимально жизнеспособного продукта (MVP) не будет преувеличением сказать, что создание прототипа до момента принятия окончательных решений архитектурного плана жизненно важно для успеха проекта. Раньше у многих была удобная отговорка, что создание прототипа дело исключительно трудоемкое. Однако сегодня для большинства проектов разработки мобильных приложений это совсем не так. Разработав прототип, вы можете гораздо быстрее получить ценные отклики от своих пользователей, а значит, гораздо более осознанно переосмыслить архитектуру и динамику своего приложения.

В этой статье я хочу поделиться методологией, которой мы обычно придерживаемся в наших проектах. При разработке прототипов для наших новых приложений мы всегда используем сочетание Node.js и MongoDB. Это позволяет нам избежать существенных трудозатрат на разработку серверной части и получить готовый сервис всего за несколько недель. Более того, полученную архитектуру можно в дальнейшем масштабировать. Теперь давайте подробнее разберем наш подход на примере недавно запущенного приложения PhotoSuerte. Это приложение позволяет случайным пользователям общаться друг с другом, обмениваясь фотографиями. Предлагаю сразу перейти к делу, чтобы не тратить ваше драгоценное время. Прочитав эту статью, вы можете сразу приступить к созданию прототипа вашего следующего мегаприложения! Continue reading