Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5b7d8f5
fix(stretcher): resolve gapless playback race between onended and tra…
ivgtr Feb 9, 2026
f83b331
fix: add defensive error handling for stretcher dynamic import and nu…
ivgtr Feb 9, 2026
d07296c
test: add tests for defensive error handling in stretcher
ivgtr Feb 9, 2026
7870a9e
test: add comprehensive tests for play(), engine, worker-manager, and…
ivgtr Feb 9, 2026
e88dd17
fix(play): guard against zero-division in loopDur, playbackRate, and …
ivgtr Feb 9, 2026
bb3b2be
refactor: extract pure functions and shared state manager, add compre…
ivgtr Feb 9, 2026
adc0db5
test: add tests for 8 utility modules and fix waveform/adapters bugs
ivgtr Feb 9, 2026
cd0fc7b
chore: upgrade vitest 2.x to 4.x and configure projects (unit + browser)
ivgtr Feb 10, 2026
2580daa
test: add browser tests with Playwright for real Web Audio API verifi…
ivgtr Feb 10, 2026
5e198c3
ci: add test workflow for unit and browser tests
ivgtr Feb 10, 2026
eb907fc
chore: add Biome linter/formatter and apply auto-format
ivgtr Feb 10, 2026
47ed2e9
docs: update CLAUDE.md with current architecture and guidelines
ivgtr Feb 10, 2026
faf318d
ci: add typecheck and Biome check steps to test workflow
ivgtr Feb 10, 2026
724093d
chore: add Claude Code skills and PreCommit hook
ivgtr Feb 10, 2026
420d49b
fix: remove invalid PreCommit hook from settings
ivgtr Feb 10, 2026
62d9f3f
chore: add /commit skill with typecheck, lint, and commit guidelines
ivgtr Feb 10, 2026
3a203df
fix: widen lookahead and add proactive schedule threshold for backgro…
ivgtr Feb 10, 2026
64e7440
test: add background tab throttling and resilience tests for stretcher
ivgtr Feb 10, 2026
7fe6a93
fix: correct WSOLA NCC reference windowing and add tempo≈1.0 bypass
ivgtr Feb 10, 2026
6c24b05
fix: guard against stale conversion results in scheduler
ivgtr Feb 10, 2026
b797731
fix: prevent scheduling races, scale trimOverlap crossfade, and debou…
ivgtr Feb 10, 2026
3659a8b
fix: guard onAllDead against double invocation and clean up postTimes…
ivgtr Feb 10, 2026
c9a61ee
fix: skip fade-in on resume to prevent audible ramp artifact
ivgtr Feb 10, 2026
2fb0e13
fix: prevent setTempo during pause from auto-resuming playback
ivgtr Feb 10, 2026
b9a4b24
fix: preserve playback position when setTempo is called during pause
ivgtr Feb 10, 2026
552c416
fix: guard conversion-scheduler callbacks against disposed state
ivgtr Feb 10, 2026
839acfb
fix: correct seek behavior during pause in stretcher engine
ivgtr Feb 10, 2026
0205bef
test: add race condition and lifecycle tests for stretcher components
ivgtr Feb 10, 2026
08d5273
test: add remaining 6 gap items for stretcher test coverage
ivgtr Feb 10, 2026
e5ad25c
feat: add progress coloring to vanilla demo waveforms
ivgtr Feb 10, 2026
0d36d27
feat: add chunk buffering demo page with legend and event log
ivgtr Feb 10, 2026
4618aee
refactor(demo): consolidate demos and use-cases into examples section
ivgtr Feb 10, 2026
d37d9e8
ci: add concurrency to cancel duplicate workflow runs
ivgtr Feb 10, 2026
dc2d023
style: fix biome formatting errors
ivgtr Feb 10, 2026
707c084
fix(ci): use head_ref for concurrency group to deduplicate PR runs
ivgtr Feb 10, 2026
daac10c
docs: rewrite README with clearer structure and updated description
ivgtr Feb 10, 2026
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
16 changes: 16 additions & 0 deletions .claude/commands/add-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
引数で指定されたモジュール($ARGUMENTS)のテスト不足箇所を特定し、テストを追加してください。

## 手順

1. `src/$ARGUMENTS` のソースを読み、公開関数・分岐・エッジケースを列挙
2. `tests/` 配下の既存テストを読み、カバーされていない箇所を特定
3. `tests/helpers/audio-mocks.ts` の共通モックを活用してテストを追加
4. 追加したテストを `npx vitest run` で実行して PASS を確認

## テスト方針

- 純粋関数 → 入出力のみ検証
- コンポーネント → audio-mocks.ts のモックを使用
- Race condition → タイマー + コールバックが絡む箇所には 3 パターン(CB先行, dispose前, 連続呼び出し)を検証
- `vi.useFakeTimers()` は動的 `import()` をブロックするため stretched playback テストでは使わない
- Worker モックは `function` キーワード必須
16 changes: 16 additions & 0 deletions .claude/commands/commit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
コミットを作成する前に、以下の品質チェックを順番に実行してください。

## 事前チェック

1. `npm run typecheck` を実行し、型エラーがないことを確認
2. `npm run lint` を実行し、lint エラーがないことを確認

いずれかが失敗した場合はコミットを中止し、エラー内容を報告してください。

## コミット作成

事前チェックがすべて通った場合のみ、以下の方針でコミットを作成してください。

- 変更を意味単位(機能追加、バグ修正、リファクタリング等)に分割し、それぞれ個別のコミットにする
- 既存のコミット履歴のスタイル(prefix、文体)を確認し、それに合わせたメッセージを書く
- 無関係な変更を1つのコミットに混ぜない
11 changes: 11 additions & 0 deletions .claude/commands/quality-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
プロジェクト全体の品質チェックを順次実行し、結果をサマリーで報告してください。

以下の順序で実行:

1. `npm run typecheck` — TypeScript 型チェック
2. `npm run check` — Biome lint + format チェック
3. `npm run test:unit` — Node 単体テスト
4. `npm run test:browser` — ブラウザテスト

各ステップの結果を PASS / FAIL で報告し、FAIL の場合はエラー詳細を含めてください。
最後に全体サマリーを表示してください。
14 changes: 14 additions & 0 deletions .claude/commands/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
`git diff HEAD` の変更差分を以下のバグ防止チェックリストに基づいてレビューしてください。

## チェック項目

1. **状態管理**: disposed チェック漏れ、不正な状態遷移、コールバック内での状態変更
2. **タイマー管理**: clearTimeout/clearInterval の漏れ、dispose 時のタイマーキャンセル
3. **リソースリーク**: AudioNode disconnect 漏れ、Worker terminate 漏れ、Blob URL revoke 漏れ
4. **async 競合**: dynamic import 後の disposed チェック、pending 操作の競合
5. **Stretcher 固有**: currentChunkIndex の更新タイミング、synthesisHop/analysisHop の関係
6. **テスト有無**: 変更箇所に対応するテストがあるか

## 報告形式

各チェック項目について PASS / WARNING / FAIL で報告し、問題がある場合は該当コードと修正案を示してください。
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Test
on: [push, pull_request]
concurrency:
group: test-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run typecheck
- run: npm run check
- run: npx playwright install --with-deps chromium
- run: npm test
56 changes: 50 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
```bash
npm run build # Build with tsup (ESM + CJS → dist/)
npm run dev # Build in watch mode
npm test # Run tests once (vitest)
npm test # Run all tests (unit + browser)
npm run test:unit # Node unit tests only
npm run test:browser # Playwright browser tests only
npm run test:watch # Run tests in watch mode
npm run typecheck # Type check (tsc --noEmit)
npm run lint # Biome lint
npm run format # Biome format (auto-fix)
npm run check # Biome check (lint + format)
```

Run a single test file:
```bash
npx vitest run tests/emitter.test.ts
npx vitest run --project=unit tests/stretcher/wsola.test.ts
```

## Architecture

10 independent modules, each a separate entry point for tree-shaking:
14 modules, each a separate entry point for tree-shaking:

| Module | File | Purpose |
|--------|------|---------|
Expand All @@ -37,11 +43,15 @@ npx vitest run tests/emitter.test.ts
| scheduler | `src/scheduler.ts` | Scheduler and clock |
| synth | `src/synth.ts` | Buffer synthesis (sine, noise, click) |
| adapters | `src/adapters.ts` | Framework integration (`getSnapshot`, `subscribeSnapshot`, `onFrame`) |
| stretcher | `src/stretcher/index.ts` | WSOLA time-stretch engine (preservePitch playback) |
| player | `src/player.ts` | WaaPlayer class (high-level facade over `play()`) |
| playback-state | `src/playback-state.ts` | Playback state manager (shared state machine) |
| playback-position | `src/playback-position.ts` | Position calculation (pure functions) |

### Key Design Patterns

- **BYO AudioContext**: Every function takes `AudioContext` as first argument. Never creates its own.
- **Function exports only**: No classes. All modules export plain functions.
- **Function exports only**: No classes (except WaaPlayer). All modules export plain functions.
- **Playback state machine**: `play()` returns a `Playback` object with states `playing` → `paused` → `stopped`. Position tracking uses `AudioContext.currentTime` for hardware-clock accuracy. Timeupdate uses `setInterval` (not rAF) to work in background tabs.
- **Dual build**: tsup outputs both ESM (`dist/index.js`) and CJS (`dist/index.cjs`) with subpath exports per module.

Expand All @@ -51,20 +61,54 @@ Core types in `src/types.ts`:
- `PlaybackState = "playing" | "paused" | "stopped"`
- `Playback` — full playback control interface
- `PlaybackSnapshot` — `{ state, position, duration, progress }`
- `PlayOptions` — offset, loop, playbackRate, through, destination, etc.
- `PlayOptions` — offset, loop, playbackRate, through, destination, preservePitch, etc.

## Test Setup

- Framework: Vitest with `globals: true`, environment: `node`
- Test location: `tests/**/*.test.ts`
- **Vitest 4.x**, projects 構成 (unit + browser)
- `npm run test:unit` — Node 環境の単体テスト
- `npm run test:browser` — Playwright Chromium ブラウザテスト
- `npm test` — 両方実行
- Test location: `tests/**/*.test.ts`, `tests/browser/*.browser.test.ts`
- Shared mocks: `tests/helpers/audio-mocks.ts`
- Coverage excludes: `src/index.ts`, `src/types.ts`

### テスト方針

- **純粋関数テスト** → `tests/<module>.test.ts`(入出力のみ検証)
- **コンポーネントテスト** → `tests/stretcher/<component>.test.ts`(audio-mocks.ts 使用)
- **Race condition テスト** → タイマー + コールバックが絡む箇所には必須(3 パターン: CB先行, dispose前, 連続呼び出し)
- `vi.useFakeTimers()` は動的 `import()` をブロックするため stretched playback テストでは使わない
- Worker モックは `function` キーワード必須(Vitest 4.x で arrow fn は `new` 不可)
- URL モックはコンストラクタ機能を保持する必要あり(Vitest 4.x 内部で `new URL()` を使用)

### バグ防止チェックリスト

コードレビュー・実装時に確認:

- **状態管理**: disposed チェック、不正遷移ガード、コールバック内の状態変更考慮
- **タイマー管理**: clearTimeout/clearInterval の確実な実行、dispose 時の全タイマーキャンセル
- **リソースリーク**: AudioNode disconnect, Worker terminate, Blob URL revoke
- **async 競合**: dynamic import の disposed チェック、pending 操作キュー
- **Stretcher 固有**: currentChunkIndex は onTransition でのみ更新、synthesisHop 固定・analysisHop を tempo 依存に

## Build Configuration

- Target: ES2020
- TypeScript: strict mode with `noUnusedLocals`, `noUnusedParameters`, `noUncheckedIndexedAccess`
- tsup: splitting enabled, sourcemaps, no minification
- Each module in `src/` is a separate entry point in `tsup.config.ts`
- Linter/Formatter: Biome (`biome.json`)

## Adding a New Module

1. `src/<module>.ts` 作成(BYO AudioContext パターン)
2. 型を `src/types.ts` に追加
3. `tsup.config.ts` にエントリ追加
4. `package.json` の `exports` にサブパス追加
5. `src/index.ts` から re-export
6. テスト作成(`tests/<module>.test.ts`)
7. この CLAUDE.md の Architecture テーブルに追記

## Demo Site

Expand Down
130 changes: 41 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,128 +1,80 @@
# waa-play

[![npm version](https://img.shields.io/npm/v/waa-play)](https://www.npmjs.com/package/waa-play)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)

Convenient composable building blocks for Web Audio API.
Web Audio API modules with WSOLA time-stretching, chunk-based streaming, and waveform extraction

Time-stretch / Streaming / Waveform / BYO AudioContext / Zero Dependencies
waa-play は Web Audio API を用いた音声操作のためのモジュール群です。ピッチ保持タイムストレッチ、ストリーミング再生、波形抽出、AudioNode チェーン接続などの機能を提供します。

[Documentation & Demo](https://ivgtr.github.io/waa/)
Web Audio API の `playbackRate` で再生速度を変更するとピッチも連動して変化します。waa-play は、ピッチを保持したまま再生速度を変えたいユースケースのために作成しました。

## Install
この実装は完璧なものではなく、すべてのユースケースに対応するわけではないことに注意してください。

```bash
npm install waa-play
```

## Features
## Documentation & Demo

### Pitch-preserving time-stretch
再生速度を変えてもピッチが変わりません。処理は別スレッドで実行されるため、UI をブロックしません。
https://ivgtr.github.io/waa/

### Streaming playback
音声を段階的に処理し、バッファリング状態をイベントで通知します。ローディング UI を簡単に実装できます。
## ピッチ保持タイムストレッチ(WSOLA)

### BYO AudioContext
既存の AudioContext やオーディオグラフをそのまま使えます。他のライブラリとの統合も容易です。
waa-play は WSOLA (Waveform Similarity Overlap-Add) アルゴリズムを採用しています。
音声信号を小さなフレームに分割し、類似した波形を重ね合わせて再構築することで、ピッチを変えずに時間的な伸縮を実現します。

### Framework integration
React・Vue・Svelte など、お好みのフレームワークで再生状態をリアクティブに扱えます。
HTML5 Audio 要素の `preservePitch` オプションと同様の効果を目指していますが、いくつかの制約があります。
WSOLA の処理はクライアントサイドで行われるため、CPU リソースを多く消費する可能性があります。
また、AudioBuffer 全体を事前に読み込む必要があるため、長い音声ファイルでは変換に時間がかかる場合があります。

### Waveform extraction
波形データを取得し、プログレスバーや波形ビジュアライザーを構築できます。

### Background-tab safe
バックグラウンドタブでも再生位置の追跡が継続します。

## Quick Start
これらの制約に対して、Worker による並列処理やチャンクベースのストリーミングで対処しています。

### Class API (WaaPlayer)
### Worker ベースの変換

最もシンプルな使い方です。`WaaPlayer` が `AudioContext` を内部管理し、全モジュールの機能を統合して提供します。
WSOLA 変換は Web Worker 内で実行され、メインスレッドのパフォーマンスへの影響を最小限に抑えています。
Worker は複数生成され、変換タスクがキューイングされて効率的に処理されます。

```ts
import { WaaPlayer } from "waa-play";
### チャンクベースのストリーミング再生

const player = new WaaPlayer();
const buffer = await player.load("/audio/track.mp3");
変換済みの音声がチャンク単位で順次再生されます。
これにより、長時間の音声でもすべての変換を待つことなく、スムーズな再生が可能です。

const gain = player.createGain(0.8);
const playback = player.play(buffer, { through: [gain] });
### イベント通知

player.dispose();
```
再生位置の更新や再生終了などのイベントが通知され、UI の更新や他の処理に利用できます。

既存の `AudioContext` を渡すこともできます。
## Quick Start

```ts
const player = new WaaPlayer(existingAudioContext);
```bash
npm install waa-play
```

### Function API
### 最も簡単な使い方(WaaPlayer)

個別の関数だけ import したい場合はこちら。全関数が `AudioContext` を第一引数に取ります。
`WaaPlayer` が `AudioContext` を内部管理し、全モジュールの機能を統合して提供します。

```ts
import { createContext, loadBuffer, play, createGain } from "waa-play";
import { WaaPlayer } from "waa-play";

const player = new WaaPlayer();
const buffer = await player.load("/audio/track.mp3");

const ctx = createContext();
const buffer = await loadBuffer(ctx, "/audio/track.mp3");
const gain = player.createGain(0.8);
const playback = player.play(buffer, { through: [gain] }); // 再生

const gain = createGain(ctx, 0.8);
const playback = play(ctx, buffer, { through: [gain] });
playback.setPlaybackRate(1.5); // 再生速度を 1.5 倍に変更(リアルタイム反映)

playback.on("timeupdate", ({ position, duration }) => {
console.log(`${position.toFixed(1)}s / ${duration.toFixed(1)}s`);
console.log(`${position.toFixed(2)}s / ${duration.toFixed(2)}s`);
});
```

## Modules
playback.dispose(); // 再生停止・リソース解放
```

必要なものだけ個別に import できます。
既存の `AudioContext` を渡すこともできます。これは例えば、他の AudioNode と接続したい場合に有用です。

```ts
import { play } from "waa-play/play";
import { loadBuffer } from "waa-play/buffer";
import { WaaPlayer } from "waa-play/player";
const player = new WaaPlayer(existingAudioContext);
```

| Module | 概要 |
|--------|------|
| `player` | 全モジュールを統合するクラスベース API |
| `context` | AudioContext のライフサイクル管理 |
| `buffer` | 音声ファイルのロード・デコード |
| `play` | 再生制御(play / pause / seek / loop / preservePitch) |
| `emitter` | 型安全なイベントエミッター |
| `nodes` | AudioNode のファクトリとチェーン接続 |
| `waveform` | 波形データの抽出(ピーク・RMS) |
| `fade` | フェード処理(in / out / crossfade) |
| `scheduler` | スケジューリングと BPM ベースのクロック |
| `synth` | 波形バッファの生成(sin / noise / click) |
| `adapters` | フレームワーク統合(React / Vue / Svelte) |
| `stretcher` | ピッチ保持タイムストレッチ(WSOLA) |

## API

詳細な使い方・API リファレンスは [Documentation & Demo](https://ivgtr.github.io/waa/) をご覧ください。

### WaaPlayer

| メソッド | 概要 |
|----------|------|
| `resume()` / `ensureRunning()` / `now()` | AudioContext 制御 |
| `load(url)` / `loadFromBlob(blob)` / `loadAll(map)` | 音声ロード |
| `play(buffer, options?)` | 再生(`Playback` を返す) |
| `createGain()` / `createFilter()` / `createPanner()` / `createCompressor()` / `createAnalyser()` | ノード生成 |
| `chain(...nodes)` / `disconnectChain(...nodes)` | グラフ接続 |
| `fadeIn()` / `fadeOut()` / `crossfade()` / `autoFade()` | フェード |
| `extractPeaks()` / `extractPeakPairs()` / `extractRMS()` | 波形抽出 |
| `createScheduler()` / `createClock()` | スケジューリング |
| `createSineBuffer()` / `createNoiseBuffer()` / `createClickBuffer()` | バッファ合成 |
| `getSnapshot()` / `subscribeSnapshot()` / `onFrame()` / `whenEnded()` / `whenPosition()` | アダプター |
| `dispose()` | リソース解放 |

## License

MIT
MIT © [ivgtr](https://github.com/ivgtr)

[![Twitter Follow](https://img.shields.io/twitter/follow/ivgtr?style=social)](https://twitter.com/ivgtr) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
Loading
Loading