Compare commits

..

No commits in common. 'b6ab373801ae008ec19a6be4a8aae50e633744dc' and '0ffcc4c4e071ceaa61ead080ce5272b7876b0285' have entirely different histories.

  1. 6
      IOModuleTestBlazor/Components/Layout/NavMenu.razor
  2. 373
      IOModuleTestBlazor/Components/Pages/RelayDrumMachine.razor

@ -20,12 +20,6 @@
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="relay-drum">
<span class="bi bi-music-note-nav-menu" aria-hidden="true"></span> Relay Drum Machine
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="serial">
<span class="bi bi-terminal-nav-menu" aria-hidden="true"></span> Serial Terminal

@ -1,373 +0,0 @@
@page "/relay-drum"
@rendermode InteractiveServer
@implements IDisposable
@using IOModuleTestBlazor.Services
@using Peak.Can.Basic
@inject ICanService CanService
<PageTitle>Relay Drum Machine</PageTitle>
<div class="container-fluid">
<h1 class="mb-3">Relay Drum Machine</h1>
@if (!CanService.IsConnected)
{
<div class="alert alert-warning mb-3">CAN not connected — connect on the Home page first.</div>
}
@* ── Transport ───────────────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-body">
<div class="d-flex align-items-center gap-4 flex-wrap">
<button class="btn @(_playing ? "btn-danger" : "btn-success") btn-lg px-4"
@onclick="TogglePlay"
disabled="@(!CanService.IsConnected)">
@(_playing ? "⏹ Stop" : "▶ Play")
</button>
<div class="d-flex align-items-center gap-2">
<label class="fw-semibold mb-0">BPM</label>
<input type="range" class="form-range" style="width:160px"
min="40" max="240" step="1"
@bind="_bpm" @bind:event="oninput" />
<span class="font-monospace fw-bold" style="min-width:3ch">@_bpm</span>
</div>
<div class="d-flex align-items-center gap-2">
<label class="fw-semibold mb-0">Pulse</label>
<input type="range" class="form-range" style="width:120px"
min="20" max="80" step="5"
@bind="_pulseDurationMs" @bind:event="oninput" />
<span class="font-monospace" style="min-width:5ch">@_pulseDurationMs ms</span>
</div>
<button class="btn btn-outline-secondary" @onclick="ClearPattern">Clear</button>
</div>
</div>
</div>
@* ── Presets ─────────────────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-header fw-semibold">Presets</div>
<div class="card-body d-flex gap-2 flex-wrap">
@foreach (var preset in _presets)
{
<button class="btn btn-outline-primary btn-sm" @onclick="() => LoadPreset(preset.Value)">
@preset.Key
</button>
}
</div>
</div>
@* ── Pattern grid ────────────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-semibold">Pattern — @_barCount bar@(_barCount != 1 ? "s" : "") (@StepCount steps, 4/4 16th-note grid)</span>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-secondary btn-sm"
@onclick="RemoveBar" disabled="@(_barCount <= 1)">− Bar</button>
<span class="font-monospace px-1">@_barCount</span>
<button class="btn btn-outline-secondary btn-sm"
@onclick="AddBar" disabled="@(_barCount >= 8)">+ Bar</button>
</div>
</div>
<div class="card-body">
@* Beat header — shown once above the first bar *@
<div class="beat-header">
<div class="bar-label-col"></div>
<div class="step-grid-col beat-header-grid">
@for (int b = 0; b < 4; b++)
{
<div class="beat-header-cell">@(b + 1)</div>
}
</div>
</div>
@* One row per bar *@
@for (int bar = 0; bar < _barCount; bar++)
{
var barIndex = bar;
<div class="bar-row @(barIndex == _currentBar ? "bar-row-active" : "")">
<div class="bar-label-col">
<span class="bar-label">B@(barIndex + 1)</span>
</div>
<div class="step-grid-col">
<div class="step-grid">
@for (int s = 0; s < 16; s++)
{
var globalStep = barIndex * 16 + s;
var localStep = s;
bool isGroupStart = localStep % 4 == 0;
<div class="step-cell @GetStepClass(globalStep) @(isGroupStart ? "beat-start" : "")"
@onclick="() => ToggleStep(globalStep)">
<span class="step-num">@(localStep + 1)</span>
</div>
}
</div>
</div>
</div>
}
</div>
</div>
@* ── Status bar ──────────────────────────────────────────────────────────── *@
<div class="card">
<div class="card-body py-2 d-flex align-items-center gap-3">
@if (_playing && _currentStep >= 0)
{
<span class="badge bg-success">● Playing</span>
<span class="font-monospace">Bar @(_currentBar + 1) · Beat @(_currentBeat + 1) · Step @(_currentStepInBar + 1)</span>
<span class="text-muted">(global step @(_currentStep + 1) / @StepCount)</span>
}
else
{
<span class="badge bg-secondary">■ Stopped</span>
}
<span class="text-muted ms-auto">@(_steps.Count(s => s)) / @StepCount steps active</span>
</div>
</div>
</div>
<style>
.beat-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.beat-header-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.beat-header-cell {
text-align: center;
font-size: 0.7rem;
font-weight: bold;
color: #6c757d;
padding-left: 4px;
}
.bar-label-col {
min-width: 36px;
flex-shrink: 0;
}
.bar-label {
font-size: 0.7rem;
font-weight: bold;
color: #6c757d;
}
.step-grid-col {
flex: 1;
min-width: 0;
}
.bar-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
border-radius: 6px;
padding: 2px 4px;
transition: background-color 0.1s;
}
.bar-row-active {
background-color: rgba(255, 193, 7, 0.08);
}
.step-grid {
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: 4px;
}
.step-cell {
aspect-ratio: 1;
border: 2px solid #495057;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.1s, box-shadow 0.1s;
user-select: none;
min-width: 0;
}
.step-cell:hover { border-color: #0d6efd; }
.step-num {
font-size: 0.6rem;
font-weight: bold;
line-height: 1;
}
.beat-start { border-left: 2px solid #6c757d; }
/* States */
.step-off { background-color: #343a40; color: #6c757d; }
.step-on { background-color: #0d6efd; border-color: #0d6efd; color: white; }
.step-cursor { box-shadow: 0 0 0 2px #ffc107; border-color: #ffc107 !important; }
.step-cursor-on { background-color: #fd7e14; border-color: #ffc107 !important; color: white;
box-shadow: 0 0 10px rgba(253,126,20,0.8), 0 0 0 2px #ffc107; }
</style>
@code {
private const int StepsPerBar = 16;
private int _barCount = 1;
private int StepCount => _barCount * StepsPerBar;
private bool[] _steps = new bool[StepsPerBar];
private int _currentStep = -1;
private int _currentBar => _currentStep < 0 ? -1 : _currentStep / StepsPerBar;
private int _currentStepInBar => _currentStep < 0 ? -1 : _currentStep % StepsPerBar;
private int _currentBeat => _currentStep < 0 ? -1 : (_currentStep % StepsPerBar) / 4;
private bool _playing;
private int _bpm = 120;
private int _pulseDurationMs = 30;
private CancellationTokenSource? _cts;
private static readonly Dictionary<string, bool[]> _presets = new()
{
["Four-on-Floor"] = [true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false],
["Kick 1+3"] = [true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false],
["Snare 2+4"] = [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false],
["Hi-Hat 8th"] = [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false],
["Hi-Hat 16th"] = [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true],
["Off-Beat"] = [false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false],
["Clave 3-2"] = [true, false, false, true, false, false, true, false, false, false, true, false, false, true, false, false],
};
private string GetStepClass(int step)
{
bool isCurrent = step == _currentStep;
bool isOn = _steps[step];
return (isCurrent, isOn) switch
{
(true, true) => "step-cursor-on",
(true, false) => "step-off step-cursor",
(false, true) => "step-on",
_ => "step-off",
};
}
private void ToggleStep(int step) => _steps[step] = !_steps[step];
private void ClearPattern()
{
Array.Clear(_steps);
StateHasChanged();
}
private void LoadPreset(bool[] pattern)
{
// Tile the 16-step preset across however many bars are active
var next = new bool[StepCount];
for (int i = 0; i < StepCount; i++)
next[i] = pattern[i % StepsPerBar];
_steps = next;
StateHasChanged();
}
private async Task AddBar()
{
await StopSequencer();
Array.Resize(ref _steps, (_barCount + 1) * StepsPerBar);
_barCount++;
}
private async Task RemoveBar()
{
if (_barCount <= 1) return;
await StopSequencer();
_barCount--;
Array.Resize(ref _steps, _barCount * StepsPerBar);
}
private async Task TogglePlay()
{
if (_playing)
await StopSequencer();
else
StartSequencer();
}
private void StartSequencer()
{
_cts?.Dispose();
_cts = new CancellationTokenSource();
_playing = true;
var steps = _steps; // snapshot so resize mid-play doesn't affect the running loop
_ = Task.Run(() => RunSequencer(steps, _cts.Token));
}
private async Task StopSequencer()
{
_cts?.Cancel();
// Yield briefly to let the background task react before we return
await Task.Delay(50);
}
private async Task RunSequencer(bool[] steps, CancellationToken ct)
{
int total = steps.Length;
try
{
while (!ct.IsCancellationRequested)
{
for (int step = 0; step < total && !ct.IsCancellationRequested; step++)
{
_currentStep = step;
try { await InvokeAsync(StateHasChanged); } catch { return; }
int stepMs = Math.Max(30, (int)(60_000.0 / _bpm / 4));
int pulseMs = Math.Min(_pulseDurationMs, stepMs / 2);
if (steps[step])
{
SendRelay(true);
await Task.Delay(pulseMs, ct);
SendRelay(false);
int rest = stepMs - pulseMs;
if (rest > 0) await Task.Delay(rest, ct);
}
else
{
await Task.Delay(stepMs, ct);
}
}
}
}
catch (OperationCanceledException) { }
finally
{
_currentStep = -1;
_playing = false;
SendRelay(false);
try { await InvokeAsync(StateHasChanged); } catch { }
}
}
private void SendRelay(bool on)
{
var data = new byte[8];
data[0] = on ? (byte)0x01 : (byte)0x02; // 0x01=TERM_ON(SET), 0x02=TERM_OFF(RESET)
CanService.Write(new PcanMessage(0x240, MessageType.Standard, 8, data, false));
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
Loading…
Cancel
Save