Add velocity sensitivity preferences

A fairly simple feature - preferences for velocity sensitivity, but a
fair amount of UI infrastructure. This patch includes a new
KnobPreference, changes to KnobView to support a horizontal layout
(which works better in a layout with more compressed vertical space),
and of course the plumbing of the preference itself.

Also some rework of the touch handler in the KeyboardView. This patch
simplifies the logic a bit and fixes a long-standing bug in which the
pressure was always read from pointer index 0 rather than the pointer
index of the key being pressed.
master
Raph Levien 11 years ago
parent f4e3d097fe
commit c7c16b022f
  1. 13
      android/res/layout/knobpreflayout_va.xml
  2. 12
      android/res/layout/knobpreflayout_vs.xml
  3. 2
      android/res/values/attrs.xml
  4. 8
      android/res/values/strings.xml
  5. 12
      android/res/xml/preferences.xml
  6. 5
      android/src/com/levien/synthesizer/android/ui/PianoActivity2.java
  7. 72
      android/src/com/levien/synthesizer/android/widgets/keyboard/KeyboardView.java
  8. 66
      android/src/com/levien/synthesizer/android/widgets/knob/KnobPreference.java
  9. 76
      android/src/com/levien/synthesizer/android/widgets/knob/KnobView.java

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<com.levien.synthesizer.android.widgets.knob.KnobView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.levien.synthesizer"
android:id="@+id/knob"
app:value="0"
app:min="0"
app:max="127"
app:numberformat="%3.0f"
app:horizontal="true"
android:layout_width="200dp"
android:layout_height="match_parent"
app:label="" />

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<com.levien.synthesizer.android.widgets.knob.KnobView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.levien.synthesizer"
android:id="@+id/knob"
app:value="0"
app:min="0"
app:max="2"
app:horizontal="true"
android:layout_width="200dp"
android:layout_height="match_parent"
app:label="" />

@ -8,6 +8,8 @@
<attr name="min" format="float" />
<attr name="max" format="float" />
<attr name="label" format="string" />
<attr name="numberformat" format="string" />
<attr name="horizontal" format="boolean" />
</declare-styleable>
<declare-styleable name="PianoView">
<attr name="octaves" format="integer" />

@ -97,4 +97,12 @@
</string-array>
<string name="pref_keyboardType_default">2row</string>
<string name="pref_velAvg">Velocity average value</string>
<string name="pref_velAvg_summary">MIDI velocity at medium pressure</string>
<string name="pref_velAvg_default">64.0</string>
<string name="pref_velSens">Velocity sensitivity</string>
<string name="pref_velSens_summary">Sensitivity of MIDI velocity to pressure</string>
<string name="pref_velSens_default">0.5</string>
</resources>

@ -6,4 +6,16 @@
android:entries="@array/pref_keyboardType_entries"
android:entryValues="@array/pref_keyboardType_values"
android:defaultValue="@string/pref_keyboardType_default" />
<com.levien.synthesizer.android.widgets.knob.KnobPreference
android:key="vel_avg"
android:title="@string/pref_velAvg"
android:summary="@string/pref_velAvg_summary"
android:widgetLayout="@layout/knobpreflayout_va"
android:defaultValue="@string/pref_velAvg_default" />
<com.levien.synthesizer.android.widgets.knob.KnobPreference
android:key="vel_sens"
android:title="@string/pref_velSens"
android:summary="@string/pref_velSens_summary"
android:widgetLayout="@layout/knobpreflayout_vs"
android:defaultValue="@string/pref_velSens_default" />
</PreferenceScreen>

@ -108,6 +108,7 @@ public class PianoActivity2 extends SynthActivity implements OnSharedPreferenceC
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(prefs, "keyboard_type");
onSharedPreferenceChanged(prefs, "vel_sens");
}
@Override
@ -122,6 +123,10 @@ public class PianoActivity2 extends SynthActivity implements OnSharedPreferenceC
if (key.equals("keyboard_type")) {
String keyboardType = prefs.getString(key, "2row");
keyboard_.setKeyboardSpec(KeyboardSpec.make(keyboardType));
} else if (key.equals("vel_sens") || key.equals("vel_avg")) {
float velSens = prefs.getFloat("vel_sens", 0.5f);
float velAvg = prefs.getFloat("vel_avg", 64);
keyboard_.setVelocitySensitivity(velSens, velAvg);
}
}

@ -51,6 +51,8 @@ public class KeyboardView extends View {
offset_ = 0.0f;
zoom_ = 1.0f;
setKeyboardSpec(KeyboardSpec.make2Row());
velSens_ = 0.5f;
velAvg_ = 64;
}
public void setKeyboardSpec(KeyboardSpec keyboardSpec) {
@ -70,6 +72,11 @@ public class KeyboardView extends View {
}
}
public void setVelocitySensitivity(float velSens, float velAvg) {
velSens_ = velSens;
velAvg_ = velAvg;
}
public void setScrollZoom(float offset, float zoom) {
offset_ = offset;
zoom_ = zoom;
@ -138,51 +145,42 @@ public class KeyboardView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
int actionCode = action & MotionEvent.ACTION_MASK;
int actionCode = event.getActionMasked();
boolean redraw = false;
if (actionCode == MotionEvent.ACTION_DOWN) {
int pointerId = event.getPointerId(0);
float x = event.getX();
float y = event.getY();
float pressure = event.getPressure();
redraw |= onTouchDown(pointerId, x, y, pressure);
} else if (actionCode == MotionEvent.ACTION_POINTER_DOWN) {
int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int pointerId = event.getPointerId(pointerIndex);
switch (actionCode) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
int index = actionCode == MotionEvent.ACTION_POINTER_DOWN ? event.getActionIndex() : 0;
int pointerId = event.getPointerId(index);
if (pointerId < FINGERS && pointerId >= 0) {
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
float pressure = event.getPressure();
float x = event.getX(index);
float y = event.getY(index);
float pressure = event.getPressure(index);
redraw |= onTouchDown(pointerId, x, y, pressure);
}
} else if (actionCode == MotionEvent.ACTION_UP) {
int pointerId = event.getPointerId(0);
float x = event.getX();
float y = event.getY();
float pressure = event.getPressure();
redraw |= onTouchUp(pointerId, x, y, pressure);
} else if (actionCode == MotionEvent.ACTION_POINTER_UP) {
int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
int pointerId = event.getPointerId(pointerIndex);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
index = actionCode == MotionEvent.ACTION_POINTER_UP ? event.getActionIndex() : 0;
pointerId = event.getPointerId(index);
if (pointerId < FINGERS && pointerId >= 0) {
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
float pressure = event.getPressure();
float x = event.getX(index);
float y = event.getY(index);
float pressure = event.getPressure(index);
redraw |= onTouchUp(pointerId, x, y, pressure);
}
} else if (actionCode == MotionEvent.ACTION_MOVE) {
for (int pointerIndex = 0; pointerIndex < event.getPointerCount(); pointerIndex++) {
int pointerId = event.getPointerId(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
for (index = 0; index < event.getPointerCount(); index++) {
pointerId = event.getPointerId(index);
if (pointerId < FINGERS) {
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
float pressure = event.getPressure();
float x = event.getX(index);
float y = event.getY(index);
float pressure = event.getPressure(index);
redraw |= onTouchMove(pointerId, x, y, pressure);
}
}
break;
}
if (redraw) {
invalidate();
@ -208,8 +206,7 @@ public class KeyboardView extends View {
}
private int computeVelocity(float pressure) {
float sensitivity = 0.5f; // TODO: configure
int velocity = (int) ((0.5f + sensitivity * (pressure - 0.5f)) * 127.0f + 0.5f);
int velocity = (int) (velSens_ * (pressure - 0.5f) * 127.0f + velAvg_ + 0.5f);
if (velocity < 1) {
velocity = 1;
} else if (velocity > 127) {
@ -279,6 +276,9 @@ public class KeyboardView extends View {
return NOTE_NAMES[note % 12] + Integer.toString(octave);
}
private float velSens_;
private float velAvg_;
private MidiListener midiListener_;
private Rect drawingRect_;

@ -0,0 +1,66 @@
/*
* Copyright 2013 Google Inc.
*
* 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 com.levien.synthesizer.android.widgets.knob;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import com.levien.synthesizer.R;
/**
* A wrapper so that a knob can be used in the preference dialog
*/
public class KnobPreference extends Preference {
public KnobPreference(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getFloat(index, 0.0f);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setValue(restoreValue ? getPersistedFloat(value_) : (Float)defaultValue);
}
private void setValue(float value) {
value_ = value;
persistFloat(value);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
KnobView knobView = (KnobView)view.findViewById(R.id.knob);
knobView.setValue(value_);
knobView.setKnobListenerUp(new KnobListener() {
public void onKnobChanged(double newValue) {
setValue((float)newValue);
}
});
}
private float value_;
}

@ -16,6 +16,8 @@
package com.levien.synthesizer.android.widgets.knob;
import java.util.Locale;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@ -50,6 +52,9 @@ public class KnobView extends View {
min_ = a.getFloat(R.styleable.KnobView_min, 0.0f);
max_ = a.getFloat(R.styleable.KnobView_max, 1.0f);
label_ = a.getString(R.styleable.KnobView_label);
String numberFormat = a.getString(R.styleable.KnobView_numberformat);
numberFormat_ = numberFormat != null ? numberFormat : "%.2f";
horizontal_ = a.getBoolean(R.styleable.KnobView_horizontal, false);
a.recycle();
// Set up the drawing structures.
@ -99,7 +104,11 @@ public class KnobView extends View {
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if (listenerUp_ != null) {
listenerUp_.onKnobChanged(getValue());
}
break;
}
}
@ -113,6 +122,13 @@ public class KnobView extends View {
listener_ = listener;
}
/**
* Sets the listener to receive events when the knob's value finalizes (on touch up).
*/
public void setKnobListenerUp(KnobListener listener) {
listenerUp_ = listener;
}
/**
* Sets the value for the knob when it is turned all the way counter-clockwise.
*/
@ -178,7 +194,7 @@ public class KnobView extends View {
rectF_.right = center + rectF_.height() / 2;
}
float border = textHeight_ + rectF_.width() * 0.05f;
float border = (horizontal_ ? 0.5f * textHeight_ : textHeight_) + rectF_.width() * 0.05f;
// Draw indicator.
@ -214,17 +230,17 @@ public class KnobView extends View {
paint_);
// Draw text.
String knobValueString = String.format("%.2f", getValue());
String knobValueString = String.format(Locale.getDefault(), numberFormat_, getValue());
Typeface typeface = Typeface.DEFAULT_BOLD;
paint_.setColor(Color.BLACK);
paint_.setTypeface(typeface);
paint_.setTextAlign(Align.CENTER);
paint_.setTextAlign(horizontal_ ? Align.RIGHT : Align.CENTER);
float x = horizontal_ ? rectF_.left: rectF_.centerX();
float y = horizontal_ ? rectF_.centerY() + 0.4f * textHeight_ :
rectF_.top + 0.8f * textHeight_;
paint_.setSubpixelText(true);
paint_.setStyle(Style.FILL);
canvas.drawText(knobValueString,
rect_.centerX(),
rectF_.top + 0.8f * textHeight_,
paint_);
canvas.drawText(knobValueString, x, y, paint_);
if (label_ != null) {
paint_.setTypeface(Typeface.DEFAULT);
@ -237,45 +253,12 @@ public class KnobView extends View {
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// Specify that 100 is preferred for both dimensions.
int width = 0;
int height = 0;
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
width = widthSize;
break;
case MeasureSpec.UNSPECIFIED:
width = 100;
break;
int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
if (!horizontal_) {
// Make it square
width = height = Math.min(width, height);
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
height = heightSize;
break;
case MeasureSpec.UNSPECIFIED:
height = 100;
break;
}
// Make it square.
if (width > height && widthMode != MeasureSpec.EXACTLY) {
width = height;
}
if (height > width && heightMode != MeasureSpec.EXACTLY) {
height = width;
}
setMeasuredDimension(width, height);
}
@ -313,6 +296,8 @@ public class KnobView extends View {
private double knobValue_;
private double min_;
private double max_;
private String numberFormat_;
private boolean horizontal_;
// Structures used in drawing that we don't want to reallocate every time we draw.
private Paint paint_;
@ -334,4 +319,5 @@ public class KnobView extends View {
// Object listening for events when the knob's value changes.
private KnobListener listener_;
private KnobListener listenerUp_;
}

Loading…
Cancel
Save