SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, iOS Chart, Android Chart and JavaScript Chart Components
Please note! These examples are new to SciChart iOS v4 release! SciChart’s OpenGL ES and Metal iOS and Metal macOS Chart library ships with hundred of Objective-C and Swift iOS&macOS Chart Examples which you can browse, play with and view the source-code. All of this is possible with the new and improved SciChart iOS Examples Suite and demo application for Mac, which ships as part of the SciChart SDK.
The Audio analyzer demo showcases how to use SciChart iOS charts in a scientific context.
Download the examples and enable your microphone to see this demo at work.
In this example we listen to the microphone on your iPad/iPhone device and create a waveform of the sound recorded in the top chart. This chart has data-points drawn in real-time on our High Performance charts. The example application then performs a Fourier Transform, creating a spectral / frequency analysis of the audio waveform and plots in the lower left chart. Finally, the histogram of the fourier transform, known as a spectrogram, is plotted in a SciChart iOS Heatmap control in the bottom right of the example.
If you are creating an app that needs to visualize scientific data from data-acquisition devices, audio spectra, or visualize radio frequency or spectral analysis choose SciChart to shortcut your development time & get to market faster with our well-tested, reliable iOS Chart library.
The Swift and Objective-C source code for the iOS and macOS Audio, Radio frequency & Spectrum Analyzer Example example is included below (Scroll down!).
Did you know that we have the source code for all our example available for free on Github?
Clone the SciChart.iOS.Examples from Github.
Also the SciChart iOS and Scichart macOS Trials contain the full source for the examples (link below).
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// AudioAnalyzerChartView.swift is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************
import Foundation
import RxSwift
class AudioAnalyzerChartView: SCDAudioAnalyzerLayoutViewController {
private let AUDIO_STREAM_BUFFER_SIZE: Int = 500000
private let MAX_FREQUENCY: Int = 10000
private let minDB: Double = -30
private let maxDB: Double = 70
private var bufferSize: Int {
return dataProvider.bufferSize
}
private var sampleRate: Int {
return dataProvider.sampleRate
}
private var hzPerDataPoint: Int {
return sampleRate / bufferSize
}
private var fftCount: Int {
return AUDIO_STREAM_BUFFER_SIZE / bufferSize
}
private var fftSize: Int {
return MAX_FREQUENCY / hzPerDataPoint
}
private var fftValuesCount: Int {
return fftSize * fftCount
}
private var dataProvider: IAudioAnalyzerDataProvider!
private var audioDS: SCIXyDataSeries!
private var fftDS: SCIXyDataSeries!
private var spectrogramDS: SCIUniformHeatmapDataSeries!
private var spectogramValues: SCDSpectogramItems!
private var disposable: Disposable?
deinit {
disposable?.dispose()
}
override func initExample() {
SCDPermissionRequestHelper.requestPermission { [weak self] granted in
if granted {
self?.dataProvider = DefaultAudioAnalyzerDataProvider()
} else {
self?.dataProvider = StubAudioAnalyzerDataProvider()
}
DispatchQueue.main.async { [weak self] in
self?.proceedWithInit()
}
}
}
private func proceedWithInit() {
audioDS = SCIXyDataSeries(xType: .long, yType: .int)
fftDS = SCIXyDataSeries(xType: .double, yType: .float)
spectrogramDS = SCIUniformHeatmapDataSeries(xType: .long, yType: .long, zType: .float, xSize: fftSize, ySize: fftCount)
initAudioStreamChart()
initFFTChart()
initSpectrogramChart()
disposable = dataProvider
.getData()
.do(onNext: { [weak self] audioData in
guard let self = self else { return }
self.audioDS.append(x: audioData.xData, y: audioData.yData)
let fftData = audioData.fftData
fftData.count = self.fftSize
self.fftDS.update(y: fftData, at: 0)
self.spectogramValues.replace(withNewItems: fftData)
self.spectrogramDS.update(z: self.spectogramValues.values)
})
.subscribe()
}
private func initAudioStreamChart() {
let xAxis = SCINumericAxis()
xAxis.autoRange = .always
xAxis.drawLabels = false
xAxis.drawMinorTicks = false
xAxis.drawMajorTicks = false
xAxis.drawMajorBands = false
xAxis.drawMinorGridLines = false
xAxis.drawMajorGridLines = false
let yAxis = SCINumericAxis()
yAxis.visibleRange = SCIDoubleRange(min: Double(Int32.min), max: Double(Int32.max))
yAxis.drawLabels = false
yAxis.drawMinorTicks = false
yAxis.drawMajorTicks = false
yAxis.drawMajorBands = false
yAxis.drawMinorGridLines = false
yAxis.drawMajorGridLines = false
audioDS.fifoCapacity = AUDIO_STREAM_BUFFER_SIZE
let rSeries = SCIFastLineRenderableSeries()
rSeries.dataSeries = audioDS
rSeries.strokeStyle = SCISolidPenStyle(color: .gray, thickness: 1.0)
SCIUpdateSuspender.usingWith(audioStreamChart) {
self.audioStreamChart.xAxes.add(items: xAxis)
self.audioStreamChart.yAxes.add(items: yAxis)
self.audioStreamChart.renderableSeries.add(items: rSeries)
}
}
private func initFFTChart() {
let xAxis = SCINumericAxis()
xAxis.drawMajorBands = false
xAxis.drawMinorGridLines = false
xAxis.maxAutoTicks = 5
xAxis.axisTitle = "Hz"
xAxis.axisTitlePlacement = .right
xAxis.axisTitleOrientation = .horizontal
let yAxis = SCINumericAxis()
yAxis.axisAlignment = .left
yAxis.visibleRange = SCIDoubleRange(min: minDB, max: maxDB)
yAxis.growBy = SCIDoubleRange(min: 0.1, max: 0.1)
yAxis.drawMinorTicks = false
yAxis.drawMinorGridLines = false
yAxis.drawMajorBands = false
yAxis.axisTitle = "dB"
yAxis.axisTitlePlacement = .top
yAxis.axisTitleOrientation = .horizontal
fftDS.fifoCapacity = fftSize
for i in 0 ..< fftSize {
fftDS.append(x: i * Int(hzPerDataPoint), y: Float.nan)
}
let rSeries = SCIFastColumnRenderableSeries()
rSeries.dataSeries = fftDS
rSeries.zeroLineY = minDB
rSeries.paletteProvider = FFTPaletteProvider()
SCIUpdateSuspender.usingWith(fftChart) {
self.fftChart.xAxes.add(items: xAxis)
self.fftChart.yAxes.add(items: yAxis)
self.fftChart.renderableSeries.add(rSeries)
}
}
private func initSpectrogramChart() {
spectogramValues = SCDSpectogramItems(capacity: fftValuesCount)
let xAxis = SCINumericAxis()
xAxis.autoRange = .always
xAxis.drawLabels = false
xAxis.drawMinorTicks = false
xAxis.drawMajorTicks = false
xAxis.drawMajorBands = false
xAxis.drawMinorGridLines = false
xAxis.drawMajorGridLines = false
xAxis.axisAlignment = .left
xAxis.flipCoordinates = true
let yAxis = SCINumericAxis()
yAxis.autoRange = .always
yAxis.drawLabels = false
yAxis.drawMinorTicks = false
yAxis.drawMajorTicks = false
yAxis.drawMajorBands = false
yAxis.drawMinorGridLines = false
yAxis.drawMajorGridLines = false
yAxis.axisAlignment = .bottom
let rSeries = SCIFastUniformHeatmapRenderableSeries()
rSeries.dataSeries = spectrogramDS
rSeries.minimum = minDB
rSeries.maximum = maxDB
rSeries.colorMap = SCIColorMap(colors: [.clear, SCIColor.fromARGBColorCode(0xFF00008B), .purple, .red, .yellow, .white], andStops: [0, 0.0001, 0.25, 0.50, 0.75, 1])
SCIUpdateSuspender.usingWith(spectrogramChart) {
self.spectrogramChart.xAxes.add(xAxis)
self.spectrogramChart.yAxes.add(yAxis)
self.spectrogramChart.renderableSeries.add(rSeries)
}
}
}
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// AudioRecorder.m is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************
#import "AudioRecorder.h"
#import <Accelerate/Accelerate.h>
#define AUDIO_DATA_TYPE_FORMAT int
@implementation AudioRecorder {
FFTSetup fftSetup;
unsigned int length;
float _max;
float _min;
}
void *refToSelf;
void AudioInputCallback(void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
unsigned int inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs) {
__weak AudioRecorder *rec = (__bridge AudioRecorder *)refToSelf;
RecordState *recordState = (RecordState *)inUserData;
if (!recordState->recording) return;
AudioQueueEnqueueBuffer(recordState->queue, inBuffer, 0, NULL);
int *samples = (AUDIO_DATA_TYPE_FORMAT *)inBuffer->mAudioData;
if (inNumberPacketDescriptions != 2048) return;
[rec formSamplesToEngine:inNumberPacketDescriptions samples:samples];
}
- (instancetype)init {
self = [super init];
if (self) {
refToSelf = (__bridge void *)(self);
_max = 0.0f;
_min = 0.0f;
}
return self;
}
- (void)startRecording:(int)sampleRate andMinBufferSize:(int)minBufferSize {
[self setupAudioFormat:&recordState.dataFormat withSampleRate:sampleRate];
recordState.currentPacket = 0;
OSStatus status = AudioQueueNewInput(&recordState.dataFormat, AudioInputCallback, &recordState, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &recordState.queue);
if (status == 0) {
for (int i = 0; i < NUM_BUFFERS; i++) {
AudioQueueAllocateBuffer(recordState.queue, minBufferSize * recordState.dataFormat.mBytesPerFrame, &recordState.buffers[i]);
AudioQueueEnqueueBuffer(recordState.queue, recordState.buffers[i], 0, nil);
}
}
recordState.recording = true;
status = AudioQueueStart(recordState.queue, NULL);
length = (unsigned int)floor(log2(minBufferSize));
fftSetup = vDSP_create_fftsetup(length, kFFTRadix2);
}
- (void)setupAudioFormat:(AudioStreamBasicDescription *)format withSampleRate:(int)sampleRate {
format->mSampleRate = sampleRate;
format->mFormatID = kAudioFormatLinearPCM;
format->mFormatFlags = kAudioFormatFlagIsSignedInteger;
format->mFramesPerPacket = 1;
format->mChannelsPerFrame = 1;
format->mBytesPerFrame = sizeof(AUDIO_DATA_TYPE_FORMAT);
format->mBytesPerPacket = sizeof(AUDIO_DATA_TYPE_FORMAT);
format->mBitsPerChannel = sizeof(AUDIO_DATA_TYPE_FORMAT) * 8;
}
- (void)stopRecording {
recordState.recording = false;
vDSP_destroy_fftsetup(fftSetup);
AudioQueueStop(recordState.queue, true);
for (int i = 0; i < NUM_BUFFERS; i++) {
AudioQueueFreeBuffer(recordState.queue, recordState.buffers[i]);
}
AudioQueueDispose(recordState.queue, true);
AudioFileClose(recordState.audioFile);
}
- (void)formSamplesToEngine:(int)capacity samples: (int*)samples {
self.samples = samples;
self.fftData = [self calculateFFT:samples size:capacity];
}
- (float *)calculateFFT:(int *)data size:(unsigned int)numSamples {
float *dataFloat = malloc(sizeof(float) * numSamples);
vDSP_vflt32(data, 1, dataFloat, 1, numSamples);
DSPSplitComplex tempSplitComplex;
tempSplitComplex.imagp = malloc(sizeof(float) * numSamples);
tempSplitComplex.realp = malloc(sizeof(float) * numSamples);
DSPComplex *audioBufferComplex = malloc(sizeof(DSPComplex) * numSamples);
for (unsigned int i = 0; i < numSamples; i++) {
audioBufferComplex[i].real = dataFloat[i];
audioBufferComplex[i].imag = 0.0f;
}
vDSP_ctoz(audioBufferComplex, 2, &tempSplitComplex, 1, numSamples);
vDSP_fft_zip(fftSetup, &tempSplitComplex, 1, length, FFT_FORWARD);
float *result = malloc(sizeof(float) * numSamples);
for (unsigned int i = 0 ; i < numSamples; i++) {
float current = sqrt(tempSplitComplex.realp[i] * tempSplitComplex.realp[i] + tempSplitComplex.imagp[i] * tempSplitComplex.imagp[i]) * 0.000025;
current = log10(current) * 10;
result[i] = current;
}
free(dataFloat);
free(audioBufferComplex);
free(tempSplitComplex.imagp);
free(tempSplitComplex.realp);
return result;
}
@end
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// DefaultAudioAnalyzerDataProvider.swift is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************
class DefaultAudioAnalyzerDataProvider: DataProviderBase<AudioData>, IAudioAnalyzerDataProvider {
let bufferSize: Int
let sampleRate: Int
private var time: Int64 = 0
private lazy var audioData: AudioData = {
return AudioData(pointsCount: bufferSize)
}()
private let audioRecorder: AudioRecorder = AudioRecorder()
var samplesValues: samplesToEngine!
var fftValues: samplesToEngineFloat!
convenience init() {
self.init(sampleRate: 44100, minBufferSize: 2048)
}
init(sampleRate: Int, minBufferSize: Int) {
self.sampleRate = sampleRate
self.bufferSize = minBufferSize
super.init(dispatchTimeInterval: .milliseconds(sampleRate / minBufferSize))
}
override func onStart() {
audioRecorder.startRecording(Int32(sampleRate), andMinBufferSize: Int32(bufferSize))
}
override func onStop() {
audioRecorder.stopRecording()
}
override func onNext() -> AudioData {
audioData.xData.clear()
audioData.yData.clear()
audioData.fftData.clear()
if let samples = audioRecorder.samples {
let sequence = time ..< time + Int64(bufferSize)
audioData.xData.add(sequence)
audioData.yData.addValues(samples, count: bufferSize)
time += Int64(bufferSize)
}
audioData.yData.withUnsafeMutablePointer { [weak self] pointer in
let fftData: UnsafeMutablePointer<Float>! = self?.audioRecorder.calculateFFT(pointer, size: UInt32(bufferSize))
audioData.fftData.addValues(fftData, count: bufferSize)
}
return audioData
}
}
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// FFTPaletteProvider.swift is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************
import Foundation
class FFTPaletteProvider: SCIPaletteProviderBase<SCIFastColumnRenderableSeries>, ISCIFillPaletteProvider, ISCIStrokePaletteProvider {
private struct Colors {
static let minColor: UInt32 = 0xFF008000
static let maxColor: UInt32 = 0xFFFF0000
// RGB chanel values for min color
static let minColorRed = SCIColor.red(minColor)
static let minColorGreen = SCIColor.green(minColor)
static let minColorBlue = SCIColor.blue(minColor)
// RGB chanel values for max color
static let maxColorRed = SCIColor.red(maxColor)
static let maxColorGreen = SCIColor.green(maxColor)
static let maxColorBlue = SCIColor.blue(maxColor)
static let diffRed = Int(maxColorRed) - Int(minColorRed)
static let diffGreen = Int(maxColorGreen) - Int(minColorGreen)
static let diffBlue = Int(maxColorBlue) - Int(minColorBlue)
}
private let colors = SCIUnsignedIntegerValues()
var fillColors: SCIUnsignedIntegerValues! { return colors }
var strokeColors: SCIUnsignedIntegerValues! { return colors }
init() {
super.init(renderableSeriesType: SCIFastColumnRenderableSeries.self)
}
override func update() {
let xyRenderPassData = renderableSeries!.currentRenderPassData as! SCIXyRenderPassData
let yCalc = xyRenderPassData.yCoordinateCalculator!
let min: Double = 0
let max: Double = yCalc.maxAsDouble
let diff = max - min
let yValues = xyRenderPassData.yValues
let size = xyRenderPassData.pointsCount
colors.count = size
for i in 0 ..< size {
let yValue = yValues!.getValueAt(i)
let fraction = (yValue - min) / diff
let red = lerp(Colors.minColorRed, Colors.diffRed, fraction)
let green = lerp(Colors.minColorGreen, Colors.diffGreen, fraction)
let blue = lerp(Colors.minColorBlue, Colors.diffBlue, fraction)
let color = SCIColor(red: red, green: green, blue: blue, alpha: 1)
colors.set(color.colorARGBCode(), at: i)
}
}
private func lerp(_ minColor: UInt8, _ diffColor: Int, _ fraction: Double) -> CGFloat {
let interpolatedValue = Double(minColor) + fraction * Double(diffColor)
return interpolatedValue < 0 ? 0 : interpolatedValue > 255 ? 1 : CGFloat(interpolatedValue / 255)
}
}
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// StubAudioAnalyzerDataProvider.swift is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
//******************************************************************************
import Foundation
class StubAudioAnalyzerDataProvider: DataProviderBase<AudioData>, IAudioAnalyzerDataProvider {
let bufferSize: Int = 2048
let sampleRate: Int = 44100
private var time: Int64 = 0
init() {
super.init(dispatchTimeInterval: .milliseconds(20))
}
private let audioRecorder: AudioRecorder = AudioRecorder()
private lazy var audioData: AudioData = {
return AudioData(pointsCount: bufferSize)
}()
private lazy var provider: IYValuesProvider = {
let providers: [IYValuesProvider] = [
FrequencySinewaveYValueProvider(amplitude: 8000, phase: 0, minFrequency: 0, maxFrequency: 1, step: 0.0000005),
NoisySinewaveYValueProvider(amplitude: 8000, phase: 0, frequency: 0.000032, noiseAmplitude: 200),
NoisySinewaveYValueProvider(amplitude: 6000, phase: 0, frequency: 0.000016, noiseAmplitude: 100),
NoisySinewaveYValueProvider(amplitude: 4000, phase: 0, frequency: 0.000064, noiseAmplitude: 100)
]
return AggregateYValueProvider(providers: providers)
}()
override func onNext() -> AudioData {
audioData.xData.clear()
audioData.yData.clear()
audioData.fftData.clear()
for _ in 0 ..< bufferSize {
audioData.xData.add(Int64(time))
audioData.yData.add(provider.getYValueForIndex(Int(time)))
time += 1
}
audioData.yData.withUnsafeMutablePointer { [weak self] pointer in
let fftData: UnsafeMutablePointer<Float>! = self?.audioRecorder.calculateFFT(pointer, size: UInt32(bufferSize))
audioData.fftData.addValues(fftData, count: bufferSize)
}
return audioData
}
}
protocol IYValuesProvider {
func getYValueForIndex(_ index: Int) -> Int32
}
class AggregateYValueProvider: IYValuesProvider {
let providers: [IYValuesProvider]
init(providers: [IYValuesProvider]) {
self.providers = providers
}
func getYValueForIndex(_ index: Int) -> Int32 {
var sum: Int32 = 0
for provider in providers {
sum += provider.getYValueForIndex(index) * 30000
}
return sum
}
}
class FrequencySinewaveYValueProvider: IYValuesProvider {
private let amplitude: Double
private let phase: Double
private let minFrequency: Double
private let maxFrequency: Double
private let step: Double
private var frequency: Double
init(amplitude: Double, phase: Double, minFrequency: Double, maxFrequency: Double, step: Double) {
self.amplitude = amplitude
self.phase = phase
self.minFrequency = minFrequency
self.maxFrequency = maxFrequency
self.step = step
self.frequency = minFrequency
}
func getYValueForIndex(_ index: Int) -> Int32 {
frequency = frequency <= maxFrequency ? frequency + step : minFrequency
let wn = 2 * Double.pi * frequency
return Int32(amplitude * sin(Double(index) * wn + phase))
}
}
class NoisySinewaveYValueProvider: IYValuesProvider {
private let amplitude: Double
private let phase: Double
private let noiseAmplitude: Double
private let wn: Double
init(amplitude: Double, phase: Double, frequency: Double, noiseAmplitude: Double) {
self.amplitude = amplitude
self.phase = phase
self.noiseAmplitude = noiseAmplitude
self.wn = 2 * Double.pi * frequency
}
func getYValueForIndex(_ index: Int) -> Int32 {
return Int32(amplitude * sin(Double(index) * wn + phase) + (Double.random(in: 0 ..< 1) - 0.5) * noiseAmplitude)
}
}