diff --git a/app/src/main/java/io/pslab/activity/SoundMeterActivity.java b/app/src/main/java/io/pslab/activity/SoundMeterActivity.java index 8f02086bd..732c6f49c 100644 --- a/app/src/main/java/io/pslab/activity/SoundMeterActivity.java +++ b/app/src/main/java/io/pslab/activity/SoundMeterActivity.java @@ -114,12 +114,9 @@ protected void onResume() { } private void reinstateConfigurations() { - SharedPreferences luxMeterConfigurations; - luxMeterConfigurations = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); - locationEnabled = luxMeterConfigurations.getBoolean(SoundmeterSettingsFragment.KEY_INCLUDE_LOCATION, true); - SoundMeterDataFragment.setParameters(); - /** - * TODO: set parameters - */ + SharedPreferences soundMeterConfigurations; + soundMeterConfigurations = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + locationEnabled = soundMeterConfigurations.getBoolean(SoundmeterSettingsFragment.KEY_INCLUDE_LOCATION, true); + SoundMeterDataFragment.setParameters(1d, 100); } } diff --git a/app/src/main/java/io/pslab/fragment/SoundMeterDataFragment.java b/app/src/main/java/io/pslab/fragment/SoundMeterDataFragment.java index 089696ef1..821182827 100644 --- a/app/src/main/java/io/pslab/fragment/SoundMeterDataFragment.java +++ b/app/src/main/java/io/pslab/fragment/SoundMeterDataFragment.java @@ -1,14 +1,15 @@ package io.pslab.fragment; import android.graphics.Bitmap; +import android.graphics.Color; import android.location.Location; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -18,6 +19,9 @@ import com.github.anastr.speedviewlib.PointerSpeedometer; import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.Legend; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; @@ -32,6 +36,10 @@ import java.util.Deque; import java.util.List; import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import butterknife.BindView; import butterknife.ButterKnife; @@ -44,7 +52,6 @@ import io.pslab.others.AudioJack; import io.pslab.others.CSVDataLine; import io.pslab.others.CSVLogger; -import io.pslab.others.CustomSnackBar; import static io.pslab.others.CSVLogger.CSV_DIRECTORY; @@ -61,6 +68,15 @@ public class SoundMeterDataFragment extends Fragment { .add("Readings") .add("Latitude") .add("Longitude"); + private static final String KEY_LOUDNESS = "key loudness"; + private static final String KEY_MAX_LOUDNESS = "key max loudness"; + private static final String KEY_MIN_LOUDNESS = "key min loudness"; + private static final String KEY_AVG_LOUDNESS = "key average loudness"; + private static final int ANIMATION_BUFFER_SIZE = 500; + + private static double refIntensity; + private static int movingAvgWindowSize; + @BindView(R.id.sound_max) TextView statMax; @BindView(R.id.sound_min) @@ -78,8 +94,11 @@ public class SoundMeterDataFragment extends Fragment { private View rootView; private Unbinder unbinder; private AudioJack audioJack; + private List recordedSoundData; + private int counter; + /** - * Thread to handle recording in background + * Thread to handle processing in background */ private HandlerThread bgThread; @@ -93,40 +112,82 @@ public class SoundMeterDataFragment extends Fragment { */ private Handler uiHandler; - private boolean isRecording; + /** + * Scheduled executor to view recorded data + */ + private ScheduledExecutorService scheduledExecutorService; + + /** + * Recorded data player handle to cancel the scheduled task created by scheduledExecutorService + */ + ScheduledFuture dataPlayerHandle; + + private boolean isProcessing; + + /** + * variable to store the starting time of recording + */ private long recordStartTime; + + /** + * variable to store resume time to calculate offset + */ + private long resumeTime; + + /** + * variable to store the current time when playing is paused + */ + private long pauseTime; + + /** + * offset to keep record of time that has already been played before pausing + */ + private long offset; + private long block; - private Deque chartQ; + + /* + Variables to store values during processing + */ private double maxRmsAmp; private double minRmsAmp; private double rmsSum; - private int count; + + /** + * Double ended queue to how chart entries for current window + */ + private Deque chartQ; + + /** + * Window to calculate the moving average + */ + private Deque movingAvgWindow; public static SoundMeterDataFragment newInstance() { return new SoundMeterDataFragment(); } - public static void setParameters() { - /** - * TODO: Parameters yet to be determined - */ - Log.i(TAG, "parameters yet to be determined"); + public static void setParameters(double refIntensity, int movingAvgWindowSize) { + SoundMeterDataFragment.refIntensity = refIntensity; + SoundMeterDataFragment.movingAvgWindowSize = movingAvgWindowSize; } - /********************************************************************************************** + /* ******************************************************************************************** * Fragment Lifecycle Methods - ********************************************************************************************** + * ******************************************************************************************** */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); soundMeter = (SoundMeterActivity) getActivity(); + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + chartQ = new ArrayDeque<>(); + counter = 0; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // Inflate the layout for this fragment rootView = inflater.inflate(R.layout.fragment_sound_meter_data, container, false); unbinder = ButterKnife.bind(this, rootView); setupInstruments(); @@ -137,13 +198,33 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onResume() { super.onResume(); startBackgroundThread(); - startRecording(); + if(soundMeter.viewingData) { + /* + * reset counter to 0 + */ + recordedSoundData = new ArrayList<>(); + recordedSoundData.addAll(soundMeter.recordedSoundData); + } else { + /* + * Start processing the sound from the environment + */ + startProcessing(); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); } @Override public void onPause() { super.onPause(); - stopRecording(); + if(soundMeter.playingData) { + pausePlaying(); + } else if (isProcessing) { + stopProcessing(); + } stopBackgroundThread(); } @@ -157,60 +238,61 @@ public void onDestroyView() { public void onDestroy() { super.onDestroy(); soundMeter = null; + scheduledExecutorService = null; + chartQ.clear(); + chartQ = null; } - /********************************************************************************************** + /* ******************************************************************************************** * Initializer methods - ********************************************************************************************** + * ******************************************************************************************** */ private void setupInstruments() { - decibelMeter.setMaxSpeed(120); - decibelMeter.setMinSpeed(1); + decibelMeter.setMaxSpeed(200.0f); + decibelMeter.setMinSpeed(0.0f); decibelMeter.setWithTremble(false); YAxis yAxis = mChart.getAxisLeft(); - yAxis.setAxisMaximum(10); - yAxis.setAxisMinimum(-10); - yAxis.setDrawZeroLine(true); + yAxis.setAxisMaximum(200); + yAxis.setAxisMinimum(0); + yAxis.setLabelCount(40); yAxis.setDrawGridLines(false); - yAxis.setLabelCount(20); + yAxis.setTextColor(Color.WHITE); + LimitLine dangerLine = new LimitLine(100, getString(R.string.limit_dangerous)); + dangerLine.setLineColor(Color.RED); + dangerLine.setTextColor(Color.RED); + yAxis.addLimitLine(dangerLine); + + XAxis x = mChart.getXAxis(); + + x.setTextColor(Color.WHITE); + x.setDrawGridLines(true); + x.setAvoidFirstLastClipping(true); + + + mChart.setTouchEnabled(true); + mChart.setHighlightPerDragEnabled(true); + mChart.setDragEnabled(true); + mChart.setScaleEnabled(true); mChart.setDrawGridBackground(false); - } + mChart.setPinchZoom(true); + mChart.setScaleYEnabled(true); + mChart.setBackgroundColor(Color.BLACK); + mChart.getDescription().setEnabled(false); - /********************************************************************************************* - * Methods related to sound recording - ********************************************************************************************* - */ - private void startRecording() { - isRecording = true; - audioJack = new AudioJack("input"); - recordStartTime = System.currentTimeMillis(); - chartQ = new ArrayDeque<>(); - bgThreadHandler.post(() -> { - while (isRecording) { - short[] buffer = audioJack.read(); - Bundle bundle = new Bundle(); - bundle.putShortArray("buffer", buffer); - Message msg = new Message(); - msg.setData(bundle); - uiHandler.sendMessage(msg); - } - }); - } + LineData data = new LineData(); + mChart.setData(data); + + Legend l = mChart.getLegend(); + l.setForm(Legend.LegendForm.LINE); + l.setTextColor(Color.WHITE); - private void stopRecording() { - isRecording = false; - audioJack.release(); - audioJack = null; - chartQ.clear(); - chartQ = null; - mChart.clear(); } - /********************************************************************************************* + /* ******************************************************************************************** * Members related to handling Background Thread - ********************************************************************************************* + * ******************************************************************************************** */ private void startBackgroundThread() { Log.i(TAG, "starting background thread"); @@ -232,83 +314,203 @@ private void stopBackgroundThread() { Log.i(TAG, "Background Thread Stopped"); } - /********************************************************************************************** - * Methods related to data visualization - ********************************************************************************************** + /* ******************************************************************************************** + * Methods related to sound processing + * ******************************************************************************************** */ - private void updateMeter(short[] buffer) { - double sqrsum = 0.0; - for (int i = 0; i < buffer.length; ++i) { - sqrsum += Math.pow(buffer[i], 2); - } - double rmsamp = Math.sqrt((sqrsum / buffer.length)); + private void startProcessing() { + isProcessing = true; + audioJack = new AudioJack("input"); + recordStartTime = System.currentTimeMillis(); + movingAvgWindow = new ArrayDeque<>(); + bgThreadHandler.post(() -> { + while (isProcessing) { + /* + * read the audio samples from the hardware device + */ + short[] buffer = audioJack.read(); + + /* + * Calculate the root mean square amplitude of the values in the buffer. + */ + double sqrsum = 0.0; + for (short val : buffer) { + sqrsum += Math.pow(val, 2); + } + double rmsamp = Math.sqrt((sqrsum / buffer.length)); + + /* + * update the moving average window + */ + if( !(movingAvgWindow.size() < movingAvgWindowSize) ) { + rmsSum -= movingAvgWindow.removeFirst(); + } + movingAvgWindow.addLast(rmsamp); + rmsSum += rmsamp; + + /* + * Calculate average, max and min root-mean-square(rms) amplitude + */ + double avgRmsAmp = rmsSum / movingAvgWindow.size(); + maxRmsAmp = Math.max(rmsamp, maxRmsAmp); + minRmsAmp = Math.min(rmsamp, minRmsAmp); - maxRmsAmp = Math.max(rmsamp, maxRmsAmp); - minRmsAmp = Math.min(rmsamp, minRmsAmp); - rmsSum = (count < Integer.MAX_VALUE) ? (rmsSum + rmsamp) : rmsamp; - count = (count < Integer.MAX_VALUE) ? (count + 1) : 1; - double avgRmsAmp = rmsSum / count; + /* + * Calculate the current, max, min and average loudness for the current instant + */ + double loudness = rmsamp > 0 ? (10 * Math.log10(rmsamp / refIntensity)) : 1; + double maxLoudness = maxRmsAmp > 0 ? (10 * Math.log10(maxRmsAmp / refIntensity)) : 1; + double minLoudness = minRmsAmp > 0 ? (10 * Math.log10(minRmsAmp / refIntensity)) : 1; + double avgLoudness = avgRmsAmp > 0 ? (10 * Math.log10(avgRmsAmp / refIntensity)) : 1; - double loudness = rmsamp > 0 ? (10 * Math.log10(rmsamp / 1d)) : 1; - double maxLoudness = maxRmsAmp > 0 ? (10 * Math.log10(maxRmsAmp / 1d)) : 1; - double minLoudness = minRmsAmp > 0 ? (10 * Math.log10(minRmsAmp / 1d)) : 1; - double avgLoudness = avgRmsAmp > 0 ? (10 * Math.log10(avgRmsAmp / 1d)) : 1; + /* + * Bundle the values to be sent to the ui handler + */ + Bundle bundle = new Bundle(); + bundle.putDouble(KEY_LOUDNESS, loudness); + bundle.putDouble(KEY_MAX_LOUDNESS, maxLoudness); + bundle.putDouble(KEY_MIN_LOUDNESS, minLoudness); + bundle.putDouble(KEY_AVG_LOUDNESS, avgLoudness); + Message msg = new Message(); + msg.setData(bundle); + uiHandler.sendMessage(msg); + } + }); + } + private void stopProcessing() { + isProcessing = false; + audioJack.release(); + audioJack = null; + resetViews(); + } + + /* ******************************************************************************************** + * Methods related to data visualization + * ******************************************************************************************** + */ + private void updateMeter(double loudness, double avgLoudness, double maxLoudness, double minLoudness) { decibelMeter.setSpeedAt((float) loudness); statMax.setText(String.format(Locale.getDefault(), PSLabSensor.SOUNDMETER_DATA_FORMAT, maxLoudness)); statMin.setText(String.format(Locale.getDefault(), PSLabSensor.SOUNDMETER_DATA_FORMAT, minLoudness)); statMean.setText(String.format(Locale.getDefault(), PSLabSensor.SOUNDMETER_DATA_FORMAT, avgLoudness)); - writeLog(System.currentTimeMillis(), (float) loudness); } - private void updateChart(short[] buffer) { - for (int i = 0; i < buffer.length; ++i) { - float x = (System.currentTimeMillis() - recordStartTime) / 1000f; - float y = buffer[i] / 1000f; - if (chartQ.size() >= buffer.length) - chartQ.removeFirst(); - chartQ.addLast(new Entry(x, y)); - Log.i(TAG, "x : " + x + " " + "y : " + y); - } + private void updateChart(double loudness, double avgLoudness, double maxLoudness, double minLoudness, long startTime, long offset) { + float x = (offset + (System.currentTimeMillis() - startTime)) / 1000f; + chartQ.addLast(new Entry(x, (float)loudness)); + if(chartQ.size() > ANIMATION_BUFFER_SIZE) + chartQ.removeFirst(); List entries = new ArrayList<>(chartQ); - LineDataSet dataSet = new LineDataSet(entries, "Amplitude"); + LineDataSet dataSet = new LineDataSet(entries, getString(R.string.sound_chart_label)); dataSet.setDrawCircles(false); - dataSet.setDrawValues(false); + dataSet.setDrawValues(true); dataSet.setLineWidth(0.5f); mChart.setData(new LineData(dataSet)); mChart.notifyDataSetChanged(); - mChart.setVisibleXRangeMaximum(entries.size()); mChart.invalidate(); } + private void resetViews() { + chartQ.clear(); + mChart.clear(); + decibelMeter.setSpeedAt(0.0f); + Log.i(TAG,"view reset complete"); + } + + /* ******************************************************************************************** + * Methods related to view previously recorded data + * ******************************************************************************************** + */ + + private void playRecordedData(long startTime, long offset) { + long period = ( recordedSoundData.get(recordedSoundData.size()-1).getTime() - + recordedSoundData.get(0).getTime() ) / recordedSoundData.size(); + dataPlayerHandle = scheduledExecutorService.scheduleWithFixedDelay(()-> { + SoundData soundData = recordedSoundData.get(counter); + uiHandler.post(() -> { + if(soundMeter.playingData) { + updateChart(soundData.getdB(), soundData.getAvgLoudness(), + soundData.getMaxLoudness(), soundData.getMinLoudness(), startTime, offset); + updateMeter(soundData.getdB(), soundData.getAvgLoudness(), + soundData.getMaxLoudness(), soundData.getMinLoudness()); + } + }); + counter ++; + if(counter == recordedSoundData.size()) { + stopPlaying(); + } + }, 0, period, TimeUnit.MILLISECONDS); + } + + private void startPlaying() { + soundMeter.startedPlay = true; + resumeTime = System.currentTimeMillis(); + playRecordedData(resumeTime, 0); + } + + private void stopPlaying() { + uiHandler.post(()-> { + dataPlayerHandle.cancel(false); + soundMeter.playingData = false; + soundMeter.startedPlay = false; + soundMeter.invalidateOptionsMenu(); + resetViews(); + counter = 0; + resumeTime = 0; + offset = 0; + pauseTime = 0; + }); + } + + private void resumePlaying() { + offset += pauseTime - resumeTime; + resumeTime = System.currentTimeMillis(); + playRecordedData(resumeTime, offset); + } + + private void pausePlaying() { + uiHandler.post(()-> { + dataPlayerHandle.cancel(false); + pauseTime = System.currentTimeMillis(); + soundMeter.playingData = false; + soundMeter.invalidateOptionsMenu(); + }); + } + /** * Method to play data which was previously recorded */ public void playData() { - CustomSnackBar.showSnackBar(getActivity().findViewById(android.R.id.content), getString(R.string.in_progress), - null, null, Snackbar.LENGTH_SHORT); - /** - * TODO: To be implemented - */ + startPlaying(); + } + /** + * Method to pause playing + */ + public void pause() { + pausePlaying(); + } + + /** + * Method to resume playing + */ + public void resume() { + resumePlaying(); } /** * Method to stop playing the previously recorded data */ public void stopData() { - CustomSnackBar.showSnackBar(getActivity().findViewById(android.R.id.content), getString(R.string.in_progress), - null, null, Snackbar.LENGTH_SHORT); - /** - * TODO: To be implemented - */ + stopPlaying(); } - /********************************************************************************************** + /* ******************************************************************************************** * Method Related to saving sound data - ********************************************************************************************** + * ******************************************************************************************** */ - private void writeLog(long timestamp, float dB) { + private void writeLog(long timestamp, float dB, float avgLoudness, float maxLoudness, float minLoudness) { SoundData soundData; if (getActivity() != null && soundMeter.isRecording) { if (soundMeter.writeHeaderToFile) { @@ -327,9 +529,10 @@ private void writeLog(long timestamp, float dB) { .add(timestamp) .add(dateTime) .add(dB) - .add(location.getLatitude()) - .add(location.getLongitude())); - soundData = new SoundData(timestamp, block, dB, location.getLatitude(), location.getLongitude()); + .add((location != null)?location.getLatitude():0.0d) + .add((location != null)?location.getLongitude():0.0d)); + soundData = new SoundData(timestamp, block, dB, avgLoudness, maxLoudness, minLoudness, + (location!=null)?location.getLatitude():0.0d, (location!=null)?location.getLongitude():0.0d); } else { String dateTime = CSVLogger.FILE_NAME_FORMAT.format(new Date(timestamp)); soundMeter.csvLogger.writeCSVFile( @@ -339,7 +542,8 @@ private void writeLog(long timestamp, float dB) { .add(dB) .add(0.0) .add(0.0)); - soundData = new SoundData(timestamp, block, dB, 0.0, 0.0); + soundData = new SoundData(timestamp, block, dB, avgLoudness, maxLoudness, minLoudness, + 0.0, 0.0); } soundMeter.recordSensorData(soundData); } else { @@ -372,6 +576,9 @@ public void saveGraph() { } } + /** + * The implementation of Handler class for the UI Thread + */ private static class UIHandler extends Handler { private SoundMeterDataFragment soundMeterDataFragment; @@ -380,15 +587,16 @@ private static class UIHandler extends Handler { } @Override - public void handleMessage(Message msg) { //handle the message passed by the background thread which is recording the audio + public void handleMessage(Message msg) { //handle the message passed by the background thread which is processing the audio if (soundMeterDataFragment.isResumed()) { - short[] buffer = msg.getData().getShortArray("buffer"); - soundMeterDataFragment.updateMeter(buffer); - /** - * TODO: smooth animation for the graph required - */ - soundMeterDataFragment.updateChart(buffer); - Log.i(TAG, "handling message " + buffer.length + buffer[0]); + Bundle bundle = msg.getData(); + double loudness = bundle.getDouble(KEY_LOUDNESS); + double maxLoudness = bundle.getDouble(KEY_MAX_LOUDNESS); + double minLoudness = bundle.getDouble(KEY_MIN_LOUDNESS); + double avgLoudness = bundle.getDouble(KEY_AVG_LOUDNESS); + soundMeterDataFragment.updateMeter(loudness, avgLoudness, maxLoudness, minLoudness); + soundMeterDataFragment.updateChart(loudness, avgLoudness, maxLoudness, minLoudness, soundMeterDataFragment.recordStartTime, 0); + soundMeterDataFragment.writeLog(System.currentTimeMillis(), (float) loudness, (float)avgLoudness, (float)maxLoudness, (float)minLoudness); } } } diff --git a/app/src/main/java/io/pslab/models/PSLabSensor.java b/app/src/main/java/io/pslab/models/PSLabSensor.java index 86a495a84..aa622e4de 100644 --- a/app/src/main/java/io/pslab/models/PSLabSensor.java +++ b/app/src/main/java/io/pslab/models/PSLabSensor.java @@ -363,6 +363,16 @@ public boolean onOptionsItemSelected(MenuItem item) { ((SoundMeterDataFragment) getSupportFragmentManager() .findFragmentByTag(getSensorName())).playData(); } + } else { + if(getSensorFragment() instanceof SoundMeterDataFragment) { + if(!playingData) { + ((SoundMeterDataFragment) getSupportFragmentManager() + .findFragmentByTag(getSensorName())).pause(); + } else { + ((SoundMeterDataFragment) getSupportFragmentManager() + .findFragmentByTag(getSensorName())).resume(); + } + } } invalidateOptionsMenu(); break; diff --git a/app/src/main/java/io/pslab/models/SoundData.java b/app/src/main/java/io/pslab/models/SoundData.java index fac6698d3..ae07b47c7 100644 --- a/app/src/main/java/io/pslab/models/SoundData.java +++ b/app/src/main/java/io/pslab/models/SoundData.java @@ -12,6 +12,9 @@ public class SoundData extends RealmObject { private long time; private long block; private float dB; + private float avgLoudness; + private float maxLoudness; + private float minLoudness; private double lat; private double lon; @@ -19,10 +22,14 @@ public SoundData() { /* no arg constructor */ } - public SoundData(long time, long block, float dB, double lat, double lon) { + public SoundData(long time, long block, float dB, float avgLoudness, float maxLoudness, + float minLoudness, double lat, double lon) { this.time = time; this.block = block; this.dB = dB; + this.avgLoudness = avgLoudness; + this.minLoudness = minLoudness; + this.maxLoudness = maxLoudness; this.lat = lat; this.lon = lon; } @@ -45,4 +52,16 @@ public double getLat() { public double getLon() { return lon; } + + public float getAvgLoudness() { + return avgLoudness; + } + + public float getMaxLoudness() { + return maxLoudness; + } + + public float getMinLoudness() { + return minLoudness; + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 164c98234..4f600d426 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1046,8 +1046,11 @@ Max (dB) Min (dB) Avg (dB) + Sound Level(dB) This feature is in progress Developers https://github.com/fossasia/pslab-android/graphs/contributors + \"Dangerous\" + Average