moeru-ai/airi · error · Error

The MAGIC Live2D driver is already playing.

Error message

The MAGIC Live2D driver is already playing.

What it means

The MAGIC Live2D driver's `start()` only accepts a new generator when idle. If a previous `start()` was called without a matching `stop()`, the internal `generator` slot is occupied and calling `start()` again throws. It guards against clobbering an active animation loop mid-generation.

Source

Thrown at packages/model-driver-magic-live2d/src/driver.ts:108

    const frameIntervalMs = 1000 / generator.sampleRateHz
    accumulatedMs += Math.min(250, Math.max(0, timestamp - lastFrameAt))
    lastFrameAt = timestamp

    let nextPose: Pose | undefined
    while (accumulatedMs >= frameIntervalMs) {
      nextPose = generatePose()
      accumulatedMs -= frameIntervalMs
    }
    if (nextPose)
      applyPose(nextPose)

    animationFrame = requestFrame(generationFrame)
  }

  function start(nextGenerator: Generator<TState>) {
    if (generator)
      throw new Error('The MAGIC Live2D driver is already playing.')

    generator = nextGenerator
    accumulatedMs = 0
    lastFrameAt = now()
    filter.reset()
    outputFrame = undefined
    options.onOutput?.(undefined)
    applyPose(generatePose())
    animationFrame = requestFrame(generationFrame)
  }

  function replace(nextGenerator: Generator<TState>) {
    if (!generator)
      throw new Error('The MAGIC Live2D driver is not playing.')

    generator = nextGenerator
    accumulatedMs = 0
    lastFrameAt = now()

View on GitHub (pinned to 9c213115f8)

Solutions

  1. Call `driver.stop()` before `driver.start(newGenerator)` when the driver may already be playing.
  2. Check for a playing/generator state (or track it in your own code) and skip the redundant `start()`.
  3. Use `replace(nextGenerator)` instead of `start()` when you intend to swap generators in-place on a playing driver.
  4. Guard against duplicate component mounts by aborting/stopping in `onUnmounted`/cleanup.

Example fix

// before
driver.start(newGenerator)
// after
if (driver.isPlaying())
  driver.replace(newGenerator)
else
  driver.start(newGenerator)
Defensive patterns

Strategy: try-catch

Validate before calling

function canStart(driver: MagicLive2dDriver): boolean {
  return !driver.isPlaying()
}

Try / catch

try {
  driver.start(generator)
} catch (error) {
  if (error instanceof Error && error.message.includes('already playing')) {
    driver.replace(generator)
  } else {
    throw error
  }
}

Prevention

When it happens

Trigger: Calling `driver.start(gen)` while the driver is already playing (i.e. after a previous `start()` with no `stop()` in between). Common when wiring a second MAGIC session or re-entrant event handler that calls `start` on each event instead of on lifecycle transitions.

Common situations: Double-mounting a Vue component so two `onMounted` handlers both call start; a reconnect or model-reload path calling start without stopping the old driver first; a button that triggers start on every click.

Related errors


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02). Data as JSON: /api/errors/20f101b530bacab9. Report an issue: GitHub.