Skip to content

[DONE] 정리 4단계 PR 16 — 가상 패널이 손으로 돌린 노브와 누른 버튼, 바뀐 패턴을 MIDI 로 보내고 /api/midi 에 답한다 - #86

Merged
patternflow-bot merged 3 commits into
devfrom
claude/studio-cleanup-4-pr16-abzste
Oct 15, 2026
Merged

patternflow-bot merged 3 commits into
devfrom
claude/studio-cleanup-4-pr16-abzste

Conversation

@patternflow-bot

@patternflow-bot patternflow-bot commented Oct 15, 2026 •

Copy link
Copy Markdown

studio / https://claude.ai/code/session_01NEBrYPPE5rzyWZ4xHhbfUE

정리 4단계 계획서 표의 PR 16 이다. 가상 패널에 tools/virtual-panel/transports/midi-out.ts 하나를 더해, MIDI 를 넣은 패널이 손으로 돌린 노브를 CC 2427 로, 손으로 누른 버튼을 노트 6063 으로, 바뀐 패턴을 Program Change 로 세션에 보내고 /api/midi 와 status 의 midi 에 답하게 했다. 머지되면 PR 18 의 보내는 쪽 MIDI 벡터와 /api/midi 벡터가 돌 패널이 생긴다.

좋아지는 것: 없음

할 일

  • 계획서 PR 16 행(k16)과 D1, D5, D8, D11, 발견 4, 7, docs/rfc-cleanup.md §2.11, PR 15([DONE] 정리 4단계 PR 15 — 가상 패널이 RTP-MIDI 세션을 받고 CC, 노트, Program Change 를 노브와 버튼, 패턴에 옮긴다 #83) 본문의 「for another k」를 읽는다
  • midi-spec.md 53~120행과 펌웨어(core_midi.h 의 observeFrame, core_midi_http.h, feature_midi.h 의 appendStatus, core_midi_rtp.h 의 appendDiagnostics)를 맞대 본다
  • tools/virtual-panel/transports/midi-out.ts, 표가 PR 16 에 적은 파일 하나
  • 300줄 안: +146
  • 버릴 시험 스크립트로 끝에서 끝까지(손 시계), 실제 시계는 레포의 rtpmidi_probe.py 로. 스크립트는 커밋하지 않았다
  • 일부러 깨뜨린 typecheck 한 번, 되돌림
  • typecheck, lint, check:drift, 링크, 잠금, 글투, doc-map
  • [DONE] 직전 dev 를 다시 받아 본다: 78b4538 그대로라 merge 할 것이 없다
  • 이 PR 의 CI: e614d5b 에서 Drift checks(38766298571), Docs point at files that exist(38766298510) 둘 다 success

한 일

midi-out.ts 는 feature: "midi" 인 transport 라 --features midi 일 때만 붙는다. caps 는 midi.ts 가 주므로 [] 다. 파일 이름이 midi.ts 보다 앞에 정렬돼 먼저 붙기 때문에, 세션은 프레임이나 요청이 올 때 midi(sim) 으로 찾는다. 기본 빌드에는 아무것도 더하지 않는다.

보내는 것. 펌웨어의 observeFrame() 처럼 프레임마다 한 번 돈다. 손의 클릭은 knobState().knobs 를 앞 프레임과 견줘 얻는다. knobs 는 손의 클릭만 세므로(계획서 발견 4) 호스트가 보낸 CC 2427, OSC, MQTT, 레인이 움직인 노브는 나가지 않는다(57행의 hand motion only, 62행). 클릭은 노브마다 outDiv 로 나뉘어 걸음이 되고 나머지는 남는다. 걸음에 outMul 을 곱하고 한 프레임에 ±63 에서 자른다. abs 는 64 에서 시작하는 위치를 0127 안에서 옮기고, rel 은 걸음을 모은다. CC 24~27 은 노브마다 50 ms 에 한 번까지 나가고 마지막 값은 늘 나간다. abs 는 마지막으로 보낸 위치와 다를 때만 보내고, rel 은 모은 합이 ±63 을 넘으면 기다리지 않고 63 을 보낸 뒤 나머지를 남긴다. 첫 메시지는 바로 나간다. 시계는 millis() 처럼 프레임 시각을 ms 정수로 내린다.

버튼은 /vp/button 의 손만 본다. 누름은 노트 온 벨로시티 127, 놓음은 노트 오프다. 한 프레임 안에서 눌렀다 놓아도 노트 오프가 따라간다. 패턴이 바뀌면 누가 바꿨든(60행, MIDI 포함) Program Change 가 나가고, 127 넘는 번호는 보내지 않는다. SELECT 에서는 osc.ts 처럼 커서 아래 패턴이다.

GET /api/midi 는 ok, channel, outDiv, outMul, outMode, host, runtime, rtpPeers, rtpPeer, ip, port, outPos, rx, tx, rxDetail 를 준다. /api/status 의 midi 는 그중 runtime 부터 rxDetail 까지다(72행, 81~93행). POST /api/midi 는 knob, outDiv, outMul, outMode, host 를 펌웨어 차례대로 읽고 하나씩 적용한다. 틀린 값은 400 과 펌웨어와 같은 오류 글이다. outDiv 나 outMul 을 정하면 다른 쪽이 1 로 돌아가고(117행), outMode 를 정하면 모아 둔 걸음을 버린다.

증거

로컬, web/:

$ git diff --stat origin/dev...HEAD
 tools/virtual-panel/transports/midi-out.ts | 146 +++++++++++++++++++++++++++++
 1 file changed, 146 insertions(+)

$ npm run typecheck        # exit 0
$ npm run lint             # exit 0
# 깨뜨린 한 번: caps: [] 를 caps: "midi" 로, 백업 파일로 되돌렸다
$ npm run typecheck
../tools/virtual-panel/transports/midi-out.ts(37,3): error TS2322: Type 'string' is not assignable to type 'string[]'.

$ npm run check:drift      # passed bom, bom-schematic, changelog, editions, features, links-absolute, markers, mqtt-vectors, osc-vectors, presets, rest-vectors, shared, virtual-panel; skipped roadmap
$ python3 .github/scripts/check_links.py
all 725 relative links resolve
$ npm run check:human-lock
check:human-lock: no locked words changed
$ npm run check:voice
read: 146 added lines in 1 file
check:voice: nothing found
$ git fetch --tags && npm run doc-map          # CSV 는 git checkout 으로 되돌렸다
1111 links from 222 files → docs/investigations/2026-10-doc-map.csv
0 do not resolve
1366 Korean/English pairs → docs/investigations/2026-10-doc-parity.csv
7 en-short, 18 ko-short, 1 missing-ko, 1340 ok

rest-vectors 의 absent-*(PR 7)는 기본 빌드의 /api/midi 가 여전히 404 임을 보고, virtual-panel(PR 8)은 midi-out.ts 까지 불러온 채로 프리셋 34개를 돌렸다. check:midi-vectors 와 check:audio-vectors 는 스크립트가 아직 없어(PR 17, PR 20) 실패하고, check:drift 는 그 둘을 부르지 않는다.

끝에서 끝까지(손 시계). start({ features: ["midi"], clock: "manual", ports: { "*": 0 } }) 로 패널을 띄우고 rtpmidi_probe.py 처럼 제어 소켓과 그다음 번호의 데이터 소켓으로 세션에 들어갔다. 손은 /vp/turn 과 /vp/button, 시간은 advance(), 받은 것은 RTP 패킷의 명령 목록을 16진수로 적었다:

peers 1
abs turn 3 -> [ 'b0 18 43' ]                      # CC 24 = 67, 64 + 3
abs turn 2 within 50ms -> []
after 57 ms more -> [ 'b0 18 45' ]                # 마지막 값 69
abs knob 2 +200 -> [ 'b0 19 7f' ]                 # 127 에서 멈춘다
rel post rel
rel turn -2 -> [ 'b0 1a 3e' ]                     # 64 - 2
rel two turns inside 50ms -> []
rel trailing -> [ 'b0 1a 4a' ]                    # 5 + 5 를 한 번에
rel 1 -> [ 'b0 1b 41' ]
rel 40, 40 paced -> [ 'b0 1b 7f' ]                # 80 은 기다리지 않고 63
rel rest -> [ 'b0 1b 51' ]                        # 남은 17
outDiv [ 4, 1, 1, 1 ] [ 1, 1, 1, 1 ]              # POST outDiv=4&knob=1
div4 turn 3 -> []
div4 turn 1 more -> [ 'b0 18 41' ]
outMul 3 [ 1, 1, 1, 1 ] [ 3, 3, 3, 3 ]            # POST outMul=3, outDiv 는 1 로
mul3 turn 1 -> [ 'b0 18 43' ]
press -> [ '90 3d 7f' ]
release -> [ '80 3d 00' ]
press+release in a frame -> [ '90 3e 7f', '80 3e 00' ]
select 200
select 2 -> [ 'c0 02' ]                           # GET /api/patterns/select?index=2
host's CC and note -> []                          # CC 24 = 65, CC 20 = 100, 노트 60 온과 오프
host's PC 3 -> [ 'c0 03' ]                        # 60행, MIDI 가 바꿔도
POST knob=5&outDiv=2 {"ok":false,"error":"knob must be 1..4"}
POST outDiv=17 {"ok":false,"error":"outDiv must be 1..16"}
POST outDiv=0 {"ok":false,"error":"outDiv must be 1..16"}
POST outMul=9 {"ok":false,"error":"outMul must be 1..8"}
POST outMode=up {"ok":false,"error":"outMode must be abs or rel"}
POST host=nope {"ok":false,"error":"host must be an IPv4 address or empty"}
POST host= {"ok":true,"runtime":true,"channel":1,"outDiv":[1,1,1,1],"outMul":[3,3,3,3],"outMode":"rel
POST host=10.0.0.9 {"ok":true,"runtime":true,"channel":1,"outDiv":[1,1,1,1],"outMul":[3,3,3,3],"outMode":"rel
PUT 404
GET {"ok":true,"runtime":true,"channel":1,"outDiv":[1,1,1,1],"outMul":[3,3,3,3],"outMode":"rel","host":"10.0.0.9","rtpPeers":1,"rtpPeer":"Tester","rx":5,"tx":15,"rxDetail":{"worker":true,"abs":[1,0,0,0],"rel":[1,0,0,0],"queueDropped":0,"txQueueDropped":0,"packetGaps":0,"parseErrors":0,"bufferOverflows":0,"pollMaxGapUs":0,"pollMaxUs":0,"queueMaxAgeUs":0,"stackMin":0},"ip":"192.168.0.42","port":47623,"outPos":[69,127,64,64]}
status caps [ 'patterns', 'params', 'sleep', 'midi' ] midi {"runtime":true,"channel":1,"outDiv":[1,1,1,1],"outMul":[3,3,3,3],"outMode":"rel","host":"10.0.0.9","rtpPeers":1,"rtpPeer":"Tester","rx":5,"tx":15,"rxDetail":{...}}

위 출력은 두 번 돌린 것을 합쳤다. 「rel 40 then 40」 자리를 「rel 1」부터의 세 줄로 바꿔 한 번 더 돌렸고, 나머지 줄은 두 번 같았다. 마지막 줄의 rxDetail 은 GET 과 같아 줄였다.

실제 시계, 레포의 프로브. PR 15 에서 midi None 과 received [] 로 비어 있던 줄이 찼다. 긴 dict 는 ... 으로 줄였다:

$ npm run virtual-panel -- --features midi --port http=80
http     80
midi     5004
$ python3 tools/rtpmidi-probe/rtpmidi_probe.py 127.0.0.1
session:
  invite 5004: OK peer ssrc=6464c465 name='Patternflow'
  invite 5005: OK peer ssrc=6464c465 name='Patternflow'
  clock sync: count=1 ts2=53719
status: {'knobs': [0, 0, 0, 0], 'params': [500, 500, 500, 500], 'paramActive': [False, False, False, False], 'active': 'Origin'} {'runtime': True, 'channel': 1, 'outDiv': [1, 1, 1, 1], 'outMul': [1, 1, 1, 1], 'outMode': 'abs', 'host': '', 'rtpPeers': 1, 'rtpPeer': 'pf-probe', 'rx': 0, 'tx': 0, 'rxDetail': {...}}
CC20=127 (knob1 absolute -> 1000)
  params [1000, 500, 500, 500] active [True, False, False, False]
CC21=0, CC22=64, CC23=100
  params [1000, 0, 504, 787] active [True, True, True, True]
CC24=67 (knob1 +3 detents)
  knobs [0, 0, 0, 0] (was 0 )
Program Change 2
  <- MIDI c0 02
  active 'Origin' -> '0510'
Note on/off 60 (button 1 press)
  midi {'runtime': True, ..., 'rtpPeers': 1, 'rtpPeer': 'pf-probe', 'rx': 8, 'tx': 1, ...}
outbound: select pattern 1 over HTTP, expect Program Change back
  <- MIDI c0 01
  received ['c0 01']
after BY: {'runtime': True, ..., 'rtpPeers': 0, 'rtpPeer': '', 'rx': 8, 'tx': 2, ...}

CC 24 와 노트 60 은 돌아오지 않고(62행), Program Change 는 돌아온다(60행).

GitHub, 이 PR: e614d5b 에서 「Drift checks / check」(38766298571)와 「Docs point at files that exist / check」(38766298510)가 success. 「Web CI」는 web/** 에서만 돌아 이 PR 에서는 돌지 않았다. typecheck 는 위 로컬 결과다.

실행법

cd web && npm ci
npm run virtual-panel -- --features midi --port http=80     # root 가 아니면 http=8080, 그때 프로브의 status 줄은 실패한다
python3 tools/rtpmidi-probe/rtpmidi_probe.py 127.0.0.1      # 레포 루트에서, 다른 창
curl -X POST 'http://127.0.0.1/api/midi?outMode=rel'        # 노브를 64 ± 걸음으로

그다음 /vp/turn?knob=1&detents=3 은 Audio MIDI Setup 이나 rtpMIDI 로 패널 세션을 연 DAW 에 CC 24 로 보인다.

가정

  • 기준 브랜치는 dev(머리말). 표가 PR 16 에 적은 파일 하나만 더했다. CHANGELOG.md, docs/rfc-cleanup-progress.md, doc-map CSV, panel.ts, midi.ts, README.md 는 안 고쳤다. 라벨은 앞 PR 들처럼 달지 않았다.
  • 원칙은 PR 13, PR 15 와 같다. 스펙이 말하는 곳은 스펙, 말하지 않는 곳은 core_midi.h 와 core_midi_http.h 다. 인자를 읽는 차례와 오류 글, outDiv 나 outMul 을 1 로 정해도 다른 쪽이 1 로 돌아가는 것, 참가자가 없을 때 나간 것도 tx 가 세는 것(emit()), SELECT 의 커서가 그쪽에서 왔다.
  • rtpPeer 는 마지막에 참가한 이름이다. PR 15 본문은 첫 이름이라 적었지만, 펌웨어는 참가할 때마다 이름을 덮는다(core_midi_rtp.h:163).
  • 손이 버스가 잡은 노브를 250 ms 안에 돌리면 펌웨어는 그 클릭을 보내지 않는다(core_midi.h:403, paramAbsoluteActive). 패널은 57행대로 손의 클릭을 보낸다. 잠든 패널에서도 같다. 펌웨어는 잠든 프레임에 observeFrame() 을 부르지 않지만(patternflow.ino:1534), 스펙은 잠을 말하지 않는다.
  • runtime 은 늘 true 고 on= 은 읽지 않는다. 스펙의 Settings 표에 없고, midi.ts 가 스위치와 상관없이 받은 것을 적용하므로 보내는 쪽만 끄면 status 가 틀린 말을 한다.
  • host 는 IPv4 나 빈 글만 받아 저장하고 보고한다. 초대는 하지 않는다. midi.ts 가 초대하지 않는다(PR 15 의 가정).
  • you 는 뺐다. http.ts 의 Request 에는 묻는 쪽 주소가 없다. port 는 펌웨어의 5004 대신 묶인 제어 포트다.
  • rxDetail 의 worker 는 true 다. midi.ts 는 소켓을 프레임과 따로 읽는다. abs, rel, queueDropped 는 midi.ts 가 센 수이고, 나머지 진단 값은 0 이다.
  • 스크린샷은 없다. 화면이 바뀌는 PR 이 아니다.
  • firmware/ 와 web/src/ 를 건드리지 않아 build.sh all, check:ci, check:panels 는 §2.1(1) 대상이 아니다. 계획서대로 typecheck 와 깨뜨린 typecheck 한 번을 적었다. 이 PR 만의 check 는 없다(check:midi-vectors 는 PR 17).

Changelog

Tools

  • The virtual panel sends MIDI back. tools/virtual-panel/transports/midi-out.ts reports a hand's turns on CC 24 to 27 (a position by default, 64 ± steps with outMode=rel, at most one message a knob every 50 ms), a hand's buttons on notes 60 to 63 and each pattern change as Program Change, by the device → host table in docs/midi-spec.md. It answers GET and POST /api/midi and adds midi to /api/status.

for another k

  • k18: join as k17 does, move a hand with /vp/turn and /vp/button, then wait. A hand's CC goes out at the end of the next frame, so by 20 ms is enough for the first; the next on the same knob waits until 50 ms after the last. Three detents on knob 1 from start are CC 24 = 67 (abs); after POST /api/midi?outMode=rel, two back are CC 24 = 62. A pattern picked over HTTP or by a host's Program Change comes back as Program Change on the next frame. A host's CC 24 to 27 and notes never come back.
  • k18, for an issue: core_midi.h:403 drops a hand's clicks on a knob the bus held under 250 ms ago, where midi-spec.md 57 says a hand's turn is reported. core_midi_http.h:107 reads on=0|1, and GET /api/midi carries you, ip, port and outPos; the Settings table names none of them.
  • k23: midi-spec.md 32 says the panel "never echoes a host's messages back to it", where 60 sends Program Change for a change "by anyone", MIDI included. The panel follows 60, as the firmware does.
  • A row after k15, if wanted: POST /api/midi?host= invites only once midi.ts sends IN from its control socket, and runtime can go false only once midi.ts drops what comes in while it is.
  • k22: tools/virtual-panel/README.md lists midi-out.ts beside midi.ts, and /api/midi.

Upstream

  • What: Add tools/virtual-panel/transports/midi-out.ts, the device → host table in docs/midi-spec.md with outDiv, outMul, outMode and the 50 ms pacing, GET and POST /api/midi and midi in /api/status, on the session midi.ts holds.
  • Checks: npm run typecheck (which reads tools/virtual-panel) and npm run lint pass; firmware/ and web/src/ are untouched.
  • Docs map: none.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NEBrYPPE5rzyWZ4xHhbfUE

@patternflow-bot patternflow-bot changed the title [WIP] 정리 4단계 PR 16 — 가상 패널이 MIDI 로 내보내는 것과 /api/midi [WIP] 정리 4단계 PR 16 — 가상 패널이 손으로 돌린 노브와 누른 버튼, 바뀐 패턴을 MIDI 로 보내고 /api/midi 에 답한다 Oct 15, 2026
@patternflow-bot patternflow-bot changed the title [WIP] 정리 4단계 PR 16 — 가상 패널이 손으로 돌린 노브와 누른 버튼, 바뀐 패턴을 MIDI 로 보내고 /api/midi 에 답한다 [DONE] 정리 4단계 PR 16 — 가상 패널이 손으로 돌린 노브와 누른 버튼, 바뀐 패턴을 MIDI 로 보내고 /api/midi 에 답한다 Oct 15, 2026
@patternflow-bot
patternflow-bot marked this pull request as ready for review October 15, 2026 16:11
@patternflow-bot
patternflow-bot merged commit 2ca64ac into dev Oct 15, 2026
2 checks passed
@patternflow-bot
patternflow-bot deleted the claude/studio-cleanup-4-pr16-abzste branch October 15, 2026 16:15
engmung pushed a commit to engmung/Patternflow that referenced this pull request Oct 15, 2026
engmung pushed a commit to engmung/Patternflow that referenced this pull request Oct 15, 2026
Patternflow-lab#11
Patternflow-lab#12
Patternflow-lab#13
Patternflow-lab#15
Patternflow-lab#17
Patternflow-lab#14
Patternflow-lab#16
Patternflow-lab#19
Patternflow-lab#20
Patternflow-lab#18
Patternflow-lab#21
Patternflow-lab#23
Patternflow-lab#24
Patternflow-lab#25
Patternflow-lab#26
Patternflow-lab#28
Patternflow-lab#29
Patternflow-lab#30
Patternflow-lab#27
Patternflow-lab#31
Patternflow-lab#32
Patternflow-lab#34
Patternflow-lab#35
Patternflow-lab#33
Patternflow-lab#36
Patternflow-lab#37
Patternflow-lab#38
Patternflow-lab#39
Patternflow-lab#40
Patternflow-lab#41
Patternflow-lab#42
Patternflow-lab#43
Patternflow-lab#46
Patternflow-lab#44
Patternflow-lab#47
Patternflow-lab#48
Patternflow-lab#49
Patternflow-lab#45
Patternflow-lab#50
Patternflow-lab#51
Patternflow-lab#52
Patternflow-lab#54
Patternflow-lab#57
Patternflow-lab#55
Patternflow-lab#53
Patternflow-lab#56
Patternflow-lab#59
Patternflow-lab#58
Patternflow-lab#60
Patternflow-lab#61
Patternflow-lab#62
Patternflow-lab#63
Patternflow-lab#64
Patternflow-lab#65
Patternflow-lab#66
Patternflow-lab#67
Patternflow-lab#68
Patternflow-lab#69
Patternflow-lab#70
Patternflow-lab#72
Patternflow-lab#73
Patternflow-lab#71
Patternflow-lab#75
Patternflow-lab#74
Patternflow-lab#77
Patternflow-lab#78
Patternflow-lab#76
Patternflow-lab#79
Patternflow-lab#80
Patternflow-lab#81
Patternflow-lab#82
Patternflow-lab#84
Patternflow-lab#83
Patternflow-lab#85
Patternflow-lab#86
Patternflow-lab#87
Patternflow-lab#88
Patternflow-lab#90
Patternflow-lab#91
Patternflow-lab#89
Patternflow-lab#92
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants