How to use and implement Save Wave Configs feature in Pocket Science Lab Wave Generator

What is a Wave Generator?

A Wave Generator is one of the most important features of PSLab. It is used to generate different kinds of waves like, sine, triangular, square, PWM. Wave generator UI is as under:

  (Figure 1 : Wave Generator Analog Mode UI)
  (Figure 2 : Wave Generator Digital Mode UI)

As can be seen the Screenshot above user is provided with options to set Frequency, Phase, Duty of different waves and once configurations are set user can either output the waves in Oscilloscope or can compare different waves in Logic Analyzer.

What is Save Wave Configs Feature?


        (Figure 3 : Wave Generator Control Buttons (View,Save,Mode))

In this feature, the user is given a ”Save” button to use this feature. 

The reason to add this feature is that, sometimes we need to perform the same experiment multiple times, is such scenarios if we have to set wave configurations everytime, it will become boring and there will be chances of errors. Hence using the save configs feature, user can currently set configurations in the Local Storage and can use it anytime later. 

Further since the Wave Configurations are saved on Local Storage as .CSV file, a user can save configs and can share the file with others so others can as well set their device to same configurations. The saved Wave Configurations can be seen in the DataLogger Activity and opening a saved log would take the user to Wave Generator Activity where all the configs will be set as per the saved log.

A sample CSV of the log data can be seen below.


(Figure 4: Wave Configs CSV file)

How is Save Configs Feature Implemented

The implementation of this feature is quite simple. There is a class named WaveData.  With the parameters of Mode(Square or PWM), Wave name, Shape, Freq, Phase and Duty. Whenever the user clicks the save configs button, the saveWaveConfigs()  function is called. This function fetches set values of different fields and creates realm objects and also write them to csv file as shown above. Once the realm objects are created, this log can be seen in the Data Logger Activity. The code to generate the realm object for the wave configs (that is the implementation of the function saveWaveConfig()) is given below.

public void saveWaveConfig(View view) {
        long block = System.currentTimeMillis();
        csvLogger.prepareLogFile();
          csvLogger.writeMetaData(getResources().getString(R.string.wave_generator));
        long timestamp;
        double lat, lon;
        String data = "Timestamp,DateTime,Mode,Wave,Shape,Freq,Phase,Duty,lat,lon\n";
        recordSensorDataBlockID(new SensorDataBlock(block, getResources().getString(R.string.wave_generator)));

So till now in the function, we create a header string for the data to be stored in the csv file. We create a block from the current system time. This block will be used to save all the realm object for this function, so all the objects created at this instance will be grouped as a single log entry in DataLoggerActivity.

double freq1 = (double) (WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.FREQUENCY));
double freq2 = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.FREQUENCY);
double phase = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.PHASE);

String waveType1 = WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";
String waveType2 = WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";

timestamp = System.currentTimeMillis();
String timeData = timestamp + "," + CSVLogger.FILE_NAME_FORMAT.format(new Date(timestamp));
String locationData = lat + "," + lon;

Next, in the function we get currently set Frequency for both analog waves and phase in the variables. We also store the selected wave shape for each of the waves. Since each entry in the csv file is required to have a timestamp and a location stamp,here we create common stamps of both types and will append it to each entry further in the function. 

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Here we check whether the currently selected mode is Analog(Square) or Digital (PWM). Above code snippet is for the SQUARE mode block. We create WaveGeneratorData object for both SI1 and SI2 waves based on the parameters we stored earlier. We also append the data to a string, data.  Which we will later use to write the log into a csv file.

else {
   double freqSqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.FREQUENCY);
   double dutySqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.DUTY) / 100;
   double dutySqr2 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.DUTY)) / 100;
   double phaseSqr2 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.PHASE) / 360;
   double dutySqr3 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.DUTY)) / 100;
   double phaseSqr3 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.PHASE) / 360;
   double dutySqr4 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.DUTY)) / 100;
   double phaseSqr4 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.PHASE) / 360;

 data += timeData + ",PWM,Sq1,PWM," + String.valueOf(freqSqr1) + ",0," + String.valueOf(dutySqr1) + "," + locationData + "\n";

 recordSensorData(new WaveGeneratorData(timestamp, block, "PWM", "Sq1", "PWM", String.valueOf(freqSqr1), "0", String.valueOf(dutySqr1), lat, lon));
}

The above code snippet shows a block of the condition when the selected mode is PWM. Here we store the set values of Freq, Phase and Duty for each SQ1, SQ2, SQ3 and SQ4 waves into variables. Once we store the values we create WaveGeneratorData objects for each of the waves and also append the data to the data string to write to the csv. The code above includes details only for SQ1, but exact same procedure is followed for SQ2, SQ3, and SQ4. One we have all the data appended to the string we call the following function to write the data to csv file. 

 csvLogger.writeCSVFile(data);

We can see that this function basically stores the current set values of different params into a WaveData object. For each of the waveforms in selected mode (analog/digital), a new instance of WaveData object is created and stored into realm.

When the user opens one of the logs, setReceivedData() function is called in WaveGeneratorActivity. This function iterates on the received realm objects and based on the attributes of each object the data is set in the UI automatically. The implementation of this function is given below, 

public void setReceivedData() {
        for (WaveGeneratorData data : recordedWaveData) {
            Log.d("data", data.toString());
            if (data.getMode().equals(MODE_SQUARE)) {
                WaveGeneratorCommon.mode_selected = WaveConst.SQUARE;
                switch (data.getWave()) {
                    case "Wave1":
                        if (data.getShape().equals("sine")) {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, SIN);
                        } else {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, TRIANGULAR);
                        }
                        WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        break;
                }
                enableInitialState();
            } 

This function iterates over the received WaveGeneratorData objects. For each object we check what is the mode of the waveData. The above code snippet is used when the mode is SQUARE. We get the waveType from the object, and since for SQUARE mode there are only 2 types : Wave1 and Wave2, we set the attributes for each wave as we get them from the objects using WaveGeneratorCommon

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Same as before if the mode of the object is PWM, there will be 4 cases : SQ1, SQ2, SQ3 and SQ4. And depending on the data stored in the received objects.

In a nutshell this features enables to save and reuse wave configuration with ease. 

A small video to explain the whole functionality of this feature can be found here. 

References

Write to a file in Android

Code Repository

PSLab Android

Tags

PSLab, Wave Generator, SaveConfig, Android, GSoC 19

Continue Reading

Implementing the discrete Seekbar for Wave Generator

The Wave Generator instrument in PSLab Android app allows us to produce waveforms having different values of properties like frequency, duty, phase etc.

The range of these properties allowed by PSLab Device are :

Table showing the range of properties that can be set for waves by PSLab device
Wave Property Range
Min Max Step Size
Frequency 10 Hz 5000 Hz 1 Hz
Phase 360°
Duty 10% 100% 10%

We can set these values using the up/down arrow buttons provided by the wave generator but the problem is that the range of values is very high and least counts are small so it is convenient to set the values using only the up and down arrow buttons.

Therefore we need something that could allow us to directly set any value of our choice while keeping the UI interactive.

The solution to this problem – “Discrete Seekbar”. It contains a slider having points at equal intervals and whose length represents the range of the values and a head that slides over the slider and is used to select a specific value from a range of values.

I have included the discrete Seekbar in Wave Generator by using a third-party library if you want to add Seekbar directly you can do that by directly using the default Seekbar widget provided by Android SDK and setting the following attribute in as shown below.

android:theme = “@style/Widget.AppCompat.SeekBar.Discrete”

Refer to this post[2] for implementing Seekbar directly without an external library.

The reason I chose this library is that:-

  • It offers various implementation of different types of Seekbar like discrete and continuous.
  • Implementation of Seekbar is simpler and it offers various customizations like thumb color, track color, tick text etc.  

In following steps I will implement the discrete Seekbar:

Step 1 Adding the dependency

For this project, I will be using an external library “IndicatorSeekbarLibrary” by Warkiz[1], for adding the dependency we need to include the following code in our build.gradle file.

dependencies{
implementation 'com.github.warkiz.widget:indicatorseekbar:2.0.9'
}

Step 2 Including the Seekbar in layout

For this step, we need to add the Seekbar widget using <com.warkiz.widget.IndicatorSeekBar> XML tag in our wave generator layout file to include the Seekbar in our layout as shown in the code below:

<com.warkiz.widget.IndicatorSeekBar
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:isb_max="5000"
    app:isb_min="0"
    app:isb_ticks_count="5"
    app:isb_thumb_color="@color/color_green"
    app:isb_thumb_size="20dp"
    app:isb_track_background_color="@color/color_gray"
    app:isb_track_background_size="2dp"
    app:isb_track_progress_color="@color/color_blue"
    app:isb_track_progress_size="4dp" />

Some important attributes used above:

app:isb_max : defines the max value that can be achieved by the Seekbar.

app:isb_min :  defines the min value that can be achieved by the Seekbar

app:isb_ticks_count: no. of ticks(interval) that has to be shown on the slider

We can see different components of Seekbar like track, indicator, thumb, tick of SeekBar in the following diagram[2].

Figure 1 depicts the different attributes of the slider
(Source – https://github.com/warkiz/IndicatorSeekBar/blob/master/README.md)

Step 3 Attaching the listener to the Seekbar in Java file

In this step we need to attach the listener to the Seekbar to record changes in the Seekbar made by the user, for this we will create a new listener with the help of onSeekBarChangeListener interface and attach it with the Seekbar as shown in following code

IndicatorSeekBar seekbar = (IndicatorSeekBar) findViewbyId(R.id.seekbar);

seekBar.setOnSeekChangeListener(new OnSeekChangeListener() {
            @Override
            public void onSeeking(SeekParams seekParams) {
                /* called when the user is sliding the thumb */
            }

            @Override
            public void onStartTrackingTouch(IndicatorSeekBar seekBar) {
                /* called when the sliding of thumb is started */
            }

            @Override
            public void onStopTrackingTouch(IndicatorSeekBar seekBar) {
                /* called when the sliding of thumb stops */
            }
        });

After following all the above steps, I  implemented the Seekbar shown in Figure 2 below in my wave generator and now it becomes really easy to set different values of properties for without having to continually press the up/down button.

Figure 2 shows the Seekbar included in wave generator beside up/down arrow button

Resources

  1. warkiz/IndicatorSeekBar library  – Github Repo of the Indicator SeekBar library
  2. http://nileshsenta.blogspot.com/2016/10/discrete-seekbar-without-third-party.html – Blog by Nilesh Shenta on how to implement discrete without third party library

 

Continue Reading

Creating Control Panel For Wave Generator using Constraint Layout

 

In the blog Creating the onScreen Monitor Using CardView I had created the monitors to view the wave properties in this blog we will create the UI of controlling panel that will be used for that monitors with multiple buttons for both analog and digital waveforms.

Which layout to choose?

In today’s world, there are millions of Android devices present with different screen sizes and densities and the major concern of an Android developer is to make the layout that fits all the devices and this task is really difficult to handle with a linear or relative layout with fixed dimensions.

To create a complex layout with lots of views inside the parent using linear layout we have to make use of the attribute layout_weight for their proper stretching and positioning, but such a complex layout require a lot of nesting of weights and  android tries to avoid it by giving a warning :

Nested Weights are bad for performance

This is because layout_weight attribute requires a widget to be measured twice[1]. When a LinearLayout with non-zero weights is nested inside another LinearLayout with non-zero weights, then the number of measurements increases exponentially.

So, to overcome this issue we will make use of special type of layout “Constraint Layout” which was introduced in Google I/O 2016.

Features of Constraint Layout:-

  • It is similar to Relative Layout as all views are laid out according to their relationship with the sibling, but it is more flexible than Relative Layout.
  • It helps to flatten the view hierarchy for complex layouts.
  • This layout is created with the help of powerful tool provided by Android which has a palette on the left-hand side from where we can drag and drop the different widgets like TextView, ImageView, Buttons etc. and on the right-hand side it provides options for positioning, setting margins and other styling option like setting color, change text style etc.
  • It automatically adjusts the layout according to the screen size and hence doesn’t require the use of layout_weight attribute.

In following steps, I will create the controlling panel for Wave generator which is a complex layout with lots of buttons with the help of constraint layout.

Step 1: Add the dependency of the Constraint Layout in the Project

To use Constraint layout add the following to your build.gradle file and sync the project

dependencies {
    implementation "com.android.support.constraint:constraint-layout:1.1.0"
}

Step 2: Applying Guidelines to the layout

Guidelines[3] are anchors that won’t be displayed in your app, they are like one line of a grid above your layout and can be used to attach or constraint your widgets to it. They are only visible on your blueprint or preview editor. These will help to position and constraint the UI components on the screen easily.

For adding guidelines :

As shown in Figure 1 Right-click anywhere on the layout -> Select helpers -> Select horizontal or vertical guideline according to your need.

Figure 1 shows the horizontal guideline being added to the layout.

And for positioning the guideline we have to set the value of attribute layout_constraintGuide_percent  

Let’s say we want the guideline to be at the middle of the screen so we’ll set :

app:layout_constraintGuide_percent=”0.50″

For my layout I have added three guideline :

  • One horizontal guideline at 50%
  • Two vertical guidelines at 30% and 65%

Doing this will bifurcate the screen into six square blocks as shown in below figure :

Figure 2 shows the blueprint of constraint layout containing two vertical and one horizontal guidelines with their percentage offset from respective bases

Step 3: Adding the buttons in the blocks

Until now we have created six squares blocks, now we have to put a button view in each of the boxes.

  • First drag and drop button view from the Palette (shown in Figure 3) on the left side inside the box.

    Figure 3 shows the layout editor palette

     

  • Then we have to set constraints of this button by clicking on the small circle present on the middle of edges and dragging it onto the side of the block facing it.

    Figure 4 shows the button widget getting constrained to sides

     

  • Set the layout_width and layout_height attribute of the button to be “0dp”, doing this the button will expand in all the direction occupying all the space with respect to the border it has been constrained with.

    Figure 6 shows the button widget expanding to all the available space in the box

Similarly, adding buttons in all the square blocks and providing proper theme color we will have a blueprint and layout as shown in Figure 6.

Figure 6 shows the waveform panel blueprint and actual layout for analog waves with six buttons

Following the same steps until now, I have created the other controlling panel layout having buttons for digital waves as shown in Figure 7

Figure 7 shows other constraint layout for digital waves having seven buttons

Detailing and combining the panels to form Complete UI

After adding both the panels we have created in this layout inside the Wave Generator we have the layout as shown in Figure 8

Figure 8 shows the UI of Wave Generator as shown by a actual Android device in the PSLab app.

As we can see on adding the panels the button created inside the layout shrink so as to adapt to the screen and giving out a beautiful button-like appearance.

Resources   

  1. Blog on Nested Weights are bad for performance
  2. Developer Article – Build a Responsive UI with ConstraintLayout
  3. Information about Guidelines
Continue Reading

Creating the onScreen Monitor Using CardView

The PSLab works as a Wave generator and the Oscilloscope when connected with the Android device. The mockup of the Wave Generator has been created in the blog Creating the Mockup for Wave Generator using Moqups.

In this blog, we will create the monitor screen in the PSLab Android app using Android studio.

Deciding the layout to use for monitors

As monitors with rounded border look appealing on android device screen so we will select CardView to be the base layout of our monitors. The CardView element allows us to add certain properties like rounded corners, elevation etc.

To use the CardView in your app, add the following dependency to build.gradle file and sync the project.

dependencies {    // CardView    
implementation 'com.android.support:cardview-v7:27.1.1'
}

Add the <android.support.v7.widget.CardView> widget to your layout and all the other views to be shown on monitor screen will be placed inside this card layout as its child layout.

Creating the monitor cards

We need to decide how much spacing is to be provided to the cards to properly view them on different screens. Here we make use of the special attribute in the View class.

android:layout_weight=1

This attribute assigns an “importance” value to a View in terms of how much space it should occupy on the screen. That is if we give equal weight to two views which inside the same parent then they will both occupy equal space.

To implement this create two CardViews inside <LinearLayout> with the same layout_weight.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_weight="1"
        card_view:cardCornerRadius="10dp" />

    <android.support.v7.widget.CardView
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_weight="1"
        card_view:cardCornerRadius="10dp" />

</LinearLayout>

So this will give us a screen like this with equally weighted monitor card views as shown in Figure 1.

Figure 1 shows two CardView with equal weights(w/2) and equal width

Both the cards have the width equal to half the screen width which is set automatically by the Android.

Creating partitions in the monitors

We have to make partitions in the monitor screen so that every characteristic of Wave Generator like wave frequency, phase, selected pin etc. can be viewed simultaneously on the screen with proper spacing.

For making partitions we need to make a separator line which we can create by using <View> and setting the attributes as below:

<View
        android:layout_width="@dimen/length_0dp"
        android:layout_height="@dimen/divider_width"
        android:background="@color/dark_grey" />

This will create a thin line having the grey color. We have to make four partitions of the card view for both monitors as shown in Figure 2.

Figure 2 shows two monitors with line separator making partitions

Populating the screen with text and image icons

We have to include the <TextView> and <ImageView> to show the property data on the monitors.

Figure 3 shows partitions in the monitor CardView

Different partitions as shown in Figure 3 can be used for showing different data such as:

  1. Top Partition:- To view the selected wave. 
  2. Left Partition:- To view the selected waveform eg:- sine, triangular 
  3. Right Partition:- To view the selected wave characteristics like frequency, phase etc. 
  4. Bottom Partition:-  To view the property selected which has been selected by user

After inserting all the desired Views and providing the Views with dummy data and icons and customizing the Views by giving proper color and spacing finally we will have a layout as shown in Figure 4.

Figure 4 shows ImageView and TextView showing different dummy data

In Figure 4, the left CardView shows all the properties for the Analog Wave Generator and the right CardView shows all the properties for Digital Wave Generator.

All the characteristics on the monitors can be controlled by the controlling panel which is work in progress.

Resources:

  1. https://developer.android.com/guide/topics/ui/layout/cardview– Article on How to Create a card based layout by Android Developer Documentation 
  2. https://stackoverflow.com/questions/5049852/android-drawing-separator-divider-line-in-layout – Stack Overflow post to create separator line

 

 

Continue Reading

Filling Audio Buffer to Generate Waves in the PSLab Android App

The PSLab Android App works as an oscilloscope and a wave generator using the audio jack of the Android device. The implementation of the oscilloscope in the Android device using the in-built mic has been discussed in the blog post “Using the Audio Jack to make an Oscilloscope in the PSLab Android App” and the same has been discussed in the context of wave generator in the blog post “Implement Wave Generation Functionality in the PSLab Android App”. This post is a continuation of the post related to the implementation of wave generation functionality in the PSLab Android App. In this post, the subject matter of discussion is the way to fill the audio buffer so that the resulting wave generated is either a Sine Wave, a Square Wave or a Sawtooth Wave. The resultant audio buffer would be played using the AudioTrack API of Android to generate the corresponding wave. The waves we are trying to generate are periodic waves.

Periodic Wave: A wave whose displacement has a periodic variation with respect to time or distance, or both.

Thus, the problem reduces to generating a pulse which will constitute a single time period of the wave. Suppose we want to generate a sine wave; if we generate a continuous stream of pulses as illustrated in the image below, we would get a continuous sine wave. This is the main concept that we shall try to implement using code.

Initialise AudioTrack Object

AudioTrack object is initialised using the following parameters:

  • STREAM TYPE: Type of stream like STREAM_SYSTEM, STREAM_MUSIC, STREAM_RING, etc. For wave generation purposes we are using stream music. Every stream has its own maximum and minimum volume level.  
  • SAMPLING RATE: It is the rate at which the source samples the audio signal.
  • BUFFER SIZE IN BYTES: Total size of the internal buffer in bytes from where the audio data is read for playback.
  • MODES: There are two modes-
    • MODE_STATIC: Audio data is transferred from Java to the native layer only once before the audio starts playing.
    • MODE_STREAM: Audio data is streamed from Java to the native layer as audio is being played.

getMinBufferSize() returns the estimated minimum buffer size required for an AudioTrack object to be created in the MODE_STREAM mode.

minTrackBufferSize = AudioTrack.getMinBufferSize(SAMPLING_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
audioTrack = new AudioTrack(
       AudioManager.STREAM_MUSIC,
       SAMPLING_RATE,
       AudioFormat.CHANNEL_OUT_MONO,
       AudioFormat.ENCODING_PCM_16BIT,
       minTrackBufferSize,
       AudioTrack.MODE_STREAM);

Fill Audio Buffer to Generate Sine Wave

Depending on the values in the audio buffer, the wave is generated by the AudioTrack object. Therefore, to generate a specific kind of wave, we need to fill the audio buffer with some specific values. The values are governed by the wave equation of the signal that we want to generate.

public short[] createBuffer(int frequency) {
   short[] buffer = new short[minTrackBufferSize];
   double f = frequency;
   double q = 0;
   double level = 16384;
   final double K = 2.0 * Math.PI / SAMPLING_RATE;

   for (int i = 0; i < minTrackBufferSize; i++) {
         f += (frequency - f) / 4096.0;
         q += (q < Math.PI) ? f * K : (f * K) - (2.0 * Math.PI);
         buffer[i] = (short) Math.round(Math.sin(q));
   }
   return buffer;
}

Fill Audio Buffer to Generate Square Wave

To generate a square wave, let’s assume the time period to be t units. So, we need the amplitude to be equal to A for t/2 units and -A for the next t/2 units. Repeating this pulse continuously, we will get a square wave.

buffer[i] = (short) ((q > 0.0) ? 1 : -1);

Fill Audio Buffer to Generate Sawtooth Wave

Ramp signals increases linearly with time. A Ramp pulse has been illustrated in the image below:

We need repeated ramp pulses to generate a continuous sawtooth wave.

buffer[i] = (short) Math.round((q / Math.PI));

Finally, when the audio buffer is generated, write it to the audio sink for playback using write() method exposed by the AudioTrack object.

audioTrack.write(buffer, 0, buffer.length);

Resources

Continue Reading

Implement Wave Generation Functionality in The PSLab Android App

The PSLab Android App works as an Oscilloscope using the audio jack of Android device. The implementation for the scope using in-built mic is discussed in the post Using the Audio Jack to make an Oscilloscope in the PSLab Android App. Another application which can be implemented by hacking the audio jack is Wave Generation. We can generate different types of signals on the wires connected to the audio jack using the Android APIs that control the Audio Hardware. In this post, I will discuss about how we can generate wave by using the Android APIs for controlling the audio hardware.

Configuration of Audio Jack for Wave Generation

Simply cut open the wire of a cheap pair of earphones to gain control of its terminals and attach alligator pins by soldering or any other hack(jugaad) that you can think of. After you are done with the tinkering of the earphone jack, it should look something like shown in the image below.

Source: edn.com

If your earphones had mic, it would have an extra wire for mic input. In any general pair of earphones the wire configuration is almost the same as shown in the image below.

Source: flickr

Android APIs for Controlling Audio Hardware

AudioRecord and AudioTrack are the two classes in Android that manages recording and playback respectively. For Wave Generation application we only need AudioTrack class.

Creating an AudioTrack object: We need the following parameters to initialise an AudioTrack object.

STREAM TYPE: Type of stream like STREAM_SYSTEM, STREAM_MUSIC, STREAM_RING, etc. For wave generation purpose we are using stream music. Every stream has its own maximum and minimum volume level.

SAMPLING RATE: it is the rate at which source samples the audio signal.

BUFFER SIZE IN BYTES: total size in bytes of the internal buffer from where the audio data is read for playback.

MODES: There are two modes

  • MODE_STATIC: Audio data is transferred from Java to native layer only once before the audio starts playing.
  • MODE_STREAM: Audio data is streamed from Java to native layer as audio is being played.

getMinBufferSize() returns the estimated minimum buffer size required for an AudioTrack object to be created in the MODE_STREAM mode.

private int minTrackBufferSize;
private static final int SAMPLING_RATE = 44100;
minTrackBufferSize = AudioTrack.getMinBufferSize(SAMPLING_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);

audioTrack = new AudioTrack(
       AudioManager.STREAM_MUSIC,
       SAMPLING_RATE,
       AudioFormat.CHANNEL_OUT_MONO,
       AudioFormat.ENCODING_PCM_16BIT,
       minTrackBufferSize,
       AudioTrack.MODE_STREAM);

Function createBuffer() creates the audio buffer that is played using the audio track object i.e audio track object would write this buffer on playback stream. Function below fills random values in the buffer due to which a random signal is generated. If we want to generate some specific wave like Square Wave, Sine Wave, Triangular Wave, we have to fill the buffer accordingly.

public short[] createBuffer(int frequency) {
   // generating a random buffer for now
   short[] buffer = new short[minTrackBufferSize];
   for (int i = 0; i < minTrackBufferSize; i++) {
       buffer[i] = (short) (random.nextInt(32767) + (-32768));
   }
   return buffer;
}

We created a write() method and passed the audio buffer created in above step as an argument to the method. This method writes audio buffer into audio stream for playback.

public void write(short[] buffer) {
   /* write buffer to audioTrack */
   audioTrack.write(buffer, 0, buffer.length);
}

Amplitude of the signal can be controlled by changing the volume level of the stream on which the buffer is being played. As we are playing the audio in music stream, so STREAM_MUSIC is passed as a parameter to the setStreamVolume() method.

value: value is amplitude level of the stream. Every stream has its different amplitude levels. getStreamMaxVolume(STREAM_TYPE) method is used to find the maximum valid amplitude level of any stream.
flag: this stackoverflow post explain all the flags of the AudioManager class.

AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, value, flag);

Roadmap

We are working on implementing methods to fill audio buffer with specific values such that waves like Sinusoidal wave, Square Wave, Sawtooth Wave can be generated during the playback of the buffer using the AudioTrack object.

Resources

Continue Reading
Close Menu