支持十指演奏的多点触控

目录

介绍

问题:键盘键不像按钮!

使用多点触控的微色调织物应用

实现

多点触控

使用示例

抽象键盘

使用额外数据

兼容性

结论


如果多点触摸屏可用,用户可以用十根手指使用Microtonal Fabric应用程序播放音乐、弹奏和弦、滑音,并在所有组合中使用其他技术。Microtonal Fabric使用统一的方法来涵盖音乐、屏幕键盘等的处理。本文提供了一个易于使用但全面的API,不仅适用于Microtonal Fabric,也适用于广泛的应用。

介绍

这是该系列的第四篇文章,专门讨论使用屏幕键盘(包括微音键盘)进行音乐研究:

  1. 使用同构计算机键盘进行音乐研究
  2. 使用半音格键盘进行微调音乐研究
  3. 声音生成器,网络音频合成器
  4. 本文

最后三篇文章专门介绍名为 Microtonal Fabric 的项目,这是一个基于 WebAudio API 的微调音乐平台。它是一个框架,用于构建通用或定制的微音键盘乐器、微音实验和计算、音乐研究以及具有可能的远程选项的音乐课程教学。该平台提供了多个使用共享JavaScript组件在Web浏览器中执行的应用程序。

另请参阅我在微调社区网站Xenharmonic Wiki上的页面。除了微调织物链接外,还有一些关于不同微调主题和个性的有用链接。

大多数Microtonal Fabric应用程序都允许用户在浏览器中播放音乐。如果多点触控屏幕可用,用户可以用十个手指演奏。多点触控界面所需的功能并不像乍一看那么简单。本文解释了问题和解决方案。它显示了如何从代码的另一部分抽象出多点触控行为,如何重用,以及如何被不同类型的屏幕键盘使用。

在所讨论的方法中,本多点触控解决方案的应用并不局限于音乐键盘。键盘的视图模型看起来像HTMLSVG元素的集合,具有两种状态:激活(向下)或停用(向上)。用户或软件可以通过许多不同的方式修改状态。首先,让我们考虑整个问题。

问题:键盘键不像按钮!

那么,为什么键盘的多点触控会出现一些问题呢?嗯,主要是因为思维的惯性可能会把我们引向错误的方向。

最常见的方法是获取某个键盘的一组键,并为每个键盘附加一些事件处理程序。它看起来很自然,但根本无法工作。

让我们看看用十个手指演奏需要什么。在屏幕上,我们有三种区域:1)某些键盘键的区域,2)键盘未被键占用的区域,以及3)键盘外的区域。当一根或多根手指触摸某个键区域内的屏幕(情况#1)时,将创建一个 Touch对象。调用低级触摸事件,但仅当给定键的区域中没有更多Touch对象时,才应调用要处理以调用键激活的语义级键盘事件。同样,当手指从屏幕上移开时,仅当键区域中没有其他Touch对象时,才应调用语义级键盘事件。

但这还不够。如果手指只是在屏幕上滑动,也可以更改按键激活。当滑动手指进入该区域或离开某个键的区域时,它可以来自或移动到#1#3类型的任何区域。根据给定密钥区域中是否存在其他Touch对象,它还可以更改此密钥的激活状态。最典型的情况是,当手指在两个或多个键上滑动时,就会发生这种情况。这种技术被称为glissando。当触摸事件分别附加到每个键时,这是无法实现的。

为什么?通过与指针事件的比较,它很容易理解。这些事件包括事件pointerleavepointerout。这些事件对于由鼠标或触摸板控制的单个指针非常有意义。但是,在触摸事件中没有类似的内容。键盘键的行为完全不像UI按钮。尽管表面上相似,但它们完全不同。

实现语义级多点触控事件的所有组合的唯一方法是将低级触控事件处理为包含所有键的某个元素。在Microsoft Fabric代码中,这是表示整个键盘的元素。让我们在实现多点触控一节中了解它是如何实现的。

在查看实现之前,读者可能希望查看使用多点触控的可用Microtonal Fabric应用程序。对于所有应用程序,都可以进行现场播放。对于每个应用程序,可以在下面找到实时播放URL

使用多点触控的微色调织物应用

应用程序

源代码

现场播放

Multi-EDO

./Multi-EDO

Multi-EDO

29-EDO

./29-EDO

29-EDO

微调游乐场

./playground

Aura's Diatonic Scales
Common-practice 12-EDO
Shruti Scales
Traditional Chinese Tonal System
Microtonal Playground Customization Demo

 Kite Giedraitis键盘
(开发中)

./Kite.Giedraitis

风筝吉德拉蒂斯

实现

这个想法是:我们需要一个单独的单元,从代表键盘的UI元素集中抽象出来。我们将为表示整个键盘的单个HTMLSVG控件设置一些触摸事件。这些事件应由某些事件来解释,这些事件可能与键盘键相关,也可能与键盘键无关。为了从用户那里提取有关密钥的信息,我们将使用反转控制

触摸功能是通过使用对setMultiTouch函数的单个调用将事件附加到表示整个键盘的某个元素来实现的,它通过三个回调处理器获取键配置信息并调用语义级键事件。让我们看看它是如何工作的。

多点触控

setMultiTouch函数假定多点触控敏感区域的以下UI模型:container包含一个或多个HTMLSVG子元素的HTMLSVG 素,它们可以是直接或间接子元素。

该函数接受四个输入参数,container和三个处理程序:

  • container:处理多点触控事件的 HTML 或 SVG
  • elementSelector:在container中选择相关子项的处理程序。
    配置文件:element => bool
    如果此处理程序返回false,则忽略该事件。从本质上讲,用户代码使用这个处理程序来定义HTML或SVG元素,这些元素将被解释为container表示的键盘的键。
  • elementHandler:用于实现主键盘功能的处理程序。
    配置文件:(element, Touch touchObject, bool on, touchEvent event) => undefined
    处理程序用于实现主要功能,例如,生成声音以响应键盘事件;处理程序接受element、触摸对象和布尔on参数,显示这是“开”还是“关”操作。基本上,此处理程序调用一个通用语义处理程序,该处理程序可以以不同的方式触发,例如,通过键盘或鼠标。从本质上讲,它实现了激活或停用键盘键(由element表示)时触发的操作,具体取决于on的值。
  • sameElementHandler:用于处理同一元素中的事件的处理程序

配置文件:(element, Touch touchObject) => undefined

“ui.components/multitouch.js”

"use strict";

const setMultiTouch = (
    container,
    elementSelector,
    elementHandler,
    sameElementHandler,
) => {

    if (!elementSelector)
        return;
    if (!container) container = document;

    const assignEvent = (element, name, handler) => {
        element.addEventListener(name, handler,
            { passive: false, capture: true });
    };
    const assignTouchStart = (element, handler) => {
        assignEvent(element, "touchstart", handler);
    };
    const assignTouchMove = (element, handler) => {
        assignEvent(element, "touchmove", handler);
    };
    const assignTouchEnd = (element, handler) => {
        assignEvent(element, "touchend", handler);
    };

    const isGoodElement = element => element && elementSelector(element); 
    const elementDictionary = {};
    
    const addRemoveElement = (touch, element, doAdd, event) => {
        if (isGoodElement(element) && elementHandler)
            elementHandler(element, touch, doAdd, event);
        if (doAdd)
            elementDictionary[touch.identifier] = element;
        else
            delete elementDictionary[touch.identifier];
    }; //addRemoveElement

    assignTouchStart(container, ev => {
        ev.preventDefault();
        if (ev.changedTouches.length < 1) return;
        const touch = ev.changedTouches[ev.changedTouches.length - 1];
        const element =
            document.elementFromPoint(touch.clientX, touch.clientY);
        addRemoveElement(touch, element, true, ev);    
    }); //assignTouchStart
    
    assignTouchMove(container, ev => {
        ev.preventDefault();
        for (let touch of ev.touches) {
            let element =
                document.elementFromPoint(touch.clientX, touch.clientY);
            const goodElement = isGoodElement(element); 
            const touchElement = elementDictionary[touch.identifier];
            if (goodElement && touchElement) {
                if (element == touchElement) {
                    if (sameElementHandler)
                        sameElementHandler(element, touch, ev)
                        continue;
                    } //if same
                addRemoveElement(touch, touchElement, false, ev);            
                addRemoveElement(touch, element, truem, ev);
            } else {
                if (goodElement)
                    addRemoveElement(touch, element, goodElement, ev);
                else
                    addRemoveElement(touch, touchElement, goodElement, ev);
            } //if    
        } //loop
    }); //assignTouchMove
    
    assignTouchEnd(container, ev => {
        ev.preventDefault();
        for (let touch of ev.changedTouches) {
            const element =
                document.elementFromPoint(touch.clientX, touch.clientY);
            addRemoveElement(touch, element, false, ev);
        } //loop
    }); //assignTouchEnd

};

setMultiTouch实现的中心点是对 document.elementFromPoint 的调用。这样,就可以找到与 Touch事件数据相关的元素。找到元素时,会检查这是否是表示键盘键的元素,并且该isGoodElement函数使用处理程序执elementSelector行此操作。如果是,则调用处理程序elementHandlersameElementHandler,具体取决于事件数据。这些调用用于处理触摸事件 "touchstart""touchmove""touchend"

让我们看看应用程序如何使用setMultiTouch

使用示例

在应用程序29-EDO中可以找到一个非常典型的使用示例。它提供了多种键盘布局和两种不同的音调系统(29-EDO和常用的12-EDO),但键盘重用了许多常用代码。对于所有键盘布局,elementSelector是基于这样一个事实:所有键盘键都是矩形 SVG元素SVGRectElement,但键盘不是,它们由 SVG 元素 SVGSVGElement 表示。

此外,键盘有一个通用的语义级处理程序handler(element, on),它控制element表示的键的突出显示和音频操作,具体取决于其布尔激活状态on。这是通过触摸 API 指针API 使用的常用处理程序。处理程序也可以由代码通过计算机键盘或其他控制元素激活。特别是,它可以由Microtonal Fabric序列记录器调用。它使调用setMultiTouch变得非常简单:

“29-EDO/ui/keyboard.js”

setMultiTouch(
    element,
    element => element.constructor == SVGRectElement,
    (element, _, on) => handler(element, on));

在这里,第一个element表示一个键盘,一个 SVGRectElement,处理程序的element参数表示键盘键。

在另一个地方,该element.constructor == SVGCircleElement表达式用于只有圆形键的应用程序。

有一个更专用的例子,其中键元素的选择是通过一些抽象JavaScript类的方法执行的。

“ui.components/abstract-keyboard.js”

class AbstractKeyboard {
    //...
    setMultiTouch(
        parentElement,
        keyElement => this.isTouchKey(parentElement, keyElement),
        (keyElement, _, on) => handler(keyElement, on));
}

从编程的角度来看,第二个例子更有趣。让我们更详细地讨论一下。

抽象键盘

最后一个示例显示了一个额外的抽象层:设置多点触控功能的通用代码段被放置在表示抽象键盘的类中。潜在地,多个终端键盘类可以从AbstractKeyboard派生,并重用多点触控设置和其他常见的键盘功能。

AbstractKeyboard类中,调用setMultiTouch中使用的函数没有完全定义:this.isTouchKey函数根本没有定义,handler函数已经定义了,但它取决于尚未定义的函数。这些函数应该在派生自AbstractKeyboard的所有终端类中实现。但是如何保证呢?

为了保证这一点,我提出了一种我称之为接口的新技术(“agnostic/interfaces.js”)。键盘类不扩展适当的接口类,它们只是实现在特定接口中定义的适当函数,即该IInterface类的后代类(agnostic/interfaces.js“)。其IInterface唯一目的是提供一种早期发现问题的方法,因为缺乏某种程度的严格接口的完全实现。要理解严格性的概念,请看同一文件中的const IInterfaceStrictness,它是不言自明的。

在应用程序Microtonal Playground的示例中,终端键盘类不是直接通过调用setMultiTouch来实现多点触控行为,而是通过以下继承图继承AbstractKeyboard来实现:

AbstractKeyboard(“ui.components\abstract-keyboard.js”)◁─ (“ui.components\grid-keyboard.js”) ― (“playground\ui\playground-keyboard.js”)GridKeyboardPlaygroungKeyboard

除了继承自AbstractKeyboard,终端类还应该实现IKeyboardGeometry

IInterface(“不可知/interfaces.js”)— (“ui.components\abstract-keyboard.js”)IKeyboardGeonetry

“ui.components\abstract-keyboard.js”

class IKeyboardGeometry extends IInterface {
    createKeys(parentElement) {}
    createCustomKeyData(keyElement, index) {}
    highlightKey(keyElement, keyboardMode) {}
    isTouchKey(parentElement, keyElement) {} // for touch interface
    get defaultChord() {} // should return array of indices of keys in default chord
    customKeyHandler(keyElement, keyData, on) {} // return false to stop embedded handling
}

此接口的实现保证了AbstractKeyboard函数的正确工作,并且IKeyboardGeometry完全实现的事实由AbstractKeyboard类的构造函数验证。

如果接口的实现不令人满意,则此验证会引发异常:

class AbstractKeyboard {

    #implementation = { mode: 0, chord: new Set(), playingElements: new(Set), chordRoot: -1, useHighlight: true };
    derivedImplementation = {};
    derivedClassConstructorArguments = [undefined];

    constructor(parentElement, ...moreArguments) {
        IKeyboardGeometry.throwIfNotImplemented(this);
        this.derivedClassConstructorArguments = moreArguments;
        //...
    }

    //...

}

有关IInterface.throwIfNotImplemented详细信息,请参阅源代码“agnostic/interfaces.js”。接口实现的验证基于构造函数在运行时执行的终端类的反射。它检查所有接口函数、属性获取器和设置器,并根据所需的严格性检查每个函数的参数数。在我们的例子中,它发生在应用程序Microtonal Playground的终端应用程序级类PlaygroungKeyboard的构建过程中。应用程序本身值得单独撰写一篇文章。

显然,这只是对一些精心设计的编译语言中发现的接口机制的模仿。它会稍后在运行时验证接口的实现,但会尽快进行。当然,该机制不能改变软件的行为,它只能促进调试,从而促进软件开发。该机制确实有效,但我认为它是实验性的,并且确实理解它的价值是可以讨论的。如有任何批评或建议,我将不胜感激。

使用额外数据

请注意,没有一个示例使用处理程序handler的第二个参数参数,接受为第二个setMultiTouch参数,即Touch类型的touchObject参数。并且不使用TouchEvent 类型的最后一个处理程序event参数。此外, setMultiTouch的最后一个参数,处理程序sameElementHandler没有被使用。但是,这些参数功能齐全,可以使用。它们保留用于高级用途。

传递给handlerTouch参数用于获取原始触摸事件的附加信息。特别是,我尝试使用值Touch.radiusXTouch.radiusY。我的想法是用手指评估触摸屏的接触区域。该信息可用于得出压力量,因此,根据该值调整音量,以增加一些性能动态。然而,我的实验表明,表演者对这个值的控制很差,它与实际压力不同。这些Touch成员属性的更主要的问题是,它们的更改不会触发任何触摸事件;仅当触摸的质心发生更改时,才会触发事件。然而,很明显,这些Touch数据对于实现一些高级效果是有用的。

处理程序的event参数用于实现弹拨弦乐器。该装置的主要目的是找出哪个取景器定义了声音频率,以防多个手指将同一根弦压在指板上。用于模仿弹拨弦乐器的乐器应用目前正在开发中。

setMultiTouch函数的sameElementHandler参数在触摸事件被触发时被调用,当触摸的位置保持在与前一个触摸事件相同的element中时。显然,此类事件不应修改element的状态。同时,此类事件可用于实现更精细的技术。例如,手指在同一键内的运动可以解释为手指控制的颤音

上面提到的所有更精细的技术都是进一步研究的问题。

兼容性

Microtonal Fabric功能基于高级和现代的JavaScipt和浏览器功能,在与某些Web浏览器一起使用时可能会失败。基本上,它可以在所有基于Chromium的浏览器上正常工作,或者更准确地说,适用于基于BlinkV8引擎的现代浏览器。它意味着ChromiumChrome和其他一些衍生浏览器。即使是最新的阿纳海姆Microsoft Edge也能正常工作。

不幸的是,现在的Mozilla浏览器出现了一些问题,无法正确运行Microtonal Fabric应用程序。特别是,Firefox for Linux无法正确处理本文中描述的触摸事件Microtonal Fabric用户和我密切关注兼容性,因此如果发生任何变化,我会尝试更新兼容性信息。

结论

我们有一个机制和一个抽象层,可以用作更通用的触摸API的语义包装器,它更接近触摸屏硬件。此层以适合屏幕键盘或其他功能相似的UI元素的视图模型的形式呈现事件。使用此层的代码调用单个setMultiTouch函数并传递其语义处理程序。

或者,在此层之上,用户可以使用基于IKeyboardGeometryJavaScript类的更具体且可能不太通用的API

这两个选项提供了一个全面的语义级机制,用于实现基于多点触控触摸屏的屏幕键盘行为的所有方面。同时,它使UI对其他输入法(如物理计算机键盘、鼠标或触摸板)保持开放。

https://www.codeproject.com/Articles/5362252/Multitouch-Support

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值