简介
从XSpeak开始,我就希望它能给用户提供最好的体验:简单、快速、响应迅速。由于它是一款录音应用,其主要组件之一就是用户用来录音的按钮。
实际上,这是我们在许多应用中都很熟悉的一种控制方式。最简单的例子就是iPhone上的标准录音备忘录应用。
这个按钮看起来简单,但它有两个功能:开始录制和停止录制。然而,在幕后,启动整个流程需要很多步骤,这远非简单。在本文中,我将涵盖录制按钮的技术和可用性方面,并尝试解释为什么让它完美运行非常重要,以及为什么它不像看起来那么简单。
XSpeak 是一款完全私有的应用程序,在对话过程中实时记录会议笔记,并通过设备端人工智能实时为您提供建议、相关背景、事实信息等帮助。
完美按钮
如果录制按钮是完美的,那么它应该是:
- 功能性的
- 响应迅速
当我说功能时,我的意思是它应该能在启用时,在我按下它时开始和停止录制。按钮应该表明状态已切换。对于通常需要数百毫秒的操作,反馈应该是即时的。
让我们想象一下反馈不是即时的,并且状态之间有一些进展。在这种情况下,用户:
- 必须等待以确保录制实际上已开始.
- 脱离了他们的自然流程。他们没有专注于自己的业务,而是在监控应用程序的状态.
- 因此产生了不必要的认知负担:"发生了什么?它开始了吗?我应该等待多久?"
- 失去了对界面的控制。他们无法纠正错误并按下停止。
- 感到轻微的沮丧,因为应用程序暂时阻止他们做想做的事情。
- 怀念从工具中期望的即时、触觉反馈。
这种不适感对某些人来说可能微不足道。然而,当用户一天做这么多次时,它可能会增加显著的开销。而这并不是我们应该期望从一个辅助工具中得到的。
一个完美的录音按钮需要具备两种品质:它必须功能正常,并且反应灵敏。按下按钮不应等待处理流程.
不完美的案例:
- 用户按下按钮.
- 按钮被禁用,显示进度状态或根本无反应。
- 一段时间后,按钮恢复正常功能,录音开始.
背后
为什么开始录音不简单?当你按下按钮时,XSpeak中会发生以下情况:

所有这些操作都是异步进行的。这意味着在每次操作期间我们都会失去流程,当我们恢复时,世界可能已经发生了变化:用户可能又按了几次按钮,之前可用的资源可能已经变得不可用,等等。
此外,它启动了辅助管理线程来重启混音器以防止两个源之间出现偏差,并重启转写器以防止模型上下文溢出。
这开始得相当不错,不是吗?大概在那之后,你对这个简单按钮的看法会改变,对此表示歉意 :)
让我们看看不同的应用是如何处理这种情况或类似复杂性的.
iPhone Voice Memos

iOS 26.5

当我按下开始时,它就开始了。当我按下停止时,它就停止了。没有更多了.
Otter
如您所见,按钮在开始录制时会变成禁用状态。这让我每次按下时都感觉有点不舒服。我感觉它没有反应且感觉很重。而且我需要等待才能停止录制.
Talat
Talat 0.11.5
录制开始时按钮被禁用。好在录制启动非常快。不过,它仍然会带来一种微小的无响应感觉。
MacWhisper
启动按钮按下和停止按钮出现之间有轻微的延迟。此外,在我开始录制后,按钮的位置会改变,这需要我付出额外的认知努力来寻找它。
萤火虫
启动时按钮被锁定。
XSpeak(XSpeak)
XSpeak 3.7
如您所见,按钮能即时响应用户操作。如果你改变主意,它也能即时反向响应.
实现
我不会在这里写下我考虑和尝试的所有方法。相反,我会从一种朴素的方法开始,直到我实现的解决方案。
让我们同意我们需要按钮的即时反馈,并且在我们的启动链期间不会禁用它。此外,让我们声明我们的状态:
每个状态可以是started或stopped。我们的目标:最终保持它们的一致性,并且永远不阻塞用户。
朴素的方法是当用户按下按钮时:
- 将
S_ui改为started. - 启动创业管道.
然而,明显的问题将是竞态条件。想象以下操作顺序:
最后,我们有S_ui = stopped和S_real = started。
我们必须使这个管道线性化,以防止出现竞争条件。首先要做的是防止启动和停止操作同时运行。我们将使用队列来实现这一点:
操作按提交顺序逐个运行。没有两个操作会重叠。
我们还需要引入一个额外的状态:
当我们想要开始或停止录制时,我们会将操作提交到队列中。这样,没有两个操作会重叠,每个操作都会等待它的时间。因此,我们总是有S_ui等于S_op上一次操作的结果。
然而,这会导致工作延迟,无法立即开始。我们仍然希望立即向用户提供反馈。为此,我们将与S_ui 和来自 Queue 的 S_real。这意味着当我们按下按钮时,S_ui 会立即改变,然后才会提交工作。这个解决方案给我们带来了以下挑战:
- 当实际队列操作开始时,世界可能已经发生了变化,操作可能不再必要了。
- 如果队列增长,可能会有显著的延迟。想象一下按钮连续被按100次的情况。我们将有100次操作,每次0.5秒,导致50秒的工作量。
在我们等待操作开始期间,世界可能已经发生了变化。这意味着用户可能已经停止了录制,重新开始,甚至在极端情况下,可能进行了多次操作。为了确定操作是否仍然有意义,我们应该比较每个操作的S_op随着当前S_ui和S_real如果S_op是started并且S_ui 是 stopped,我们不应该再开始了,所以我们直接退出。当 S_op 是 stopped 时也是一样,但是 S_ui 是 已开始。此外,如果 S_op 已经等于 S_real,那么工作已经完成了,所以我们同样退出。
这意味着第一个 和 最早的 操作,其
S_op等于当前S_ui且不等于S_real的将执行工作。这项更改显著减少了提交和实际工作开始之间的延迟。
我们还有一件事要做,以进一步提高性能。想象以下操作顺序:
如果用户在启动操作正在进行时按下停止,我们必须等待启动操作完成。这会导致不必要的延迟和额外的工作。
为了解决这个问题,我们将我们操作期间安排异步工作的每个挂起点视为潜在的中断点。在每一步等待之后,我们将检查目标S_ui 仍然没有变化。如果它改变了,我们会停止操作并返回。
然而,当我们改变状态,比如开始物理麦克风录音,事情就变得复杂了,因为我们应该恢复那个状态。但是,这正是相反操作会做的事情。所以为了保持一致性,在执行任何改变状态的步骤后,我们必须完成操作,然后相反操作会恢复一切。最后,我们就会得到期望的结果。S_real 等于 S_ui.
在每次挂起点,我们重新检查目标状态。如果它改变了,我们放弃并返回.
实际上,存在更多复杂性,因为有时我们有非标准的用户流程。但这种架构,其中每个音频操作都通过队列,允许我们保持一致和可靠的状态,并为我们提供了改进应用的良好基础。
所有产品名称、标志和品牌均为各自所有者的财产。使用这些名称、标志和品牌并不构成认可。










































