SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, iOS Chart, Android Chart and JavaScript Chart Components
SciChart Android ships with ~90Â Android Chart Examples which you can browse, play with, view the source-code and even export each SciChart Android Chart Example to a stand-alone Android Studio project. All of this is possible with the new and improved SciChart Android Examples Suite, which ships as part of our Android Charts SDK.
The Audio analyzer demo showcases how to use SciChart Android 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 Android device or embedded systems and create a waveform of the sound recorded in the top chart. This chart has 500,000 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 Android 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 Android Chart library.
The full source code for the Android Audio, Radio frequency and Spectrum Analyzer example is included below (Scroll down!).
Did you know you can also view the source code from one of the following sources as well?
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2021. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// AudioAnalyzerShowcaseFragment.kt is part of SCICHART®, High Performance Scientific Charts
// For full terms and conditions of the license, see http://www.scichart.com/scichart-eula/
//
// This source code is protected by international copyright law. Unauthorized
// reproduction, reverse-engineering, or distribution of all or any portion of
// this source code is strictly prohibited.
//
// This source code contains confidential and proprietary trade secrets of
// SciChart Ltd., and should at no time be copied, transferred, sold,
// distributed or made available without express written permission.
//******************************************************************************
package com.scichart.examples.fragments.featuredApps.scientificCharts.kt.audioAnalyzer
import android.util.Log
import android.view.LayoutInflater
import com.scichart.charting.visuals.SciChartSurface
import com.scichart.charting.visuals.axes.AutoRange.Always
import com.scichart.charting.visuals.axes.AxisAlignment.Bottom
import com.scichart.charting.visuals.axes.AxisAlignment.Left
import com.scichart.charting.visuals.axes.AxisTitleOrientation.Horizontal
import com.scichart.charting.visuals.axes.AxisTitlePlacement.Right
import com.scichart.charting.visuals.axes.AxisTitlePlacement.Top
import com.scichart.charting.visuals.renderableSeries.ColorMap
import com.scichart.charting.visuals.renderableSeries.FastColumnRenderableSeries
import com.scichart.charting.visuals.renderableSeries.data.XyRenderPassData
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IFillPaletteProvider
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IStrokePaletteProvider
import com.scichart.charting.visuals.renderableSeries.paletteProviders.PaletteProviderBase
import com.scichart.core.model.DoubleValues
import com.scichart.core.model.IntegerValues
import com.scichart.core.utility.NumberUtil
import com.scichart.data.model.DoubleRange
import com.scichart.drawing.utility.ColorUtil.*
import com.scichart.examples.data.Radix2FFT
import com.scichart.examples.databinding.ExampleAudioAnalyzerFragmentBinding
import com.scichart.examples.fragments.base.ShowcaseExampleBaseFragment
import com.scichart.examples.fragments.featuredApps.scientificCharts.audioAnalyzer.AudioData
import com.scichart.examples.fragments.featuredApps.scientificCharts.audioAnalyzer.DefaultAudioAnalyzerDataProvider
import com.scichart.examples.fragments.featuredApps.scientificCharts.audioAnalyzer.IAudioAnalyzerDataProvider
import com.scichart.examples.fragments.featuredApps.scientificCharts.audioAnalyzer.StubAudioAnalyzerDataProvider
import com.scichart.examples.utils.scichartExtensions.*
class AudioAnalyzerShowcaseFragment : ShowcaseExampleBaseFragment<ExampleAudioAnalyzerFragmentBinding>() {
private val fftData = DoubleValues()
private val spectrogramValues = DoubleValues(fftValuesCount)
private val audioDS = XyDataSeries<Long, Short>().apply { fifoCapacity = AUDIO_STREAM_BUFFER_SIZE }
private val fftDS = XyDataSeries<Double, Double>().apply { fifoCapacity = fftSize }
private val spectrogramDS = UniformHeatmapDataSeries<Long, Long, Double>(fftSize, fftCount)
override fun inflateBinding(inflater: LayoutInflater): ExampleAudioAnalyzerFragmentBinding {
return ExampleAudioAnalyzerFragmentBinding.inflate(inflater)
}
override fun initExample(binding: ExampleAudioAnalyzerFragmentBinding) {
initAudioStreamChart(binding.audioStreamChart)
initFFTChart(binding.fftChart)
initSpectrogramChart(binding.spectrogramChart)
dataProvider.data.doOnNext { audioData: AudioData ->
audioDS.append(audioData.xData, audioData.yData)
fft.run(audioData.yData, fftData)
fftData.setSize(fftSize)
fftDS.updateRangeYAt(0, fftData)
val spectrogramItems: DoubleArray = spectrogramValues.itemsArray
val fftItems: DoubleArray = fftData.itemsArray
System.arraycopy(spectrogramItems, fftSize, spectrogramItems, 0, fftOffsetValueCount)
System.arraycopy(fftItems, 0, spectrogramItems, fftOffsetValueCount, fftSize)
spectrogramDS.updateZValues(spectrogramValues)
}.compose(bindToLifecycle()).subscribe()
}
private fun initAudioStreamChart(surface: SciChartSurface) {
surface.suspendUpdates {
xAxes { numericAxis {
autoRange = Always
drawLabels = false
drawMajorBands = false
drawMinorTicks = false
drawMajorTicks = false
drawMinorGridLines = false
drawMajorGridLines = false
}}
yAxes { numericAxis {
visibleRange = DoubleRange(Short.MIN_VALUE.toDouble(), Short.MAX_VALUE.toDouble())
drawLabels = false
drawMajorBands = false
drawMinorTicks = false
drawMajorTicks = false
drawMinorGridLines = false
drawMajorGridLines = false
}}
renderableSeries {
fastLineRenderableSeries {
dataSeries = audioDS
strokeStyle = SolidPenStyle(Grey)
}
}
}
}
private fun initFFTChart(surface: SciChartSurface) {
surface.suspendUpdates {
xAxes { numericAxis {
drawMajorBands = false
maxAutoTicks = 5
axisTitle = "Hz"
axisTitlePlacement = Right
axisTitleOrientation = Horizontal
}}
yAxes { numericAxis {
axisAlignment = Left
visibleRange = DoubleRange(-30.0, 70.0)
growBy = DoubleRange(0.1, 0.1)
drawMinorTicks = false
drawMinorGridLines = false
drawMajorBands = false
axisTitle = "dB"
axisTitlePlacement = Top
axisTitleOrientation = Horizontal
}}
renderableSeries {
fastColumnRenderableSeries {
dataSeries = fftDS.apply {
for (i in 0 until fftSize) {
append(i * hzPerDataPoint, 0.0)
}
}
paletteProvider = FFTPaletteProvider()
zeroLineY = -30.0
}
}
}
}
private fun initSpectrogramChart(surface: SciChartSurface) {
spectrogramValues.setSize(fftOffsetValueCount)
surface.suspendUpdates {
xAxes { numericAxis {
autoRange = Always
drawLabels = false
drawMajorBands = false
drawMinorTicks = false
drawMajorTicks = false
drawMinorGridLines = false
drawMajorGridLines = false
axisAlignment = Left
flipCoordinates = true
}}
yAxes { numericAxis {
autoRange = Always
drawLabels = false
drawMajorBands = false
drawMinorTicks = false
drawMajorTicks = false
drawMinorGridLines = false
drawMajorGridLines = false
axisAlignment = Bottom
flipCoordinates = true
}}
renderableSeries {
fastUniformHeatmapRenderableSeries {
dataSeries = spectrogramDS
minimum = -30.0
maximum = 70.0
colorMap = ColorMap(
intArrayOf(Transparent, DarkBlue, Purple, Red, Yellow, White),
floatArrayOf(0f, 0.0001f, 0.25f, 0.50f, 0.75f, 1f)
)
}
}
}
}
private class FFTPaletteProvider : PaletteProviderBase<FastColumnRenderableSeries>(FastColumnRenderableSeries::class.java), IFillPaletteProvider, IStrokePaletteProvider {
private val colors = IntegerValues()
private val minColor = Green
private val maxColor = Red
// RGB channel values for min color
private val minColorRed = red(minColor)
private val minColorGreen = green(minColor)
private val minColorBlue = blue(minColor)
// RGB channel values for max color
private val maxColorRed = red(maxColor)
private val maxColorGreen = green(maxColor)
private val maxColorBlue = blue(maxColor)
private val diffRed = maxColorRed - minColorRed
private val diffGreen = maxColorGreen - minColorGreen
private val diffBlue = maxColorBlue - minColorBlue
override fun getFillColors(): IntegerValues = colors
override fun getStrokeColors(): IntegerValues = colors
override fun update() {
val currentRenderPassData = renderableSeries!!.currentRenderPassData
val xyRenderPassData = currentRenderPassData as XyRenderPassData
val yCalc = xyRenderPassData.yCoordinateCalculator
val min = yCalc.minAsDouble
val max = yCalc.maxAsDouble
val diff = max - min
val yValues = xyRenderPassData.yValues
val size = xyRenderPassData.pointsCount()
colors.setSize(size)
val yItems = yValues.itemsArray
val colorItems = colors.itemsArray
for (i in 0 until size) {
val yValue = yItems[i]
val fraction = (yValue - min) / diff
val red = lerp(minColorRed, diffRed, fraction)
val green = lerp(minColorGreen, diffGreen, fraction)
val blue = lerp(minColorBlue, diffBlue, fraction)
colorItems[i] = rgb(red, green, blue)
}
}
companion object {
private fun lerp(minColor: Int, diffColor: Int, fraction: Double): Int {
val intepolatedValue = minColor + fraction * diffColor
return NumberUtil.constrain(intepolatedValue, 0.0, 255.0).toInt()
}
}
}
companion object {
private const val AUDIO_STREAM_BUFFER_SIZE = 500000
private const val MAX_FREQUENCY = 10000
private val dataProvider = createDateProvider()
private val bufferSize = dataProvider.bufferSize
private val sampleRate = dataProvider.sampleRate
private val hzPerDataPoint = sampleRate.toDouble() / bufferSize
private val fftSize = (MAX_FREQUENCY / hzPerDataPoint).toInt()
private val fftCount = AUDIO_STREAM_BUFFER_SIZE / bufferSize
private val fftValuesCount = fftSize * fftCount
private val fftOffsetValueCount = fftValuesCount - fftSize
private val fft = Radix2FFT(bufferSize)
private fun createDateProvider(): IAudioAnalyzerDataProvider {
return try {
DefaultAudioAnalyzerDataProvider()
} catch (ex: Exception) {
Log.d("AudioAnalyzer", "Initialization of DefaultAudioAnalyzerDataProvider failed. Using stub implementation instead", ex)
StubAudioAnalyzerDataProvider()
}
}
}
}
//******************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2020. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// AudioAnalyzerShowcaseFragment.java 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.
//******************************************************************************
package com.scichart.examples.fragments.featuredApps.scientificCharts.audioAnalyzer;
import android.util.Log;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import com.scichart.charting.model.dataSeries.UniformHeatmapDataSeries;
import com.scichart.charting.model.dataSeries.XyDataSeries;
import com.scichart.charting.numerics.coordinateCalculators.ICoordinateCalculator;
import com.scichart.charting.visuals.SciChartSurface;
import com.scichart.charting.visuals.axes.AutoRange;
import com.scichart.charting.visuals.axes.AxisAlignment;
import com.scichart.charting.visuals.axes.AxisTitleOrientation;
import com.scichart.charting.visuals.axes.AxisTitlePlacement;
import com.scichart.charting.visuals.axes.NumericAxis;
import com.scichart.charting.visuals.renderableSeries.ColorMap;
import com.scichart.charting.visuals.renderableSeries.FastColumnRenderableSeries;
import com.scichart.charting.visuals.renderableSeries.FastLineRenderableSeries;
import com.scichart.charting.visuals.renderableSeries.FastUniformHeatmapRenderableSeries;
import com.scichart.charting.visuals.renderableSeries.data.ISeriesRenderPassData;
import com.scichart.charting.visuals.renderableSeries.data.XyRenderPassData;
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IFillPaletteProvider;
import com.scichart.charting.visuals.renderableSeries.paletteProviders.IStrokePaletteProvider;
import com.scichart.charting.visuals.renderableSeries.paletteProviders.PaletteProviderBase;
import com.scichart.core.framework.UpdateSuspender;
import com.scichart.core.model.DoubleValues;
import com.scichart.core.model.IntegerValues;
import com.scichart.core.utility.NumberUtil;
import com.scichart.examples.data.Radix2FFT;
import com.scichart.examples.databinding.ExampleAudioAnalyzerFragmentBinding;
import com.scichart.examples.fragments.base.ShowcaseExampleBaseFragment;
import static com.scichart.drawing.utility.ColorUtil.*;
public class AudioAnalyzerShowcaseFragment extends ShowcaseExampleBaseFragment<ExampleAudioAnalyzerFragmentBinding> {
private static final int AUDIO_STREAM_BUFFER_SIZE = 500000;
private static final int MAX_FREQUENCY = 10000;
private final IAudioAnalyzerDataProvider dataProvider = createDateProvider();
private final int bufferSize = dataProvider.getBufferSize();
private final int sampleRate = dataProvider.getSampleRate();
private final Radix2FFT fft = new Radix2FFT(bufferSize);
private final double hzPerDataPoint = (double)sampleRate / bufferSize;
private final int fftSize = (int)(MAX_FREQUENCY / hzPerDataPoint);
private final int fftCount = AUDIO_STREAM_BUFFER_SIZE / bufferSize;
private final int fftValuesCount = fftSize * fftCount;
private final int fftOffsetValueCount = fftValuesCount - fftSize;
private final DoubleValues fftData = new DoubleValues();
private final DoubleValues spectrogramValues = new DoubleValues(fftValuesCount);
private final XyDataSeries<Long, Short> audioDS = new XyDataSeries<>(Long.class, Short.class);
private final XyDataSeries<Double, Double> fftDS = new XyDataSeries<>(Double.class, Double.class);
private final UniformHeatmapDataSeries<Long, Long, Double> spectrogramDS = new UniformHeatmapDataSeries<>(Long.class, Long.class, Double.class, fftSize, fftCount);
@NonNull
@Override
protected ExampleAudioAnalyzerFragmentBinding inflateBinding(@NonNull LayoutInflater inflater) {
return ExampleAudioAnalyzerFragmentBinding.inflate(inflater);
}
@Override
protected void initExample(@NonNull ExampleAudioAnalyzerFragmentBinding binding) {
initAudioStreamChart(binding.audioStreamChart);
initFFTChart(binding.fftChart);
initSpectrogramChart(binding.spectrogramChart);
dataProvider.getData().doOnNext(audioData -> {
audioDS.append(audioData.xData, audioData.yData);
fft.run(audioData.yData, fftData);
fftData.setSize(fftSize);
fftDS.updateRangeYAt(0, fftData);
final double[] spectrogramItems = spectrogramValues.getItemsArray();
final double[] fftItems = fftData.getItemsArray();
System.arraycopy(spectrogramItems, fftSize, spectrogramItems, 0, fftOffsetValueCount);
System.arraycopy(fftItems, 0, spectrogramItems, fftOffsetValueCount, fftSize);
spectrogramDS.updateZValues(spectrogramValues);
}).compose(bindToLifecycle()).subscribe();
}
private void initAudioStreamChart(SciChartSurface surface) {
final NumericAxis xAxis = sciChartBuilder.newNumericAxis()
.withAutoRangeMode(AutoRange.Always)
.withDrawLabels(false)
.withDrawMinorTicks(false)
.withDrawMajorTicks(false)
.withDrawMajorBands(false)
.withDrawMinorGridLines(false)
.withDrawMajorGridLines(false)
.build();
final NumericAxis yAxis = sciChartBuilder.newNumericAxis()
.withVisibleRange(Short.MIN_VALUE, Short.MAX_VALUE)
.withDrawLabels(false)
.withDrawMinorTicks(false)
.withDrawMajorTicks(false)
.withDrawMajorBands(false)
.withDrawMinorGridLines(false)
.withDrawMajorGridLines(false)
.build();
audioDS.setFifoCapacity(AUDIO_STREAM_BUFFER_SIZE);
final FastLineRenderableSeries rs = sciChartBuilder.newLineSeries().withDataSeries(audioDS)
.withStrokeStyle(Grey, 1f)
.build();
UpdateSuspender.using(surface, () -> {
surface.getXAxes().add(xAxis);
surface.getYAxes().add(yAxis);
surface.getRenderableSeries().add(rs);
});
}
private void initFFTChart(SciChartSurface surface) {
final NumericAxis xAxis = sciChartBuilder.newNumericAxis()
.withDrawMajorBands(false)
.withMaxAutoTicks(5)
.withAxisTitle("Hz")
.withAxisTitlePlacement(AxisTitlePlacement.Right)
.withAxisTitleOrientation(AxisTitleOrientation.Horizontal)
.build();
final NumericAxis yAxis = sciChartBuilder.newNumericAxis()
.withAxisAlignment(AxisAlignment.Left)
.withVisibleRange(-30, 70)
.withGrowBy(0.1, 0.1)
.withDrawMinorTicks(false)
.withDrawMinorGridLines(false)
.withDrawMajorBands(false)
.withAxisTitle("dB")
.withAxisTitlePlacement(AxisTitlePlacement.Top)
.withAxisTitleOrientation(AxisTitleOrientation.Horizontal)
.build();
fftDS.setFifoCapacity(fftSize);
for (int i = 0; i < fftSize; i++) {
fftDS.append(i * hzPerDataPoint, 0d);
}
final FastColumnRenderableSeries rs = sciChartBuilder.newColumnSeries()
.withDataSeries(fftDS)
.withPaletteProvider(new FFTPaletteProvider())
.withZeroLine(-30) // set zero line equal to VisibleRange.Min
.build();
UpdateSuspender.using(surface, () -> {
surface.getXAxes().add(xAxis);
surface.getYAxes().add(yAxis);
surface.getRenderableSeries().add(rs);
});
}
private void initSpectrogramChart(SciChartSurface surface) {
spectrogramValues.setSize(fftValuesCount);
final NumericAxis xAxis = sciChartBuilder.newNumericAxis()
.withAutoRangeMode(AutoRange.Always)
.withDrawLabels(false)
.withDrawMinorTicks(false)
.withDrawMajorTicks(false)
.withDrawMajorBands(false)
.withDrawMinorGridLines(false)
.withDrawMajorGridLines(false)
.withAxisAlignment(AxisAlignment.Left)
.withFlipCoordinates(true)
.build();
final NumericAxis yAxis = sciChartBuilder.newNumericAxis()
.withAutoRangeMode(AutoRange.Always)
.withDrawLabels(false)
.withDrawMinorTicks(false)
.withDrawMajorTicks(false)
.withDrawMajorBands(false)
.withDrawMinorGridLines(false)
.withDrawMajorGridLines(false)
.withAxisAlignment(AxisAlignment.Bottom)
.withFlipCoordinates(true)
.build();
final FastUniformHeatmapRenderableSeries rs = sciChartBuilder.newUniformHeatmap()
.withDataSeries(spectrogramDS)
.withMinimum(-30)
.withMaximum(70)
.withColorMap(new ColorMap(
new int[]{Transparent, DarkBlue, Purple, Red, Yellow, White},
new float[]{0f, 0.0001f, 0.25f, 0.50f, 0.75f, 1f}
)).build();
UpdateSuspender.using(surface, () -> {
surface.getXAxes().add(xAxis);
surface.getYAxes().add(yAxis);
surface.getRenderableSeries().add(rs);
});
}
private static IAudioAnalyzerDataProvider createDateProvider(){
try {
return new DefaultAudioAnalyzerDataProvider();
} catch (Exception ex) {
Log.d("AudioAnalyzer", "Initialization of DefaultAudioAnalyzerDataProvider failed. Using stub implementation instead", ex);
return new StubAudioAnalyzerDataProvider();
}
}
private static class FFTPaletteProvider extends PaletteProviderBase<FastColumnRenderableSeries> implements IFillPaletteProvider, IStrokePaletteProvider {
private final IntegerValues colors = new IntegerValues();
private final int minColor = Green;
private final int maxColor = Red;
// RGB channel values for min color
private final int minColorRed = red(minColor);
private final int minColorGreen = green(minColor);
private final int minColorBlue = blue(minColor);
// RGB channel values for max color
private final int maxColorRed = red(maxColor);
private final int maxColorGreen = green(maxColor);
private final int maxColorBlue = blue(maxColor);
private final int diffRed = maxColorRed - minColorRed;
private final int diffGreen = maxColorGreen - minColorGreen;
private final int diffBlue = maxColorBlue - minColorBlue;
FFTPaletteProvider() {
super(FastColumnRenderableSeries.class);
}
@Override
public IntegerValues getFillColors() {
return colors;
}
@Override
public IntegerValues getStrokeColors() {
return colors;
}
@Override
public void update() {
final ISeriesRenderPassData currentRenderPassData = renderableSeries.getCurrentRenderPassData();
final XyRenderPassData xyRenderPassData = (XyRenderPassData) currentRenderPassData;
final ICoordinateCalculator yCalc = xyRenderPassData.getYCoordinateCalculator();
final double min = yCalc.getMinAsDouble();
final double max = yCalc.getMaxAsDouble();
final double diff = max - min;
final DoubleValues yValues = xyRenderPassData.yValues;
final int size = xyRenderPassData.pointsCount();
colors.setSize(size);
final double[] yItems = yValues.getItemsArray();
final int[] colorItems = colors.getItemsArray();
for (int i = 0; i < size; i++) {
final double yValue = yItems[i];
final double fraction = (yValue - min) / diff;
final int red = lerp(minColorRed, diffRed, fraction);
final int green = lerp(minColorGreen, diffGreen, fraction);
final int blue = lerp(minColorBlue, diffBlue, fraction);
colorItems[i] = rgb(red, green, blue);
}
}
private static int lerp(int minColor, int diffColor, double fraction) {
final double intepolatedValue = minColor + fraction * diffColor;
return (int) NumberUtil.constrain(intepolatedValue, 0, 255);
}
}
}