Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MIDI input support #2804

Merged
merged 26 commits into from
May 3, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
748dd68
Implement MIDI input
holly-hacker Aug 28, 2019
8f4ff56
Don't throw if MIDI input cannot be initialized (eg. missing asound)
holly-hacker Aug 28, 2019
d1d88ff
Enable MidiInputHandler for Android and iOS
holly-hacker Aug 28, 2019
b67a97b
Handle Running Status messages
holly-hacker Aug 29, 2019
5e7cab0
Handle midi event edge cases
holly-hacker Aug 29, 2019
0c43156
Include velocity in Midi events
holly-hacker Aug 29, 2019
14d2d11
Move MidiInputHandler to correct namespace
holly-hacker Aug 29, 2019
0a16a99
Manually (and properly) parse MIDI messages
holly-hacker Aug 29, 2019
068b07f
Fix code style issues
holly-hacker Aug 29, 2019
a22e62e
Remove remaining TODO comment
holly-hacker Aug 29, 2019
21d6ada
Fix missing imports in Android and iOS projects
holly-hacker Aug 29, 2019
c11b145
Add Midi keys to InputKey
holly-hacker Aug 29, 2019
78b1c63
Remove accidental using statement
holly-hacker Aug 29, 2019
c669b13
Track Midi events in FrameStatistics
holly-hacker Aug 29, 2019
6f588aa
Update managed-midi to 1.9.6
holly-hacker Sep 5, 2019
0a408fd
Merge remote-tracking branch 'origin/master' into midi-input
smoogipoo Sep 10, 2019
feeda0a
Update managed-midi
smoogipoo Sep 10, 2019
dd1ad39
Merge branch 'master' into midi-input
smoogipoo Apr 27, 2020
3c154a8
Remove unnecessary clone
smoogipoo Apr 27, 2020
c633835
Update managed-midi
holly-hacker May 2, 2020
fc54002
Fix return parameter of OnMidiUp, add doc comments
holly-hacker May 2, 2020
aac4366
Use ButtonEventManager instead of manually calling handleMidiKey*
holly-hacker May 2, 2020
cc44d6c
Improve error handling in case of invalid MIDI messages
holly-hacker May 2, 2020
6769cd1
Fix up debug logging
holly-hacker May 2, 2020
b7fcba7
Merge branch 'master' into midi-input
smoogipoo May 3, 2020
89ad557
Make velocities public
smoogipoo May 3, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion osu.Framework.Android/AndroidGameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public override ITextInputSource GetTextInput()
=> new AndroidTextInput(gameView);

protected override IEnumerable<InputHandler> CreateAvailableInputHandlers()
=> new InputHandler[] { new AndroidKeyboardHandler(gameView), new AndroidTouchHandler(gameView) };
=> new InputHandler[] { new AndroidKeyboardHandler(gameView), new AndroidTouchHandler(gameView), new MidiInputHandler() };

protected override Storage GetStorage(string baseName)
=> new AndroidStorage(baseName, this);
Expand Down
102 changes: 102 additions & 0 deletions osu.Framework.Tests/Visual/Input/TestSceneMidi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;

namespace osu.Framework.Tests.Visual.Input
{
public class TestSceneMidi : FrameworkTestScene
{
public TestSceneMidi()
{
var keyFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};

for (MidiKey k = MidiKey.A0; k < MidiKey.C8; k++)
keyFlow.Add(new MidiKeyHandler(k));

Child = keyFlow;
}

protected override bool OnMidiDown(MidiDownEvent e)
{
Console.WriteLine(e);
return base.OnMidiDown(e);
}

protected override bool OnMidiUp(MidiUpEvent e)
{
Console.WriteLine(e);
return base.OnMidiUp(e);
}

protected override bool Handle(UIEvent e)
{
if (!(e is MouseEvent))
Console.WriteLine("Event: " + e);
return base.Handle(e);
}

private class MidiKeyHandler : CompositeDrawable
{
private readonly Drawable background;

public override bool HandleNonPositionalInput => true;

private readonly MidiKey key;

public MidiKeyHandler(MidiKey key)
{
this.key = key;

Size = new Vector2(50);

InternalChildren = new[]
{
background = new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkGreen,
Alpha = 0,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
new SpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = key.ToString().Replace("Sharp", "#")
}
};
}

protected override bool OnMidiDown(MidiDownEvent e)
{
if (e.Key != key)
return base.OnMidiDown(e);

background.FadeIn(100, Easing.OutQuint);
return true;
}

protected override bool OnMidiUp(MidiUpEvent e)
{
if (e.Key != key)
return base.OnMidiUp(e);

background.FadeOut(100);
return true;
}
}
}
}
2 changes: 1 addition & 1 deletion osu.Framework.iOS/IOSGameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected override void PerformExit(bool immediately)
public override ITextInputSource GetTextInput() => new IOSTextInput(gameView);

protected override IEnumerable<InputHandler> CreateAvailableInputHandlers() =>
new InputHandler[] { new IOSTouchHandler(gameView), keyboardHandler = new IOSKeyboardHandler(gameView), rawKeyboardHandler = new IOSRawKeyboardHandler() };
new InputHandler[] { new IOSTouchHandler(gameView), keyboardHandler = new IOSKeyboardHandler(gameView), rawKeyboardHandler = new IOSRawKeyboardHandler(), new MidiInputHandler() };

protected override Storage GetStorage(string baseName) => new IOSStorage(baseName, this);

Expand Down
8 changes: 8 additions & 0 deletions osu.Framework/Graphics/Drawable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,12 @@ public bool TriggerEvent(UIEvent e)
case JoystickReleaseEvent joystickRelease:
return OnJoystickRelease(joystickRelease);

case MidiDownEvent midiDown:
return OnMidiDown(midiDown);

case MidiUpEvent midiUp:
return OnMidiUp(midiUp);

default:
return Handle(e);
}
Expand Down Expand Up @@ -1954,6 +1960,8 @@ protected virtual void OnFocusLost(FocusLostEvent e)
protected virtual bool OnKeyUp(KeyUpEvent e) => Handle(e);
protected virtual bool OnJoystickPress(JoystickPressEvent e) => Handle(e);
protected virtual bool OnJoystickRelease(JoystickReleaseEvent e) => Handle(e);
protected virtual bool OnMidiDown(MidiDownEvent e) => Handle(e);
protected virtual bool OnMidiUp(MidiUpEvent e) => Handle(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return void (and false in TriggerEvent()).


#endregion

Expand Down
16 changes: 16 additions & 0 deletions osu.Framework/Input/Events/MidiDownEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using JetBrains.Annotations;
using osu.Framework.Input.States;

namespace osu.Framework.Input.Events
{
public class MidiDownEvent : MidiEvent
{
// TODO: velocity

public MidiDownEvent([NotNull] InputState state, MidiKey key)
: base(state, key) { }
}
}
38 changes: 38 additions & 0 deletions osu.Framework/Input/Events/MidiEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Input.States;

namespace osu.Framework.Input.Events
{
public abstract class MidiEvent : UIEvent
{
public readonly MidiKey Key;

/// <summary>
/// Whether a specific key is pressed.
/// </summary>
public bool IsPressed(MidiKey key) => CurrentState.Midi.Keys.IsPressed(key);

/// <summary>
/// Whether any key is pressed.
/// </summary>
public bool HasAnyKeyPressed => CurrentState.Midi.Keys.HasAnyButtonPressed;

/// <summary>
/// List of currently pressed keys.
/// </summary>
public IEnumerable<MidiKey> PressedKeys => CurrentState.Midi.Keys;

public MidiEvent([NotNull] InputState state, MidiKey key)
: base(state)
{
this.Key = key;
}

public override string ToString() => $"{GetType().ReadableName()}({Key})";
}
}
16 changes: 16 additions & 0 deletions osu.Framework/Input/Events/MidiUpEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using JetBrains.Annotations;
using osu.Framework.Input.States;

namespace osu.Framework.Input.Events
{
public class MidiUpEvent : MidiEvent
{
// TODO: velocity

public MidiUpEvent([NotNull] InputState state, MidiKey key)
: base(state, key) { }
}
}
107 changes: 107 additions & 0 deletions osu.Framework/Input/Handlers/MidiInputHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using Commons.Music.Midi;
using osu.Framework.Input.StateChanges;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;

namespace osu.Framework.Input.Handlers
{
public class MidiInputHandler : InputHandler
{
public override bool IsActive => true;
public override int Priority => 0;

private ScheduledDelegate scheduledRefreshDevices;

private readonly Dictionary<string, IMidiInput> openedDevices = new Dictionary<string, IMidiInput>();

public override bool Initialize(GameHost host)
{
// Try to initialize. This can throw on Linux if asound cannot be found.
try {
holly-hacker marked this conversation as resolved.
Show resolved Hide resolved
var unused = MidiAccessManager.Default.Inputs.ToList();
} catch (Exception e) {
Logger.Error(e, RuntimeInfo.OS == RuntimeInfo.Platform.Linux
? "Couldn't list input devices, is libasound2-dev installed?"
: "Couldn't list input devices.");
return false;
}

Enabled.BindValueChanged(e =>
{
if (e.NewValue)
{
host.InputThread.Scheduler.Add(scheduledRefreshDevices = new ScheduledDelegate(refreshDevices, 0, 500));
}
else {
scheduledRefreshDevices?.Cancel();

foreach (var value in openedDevices.Values) {
value.MessageReceived -= onMidiMessageReceived;
}

openedDevices.Clear();
}
}, true);

return true;
}

private void refreshDevices()
{
var inputs = MidiAccessManager.Default.Inputs.ToList();

// check removed devices
foreach (string key in openedDevices.Keys.ToArray()) {
var value = openedDevices[key];

if (inputs.All(i => i.Id != key)) {
value.MessageReceived -= onMidiMessageReceived;
openedDevices.Remove(key);

Logger.Log($"Disconnected MIDI device: {value.Details.Name}");
}
}

// check added devices
foreach (IMidiPortDetails input in inputs) {
if (openedDevices.All(x => x.Key != input.Id)) {
var newInput = MidiAccessManager.Default.OpenInputAsync(input.Id).Result;
newInput.MessageReceived += onMidiMessageReceived;
openedDevices[input.Id] = newInput;

Logger.Log($"Connected MIDI device: {newInput.Details.Name}");
}
}
}

private void onMidiMessageReceived(object sender, MidiReceivedEventArgs e)
{
var events = MidiEvent.Convert(e.Data, e.Start, e.Length);

foreach (MidiEvent midiEvent in events) {
var key = midiEvent.Msb;
var velocity = midiEvent.Lsb;
Logger.Log("Event " + midiEvent);

switch (midiEvent.EventType) {
case MidiEvent.NoteOn:
Logger.Log($"NoteOn: {(MidiKey)key}/{velocity/64f:P}");
PendingInputs.Enqueue(new MidiKeyInput((MidiKey)midiEvent.Msb, true));
break;

case MidiEvent.NoteOff:
Logger.Log($"NoteOff: {(MidiKey)key}/{velocity/64f:P}");
PendingInputs.Enqueue(new MidiKeyInput((MidiKey)midiEvent.Msb, false));
break;
}
}
}
}
}
24 changes: 23 additions & 1 deletion osu.Framework/Input/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public abstract class InputManager : Container, IInputStateChangeHandler
/// The initial input state. <see cref="CurrentState"/> is always equal (as a reference) to the value returned from this.
/// <see cref="InputState.Mouse"/>, <see cref="InputState.Keyboard"/> and <see cref="InputState.Joystick"/> should be non-null.
/// </summary>
protected virtual InputState CreateInitialState() => new InputState(new MouseState { IsPositionValid = false }, new KeyboardState(), new JoystickState());
protected virtual InputState CreateInitialState() => new InputState(new MouseState { IsPositionValid = false }, new KeyboardState(), new JoystickState(), new MidiState());

/// <summary>
/// The last processed state.
Expand Down Expand Up @@ -429,6 +429,20 @@ protected virtual void HandleJoystickButtonStateChange(ButtonStateChangeEvent<Jo
}
}

protected virtual void HandleMidiKeyStateChange(MidiStateChangeEvent midiKeyStateChange)
{
var state = midiKeyStateChange.State;
var key = midiKeyStateChange.Button;
var kind = midiKeyStateChange.Kind;

if (kind == ButtonStateChangeKind.Pressed) {
handleMidiKeyDown(state, key);
}
else {
handleMidiKeyUp(state, key);
}
}

public virtual void HandleInputStateChange(InputStateChangeEvent inputStateChange)
{
switch (inputStateChange)
Expand All @@ -452,6 +466,10 @@ public virtual void HandleInputStateChange(InputStateChangeEvent inputStateChang
case ButtonStateChangeEvent<JoystickButton> joystickButtonStateChange:
HandleJoystickButtonStateChange(joystickButtonStateChange);
return;

case MidiStateChangeEvent midiKeyStateChange:
HandleMidiKeyStateChange(midiKeyStateChange);
return;
}
}

Expand Down Expand Up @@ -495,6 +513,10 @@ protected virtual void HandleMouseButtonStateChange(ButtonStateChangeEvent<Mouse

private bool handleJoystickRelease(InputState state, JoystickButton button) => PropagateBlockableEvent(NonPositionalInputQueue, new JoystickReleaseEvent(state, button));

private bool handleMidiKeyDown(InputState state, MidiKey key) => PropagateBlockableEvent(NonPositionalInputQueue, new MidiDownEvent(state, key));

private bool handleMidiKeyUp(InputState state, MidiKey key) => PropagateBlockableEvent(NonPositionalInputQueue, new MidiUpEvent(state, key));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be updated to use the new ButtonEventManager stuff. See how it's implemented for joysticks / mouse buttons / keys for reference.


/// <summary>
/// Triggers events on drawables in <paramref cref="drawables"/> until it is handled.
/// </summary>
Expand Down
Loading