prompt_objects 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +211 -19
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1350 -18
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +190 -432
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +561 -52
  65. data/lib/prompt_objects/server/app.rb +134 -1
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -207
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +774 -15
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +78 -0
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +58 -0
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +57 -0
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/set_env_data.rb +101 -0
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +36 -6
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +6 -2
  92. data/templates/arc-agi-1/objects/solver.md +14 -4
  93. data/templates/arc-agi-1/objects/verifier.md +6 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +28 -1
  230. metadata +175 -65
  231. data/CLAUDE.md +0 -176
  232. data/Gemfile +0 -10
  233. data/Gemfile.lock +0 -233
  234. data/IMPLEMENTATION_PLAN.md +0 -1073
  235. data/Rakefile +0 -27
  236. data/design-doc-v2.md +0 -1232
  237. data/frontend/.gitignore +0 -3
  238. data/frontend/index.html +0 -17
  239. data/frontend/package-lock.json +0 -4540
  240. data/frontend/package.json +0 -36
  241. data/frontend/postcss.config.js +0 -6
  242. data/frontend/src/App.tsx +0 -103
  243. data/frontend/src/canvas/CanvasView.tsx +0 -113
  244. data/frontend/src/canvas/ForceLayout.ts +0 -115
  245. data/frontend/src/canvas/SceneManager.ts +0 -587
  246. data/frontend/src/canvas/canvasStore.ts +0 -47
  247. data/frontend/src/canvas/constants.ts +0 -95
  248. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  249. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  250. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  251. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  252. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  253. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  254. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  255. data/frontend/src/canvas/types.ts +0 -24
  256. data/frontend/src/components/ContextMenu.tsx +0 -68
  257. data/frontend/src/components/Inspector.tsx +0 -232
  258. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  259. data/frontend/src/components/MethodList.tsx +0 -90
  260. data/frontend/src/components/ModelSelector.tsx +0 -115
  261. data/frontend/src/components/NotificationPanel.tsx +0 -130
  262. data/frontend/src/components/ObjectList.tsx +0 -78
  263. data/frontend/src/components/PaneSlot.tsx +0 -30
  264. data/frontend/src/components/SourcePane.tsx +0 -202
  265. data/frontend/src/components/SystemBar.tsx +0 -74
  266. data/frontend/src/components/Transcript.tsx +0 -76
  267. data/frontend/src/components/UsagePanel.tsx +0 -105
  268. data/frontend/src/components/Workspace.tsx +0 -260
  269. data/frontend/src/components/index.ts +0 -12
  270. data/frontend/src/hooks/useResize.ts +0 -55
  271. data/frontend/src/hooks/useWebSocket.ts +0 -517
  272. data/frontend/src/index.css +0 -103
  273. data/frontend/src/main.tsx +0 -10
  274. data/frontend/src/store/index.ts +0 -279
  275. data/frontend/src/types/index.ts +0 -165
  276. data/frontend/tailwind.config.js +0 -44
  277. data/frontend/tsconfig.json +0 -30
  278. data/frontend/vite.config.ts +0 -29
  279. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
  280. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
  281. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  282. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  283. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  284. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  285. data/lib/prompt_objects/universal/think.rb +0 -41
  286. data/prompt_objects.gemspec +0 -50
  287. data/templates/basic/.gitignore +0 -2
  288. data/templates/developer/.gitignore +0 -5
  289. data/templates/empty/.gitignore +0 -5
  290. data/templates/empty/objects/.gitkeep +0 -0
  291. data/templates/minimal/.gitignore +0 -5
  292. data/templates/writer/.gitignore +0 -5
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: voice_pad
3
+ description: Sustained harmony specialist who creates slow, connected chord beds
4
+ tools:
5
+ - set_instrument
6
+ - set_voice_level
7
+ - play_pattern
8
+ - stop_voice
9
+ ---
10
+
11
+ # Pad voice
12
+
13
+ You create the harmonic bed. Always read `composition` first.
14
+
15
+ ## Performance contract
16
+
17
+ When asked to start, join, activate, or revise your part, perform the change in
18
+ that same turn. On first activation call `play_pattern` once with `instrument:
19
+ air_pad`, the chosen `level`, and the complete pattern so setup and playback are
20
+ one operation. On later changes, call every playback tool needed to make the
21
+ requested change real. Do not merely
22
+ draft notation, ask “shall I activate this?”, or wait for confirmation: the
23
+ conductor's request is authorization. A pattern written only in prose makes no
24
+ sound. Save state only after the playback calls return without an error, and
25
+ report the calls you actually completed.
26
+
27
+ Use voice name `pad`, instrument `air_pad`, and level 0.42-0.62. Write one chord
28
+ event for every chord in the progression, normally one chord every four beats.
29
+ Join simultaneous notes with `+`; do not arpeggiate a chord that should be held.
30
+ Keep voicings between C3 and E4, use three or four notes, and favor inversions
31
+ that move each note only a short distance. Use the optional fourth event field
32
+ as a gate slightly longer than the four-beat step so adjacent chords overlap
33
+ gently instead of exposing a gap.
34
+
35
+ For Cmaj7 / Am7 / Fmaj7 / G6 at one bar per chord, the structural shape is:
36
+
37
+ ```text
38
+ C3+G3+B3+E4@4@0.58@4.5;C3+E3+G3+A3@4@0.55@4.5;C3+F3+A3+E4@4@0.54@4.5;D3+G3+B3+E4@4@0.56@4.5
39
+ ```
40
+
41
+ Compose your own voicings from the actual contract. Change them sparingly. You
42
+ may insert a full-progression rest only for a deliberate breakdown; normally
43
+ you are the quiet continuity underneath the other voices.
44
+
45
+ After every submitted playback change, save `voice_pad_state` with instrument,
46
+ level, pattern, voicing intent, and which contract you followed.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: voice_percussion
3
+ description: Sample-based percussionist who adds a barely-there pulse only when useful
4
+ tools:
5
+ - set_instrument
6
+ - set_voice_level
7
+ - play_pattern
8
+ - stop_voice
9
+ ---
10
+
11
+ # Percussion voice
12
+
13
+ You add pulse, not spectacle. Always read `composition` first.
14
+
15
+ ## Performance contract
16
+
17
+ When asked to start, join, activate, or revise your part, perform the change in
18
+ that same turn. On first activation call `play_pattern` once with `instrument:
19
+ gentle_kit`, the chosen `level`, and the complete pattern so setup and playback
20
+ are one operation. On later changes, call every playback tool needed to make
21
+ the requested change real. Do not merely
22
+ draft notation, ask “shall I activate this?”, or wait for confirmation: the
23
+ conductor's request is authorization. A pattern written only in prose makes no
24
+ sound. Save state only after the playback calls return without an error, and
25
+ report the calls you actually completed. If the correct musical choice is
26
+ silence, call `stop_voice` rather than merely saying you will remain silent.
27
+
28
+ Use voice name `percussion`, instrument `gentle_kit`, and level 0.22-0.42. Your
29
+ tokens trigger real Sonic Pi samples: `kick`, `hat`, `snare`, and `tick`. Every
30
+ silent subdivision must be an explicit `R` event.
31
+
32
+ For talk-safe background music, provide a quiet four-beat pulse like:
33
+
34
+ ```text
35
+ kick@0.5@0.45;R@0.5;hat@0.5@0.28;R@0.5;tick@0.5@0.25;R@0.5;hat@0.5@0.26;R@0.5
36
+ ```
37
+
38
+ Use at most one kick per bar. Hats or ticks may quietly mark alternating beats
39
+ or offbeats; snare is optional and rare. Fills are inappropriate unless the
40
+ conductor explicitly calls for a transition. If keys already carry a clear
41
+ pulse, simplify and lower your level instead of automatically disappearing.
42
+
43
+ After every submitted playback change, save `voice_percussion_state` with level,
44
+ pattern, density, and why percussion is helping. Stopping yourself is a valid
45
+ musical choice.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: voice_texture
3
+ description: Minimal colorist who places rare, quiet tones around the ensemble
4
+ tools:
5
+ - set_instrument
6
+ - set_voice_level
7
+ - play_pattern
8
+ - stop_voice
9
+ ---
10
+
11
+ # Texture voice
12
+
13
+ You provide one glint of color, then disappear. Always read `composition` first.
14
+
15
+ ## Performance contract
16
+
17
+ When asked to start, join, activate, or revise your part, perform the change in
18
+ that same turn. On first activation call `play_pattern` once with the chosen
19
+ `instrument`, `level`, and complete pattern so setup and playback are one
20
+ operation. On later changes, call every playback tool needed to make the
21
+ requested change real. Do not merely
22
+ draft notation, ask “shall I activate this?”, or wait for confirmation: the
23
+ conductor's request is authorization. A pattern written only in prose makes no
24
+ sound. Save state only after the playback calls return without an error, and
25
+ report the calls you actually completed.
26
+
27
+ Use voice name `texture`. Choose `felt_pluck`, `sine_glow`, or—very
28
+ sparingly—`glass_bell`, at level 0.3-0.55. Stay around G4-E5. Build a 16- or
29
+ 32-beat pattern with one to three attacks and long explicit rests. Use chord
30
+ tones or shared tones between adjacent chords so the accent does not clash.
31
+
32
+ A shape such as `G4@1@0.4;R@15` demonstrates the right amount of space. Compose
33
+ the actual note and placement from the contract. Never double the keys motif
34
+ continuously and never use rapid bells.
35
+
36
+ After every submitted playback change, save `voice_texture_state` with
37
+ instrument, level, pattern, and the coloristic purpose of each attack.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ class PlayPattern < Primitive
8
+ execution_policy parallel_safe: true, side_effect: :write, resource_keys: ->(arguments, _context, _capability) {
9
+ ["music:voice:#{arguments[:voice] || arguments['voice']}"]
10
+ }
11
+
12
+ MAX_EVENTS = 32
13
+ MAX_BEATS = 64.0
14
+ NOTE = /\A(?:\d{1,3}|[A-Ga-g](?:s|b|#)?-?\d)\z/
15
+ DRUMS = %w[kick hat snare tick].freeze
16
+ PRESETS = %w[soft_keys air_pad warm_bass felt_pluck glass_bell sine_glow gentle_kit].freeze
17
+
18
+ def name
19
+ "play_pattern"
20
+ end
21
+
22
+ def description
23
+ "Activate or replace a voice's repeating pattern at a synchronized boundary. Syntax: semicolon-separated " \
24
+ "sound@step_beats@amp@gate_beats events; amp and gate are optional. Gate controls how long pitched sound " \
25
+ "rings independently of the time to the next event. Optional instrument and level make first activation " \
26
+ "atomic. Sounds are notes (C4), chords (C3+E3+G3), R rests, or gentle_kit tokens."
27
+ end
28
+
29
+ def parameters
30
+ {
31
+ type: "object",
32
+ properties: {
33
+ voice: { type: "string", description: "Voice that owns this loop" },
34
+ pattern: {
35
+ type: "string",
36
+ description: "Events sound@step_beats@amp@gate_beats separated by semicolons; amp and gate are optional. Up to 32 events / 64 step beats."
37
+ },
38
+ instrument: { type: "string", enum: PRESETS, description: "Optional curated instrument to apply with this pattern" },
39
+ level: { type: "number", minimum: 0.0, maximum: 1.0, description: "Optional voice level to apply with this pattern" }
40
+ },
41
+ required: %w[voice pattern]
42
+ }
43
+ end
44
+
45
+ def receive(message, context:)
46
+ voice = (message[:voice] || message["voice"]).to_s.strip
47
+ pattern = (message[:pattern] || message["pattern"]).to_s.strip
48
+ instrument = message[:instrument] || message["instrument"]
49
+ level = message.key?(:level) ? message[:level] : message["level"]
50
+ return "Error: voice cannot be empty" if voice.empty?
51
+
52
+ events, error = self.class.validate(pattern)
53
+ return "Error: #{error}" if error
54
+ if instrument && !PRESETS.include?(instrument.to_s)
55
+ return "Error: unknown instrument '#{instrument}'. Choose: #{PRESETS.join(', ')}"
56
+ end
57
+ if !level.nil? && (!level.is_a?(Numeric) || !level.to_f.between?(0.0, 1.0))
58
+ return "Error: level must be a number from 0.0 to 1.0"
59
+ end
60
+
61
+ OscBridge.send_message("/music/instrument", voice, instrument.to_s) if instrument
62
+ OscBridge.send_message("/music/level", voice, level.to_f) unless level.nil?
63
+ OscBridge.send_message("/music/pattern", voice, pattern)
64
+ beats = events.sum { |event| event[:duration] }
65
+ setup = []
66
+ setup << instrument.to_s if instrument
67
+ setup << "level #{level.to_f.round(2)}" unless level.nil?
68
+ suffix = setup.empty? ? "" : " with #{setup.join(', ')}"
69
+ "Queued #{events.length}-event / #{beats.round(2)}-beat pattern for #{voice}#{suffix}. It will replace the loop at a synchronized boundary."
70
+ rescue => e
71
+ "Error setting pattern: #{e.message}. Is Sonic Pi running with the boot script loaded?"
72
+ end
73
+
74
+ def self.validate(pattern)
75
+ return [nil, "pattern cannot be empty"] if pattern.empty?
76
+
77
+ raw_events = pattern.split(";", -1).map(&:strip)
78
+ return [nil, "pattern contains an empty event"] if raw_events.any?(&:empty?)
79
+ return [nil, "pattern has more than #{MAX_EVENTS} events"] if raw_events.length > MAX_EVENTS
80
+
81
+ events = raw_events.map.with_index do |raw, index|
82
+ parts = raw.split("@", -1).map(&:strip)
83
+ return [nil, "event #{index + 1} must be sound@step_beats, sound@step_beats@amp, or sound@step_beats@amp@gate_beats"] unless parts.length.between?(2, 4)
84
+
85
+ sound = parts[0]
86
+ return [nil, "event #{index + 1} has an invalid sound '#{sound}'"] unless valid_sound?(sound)
87
+
88
+ duration = Float(parts[1])
89
+ return [nil, "event #{index + 1} duration must be 0.125..16 beats"] unless duration.between?(0.125, 16.0)
90
+
91
+ amp = parts.length == 3 ? Float(parts[2]) : 0.7
92
+ amp = Float(parts[2]) if parts.length == 4
93
+ return [nil, "event #{index + 1} amplitude must be 0.0..1.0"] unless amp.between?(0.0, 1.0)
94
+
95
+ gate = parts.length == 4 ? Float(parts[3]) : duration
96
+ return [nil, "event #{index + 1} gate must be 0.05..16 beats"] unless gate.between?(0.05, 16.0)
97
+
98
+ { sound: sound, duration: duration, amp: amp, gate: gate }
99
+ rescue ArgumentError, TypeError
100
+ return [nil, "event #{index + 1} step, amplitude, and gate must be numbers"]
101
+ end
102
+
103
+ total = events.sum { |event| event[:duration] }
104
+ return [nil, "pattern is longer than #{MAX_BEATS.to_i} beats"] if total > MAX_BEATS
105
+
106
+ [events, nil]
107
+ end
108
+
109
+ def self.valid_sound?(sound)
110
+ return true if sound.casecmp("R").zero? || DRUMS.include?(sound.downcase)
111
+
112
+ notes = sound.split("+")
113
+ notes.length.between?(1, 6) && notes.all? { |candidate| NOTE.match?(candidate) }
114
+ end
115
+ private_class_method :valid_sound?
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "socket"
5
+ require_relative "support/osc_bridge"
6
+
7
+ module PromptObjects
8
+ module Primitives
9
+ class QueryMusicEngine < Primitive
10
+ execution_policy parallel_safe: true, side_effect: :read
11
+
12
+ RESPONSE_PORT = 4561
13
+ TIMEOUT = 2
14
+
15
+ def name
16
+ "query_music_engine"
17
+ end
18
+
19
+ def description
20
+ "Query the actual Sonic Pi engine state: tempo, voice instruments, levels, active flags, and patterns."
21
+ end
22
+
23
+ def parameters
24
+ { type: "object", properties: {}, required: [] }
25
+ end
26
+
27
+ def receive(message, context:)
28
+ socket = UDPSocket.new
29
+ socket.bind("127.0.0.1", RESPONSE_PORT)
30
+ OscBridge.send_message("/music/query", RESPONSE_PORT)
31
+ ready = IO.select([socket], nil, nil, TIMEOUT)
32
+ return no_response(socket) unless ready
33
+
34
+ data, = socket.recvfrom(8192)
35
+ socket.close
36
+ state = parse_osc_string(data)
37
+ return "Received an empty response from Sonic Pi." if state.nil? || state.empty?
38
+
39
+ format_state(state)
40
+ rescue Errno::EADDRINUSE
41
+ "Port #{RESPONSE_PORT} is busy; another query may be in progress. Try again shortly."
42
+ rescue => e
43
+ socket&.close
44
+ "Error querying Sonic Pi: #{e.message}"
45
+ end
46
+
47
+ private
48
+
49
+ def no_response(socket)
50
+ socket.close
51
+ "Sonic Pi is not responding. Open Sonic Pi and run extras/sonic_pi_boot.rb with run_file."
52
+ end
53
+
54
+ def parse_osc_string(data)
55
+ address_end = data.index("\x00")
56
+ return unless address_end
57
+
58
+ position = address_end + 1
59
+ position += 1 while position % 4 != 0
60
+ return unless data[position] == ","
61
+
62
+ tag_end = data.index("\x00", position)
63
+ return unless tag_end && data[position + 1] == "s"
64
+
65
+ position = tag_end + 1
66
+ position += 1 while position % 4 != 0
67
+ string_end = data.index("\x00", position)
68
+ data[position...string_end] if string_end
69
+ end
70
+
71
+ def format_state(state)
72
+ parts = state.split("||")
73
+ header = parts.shift.to_s.split("|", -1)
74
+ voices = parts.each_with_object({}) do |part, result|
75
+ name, active, instrument, level, pattern = part.split("|", 5)
76
+ result[name] = {
77
+ "active" => active == "true",
78
+ "instrument" => instrument,
79
+ "level" => level.to_f,
80
+ "pattern" => pattern
81
+ }
82
+ end
83
+
84
+ JSON.pretty_generate(
85
+ "connected" => true,
86
+ "bpm" => header[0].to_i,
87
+ "slots_used" => header[1].to_i,
88
+ "slots_free" => header[2].to_i,
89
+ "voices" => voices
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ class SetInstrument < Primitive
8
+ execution_policy parallel_safe: true, side_effect: :write, resource_keys: ->(arguments, _context, _capability) {
9
+ ["music:voice:#{arguments[:voice] || arguments['voice']}"]
10
+ }
11
+
12
+ PRESETS = %w[soft_keys air_pad warm_bass felt_pluck glass_bell sine_glow gentle_kit].freeze
13
+
14
+ def name
15
+ "set_instrument"
16
+ end
17
+
18
+ def description
19
+ "Choose a curated, level-matched instrument: #{PRESETS.join(', ')}. " \
20
+ "gentle_kit accepts kick, hat, snare, and tick pattern tokens."
21
+ end
22
+
23
+ def parameters
24
+ {
25
+ type: "object",
26
+ properties: {
27
+ voice: { type: "string", description: "Stable voice name, such as keys, pad, bass, percussion, or texture" },
28
+ instrument: { type: "string", enum: PRESETS, description: "A curated instrument preset" }
29
+ },
30
+ required: %w[voice instrument]
31
+ }
32
+ end
33
+
34
+ def receive(message, context:)
35
+ voice = (message[:voice] || message["voice"]).to_s.strip
36
+ instrument = (message[:instrument] || message["instrument"]).to_s.strip
37
+ return "Error: voice cannot be empty" if voice.empty?
38
+ return "Error: unknown instrument '#{instrument}'. Choose: #{PRESETS.join(', ')}" unless PRESETS.include?(instrument)
39
+
40
+ OscBridge.send_message("/music/instrument", voice, instrument)
41
+ "Set #{voice} to #{instrument}; the change takes effect at its next synchronized pattern boundary."
42
+ rescue => e
43
+ "Error setting instrument: #{e.message}. Is Sonic Pi running with the boot script loaded?"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ class SetTempo < Primitive
8
+ execution_policy parallel_safe: false, side_effect: :exclusive
9
+
10
+ MIN_BPM = 50
11
+ MAX_BPM = 120
12
+
13
+ def name
14
+ "set_tempo"
15
+ end
16
+
17
+ def description
18
+ "Set the ensemble's shared tempo to 50-120 BPM. Every voice stays on the same bar clock."
19
+ end
20
+
21
+ def parameters
22
+ {
23
+ type: "object",
24
+ properties: {
25
+ bpm: { type: "integer", minimum: MIN_BPM, maximum: MAX_BPM,
26
+ description: "Shared tempo. For background music, prefer 65-82 BPM." }
27
+ },
28
+ required: ["bpm"]
29
+ }
30
+ end
31
+
32
+ def receive(message, context:)
33
+ bpm = (message[:bpm] || message["bpm"]).to_i
34
+ return "Error: bpm must be between #{MIN_BPM} and #{MAX_BPM}" unless bpm.between?(MIN_BPM, MAX_BPM)
35
+
36
+ OscBridge.send_message("/music/tempo", bpm)
37
+ "Tempo will change to #{bpm} BPM on the shared clock."
38
+ rescue => e
39
+ "Error setting tempo: #{e.message}. Is Sonic Pi running with the boot script loaded?"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ class SetVoiceLevel < Primitive
8
+ execution_policy parallel_safe: true, side_effect: :write, resource_keys: ->(arguments, _context, _capability) {
9
+ ["music:voice:#{arguments[:voice] || arguments['voice']}"]
10
+ }
11
+
12
+ def name
13
+ "set_voice_level"
14
+ end
15
+
16
+ def description
17
+ "Set a voice's mix level from 0.0 (silent) to 1.0 (preset maximum). Prefer 0.35-0.75."
18
+ end
19
+
20
+ def parameters
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ voice: { type: "string", description: "Voice to mix" },
25
+ level: { type: "number", minimum: 0.0, maximum: 1.0, description: "Safe scale within the preset's baked-in level" }
26
+ },
27
+ required: %w[voice level]
28
+ }
29
+ end
30
+
31
+ def receive(message, context:)
32
+ voice = (message[:voice] || message["voice"]).to_s.strip
33
+ level = Float(message[:level] || message["level"])
34
+ return "Error: voice cannot be empty" if voice.empty?
35
+ return "Error: level must be between 0.0 and 1.0" unless level.between?(0.0, 1.0)
36
+
37
+ OscBridge.send_message("/music/level", voice, level)
38
+ "Set #{voice} level to #{level.round(2)}."
39
+ rescue ArgumentError, TypeError
40
+ "Error: level must be a number between 0.0 and 1.0"
41
+ rescue => e
42
+ "Error setting voice level: #{e.message}. Is Sonic Pi running with the boot script loaded?"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/osc_bridge"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ class StopVoice < Primitive
8
+ execution_policy parallel_safe: false, side_effect: :exclusive
9
+
10
+ def name
11
+ "stop_voice"
12
+ end
13
+
14
+ def description
15
+ "Stop one voice at its next boundary, or use voice 'all' for immediate ensemble silence."
16
+ end
17
+
18
+ def parameters
19
+ {
20
+ type: "object",
21
+ properties: { voice: { type: "string", description: "Voice name, or all" } },
22
+ required: ["voice"]
23
+ }
24
+ end
25
+
26
+ def receive(message, context:)
27
+ voice = (message[:voice] || message["voice"]).to_s.strip
28
+ return "Error: voice cannot be empty" if voice.empty?
29
+
30
+ OscBridge.send_message("/music/stop", voice)
31
+ voice == "all" ? "Stopped all music." : "Queued stop for #{voice}."
32
+ rescue => e
33
+ "Error stopping voice: #{e.message}. Is Sonic Pi running with the boot script loaded?"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "osc-ruby"
4
+
5
+ module PromptObjects
6
+ module Primitives
7
+ module OscBridge
8
+ SONIC_PI_HOST = ENV.fetch("SONIC_PI_HOST", "127.0.0.1")
9
+ SONIC_PI_PORT = ENV.fetch("SONIC_PI_PORT", "4560").to_i
10
+
11
+ @client = nil
12
+ @mutex = Mutex.new
13
+
14
+ def self.client
15
+ @mutex.synchronize do
16
+ @client ||= OSC::Client.new(SONIC_PI_HOST, SONIC_PI_PORT)
17
+ end
18
+ end
19
+
20
+ def self.send_message(path, *args)
21
+ client.send(OSC::Message.new(path, *args))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Colin Harris
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ module OSC
2
+ class AddressPattern
3
+ def initialize(pattern)
4
+ @pattern = pattern
5
+
6
+ generate_regex_from_pattern
7
+ end
8
+
9
+ def match?(address)
10
+ !!(@re.nil? || @re.match(address))
11
+ end
12
+
13
+ private
14
+
15
+ def generate_regex_from_pattern
16
+ case @pattern
17
+ when nil
18
+ @re = @pattern
19
+ when Regexp
20
+ @re = @pattern
21
+ when String
22
+ # i'm unsure what this does
23
+ # @pattern.gsub!(/[.^(|)]/, '\\1')
24
+
25
+ # handles osc single char wildcard matching
26
+ @pattern.gsub!(/\?/, '[^/]')
27
+
28
+ # handles ** - used in creating osc routers
29
+ # TODO: turns out the OSC 1.1 spec says that we shoudl use "//"
30
+ # this will be implemented in the next major release of this gem
31
+ #@pattern.gsub!(/\*\*/, '.*' )
32
+
33
+ # handles osc * - 0 or more matching
34
+ @pattern.gsub!(/\*[^\*]/, '[^/]*')
35
+
36
+ # handles [!] matching
37
+ @pattern.gsub!(/\[!/, '[^')
38
+
39
+ # handles {} matching
40
+ @pattern.gsub!(/\{/, '(')
41
+ @pattern.gsub!(/,/, '|')
42
+ @pattern.gsub!(/\}/, ')')
43
+
44
+
45
+ # keeps from matching before the begining of the pattern
46
+ @pattern.gsub!(/\A/, '\A')
47
+
48
+ # keeps from matching beyond the end,
49
+ # eg. pattern /hi does not match /hidden
50
+ @pattern.gsub!(/\z/, '\z')
51
+
52
+ @re = Regexp.new(@pattern)
53
+ else
54
+ raise(ArgumentError, 'invalid pattern')
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ require 'ipaddr'
2
+
3
+ module OSC
4
+ class BroadcastClient
5
+
6
+ BROADCAST_ADDRESS = '<broadcast>'
7
+
8
+ attr_reader :port
9
+
10
+ def initialize(port, local_ip = nil)
11
+ @port = port
12
+ @so = UDPSocket.new
13
+ @so.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
14
+ if local_ip
15
+ @so.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(local_ip).hton
16
+ @so.bind(local_ip, 0)
17
+ end
18
+ end
19
+
20
+ def send(mesg)
21
+ @so.send(mesg.encode, 0, BROADCAST_ADDRESS, @port)
22
+ end
23
+ end
24
+ end