/* Copyright 2014 Eddy Xiao <[email protected]> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package github.bewantbe.audio_analyzer_for_android; import android.app.AlertDialog; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Build; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.NonNull; import android.text.Html; import android.text.method.ScrollingMovementMethod; import android.text.method.LinkMovementMethod; import android.util.TypedValue; import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.PopupWindow; import android.widget.TextView; import android.widget.Toast; /** * Operate the views in the UI here. * Should run on UI thread in general. */ class AnalyzerViews { final String TAG = "AnalyzerViews"; private final AnalyzerActivity activity; final AnalyzerGraphic graphView; private float DPRatio; private float listItemTextSize = 20; // see R.dimen.button_text_fontsize private float listItemTitleTextSize = 12; // see R.dimen.button_text_small_fontsize private double fpsLimit = 8; private StringBuilder textCur = new StringBuilder(""); // for textCurChar private StringBuilder textRMS = new StringBuilder(""); private StringBuilder textPeak = new StringBuilder(""); private StringBuilder textRec = new StringBuilder(""); private char[] textRMSChar; // for text in R.id.textview_RMS private char[] textCurChar; // for text in R.id.textview_cur private char[] textPeakChar; // for text in R.id.textview_peak private char[] textRecChar; // for text in R.id.textview_rec PopupWindow popupMenuSampleRate; PopupWindow popupMenuFFTLen; PopupWindow popupMenuAverage; boolean bWarnOverrun = true; AnalyzerViews(AnalyzerActivity _activity) { activity = _activity; graphView = (AnalyzerGraphic) activity.findViewById(R.id.plot); Resources res = activity.getResources(); listItemTextSize = res.getDimension(R.dimen.button_text_fontsize); listItemTitleTextSize = res.getDimension(R.dimen.button_text_small_fontsize); DPRatio = res.getDisplayMetrics().density; textRMSChar = new char[res.getString(R.string.textview_RMS_text).length()]; textCurChar = new char[res.getString(R.string.textview_cur_text).length()]; textRecChar = new char[res.getString(R.string.textview_rec_text).length()]; textPeakChar = new char[res.getString(R.string.textview_peak_text).length()]; /// initialize pop up window items list // http://www.codeofaninja.com/2013/04/show-listview-as-drop-down-android.html popupMenuSampleRate = popupMenuCreate( AnalyzerUtil.validateAudioRates( res.getStringArray(R.array.sample_rates)), R.id.button_sample_rate); popupMenuFFTLen = popupMenuCreate( res.getStringArray(R.array.fft_len), R.id.button_fftlen); popupMenuAverage = popupMenuCreate( res.getStringArray(R.array.fft_ave_num), R.id.button_average); setTextViewFontSize(); } // Set text font size of textview_cur and textview_peak // according to space left //@SuppressWarnings("deprecation") private void setTextViewFontSize() { TextView tv = (TextView) activity.findViewById(R.id.textview_cur); // At this point tv.getWidth(), tv.getLineCount() will return 0 Display display = activity.getWindowManager().getDefaultDisplay(); // pixels left float px = display.getWidth() - activity.getResources().getDimension(R.dimen.textview_RMS_layout_width) - 5; float fs = tv.getTextSize(); // size in pixel // shrink font size if it can not fit in one line. final String text = activity.getString(R.string.textview_peak_text); // note: mTestPaint.measureText(text) do not scale like sp. Paint mTestPaint = new Paint(); mTestPaint.setTextSize(fs); mTestPaint.setTypeface(Typeface.MONOSPACE); while (mTestPaint.measureText(text) > px && fs > 5) { fs -= 0.5; mTestPaint.setTextSize(fs); } ((TextView) activity.findViewById(R.id.textview_cur)).setTextSize(TypedValue.COMPLEX_UNIT_PX, fs); ((TextView) activity.findViewById(R.id.textview_peak)).setTextSize(TypedValue.COMPLEX_UNIT_PX, fs); } // Prepare the spectrum and spectrogram plot (from scratch or full reset) // Should be called before samplingThread starts. void setupView(AnalyzerParameters analyzerParam) { graphView.setupPlot(analyzerParam); } // Will be called by SamplingLoop (in another thread) void update(final double[] spectrumDBcopy) { graphView.saveSpectrum(spectrumDBcopy); activity.runOnUiThread(new Runnable() { @Override public void run() { // data will get out of synchronize here invalidateGraphView(); } }); } private double wavSecOld = 0; // used to reduce frame rate void updateRec(double wavSec) { if (wavSecOld > wavSec) { wavSecOld = wavSec; } if (wavSec - wavSecOld < 0.1) { return; } wavSecOld = wavSec; activity.runOnUiThread(new Runnable() { @Override public void run() { // data will get out of synchronize here invalidateGraphView(AnalyzerViews.VIEW_MASK_RecTimeLable); } }); } void notifyWAVSaved(final String path) { String text = "WAV saved to " + path; notifyToast(text); } void notifyToast(final String st) { activity.runOnUiThread(new Runnable() { @Override public void run() { Context context = activity.getApplicationContext(); Toast toast = Toast.makeText(context, st, Toast.LENGTH_SHORT); toast.show(); } }); } private long lastTimeNotifyOverrun = 0; void notifyOverrun() { if (!bWarnOverrun) { return; } long t = SystemClock.uptimeMillis(); if (t - lastTimeNotifyOverrun > 6000) { lastTimeNotifyOverrun = t; activity.runOnUiThread(new Runnable() { @Override public void run() { Context context = activity.getApplicationContext(); String text = "Recorder buffer overrun!\nYour cell phone is too slow.\nTry lower sampling rate or higher average number."; Toast toast = Toast.makeText(context, text, Toast.LENGTH_LONG); toast.show(); } }); } } void showInstructions() { TextView tv = new TextView(activity); tv.setMovementMethod(LinkMovementMethod.getInstance()); tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15); tv.setText(fromHtml(activity.getString(R.string.instructions_text))); PackageInfo pInfo = null; String version = "\n" + activity.getString(R.string.app_name) + " Version: "; try { pInfo = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0); version += pInfo.versionName; } catch (PackageManager.NameNotFoundException e) { version += "(Unknown)"; } tv.append(version); new AlertDialog.Builder(activity) .setTitle(R.string.instructions_title) .setView(tv) .setNegativeButton(R.string.dismiss, null) .create().show(); } void showPermissionExplanation(int resId) { TextView tv = new TextView(activity); tv.setMovementMethod(new ScrollingMovementMethod()); tv.setText(fromHtml(activity.getString(resId))); new AlertDialog.Builder(activity) .setTitle(R.string.permission_explanation_title) .setView(tv) .setNegativeButton(R.string.dismiss, null) .create().show(); } // Thanks http://stackoverflow.com/questions/37904739/html-fromhtml-deprecated-in-android-n @SuppressWarnings("deprecation") public static android.text.Spanned fromHtml(String source) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); // or Html.FROM_HTML_MODE_COMPACT } else { return Html.fromHtml(source); } } void enableSaveWavView(boolean bSaveWav) { if (bSaveWav) { ((TextView) activity.findViewById(R.id.textview_rec)).setHeight((int)(19*DPRatio)); } else { ((TextView) activity.findViewById(R.id.textview_rec)).setHeight((int)(0*DPRatio)); } } @SuppressWarnings("deprecation") void showPopupMenu(View view) { // popup menu position // In API 19, we can use showAsDropDown(View anchor, int xoff, int yoff, int gravity) // The problem in showAsDropDown (View anchor, int xoff, int yoff) is // it may show the window in wrong direction (so that we can't see it) int[] wl = new int[2]; view.getLocationInWindow(wl); int x_left = wl[0]; int y_bottom = activity.getWindowManager().getDefaultDisplay().getHeight() - wl[1]; int gravity = android.view.Gravity.START | android.view.Gravity.BOTTOM; switch (view.getId()) { case R.id.button_sample_rate: popupMenuSampleRate.showAtLocation(view, gravity, x_left, y_bottom); // popupMenuSampleRate.showAsDropDown(view, 0, 0); break; case R.id.button_fftlen: popupMenuFFTLen.showAtLocation(view, gravity, x_left, y_bottom); // popupMenuFFTLen.showAsDropDown(view, 0, 0); break; case R.id.button_average: popupMenuAverage.showAtLocation(view, gravity, x_left, y_bottom); // popupMenuAverage.showAsDropDown(view, 0, 0); break; } } // Maybe put this PopupWindow into a class private PopupWindow popupMenuCreate(String[] popUpContents, int resId) { // initialize a pop up window type PopupWindow popupWindow = new PopupWindow(activity); // the drop down list is a list view ListView listView = new ListView(activity); // set our adapter and pass our pop up window contents ArrayAdapter<String> aa = popupMenuAdapter(popUpContents); listView.setAdapter(aa); // set the item click listener listView.setOnItemClickListener(activity); // button resource ID, so we can trace back which button is pressed listView.setTag(resId); // get max text width Paint mTestPaint = new Paint(); mTestPaint.setTextSize(listItemTextSize); float w = 0; // max text width in pixel float wi; for (String popUpContent : popUpContents) { String sts[] = popUpContent.split("::"); if (sts.length == 0) continue; String st = sts[0]; if (sts.length == 2 && sts[1].equals("0")) { mTestPaint.setTextSize(listItemTitleTextSize); wi = mTestPaint.measureText(st); mTestPaint.setTextSize(listItemTextSize); } else { wi = mTestPaint.measureText(st); } if (w < wi) { w = wi; } } // left and right padding, at least +7, or the whole app will stop respond, don't know why w = w + 23 * DPRatio; if (w < 40 * DPRatio) { w = 40 * DPRatio; } // some other visual settings popupWindow.setFocusable(true); popupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); // Set window width according to max text width popupWindow.setWidth((int)w); // also set button width ((Button) activity.findViewById(resId)).setWidth((int)(w + 5 * DPRatio)); // Set the text on button in loadPreferenceForView() // set the list view as pop up window content popupWindow.setContentView(listView); return popupWindow; } /* * adapter where the list values will be set */ private ArrayAdapter<String> popupMenuAdapter(String itemTagArray[]) { return new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, itemTagArray) { @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { // setting the ID and text for every items in the list String item = getItem(position); String[] itemArr = item.split("::"); String text = itemArr[0]; String id = itemArr[1]; // visual settings for the list item TextView listItem = new TextView(activity); if (id.equals("0")) { listItem.setText(text); listItem.setTag(id); listItem.setTextSize(listItemTitleTextSize / DPRatio); listItem.setPadding(5, 5, 5, 5); listItem.setTextColor(Color.GREEN); listItem.setGravity(android.view.Gravity.CENTER); } else { listItem.setText(text); listItem.setTag(id); listItem.setTextSize(listItemTextSize / DPRatio); listItem.setPadding(5, 5, 5, 5); listItem.setTextColor(Color.WHITE); listItem.setGravity(android.view.Gravity.CENTER); } return listItem; } }; } private void refreshCursorLabel() { double f1 = graphView.getCursorFreq(); textCur.setLength(0); textCur.append(activity.getString(R.string.text_cur)); SBNumFormat.fillInNumFixedWidthPositive(textCur, f1, 5, 1); textCur.append("Hz("); AnalyzerUtil.freq2Cent(textCur, f1, " "); textCur.append(") "); SBNumFormat.fillInNumFixedWidth(textCur, graphView.getCursorDB(), 3, 1); textCur.append("dB"); textCur.getChars(0, Math.min(textCur.length(), textCurChar.length), textCurChar, 0); ((TextView) activity.findViewById(R.id.textview_cur)) .setText(textCurChar, 0, Math.min(textCur.length(), textCurChar.length)); } private void refreshRMSLabel(double dtRMSFromFT) { textRMS.setLength(0); textRMS.append("RMS:dB \n"); SBNumFormat.fillInNumFixedWidth(textRMS, 20*Math.log10(dtRMSFromFT), 3, 1); textRMS.getChars(0, Math.min(textRMS.length(), textRMSChar.length), textRMSChar, 0); TextView tv = (TextView) activity.findViewById(R.id.textview_RMS); tv.setText(textRMSChar, 0, textRMSChar.length); tv.invalidate(); } private void refreshPeakLabel(double maxAmpFreq, double maxAmpDB) { textPeak.setLength(0); textPeak.append(activity.getString(R.string.text_peak)); SBNumFormat.fillInNumFixedWidthPositive(textPeak, maxAmpFreq, 5, 1); textPeak.append("Hz("); AnalyzerUtil.freq2Cent(textPeak, maxAmpFreq, " "); textPeak.append(") "); SBNumFormat.fillInNumFixedWidth(textPeak, maxAmpDB, 3, 1); textPeak.append("dB"); textPeak.getChars(0, Math.min(textPeak.length(), textPeakChar.length), textPeakChar, 0); TextView tv = (TextView) activity.findViewById(R.id.textview_peak); tv.setText(textPeakChar, 0, textPeakChar.length); tv.invalidate(); } private void refreshRecTimeLable(double wavSec, double wavSecRemain) { // consist with @string/textview_rec_text textRec.setLength(0); textRec.append(activity.getString(R.string.text_rec)); SBNumFormat.fillTime(textRec, wavSec, 1); textRec.append(activity.getString(R.string.text_remain)); SBNumFormat.fillTime(textRec, wavSecRemain, 0); textRec.getChars(0, Math.min(textRec.length(), textRecChar.length), textRecChar, 0); ((TextView) activity.findViewById(R.id.textview_rec)) .setText(textRecChar, 0, Math.min(textRec.length(), textRecChar.length)); } private long timeToUpdate = SystemClock.uptimeMillis(); private volatile boolean isInvalidating = false; // Invalidate graphView in a limited frame rate void invalidateGraphView() { invalidateGraphView(-1); } private static final int VIEW_MASK_graphView = 1<<0; private static final int VIEW_MASK_textview_RMS = 1<<1; private static final int VIEW_MASK_textview_peak = 1<<2; private static final int VIEW_MASK_CursorLabel = 1<<3; private static final int VIEW_MASK_RecTimeLable = 1<<4; private void invalidateGraphView(int viewMask) { if (isInvalidating) { return ; } isInvalidating = true; long frameTime; // time delay for next frame if (graphView.getShowMode() != AnalyzerGraphic.PlotMode.SPECTRUM) { frameTime = (long)(1000/fpsLimit); // use a much lower frame rate for spectrogram } else { frameTime = 1000/60; } long t = SystemClock.uptimeMillis(); // && !graphView.isBusy() if (t >= timeToUpdate) { // limit frame rate timeToUpdate += frameTime; if (timeToUpdate < t) { // catch up current time timeToUpdate = t+frameTime; } idPaddingInvalidate = false; // Take care of synchronization of graphView.spectrogramColors and iTimePointer, // and then just do invalidate() here. if ((viewMask & VIEW_MASK_graphView) != 0) graphView.invalidate(); // RMS if ((viewMask & VIEW_MASK_textview_RMS) != 0) refreshRMSLabel(activity.dtRMSFromFT); // peak frequency if ((viewMask & VIEW_MASK_textview_peak) != 0) refreshPeakLabel(activity.maxAmpFreq, activity.maxAmpDB); if ((viewMask & VIEW_MASK_CursorLabel) != 0) refreshCursorLabel(); if ((viewMask & VIEW_MASK_RecTimeLable) != 0 && activity.samplingThread != null) refreshRecTimeLable(activity.samplingThread.wavSec, activity.samplingThread.wavSecRemain); } else { if (! idPaddingInvalidate) { idPaddingInvalidate = true; paddingViewMask = viewMask; paddingInvalidateHandler.postDelayed(paddingInvalidateRunnable, timeToUpdate - t + 1); } else { paddingViewMask |= viewMask; } } isInvalidating = false; } void setFpsLimit(double _fpsLimit) { fpsLimit = _fpsLimit; } private volatile boolean idPaddingInvalidate = false; private volatile int paddingViewMask = -1; private Handler paddingInvalidateHandler = new Handler(); // Am I need to use runOnUiThread() ? private final Runnable paddingInvalidateRunnable = new Runnable() { @Override public void run() { if (idPaddingInvalidate) { // It is possible that t-timeToUpdate <= 0 here, don't know why invalidateGraphView(paddingViewMask); } } }; }