Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 11 additions & 9 deletions docs/v3/bridge-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ C#→JSは `window.tcb.receive(<JSON>)` の一入口にする。JSONはSystem.Te

enumは言語間で安定する文字列、IDは不変文字列とする。

Host は値が `null` の項目を JSON からキーごと省く(`Protocol.Json` の `WhenWritingNull`)。そのため null になりうる項目は `?:X|null` と書き、Web は「キーが無い(undefined)」を `null` と同じに扱う。`!== null` だけで判定すると undefined を素通りさせ、描画が例外で止まる(v3.2.1 の内部生成フリーズ)。

```ts
type FrameRate = "24"|"25"|"29.97df"|"30";
interface AppState {
sessionId:string;
project:{displayName:string;filePath:string|null;dirty:boolean;canUndo:boolean;canRedo:boolean;recentFiles:string[]};
project:{displayName:string;filePath?:string|null;dirty:boolean;canUndo:boolean;canRedo:boolean;recentFiles:string[]};
mode:"ltc"|"generate";
transport:{status:"stopped"|"receiving"|"freerun"|"signalLost"|"error";statusText:string;detailText:string;signalErrorRatePercent:number|null;locked:boolean;triggerMuted:boolean;autoMuteEnabled:boolean;autoMutedCueId:string|null;autoUnmuteAtUtc:string|null;lastLtcReceivedAtUtc:string|null};
receive:{selectedDeviceId:string|null;devices:AudioDevice[];offset:string;triggerWindowFrames:number;freerunDurationSeconds:number};
generator:{running:boolean;startTime:string;frameRate:FrameRate;selectedOutputDeviceId:string|null;outputDevices:AudioDevice[];volume:number;ltcOutputActive:boolean;settingsPendingReset:boolean};
currentClock:ClockState; nextCue:NextCueState|null; cues:CueDto[]; hosts:HostDto[];
transport:{status:"stopped"|"receiving"|"freerun"|"signalLost"|"error";statusText:string;detailText:string;signalErrorRatePercent?:number|null;locked:boolean;triggerMuted:boolean;autoMuteEnabled:boolean;autoMutedCueId?:string|null;autoUnmuteAtUtc?:string|null;lastLtcReceivedAtUtc?:string|null};
receive:{selectedDeviceId?:string|null;devices:AudioDevice[];offset:string;triggerWindowFrames:number;freerunDurationSeconds:number};
generator:{running:boolean;startTime:string;loopEndTime?:string|null;frameRate:FrameRate;selectedOutputDeviceId?:string|null;outputDevices:AudioDevice[];volume:number;ltcOutputActive:boolean;settingsPendingReset:boolean};
currentClock:ClockState; nextCue?:NextCueState|null; cues:CueDto[]; hosts:HostDto[];
cueSync:{oscAddress:string;targetHostIds:string[]};
triggerPanel:{rows:number;columns:number;buttons:TriggerButtonDto[]};
logs:LogDto[];
Expand All @@ -46,7 +48,7 @@ interface AppState {
interface ClockState {raw:string;display:string;totalFrames:number;frameRate:FrameRate;dropFrame:boolean;receivedAtMonotonicMs:number;nextCueId?:string|null;framesUntilNextCue?:number|null}
interface NextCueState {id:string;name:string;triggerTime:string;effectiveTriggerTime:string;framesUntil:number}
interface AudioDevice {id:string;name:string;loopback:boolean}
interface CueDto {id:string;name:string;memo:string;triggerTime:string;effectiveTriggerTime:string;frameRate:FrameRate;oscAddress:string;additionalOscAddresses:string[];arguments:OscArgumentDto[];targetHostIds:string[];enabled:boolean;sendTriggerTimeAsSeconds:boolean;sendTimecode:string|null;triggerOffset:string|null;autoMuteOnFire:boolean;autoUnmuteAfter:string|null;runtime:{lastTriggeredAtUtc:string|null;flashToken:number;muteCountdownText:string}}
interface CueDto {id:string;name:string;memo:string;triggerTime:string;effectiveTriggerTime:string;frameRate:FrameRate;oscAddress:string;additionalOscAddresses:string[];arguments:OscArgumentDto[];targetHostIds:string[];enabled:boolean;sendTriggerTimeAsSeconds:boolean;sendTimecode?:string|null;triggerOffset?:string|null;autoMuteOnFire:boolean;autoUnmuteAfter?:string|null;runtime:{lastTriggeredAtUtc?:string|null;flashToken:number;muteCountdownText:string}}
type OscArgumentDto={type:"int32";value:number}|{type:"float32";value:number}|{type:"string";value:string};
interface HostDto {id:string;name:string;ipAddress:string;port:number;enabled:boolean;reachability:"unknown"|"checking"|"reachable"|"unreachable"}
interface TriggerButtonDto {id:string;row:number;column:number;label:string;oscAddress:string;arguments:OscArgumentDto[];targetHostIds:string[]}
Expand Down Expand Up @@ -99,7 +101,7 @@ M0合否目安(推測): clock+wave、1000 cues表示を10分継続し、入
|`receive.setOffset`|`{value}`|`{normalized}` / `validation`|
|`receive.setTriggerWindow`|`{frames}`|`{frames}` / `validation`|
|`receive.setFreerunDuration`|`{seconds}`|`validation`|
|`generator.configure`|`{startTime,frameRate,outputDeviceId,volume}`|`validation`,`deviceNotFound`|
|`generator.configure`|`{startTime?,frameRate?,loopEndTime?,outputDeviceId?,volume?}`|`validation`,`deviceNotFound`。省いた項目は今の値のまま。`loopEndTime` は空文字か null でループ解除、開始 TC 以前なら `validation`。どれか 1 つでも `validation` になれば何も変えない|
|`generator.start/stop/reset`|`{}`|`audioError`,`invalidState`|
|`mute.set`, `autoMute.setEnabled`|`{muted? ,enabled?}`|`invalidState`|
|`cue.add/update`|`{id?,cue:CueDraft}`|`{id?}` / `notFound`,`validation`,`conflict`|
Expand Down Expand Up @@ -131,9 +133,9 @@ Host はページ読込完了後、`window.tcb.hostAttached()` を 250ms 間隔
## WaveState(確定)

```ts
interface WaveState { sampleRate:number; windowMs:number; min:number[]; max:number[]; levelDbfs:number|null }
interface WaveState { sampleRate:number; windowMs:number; min:number[]; max:number[]; levelDbfs?:number|null }
```
`min[i]`/`max[i]` は表示 i 点目の区間の最小/最大(−1..1)。点数は Web の `viewport.waveformWidth/2`(16..2048)。`levelDbfs` は直近区間のピーク(dBFS)。表示区間は UI 改修前(v2)と同じ約 0.1 フレーム分(30fps 基準で約 3.3ms、48kHz で 160 サンプル)なので、点数がサンプル数を上回るときは `min[i] == max[i]`(1 点 1 サンプル)になる。Web は塗りだけでなく線(stroke)で描くこと。
`min[i]`/`max[i]` は表示 i 点目の区間の最小/最大(−1..1)。点数は Web の `viewport.waveformWidth/2`(16..2048)。`levelDbfs` は直近区間のピーク(dBFS)。無音(ピーク 0)なら null なので、キーごと届かない。表示区間は UI 改修前(v2)と同じ約 0.1 フレーム分(30fps 基準で約 3.3ms、48kHz で 160 サンプル)なので、点数がサンプル数を上回るときは `min[i] == max[i]`(1 点 1 サンプル)になる。Web は塗りだけでなく線(stroke)で描くこと。

## M2b で確定した command の引数と結果(Host 実装済み, 2026-09-11)

Expand Down
40 changes: 21 additions & 19 deletions src/TimecodeBridge.Host/Bridge/CommandRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,47 +216,49 @@ public ResultMessage Execute(WebMessage msg)
{
var g = _state.Generator;
var previousOutput = g.OutputDeviceId; var previousVolume = g.VolumeLevel; var previousRate = g.FrameRate; var previousStart = g.StartTime; var previousLoopEnd = g.LoopEndTime;
if (Str(a, "frameRate") is { } rateCode)
{
if (!HostState.TryParseFrameRate(rateCode, out var rate)) return Validation(id, "frameRate", "24 / 25 / 29.97df / 30");
g.FrameRate = rate;
}
// 検証を全部済ませてから書き換える。途中で弾くと、開始 TC だけ新しく終了 TC は古いまま、のような組み合わせが残る
var rate = g.FrameRate;
if (Str(a, "frameRate") is { } rateCode && !HostState.TryParseFrameRate(rateCode, out rate))
return Validation(id, "frameRate", "24 / 25 / 29.97df / 30");
TimecodeValue startTc;
if (Str(a, "startTime") is { } start)
{
if (!HostState.TryParseTimecode(start, g.FrameRate, out var tc)) return Validation(id, "startTime", "HH:MM:SS:FF の形式で入力してください");
g.StartTime = tc;
if (!HostState.TryParseTimecode(start, rate, out startTc)) return Validation(id, "startTime", "HH:MM:SS:FF の形式で入力してください");
}
else
{
g.StartTime = new TimecodeValue(g.StartTime.Hours, g.StartTime.Minutes, g.StartTime.Seconds, Math.Min(g.StartTime.Frames, g.FrameRate.FramesPerSecond() - 1), g.FrameRate);
startTc = new TimecodeValue(g.StartTime.Hours, g.StartTime.Minutes, g.StartTime.Seconds, Math.Min(g.StartTime.Frames, rate.FramesPerSecond() - 1), rate);
}
// loopEndTime は空文字と null でループ解除。範囲として成立しない値は弾く
var loopEndTc = g.LoopEndTime;
if (Has(a, "loopEndTime"))
{
loopEndTc = null;
if (Str(a, "loopEndTime") is { Length: > 0 } loopEnd)
{
if (!HostState.TryParseTimecode(loopEnd, g.FrameRate, out var endTc)) return Validation(id, "loopEndTime", "HH:MM:SS:FF の形式で入力してください");
if (endTc.TotalFrames() <= g.StartTime.TotalFrames()) return Validation(id, "loopEndTime", "開始 TC より後の時刻を指定してください");
g.LoopEndTime = endTc;
}
else
{
g.LoopEndTime = null;
if (!HostState.TryParseTimecode(loopEnd, rate, out var endTc)) return Validation(id, "loopEndTime", "HH:MM:SS:FF の形式で入力してください");
if (endTc.TotalFrames() <= startTc.TotalFrames()) return Validation(id, "loopEndTime", "開始 TC より後の時刻を指定してください");
loopEndTc = endTc;
}
}
else if (g.LoopEndTime is { } keptEnd)
{
// レート変更で末尾のフレーム番号が範囲外になり、
// 開始 TC の変更で範囲として成立しなくなることがある
var clamped = new TimecodeValue(keptEnd.Hours, keptEnd.Minutes, keptEnd.Seconds, Math.Min(keptEnd.Frames, g.FrameRate.FramesPerSecond() - 1), g.FrameRate);
g.LoopEndTime = clamped.TotalFrames() > g.StartTime.TotalFrames() ? clamped : null;
var clamped = new TimecodeValue(keptEnd.Hours, keptEnd.Minutes, keptEnd.Seconds, Math.Min(keptEnd.Frames, rate.FramesPerSecond() - 1), rate);
loopEndTc = clamped.TotalFrames() > startTc.TotalFrames() ? clamped : null;
}
if (Has(a, "outputDeviceId")) g.OutputDeviceId = Str(a, "outputDeviceId") ?? string.Empty;
var volume = g.VolumeLevel;
if (Num(a, "volume") is { } vol)
{
if (vol < 0 || vol > 1) return Validation(id, "volume", "0〜1");
g.VolumeLevel = (float)vol;
volume = (float)vol;
}
g.FrameRate = rate;
g.StartTime = startTc;
g.LoopEndTime = loopEndTc;
if (Has(a, "outputDeviceId")) g.OutputDeviceId = Str(a, "outputDeviceId") ?? string.Empty;
g.VolumeLevel = volume;
// 生成中は出力先と音量をその場で反映する。開始 TC・フレームレート・ループ範囲は再生し直すまで保留。
// 一時停止中に変えた場合も同じで、再開ではなく最初から掛け直さないと反映されない
if (_engine.ActiveSource == TimecodeSourceType.Generator)
Expand Down
16 changes: 8 additions & 8 deletions src/TimecodeBridge.Web/dist/assets/app.js

Large diffs are not rendered by default.

Loading
Loading