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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +142 -0
- data/README.md +211 -19
- data/exe/poop +5 -0
- data/exe/prompt_objects +1 -1108
- data/exe/prompt_objects_mcp +5 -3
- data/lib/prompt_objects/capability.rb +33 -0
- data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
- data/lib/prompt_objects/cli/env_command.rb +435 -0
- data/lib/prompt_objects/cli/events_command.rb +145 -0
- data/lib/prompt_objects/cli/explore_command.rb +155 -0
- data/lib/prompt_objects/cli/helpers.rb +91 -0
- data/lib/prompt_objects/cli/message_command.rb +216 -0
- data/lib/prompt_objects/cli/repl_command.rb +366 -0
- data/lib/prompt_objects/cli/serve_command.rb +162 -0
- data/lib/prompt_objects/cli.rb +64 -422
- data/lib/prompt_objects/connectors/base.rb +16 -29
- data/lib/prompt_objects/connectors/mcp.rb +82 -98
- data/lib/prompt_objects/domain/records.rb +45 -0
- data/lib/prompt_objects/environment/exporter.rb +5 -4
- data/lib/prompt_objects/environment/git.rb +21 -1
- data/lib/prompt_objects/environment/importer.rb +5 -4
- data/lib/prompt_objects/environment/manager.rb +44 -6
- data/lib/prompt_objects/environment/manifest.rb +26 -4
- data/lib/prompt_objects/environment.rb +1350 -18
- data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
- data/lib/prompt_objects/execution/context.rb +137 -0
- data/lib/prompt_objects/execution/contracts.rb +59 -0
- data/lib/prompt_objects/execution/permit_pool.rb +49 -0
- data/lib/prompt_objects/execution/policy.rb +47 -0
- data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
- data/lib/prompt_objects/execution/runner.rb +498 -0
- data/lib/prompt_objects/execution/scheduler.rb +325 -0
- data/lib/prompt_objects/llm/factory.rb +4 -4
- data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
- data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
- data/lib/prompt_objects/llm/pricing.rb +9 -2
- data/lib/prompt_objects/llm/response.rb +9 -4
- data/lib/prompt_objects/loader.rb +28 -1
- data/lib/prompt_objects/mcp/server.rb +6 -3
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
- data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
- data/lib/prompt_objects/operations/diagnostics.rb +115 -0
- data/lib/prompt_objects/operations/retention.rb +75 -0
- data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
- data/lib/prompt_objects/primitives/http_get.rb +2 -0
- data/lib/prompt_objects/primitives/list_files.rb +2 -0
- data/lib/prompt_objects/primitives/read_file.rb +2 -0
- data/lib/prompt_objects/primitives/think.rb +36 -0
- data/lib/prompt_objects/primitives/write_file.rb +9 -0
- data/lib/prompt_objects/prompt_object.rb +190 -432
- data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
- data/lib/prompt_objects/repositories/base_repository.rb +37 -0
- data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
- data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
- data/lib/prompt_objects/repositories/event_repository.rb +105 -0
- data/lib/prompt_objects/repositories/message_repository.rb +73 -0
- data/lib/prompt_objects/repositories/run_repository.rb +254 -0
- data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
- data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
- data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
- data/lib/prompt_objects/server/api/routes.rb +561 -52
- data/lib/prompt_objects/server/app.rb +134 -1
- data/lib/prompt_objects/server/file_watcher.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
- data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +202 -207
- data/lib/prompt_objects/server.rb +170 -1
- data/lib/prompt_objects/service.rb +129 -0
- data/lib/prompt_objects/session/store.rb +774 -15
- data/lib/prompt_objects/universal/add_capability.rb +1 -1
- data/lib/prompt_objects/universal/create_capability.rb +4 -2
- data/lib/prompt_objects/universal/delete_env_data.rb +78 -0
- data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
- data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
- data/lib/prompt_objects/universal/get_env_data.rb +58 -0
- data/lib/prompt_objects/universal/get_run_status.rb +30 -0
- data/lib/prompt_objects/universal/join_runs.rb +35 -0
- data/lib/prompt_objects/universal/list_env_data.rb +57 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
- data/lib/prompt_objects/universal/render_artifact.rb +91 -0
- data/lib/prompt_objects/universal/request_primitive.rb +1 -1
- data/lib/prompt_objects/universal/set_env_data.rb +101 -0
- data/lib/prompt_objects/version.rb +5 -0
- data/lib/prompt_objects.rb +36 -6
- data/objects/coordinator.md +8 -4
- data/templates/arc-agi-1/objects/data_manager.md +2 -2
- data/templates/arc-agi-1/objects/observer.md +6 -2
- data/templates/arc-agi-1/objects/solver.md +14 -4
- data/templates/arc-agi-1/objects/verifier.md +6 -2
- data/templates/arc-agi-3/manifest.yml +19 -0
- data/templates/arc-agi-3/objects/analyst.md +79 -0
- data/templates/arc-agi-3/objects/player.md +76 -0
- data/templates/arc-agi-3/objects/strategist.md +97 -0
- data/templates/arc-agi-3/primitives/action_log.rb +133 -0
- data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
- data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
- data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
- data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
- data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
- data/templates/basic/objects/basic.md +11 -3
- data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
- data/templates/chiptune/manifest.yml +20 -0
- data/templates/chiptune/objects/conductor.md +184 -0
- data/templates/chiptune/objects/sonic_pi.md +65 -0
- data/templates/chiptune/objects/voice_bass.md +44 -0
- data/templates/chiptune/objects/voice_lead.md +44 -0
- data/templates/chiptune/objects/voice_pad.md +43 -0
- data/templates/chiptune/objects/voice_perc.md +45 -0
- data/templates/chiptune/primitives/play_note.rb +59 -0
- data/templates/chiptune/primitives/play_sequence.rb +55 -0
- data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
- data/templates/chiptune/primitives/set_synth.rb +57 -0
- data/templates/chiptune/primitives/set_tempo.rb +45 -0
- data/templates/chiptune/primitives/stop_voice.rb +46 -0
- data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/developer/objects/code_reviewer.md +1 -1
- data/templates/developer/objects/coordinator.md +16 -3
- data/templates/developer/objects/debugger.md +3 -3
- data/templates/empty/objects/assistant.md +10 -5
- data/templates/minimal/objects/assistant.md +10 -5
- data/templates/music/extras/sonic_pi_boot.rb +220 -0
- data/templates/music/manifest.yml +16 -0
- data/templates/music/objects/conductor.md +154 -0
- data/templates/music/objects/sonic_pi.md +65 -0
- data/templates/music/primitives/play_note.rb +55 -0
- data/templates/music/primitives/play_sequence.rb +51 -0
- data/templates/music/primitives/query_sonic_pi.rb +139 -0
- data/templates/music/primitives/set_synth.rb +53 -0
- data/templates/music/primitives/set_tempo.rb +43 -0
- data/templates/music/primitives/stop_voice.rb +44 -0
- data/templates/music/primitives/support/osc_bridge.rb +33 -0
- data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/music-beginner/README.md +100 -0
- data/templates/music-beginner/extras/README.md +27 -0
- data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
- data/templates/music-beginner/manifest.yml +25 -0
- data/templates/music-beginner/objects/conductor.md +222 -0
- data/templates/music-beginner/objects/engine_setup.md +54 -0
- data/templates/music-beginner/objects/voice_bass.md +43 -0
- data/templates/music-beginner/objects/voice_keys.md +48 -0
- data/templates/music-beginner/objects/voice_pad.md +46 -0
- data/templates/music-beginner/objects/voice_percussion.md +45 -0
- data/templates/music-beginner/objects/voice_texture.md +37 -0
- data/templates/music-beginner/primitives/play_pattern.rb +118 -0
- data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
- data/templates/music-beginner/primitives/set_instrument.rb +47 -0
- data/templates/music-beginner/primitives/set_tempo.rb +43 -0
- data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
- data/templates/music-beginner/primitives/stop_voice.rb +37 -0
- data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/music-pro/extras/AUDITION.md +100 -0
- data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
- data/templates/music-pro/manifest.yml +19 -0
- data/templates/music-pro/objects/conductor.md +126 -0
- data/templates/music-pro/objects/sonic_pi.md +35 -0
- data/templates/music-pro/objects/voice_harmony.md +48 -0
- data/templates/music-pro/objects/voice_rhythm.md +43 -0
- data/templates/music-pro/objects/voice_texture.md +42 -0
- data/templates/music-pro/primitives/define_patch.rb +46 -0
- data/templates/music-pro/primitives/query_music_engine.rb +69 -0
- data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
- data/templates/music-pro/primitives/set_tempo.rb +31 -0
- data/templates/music-pro/primitives/stop_voice.rb +31 -0
- data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
- data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
- data/templates/music-pro/primitives/trigger_event.rb +44 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
- data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
- data/templates/writer/objects/coordinator.md +14 -3
- data/templates/writer/objects/editor.md +1 -1
- data/templates/writer/objects/researcher.md +2 -2
- data/tools/thread-explorer.html +28 -1
- metadata +175 -65
- data/CLAUDE.md +0 -176
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -233
- data/IMPLEMENTATION_PLAN.md +0 -1073
- data/Rakefile +0 -27
- data/design-doc-v2.md +0 -1232
- data/frontend/.gitignore +0 -3
- data/frontend/index.html +0 -17
- data/frontend/package-lock.json +0 -4540
- data/frontend/package.json +0 -36
- data/frontend/postcss.config.js +0 -6
- data/frontend/src/App.tsx +0 -103
- data/frontend/src/canvas/CanvasView.tsx +0 -113
- data/frontend/src/canvas/ForceLayout.ts +0 -115
- data/frontend/src/canvas/SceneManager.ts +0 -587
- data/frontend/src/canvas/canvasStore.ts +0 -47
- data/frontend/src/canvas/constants.ts +0 -95
- data/frontend/src/canvas/controls/CameraControls.ts +0 -98
- data/frontend/src/canvas/edges/MessageArc.ts +0 -149
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
- data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
- data/frontend/src/canvas/nodes/PONode.ts +0 -249
- data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
- data/frontend/src/canvas/types.ts +0 -24
- data/frontend/src/components/ContextMenu.tsx +0 -68
- data/frontend/src/components/Inspector.tsx +0 -232
- data/frontend/src/components/MarkdownMessage.tsx +0 -155
- data/frontend/src/components/MethodList.tsx +0 -90
- data/frontend/src/components/ModelSelector.tsx +0 -115
- data/frontend/src/components/NotificationPanel.tsx +0 -130
- data/frontend/src/components/ObjectList.tsx +0 -78
- data/frontend/src/components/PaneSlot.tsx +0 -30
- data/frontend/src/components/SourcePane.tsx +0 -202
- data/frontend/src/components/SystemBar.tsx +0 -74
- data/frontend/src/components/Transcript.tsx +0 -76
- data/frontend/src/components/UsagePanel.tsx +0 -105
- data/frontend/src/components/Workspace.tsx +0 -260
- data/frontend/src/components/index.ts +0 -12
- data/frontend/src/hooks/useResize.ts +0 -55
- data/frontend/src/hooks/useWebSocket.ts +0 -517
- data/frontend/src/index.css +0 -103
- data/frontend/src/main.tsx +0 -10
- data/frontend/src/store/index.ts +0 -279
- data/frontend/src/types/index.ts +0 -165
- data/frontend/tailwind.config.js +0 -44
- data/frontend/tsconfig.json +0 -30
- data/frontend/vite.config.ts +0 -29
- data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
- data/lib/prompt_objects/universal/add_primitive.rb +0 -118
- data/lib/prompt_objects/universal/ask_human.rb +0 -109
- data/lib/prompt_objects/universal/create_primitive.rb +0 -174
- data/lib/prompt_objects/universal/list_primitives.rb +0 -145
- data/lib/prompt_objects/universal/think.rb +0 -41
- data/prompt_objects.gemspec +0 -50
- data/templates/basic/.gitignore +0 -2
- data/templates/developer/.gitignore +0 -5
- data/templates/empty/.gitignore +0 -5
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/minimal/.gitignore +0 -5
- data/templates/writer/.gitignore +0 -5
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
# Prompt Objects Music Pro — expressive live-pattern engine for Sonic Pi.
|
|
2
|
+
#
|
|
3
|
+
# In Sonic Pi, run this one line (using the environment's absolute path):
|
|
4
|
+
# run_file "/ABSOLUTE/PATH/extras/sonic_pi_boot.rb"
|
|
5
|
+
#
|
|
6
|
+
# The engine receives structured OSC from Prompt Objects. Patterns are replaced
|
|
7
|
+
# atomically at bar boundaries; voices remain independent, synchronized live
|
|
8
|
+
# collaborators rather than parts in a pre-rendered song.
|
|
9
|
+
|
|
10
|
+
require "json"
|
|
11
|
+
|
|
12
|
+
use_bpm 78
|
|
13
|
+
set :po_bpm, 78.0
|
|
14
|
+
set :po_beats_per_bar, 4.0
|
|
15
|
+
set :po_master_amp, 0.7
|
|
16
|
+
set :po_max_voices, 12
|
|
17
|
+
set :po_voice_map, {}
|
|
18
|
+
set :po_next_slot, 0
|
|
19
|
+
set :po_patch_names, []
|
|
20
|
+
|
|
21
|
+
12.times do |i|
|
|
22
|
+
set :"po_v#{i}_active", false
|
|
23
|
+
set :"po_v#{i}_definition", ""
|
|
24
|
+
set :"po_v#{i}_cycle", 0
|
|
25
|
+
set :"po_v#{i}_name", ""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
define :po_clamp do |value, low, high|
|
|
29
|
+
[[value.to_f, low.to_f].max, high.to_f].min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
define :po_symbolize_hash do |value|
|
|
33
|
+
out = {}
|
|
34
|
+
if value.is_a?(Hash)
|
|
35
|
+
value.each do |key, item|
|
|
36
|
+
out[key.to_s.to_sym] = item
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
out
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
define :po_safe_identifier do |value|
|
|
43
|
+
value.to_s.match?(/\A[a-zA-Z][a-zA-Z0-9_:-]{0,79}\z/)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
define :po_store_patch do |name, patch|
|
|
47
|
+
return false unless po_safe_identifier(name)
|
|
48
|
+
return false unless patch.is_a?(Hash)
|
|
49
|
+
return false unless ["synth", "sample"].include?((patch["kind"] || "synth").to_s)
|
|
50
|
+
source = patch["synth"] || patch["sample"]
|
|
51
|
+
return false unless po_safe_identifier(source)
|
|
52
|
+
effects = patch["effects"] || []
|
|
53
|
+
return false unless effects.is_a?(Array) && effects.length <= 3
|
|
54
|
+
params = patch["params"] || {}
|
|
55
|
+
return false unless params.is_a?(Hash) && params.length <= 24
|
|
56
|
+
effects.each do |effect|
|
|
57
|
+
return false unless effect.is_a?(Hash) && po_safe_identifier(effect["name"])
|
|
58
|
+
fx_params = effect["params"] || {}
|
|
59
|
+
return false unless fx_params.is_a?(Hash) && fx_params.length <= 24
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
set :"po_patch_#{name}", JSON.generate(patch)
|
|
63
|
+
names = get(:po_patch_names) || []
|
|
64
|
+
unless names.include?(name.to_s)
|
|
65
|
+
replacement = names.to_a + [name.to_s]
|
|
66
|
+
set :po_patch_names, replacement
|
|
67
|
+
end
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# These are starting points, not a whitelist. Agents can replace them or define
|
|
72
|
+
# completely new synth/sample/effect patches through define_patch.
|
|
73
|
+
po_store_patch("soft_rodeo", {
|
|
74
|
+
"kind" => "synth", "synth" => "rodeo",
|
|
75
|
+
"params" => { "amp" => 0.16, "cutoff" => 72, "release" => 1.8 },
|
|
76
|
+
"effects" => [{ "name" => "reverb", "params" => { "mix" => 0.18, "room" => 0.65 } }]
|
|
77
|
+
})
|
|
78
|
+
po_store_patch("air_blade", {
|
|
79
|
+
"kind" => "synth", "synth" => "blade",
|
|
80
|
+
"params" => { "amp" => 0.11, "attack" => 0.8, "release" => 3.5, "cutoff" => 72 },
|
|
81
|
+
"effects" => [{ "name" => "reverb", "params" => { "mix" => 0.28, "room" => 0.8 } }]
|
|
82
|
+
})
|
|
83
|
+
po_store_patch("foundation", {
|
|
84
|
+
"kind" => "synth", "synth" => "bass_foundation",
|
|
85
|
+
"params" => { "amp" => 0.15, "cutoff" => 70, "release" => 0.5 },
|
|
86
|
+
"effects" => []
|
|
87
|
+
})
|
|
88
|
+
po_store_patch("tiny_kalimba", {
|
|
89
|
+
"kind" => "synth", "synth" => "kalimba",
|
|
90
|
+
"params" => { "amp" => 0.1, "release" => 1.2 },
|
|
91
|
+
"effects" => [{ "name" => "reverb", "params" => { "mix" => 0.2, "room" => 0.7 } }]
|
|
92
|
+
})
|
|
93
|
+
po_store_patch("chip_raw", {
|
|
94
|
+
"kind" => "synth", "synth" => "chiplead",
|
|
95
|
+
"params" => { "amp" => 0.12, "release" => 0.2 },
|
|
96
|
+
"effects" => []
|
|
97
|
+
})
|
|
98
|
+
po_store_patch("soft_drum", {
|
|
99
|
+
"kind" => "sample", "sample" => "bd_haus",
|
|
100
|
+
"params" => { "amp" => 0.16 }, "effects" => []
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
define :po_patch do |patch_ref|
|
|
104
|
+
if patch_ref.is_a?(Hash)
|
|
105
|
+
return patch_ref
|
|
106
|
+
end
|
|
107
|
+
raw = get(:"po_patch_#{patch_ref}")
|
|
108
|
+
begin
|
|
109
|
+
raw.to_s.empty? ? JSON.parse(get(:po_patch_soft_rodeo)) : JSON.parse(raw)
|
|
110
|
+
rescue
|
|
111
|
+
JSON.parse(get(:po_patch_soft_rodeo))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
define :po_voice_slot do |name|
|
|
116
|
+
name = name.to_s
|
|
117
|
+
return nil unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_-]{0,39}\z/)
|
|
118
|
+
map = get(:po_voice_map) || {}
|
|
119
|
+
return map[name] unless map[name].nil?
|
|
120
|
+
|
|
121
|
+
idx = get(:po_next_slot).to_i
|
|
122
|
+
if idx >= get(:po_max_voices).to_i
|
|
123
|
+
puts "Music Pro: voice limit reached; ignored #{name}"
|
|
124
|
+
return nil
|
|
125
|
+
end
|
|
126
|
+
replacement = {}
|
|
127
|
+
map.each { |key, value| replacement[key] = value }
|
|
128
|
+
replacement[name] = idx
|
|
129
|
+
set :po_voice_map, replacement
|
|
130
|
+
set :po_next_slot, idx + 1
|
|
131
|
+
set :"po_v#{idx}_name", name
|
|
132
|
+
puts "Music Pro: registered #{name} in slot #{idx}"
|
|
133
|
+
idx
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Split on whitespace only at the top nesting level. This small parser keeps the
|
|
137
|
+
# notation concise without evaluating agent-authored Ruby code.
|
|
138
|
+
define :po_tokens do |text|
|
|
139
|
+
tokens = []
|
|
140
|
+
token = ""
|
|
141
|
+
depths = { "[" => 0, "(" => 0, "<" => 0, "{" => 0 }
|
|
142
|
+
closes = { "]" => "[", ")" => "(", ">" => "<", "}" => "{" }
|
|
143
|
+
text.to_s.each_char do |char|
|
|
144
|
+
if depths.key?(char)
|
|
145
|
+
depths[char] += 1
|
|
146
|
+
token += char
|
|
147
|
+
elsif closes.key?(char)
|
|
148
|
+
depths[closes[char]] -= 1
|
|
149
|
+
token += char
|
|
150
|
+
elsif char.match?(/\s/) && depths.values.all? { |depth| depth == 0 }
|
|
151
|
+
unless token.empty?
|
|
152
|
+
tokens << token
|
|
153
|
+
token = ""
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
token += char
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
tokens << token unless token.empty?
|
|
160
|
+
tokens
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
define :po_scalar do |value|
|
|
164
|
+
value = value.to_s.strip
|
|
165
|
+
return true if value == "true"
|
|
166
|
+
return false if value == "false"
|
|
167
|
+
return value.to_f if value.match?(/\A-?\d+(\.\d+)?\z/)
|
|
168
|
+
value
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
define :po_event_options do |raw|
|
|
172
|
+
options = {}
|
|
173
|
+
raw.to_s.split(",").each do |pair|
|
|
174
|
+
key, value = pair.split("=", 2)
|
|
175
|
+
options[key.to_s.strip] = po_scalar(value) unless key.to_s.strip.empty? || value.nil?
|
|
176
|
+
end
|
|
177
|
+
options
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
define :po_note do |value|
|
|
181
|
+
str = value.to_s.strip
|
|
182
|
+
return str.to_i if str.match?(/\A\d+\z/)
|
|
183
|
+
note(str.to_sym)
|
|
184
|
+
rescue
|
|
185
|
+
60
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
define :po_parse_token do |raw_token, cycle|
|
|
189
|
+
token = raw_token.to_s
|
|
190
|
+
options = {}
|
|
191
|
+
if token.end_with?("}")
|
|
192
|
+
open = token.rindex("{")
|
|
193
|
+
if open
|
|
194
|
+
options = po_event_options(token[(open + 1)...-1])
|
|
195
|
+
token = token[0...open]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
repeat = 1
|
|
200
|
+
repeat_match = token.match(/\*(\d+)\z/)
|
|
201
|
+
if repeat_match
|
|
202
|
+
repeat = po_clamp(repeat_match[1].to_i, 1, 16).to_i
|
|
203
|
+
token = token.sub(/\*\d+\z/, "")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
duration = nil
|
|
207
|
+
duration_match = token.match(/\/([0-9]+(?:\.[0-9]+)?)\z/)
|
|
208
|
+
if duration_match
|
|
209
|
+
duration = po_clamp(duration_match[1].to_f, 0.125, 64.0)
|
|
210
|
+
token = token.sub(/\/[0-9]+(?:\.[0-9]+)?\z/, "")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
events = []
|
|
214
|
+
if token.start_with?("[") && token.end_with?("]")
|
|
215
|
+
events = po_parse_pattern(token[1...-1], cycle)
|
|
216
|
+
if duration && !events.empty?
|
|
217
|
+
total = events.map { |event| event["duration"].to_f }.inject(0.0, :+)
|
|
218
|
+
duration_ratio = total > 0 ? duration / total : 1.0
|
|
219
|
+
events.each { |event| event["duration"] = event["duration"].to_f * duration_ratio }
|
|
220
|
+
end
|
|
221
|
+
elsif token.start_with?("<") && token.end_with?(">")
|
|
222
|
+
choices = po_tokens(token[1...-1])
|
|
223
|
+
chosen = choices.empty? ? "~" : choices[cycle.to_i % choices.length]
|
|
224
|
+
events = po_parse_token(chosen + (duration ? "/#{duration}" : ""), cycle)
|
|
225
|
+
else
|
|
226
|
+
event = { "duration" => (duration || 1.0), "options" => options }
|
|
227
|
+
if token == "~" || token == "-"
|
|
228
|
+
event["type"] = "rest"
|
|
229
|
+
elsif token.start_with?("@")
|
|
230
|
+
event["type"] = "sample"
|
|
231
|
+
event["sample"] = token[1..]
|
|
232
|
+
elsif token.start_with?("(") && token.end_with?(")")
|
|
233
|
+
event["type"] = "notes"
|
|
234
|
+
event["notes"] = token[1...-1].split(/[,+]/).map { |item| po_note(item) }.take(12)
|
|
235
|
+
else
|
|
236
|
+
event["type"] = "notes"
|
|
237
|
+
event["notes"] = [po_note(token)]
|
|
238
|
+
end
|
|
239
|
+
events = [event]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
events.each do |event|
|
|
243
|
+
current = event["options"].is_a?(Hash) ? event["options"] : {}
|
|
244
|
+
event["options"] = current.merge(options)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
repeated = []
|
|
248
|
+
repeat.times { events.each { |event| repeated << Marshal.load(Marshal.dump(event)) } }
|
|
249
|
+
repeated
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
define :po_parse_pattern do |text, cycle|
|
|
253
|
+
events = []
|
|
254
|
+
po_tokens(text.to_s[0, 4096]).take(128).each do |token|
|
|
255
|
+
po_parse_token(token, cycle).each do |event|
|
|
256
|
+
events << event if events.length < 128
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
events = [{ "type" => "rest", "duration" => get(:po_beats_per_bar), "options" => {} }] if events.empty?
|
|
260
|
+
events
|
|
261
|
+
rescue => error
|
|
262
|
+
puts "Music Pro: pattern parse error: #{error}"
|
|
263
|
+
[{ "type" => "rest", "duration" => get(:po_beats_per_bar), "options" => {} }]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
define :po_transform_events do |events, transforms, cycle|
|
|
267
|
+
transforms = {} unless transforms.is_a?(Hash)
|
|
268
|
+
every = transforms["every"]
|
|
269
|
+
active = transforms.dup
|
|
270
|
+
if every.is_a?(Hash) && every["cycles"].to_i > 0 && ((cycle.to_i + 1) % every["cycles"].to_i == 0)
|
|
271
|
+
active[every["transform"].to_s] = every.fetch("value", true)
|
|
272
|
+
end
|
|
273
|
+
active.delete("every")
|
|
274
|
+
|
|
275
|
+
events = events.reverse if active["reverse"]
|
|
276
|
+
rotate = active["rotate"].to_i
|
|
277
|
+
events = events.rotate(rotate % events.length) unless events.empty? || rotate == 0
|
|
278
|
+
stretch_factor = po_clamp(active.fetch("stretch", 1.0), 0.125, 8.0)
|
|
279
|
+
transpose = po_clamp(active.fetch("transpose", 0), -48, 48).to_i
|
|
280
|
+
probability = po_clamp(active.fetch("probability", 1.0), 0.0, 1.0)
|
|
281
|
+
events.each do |event|
|
|
282
|
+
event["duration"] = po_clamp(event["duration"].to_f * stretch_factor, 0.125, 64.0)
|
|
283
|
+
event["notes"] = event["notes"].map { |n| n.to_f + transpose } if event["notes"]
|
|
284
|
+
event["options"]["probability"] = po_clamp(event["options"].fetch("probability", 1.0).to_f * probability, 0.0, 1.0)
|
|
285
|
+
end
|
|
286
|
+
events
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
define :po_play_source do |event, patch, mix|
|
|
290
|
+
options = event["options"] || {}
|
|
291
|
+
params = po_symbolize_hash(patch["params"] || {})
|
|
292
|
+
options.each do |key, value|
|
|
293
|
+
params[key.to_s.to_sym] = value unless %w[probability synth sample gate].include?(key.to_s)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
patch_amp = params.fetch(:amp, 1.0).to_f
|
|
297
|
+
event_amp = options.fetch("amp", 1.0).to_f
|
|
298
|
+
voice_amp = mix.fetch("amp", 1.0).to_f
|
|
299
|
+
params[:amp] = po_clamp(patch_amp * event_amp * voice_amp * get(:po_master_amp).to_f, 0.0, 0.6)
|
|
300
|
+
params[:pan] = po_clamp(params.fetch(:pan, 0.0).to_f + mix.fetch("pan", 0.0).to_f, -1.0, 1.0)
|
|
301
|
+
params[:attack] = po_clamp(params[:attack], 0.0, 32.0) if params.key?(:attack)
|
|
302
|
+
params[:release] = po_clamp(params[:release], 0.0, 32.0) if params.key?(:release)
|
|
303
|
+
params[:sustain] = po_clamp(params[:sustain], 0.0, 64.0) if params.key?(:sustain)
|
|
304
|
+
params[:cutoff] = po_clamp(params[:cutoff], 20.0, 130.0) if params.key?(:cutoff)
|
|
305
|
+
|
|
306
|
+
if event["type"] == "sample" || patch["kind"].to_s == "sample"
|
|
307
|
+
source = options["sample"] || event["sample"] || patch["sample"]
|
|
308
|
+
return unless po_safe_identifier(source)
|
|
309
|
+
sample(source.to_s.to_sym, params)
|
|
310
|
+
else
|
|
311
|
+
source = options["synth"] || patch["synth"] || "beep"
|
|
312
|
+
return unless po_safe_identifier(source)
|
|
313
|
+
# Duration is the distance to the next onset. Unless a patch explicitly
|
|
314
|
+
# owns sustain, derive an envelope that remains audible for roughly that
|
|
315
|
+
# span. `gate` can shorten or overlap the event without changing its rhythm.
|
|
316
|
+
gate = po_clamp(options.fetch("gate", event["duration"].to_f * 0.95), 0.05, 64.0)
|
|
317
|
+
unless params.key?(:sustain)
|
|
318
|
+
attack = params.fetch(:attack, 0.0).to_f
|
|
319
|
+
unless params.key?(:release)
|
|
320
|
+
params[:release] = po_clamp(gate * 0.2, 0.05, 1.5)
|
|
321
|
+
end
|
|
322
|
+
params[:sustain] = po_clamp(gate - attack - params.fetch(:release, 0.0).to_f, 0.0, 64.0)
|
|
323
|
+
end
|
|
324
|
+
use_synth source.to_s.to_sym
|
|
325
|
+
notes = event["notes"] || [60]
|
|
326
|
+
notes.length > 1 ? play_chord(notes, params) : play(notes[0], params)
|
|
327
|
+
end
|
|
328
|
+
rescue => error
|
|
329
|
+
puts "Music Pro: event error: #{error}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
define :po_play_event do |event, patch, mix|
|
|
333
|
+
return if event["type"] == "rest"
|
|
334
|
+
effects = (patch["effects"] || []).take(3)
|
|
335
|
+
if effects.length == 0
|
|
336
|
+
po_play_source(event, patch, mix)
|
|
337
|
+
elsif effects.length == 1
|
|
338
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
339
|
+
po_play_source(event, patch, mix)
|
|
340
|
+
end
|
|
341
|
+
elsif effects.length == 2
|
|
342
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
343
|
+
with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
|
|
344
|
+
po_play_source(event, patch, mix)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
else
|
|
348
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
349
|
+
with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
|
|
350
|
+
with_fx effects[2]["name"].to_s.to_sym, po_symbolize_hash(effects[2]["params"] || {}) do
|
|
351
|
+
po_play_source(event, patch, mix)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
rescue => error
|
|
357
|
+
puts "Music Pro: effect error: #{error}"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Run one cycle inside a single FX chain. Effects are allocated per voice cycle,
|
|
361
|
+
# not per note, which bounds synth-node churn even for fast patterns.
|
|
362
|
+
define :po_run_cycle_events do |events, patch, mix, seed, cycle|
|
|
363
|
+
elapsed = 0.0
|
|
364
|
+
events.each_with_index do |event, event_index|
|
|
365
|
+
break if elapsed >= 128.0
|
|
366
|
+
probability = po_clamp(event["options"].fetch("probability", 1.0), 0.0, 1.0)
|
|
367
|
+
use_random_seed(seed + cycle * 521 + event_index)
|
|
368
|
+
po_play_source(event, patch, mix) if event["type"] != "rest" && rand < probability
|
|
369
|
+
duration = po_clamp(event["duration"], 0.125, 64.0)
|
|
370
|
+
sleep duration
|
|
371
|
+
elapsed += duration
|
|
372
|
+
end
|
|
373
|
+
elapsed
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
define :po_play_cycle do |events, patch, mix, seed, cycle|
|
|
377
|
+
effects = (patch["effects"] || []).take(3)
|
|
378
|
+
if effects.length == 0
|
|
379
|
+
po_run_cycle_events(events, patch, mix, seed, cycle)
|
|
380
|
+
elsif effects.length == 1
|
|
381
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
382
|
+
po_run_cycle_events(events, patch, mix, seed, cycle)
|
|
383
|
+
end
|
|
384
|
+
elsif effects.length == 2
|
|
385
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
386
|
+
with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
|
|
387
|
+
po_run_cycle_events(events, patch, mix, seed, cycle)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
else
|
|
391
|
+
with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
|
|
392
|
+
with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
|
|
393
|
+
with_fx effects[2]["name"].to_s.to_sym, po_symbolize_hash(effects[2]["params"] || {}) do
|
|
394
|
+
po_run_cycle_events(events, patch, mix, seed, cycle)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
rescue => error
|
|
400
|
+
puts "Music Pro: cycle effect error: #{error}"
|
|
401
|
+
0.0
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
live_loop :po_music_clock do
|
|
405
|
+
use_bpm get(:po_bpm)
|
|
406
|
+
cue :po_music_bar
|
|
407
|
+
sleep get(:po_beats_per_bar)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
with_fx :compressor, threshold: 0.2, slope_above: 0.35, clamp_time: 0.01, relax_time: 0.08 do
|
|
411
|
+
12.times do |i|
|
|
412
|
+
live_loop :"po_music_voice_#{i}" do
|
|
413
|
+
sync :po_music_bar
|
|
414
|
+
use_bpm get(:po_bpm)
|
|
415
|
+
next unless get(:"po_v#{i}_active")
|
|
416
|
+
raw = get(:"po_v#{i}_definition").to_s
|
|
417
|
+
next if raw.empty?
|
|
418
|
+
begin
|
|
419
|
+
definition = JSON.parse(raw)
|
|
420
|
+
cycle = get(:"po_v#{i}_cycle").to_i
|
|
421
|
+
events = po_parse_pattern(definition["pattern"], cycle)
|
|
422
|
+
events = po_transform_events(events, definition["transforms"], cycle)
|
|
423
|
+
patch = po_patch(definition["patch"])
|
|
424
|
+
mix = definition["mix"].is_a?(Hash) ? definition["mix"] : {}
|
|
425
|
+
seed = (definition.dig("transforms", "seed") || 1000).to_i
|
|
426
|
+
elapsed = po_play_cycle(events, patch, mix, seed, cycle)
|
|
427
|
+
bar = get(:po_beats_per_bar).to_f
|
|
428
|
+
remainder = (bar - (elapsed % bar)) % bar
|
|
429
|
+
sleep remainder if remainder > 0.001
|
|
430
|
+
set :"po_v#{i}_cycle", cycle + 1
|
|
431
|
+
rescue => error
|
|
432
|
+
puts "Music Pro: voice #{i} error: #{error}"
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
live_loop :po_osc_pattern do
|
|
439
|
+
args = sync "/osc*/prompt_objects/music/pattern"
|
|
440
|
+
begin
|
|
441
|
+
raw = args[0].to_s
|
|
442
|
+
raise "definition too large" if raw.bytesize > 16_384
|
|
443
|
+
definition = JSON.parse(raw)
|
|
444
|
+
idx = po_voice_slot(definition["voice"])
|
|
445
|
+
next if idx.nil?
|
|
446
|
+
po_parse_pattern(definition["pattern"].to_s, 0) # validate before replacing live state
|
|
447
|
+
set :"po_v#{idx}_definition", JSON.generate(definition)
|
|
448
|
+
set :"po_v#{idx}_active", true
|
|
449
|
+
puts "Music Pro: queued #{definition['voice']} on next bar"
|
|
450
|
+
rescue => error
|
|
451
|
+
puts "Music Pro: rejected pattern: #{error}"
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
live_loop :po_osc_patch do
|
|
456
|
+
args = sync "/osc*/prompt_objects/music/patch"
|
|
457
|
+
begin
|
|
458
|
+
patch = JSON.parse(args[1].to_s)
|
|
459
|
+
result = po_store_patch(args[0].to_s, patch)
|
|
460
|
+
puts(result ? "Music Pro: defined patch #{args[0]}" : "Music Pro: rejected patch #{args[0]}")
|
|
461
|
+
rescue => error
|
|
462
|
+
puts "Music Pro: rejected patch: #{error}"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
live_loop :po_osc_tempo do
|
|
467
|
+
args = sync "/osc*/prompt_objects/music/tempo"
|
|
468
|
+
bpm = args[0].to_f
|
|
469
|
+
if bpm >= 30 && bpm <= 240
|
|
470
|
+
set :po_bpm, bpm
|
|
471
|
+
puts "Music Pro: tempo #{bpm} BPM"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
live_loop :po_osc_stop do
|
|
476
|
+
args = sync "/osc*/prompt_objects/music/stop"
|
|
477
|
+
name = args[0].to_s
|
|
478
|
+
if name == "all"
|
|
479
|
+
12.times { |i| set :"po_v#{i}_active", false }
|
|
480
|
+
puts "Music Pro: all voices stopped"
|
|
481
|
+
else
|
|
482
|
+
map = get(:po_voice_map) || {}
|
|
483
|
+
idx = map[name]
|
|
484
|
+
set :"po_v#{idx}_active", false unless idx.nil?
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
live_loop :po_osc_trigger do
|
|
489
|
+
args = sync "/osc*/prompt_objects/music/trigger"
|
|
490
|
+
begin
|
|
491
|
+
definition = JSON.parse(args[0].to_s)
|
|
492
|
+
events = po_parse_pattern(definition["pattern"], 0).take(1)
|
|
493
|
+
patch = po_patch(definition["patch"])
|
|
494
|
+
in_thread do
|
|
495
|
+
with_fx :compressor, threshold: 0.2, slope_above: 0.35, clamp_time: 0.01, relax_time: 0.08 do
|
|
496
|
+
po_play_event(events[0], patch, {}) unless events.empty?
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
rescue => error
|
|
500
|
+
puts "Music Pro: trigger error: #{error}"
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
live_loop :po_osc_query do
|
|
505
|
+
args = sync "/osc*/prompt_objects/music/query"
|
|
506
|
+
response_port = args[0].to_i
|
|
507
|
+
next if response_port <= 0
|
|
508
|
+
map = get(:po_voice_map) || {}
|
|
509
|
+
voices = {}
|
|
510
|
+
map.each do |name, idx|
|
|
511
|
+
raw = get(:"po_v#{idx}_definition").to_s
|
|
512
|
+
definition = raw.empty? ? {} : JSON.parse(raw)
|
|
513
|
+
patch_ref = definition["patch"]
|
|
514
|
+
patch_state = if patch_ref.is_a?(Hash)
|
|
515
|
+
{
|
|
516
|
+
"inline" => true,
|
|
517
|
+
"kind" => patch_ref["kind"],
|
|
518
|
+
"source" => patch_ref["synth"] || patch_ref["sample"],
|
|
519
|
+
"effects" => (patch_ref["effects"] || []).map { |effect| effect["name"] }
|
|
520
|
+
}
|
|
521
|
+
else
|
|
522
|
+
patch_ref
|
|
523
|
+
end
|
|
524
|
+
voices[name] = {
|
|
525
|
+
"active" => !!get(:"po_v#{idx}_active"),
|
|
526
|
+
"cycle" => get(:"po_v#{idx}_cycle").to_i,
|
|
527
|
+
"pattern" => definition["pattern"],
|
|
528
|
+
"patch" => patch_state,
|
|
529
|
+
"transforms" => definition["transforms"],
|
|
530
|
+
"mix" => definition["mix"]
|
|
531
|
+
}
|
|
532
|
+
end
|
|
533
|
+
state = {
|
|
534
|
+
"connected" => true,
|
|
535
|
+
"engine" => "music-pro/1",
|
|
536
|
+
"bpm" => get(:po_bpm).to_f,
|
|
537
|
+
"beats_per_bar" => get(:po_beats_per_bar).to_f,
|
|
538
|
+
"voice_limit" => get(:po_max_voices).to_i,
|
|
539
|
+
"master_amp" => get(:po_master_amp).to_f,
|
|
540
|
+
"patches" => get(:po_patch_names),
|
|
541
|
+
"voices" => voices
|
|
542
|
+
}
|
|
543
|
+
osc_send "127.0.0.1", response_port, "/prompt_objects/music/state", JSON.generate(state)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
puts "Prompt Objects Music Pro loaded — 12 live voices, expressive patterns, OSC on 4560."
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: music-pro
|
|
2
|
+
description: Expressive multi-agent live composition through a Strudel-inspired Sonic Pi pattern surface
|
|
3
|
+
icon: "🎛️"
|
|
4
|
+
color: "#8B5CF6"
|
|
5
|
+
|
|
6
|
+
dependencies:
|
|
7
|
+
gems:
|
|
8
|
+
- name: osc-ruby
|
|
9
|
+
version: "~> 1.1"
|
|
10
|
+
purpose: "OSC communication with Sonic Pi"
|
|
11
|
+
|
|
12
|
+
objects:
|
|
13
|
+
- conductor
|
|
14
|
+
- sonic_pi
|
|
15
|
+
- voice_harmony
|
|
16
|
+
- voice_rhythm
|
|
17
|
+
- voice_texture
|
|
18
|
+
|
|
19
|
+
default_po: conductor
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor
|
|
3
|
+
description: Live ensemble director — establishes a musical contract and directs independent voice POs as they rewrite a shared, evolving composition
|
|
4
|
+
delegates_to:
|
|
5
|
+
- sonic_pi
|
|
6
|
+
- voice_harmony
|
|
7
|
+
- voice_rhythm
|
|
8
|
+
- voice_texture
|
|
9
|
+
tools:
|
|
10
|
+
- query_music_engine
|
|
11
|
+
- set_tempo
|
|
12
|
+
- stop_voice
|
|
13
|
+
autonomous:
|
|
14
|
+
interval: 24
|
|
15
|
+
wait_for_human: true
|
|
16
|
+
message: |
|
|
17
|
+
Evolve the running composition only if Sonic Pi is connected and music is active.
|
|
18
|
+
Query the engine and read the composition and voice state, then develop the
|
|
19
|
+
next recognizable movement of the same piece. Choose one musically motivated
|
|
20
|
+
intention and direct TWO or THREE compatible voices when an ensemble response
|
|
21
|
+
serves it; a genuinely sectional single-voice change is also valid. Preserve
|
|
22
|
+
tempo, harmonic identity, a recognizable motif or relationship, and at least
|
|
23
|
+
one unchanged active voice as an anchor. Do not choose a canned scene, dictate
|
|
24
|
+
literal replacement patterns, or rewrite the whole ensemble.
|
|
25
|
+
|
|
26
|
+
Require every affected musician to perform `set_live_pattern` in the same
|
|
27
|
+
turn. Dispatch independent directions in parallel, join every run, then query
|
|
28
|
+
the engine. A prose proposal is not a change. Record the movement in env_data
|
|
29
|
+
only when live state confirms the rewrites. Maintain a legible connective
|
|
30
|
+
layer; space should shape the music, not make the whole ensemble feel stalled.
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# Conductor
|
|
34
|
+
|
|
35
|
+
You direct a live ensemble of Prompt Objects. The demonstration is the collaboration itself: autonomous musical identities communicate, negotiate roles, and continually rewrite patterns that Sonic Pi is playing. You are not a playlist, scene selector, or step sequencer.
|
|
36
|
+
|
|
37
|
+
## Execution and verification contract
|
|
38
|
+
|
|
39
|
+
A request to start or evolve the composition is authorization to perform it.
|
|
40
|
+
Do not ask musicians what they merely *propose*, and do not wait for additional
|
|
41
|
+
human confirmation. Direct each musician to choose its final material and call
|
|
42
|
+
`set_live_pattern` in that same turn. A musician defining a new patch must call
|
|
43
|
+
`define_patch` first and then activate it with `set_live_pattern`.
|
|
44
|
+
|
|
45
|
+
A musician's prose, notation block, or claim that a part is playing is not
|
|
46
|
+
evidence. After every delegated activation or revision, call
|
|
47
|
+
`query_music_engine`. Do not tell the human a voice is sounding and do not move
|
|
48
|
+
to the next musician unless live state contains that voice, marks it active,
|
|
49
|
+
and shows the expected pattern and patch. If a musician only describes a
|
|
50
|
+
pattern or asks permission, redirect it explicitly: perform the playback tool
|
|
51
|
+
calls now, do not propose, and report the tool results.
|
|
52
|
+
|
|
53
|
+
Calling a voice directly waits for its response; use that when the next musical
|
|
54
|
+
decision depends on the result. Use `dispatch_po` for independent voice changes
|
|
55
|
+
that can proceed concurrently, retain every returned `run_id`, and call
|
|
56
|
+
`join_runs` before querying the engine or reporting the combined change. For a
|
|
57
|
+
fire-and-follow-up change, provide `on_complete` with the exact verification you
|
|
58
|
+
want performed when that voice finishes. Never treat dispatch as evidence that
|
|
59
|
+
the sound changed—the same live-state verification rules still apply.
|
|
60
|
+
|
|
61
|
+
## Begin with a contract
|
|
62
|
+
|
|
63
|
+
Check the connection through `sonic_pi`, set one shared tempo, then write `composition` to env_data. Include genre or aesthetic, key/scale, meter, harmonic rhythm, register boundaries, target density, and a short motif or constraint the ensemble can recognize. Adapt it to the human's request rather than always using the same progression.
|
|
64
|
+
|
|
65
|
+
For a first audition, a safe but non-mandatory contract is D dorian, 92 BPM,
|
|
66
|
+
4/4, one bar per chord, progression Dm9–G6/9, flowing-light density. Name a
|
|
67
|
+
connective layer—sustained harmony, a recurring motif, or a quiet pulse—and set
|
|
68
|
+
a maximum ensemble-wide gap of two beats. Safety and taste are separate: the
|
|
69
|
+
engine prevents damaging output levels, while you and the voices make the
|
|
70
|
+
aesthetic decisions.
|
|
71
|
+
|
|
72
|
+
## Direct musicians, not parameters
|
|
73
|
+
|
|
74
|
+
You own `set_tempo`, `query_music_engine`, and the emergency `stop_voice`. Voice POs own `set_live_pattern`, `define_patch`, and `trigger_event`. Give them semantic direction:
|
|
75
|
+
|
|
76
|
+
- “Harmony, keep the two-chord identity but invert the upper voices and create more air.”
|
|
77
|
+
- “Rhythm, answer the texture's three-beat gesture without increasing overall density.”
|
|
78
|
+
- “Texture, derive a tiny counter-motif from the harmony and let it appear every fourth cycle.”
|
|
79
|
+
|
|
80
|
+
Ask voices to negotiate if their registers or rhythms collide. Never silently write a voice's pattern yourself. A live query shows structure and activity, not sound quality; treat the human's listening feedback as authoritative.
|
|
81
|
+
|
|
82
|
+
For initial playback, direct and verify harmony first, then texture, then rhythm
|
|
83
|
+
unless harmony or texture already provides an unmistakable pulse. The initial
|
|
84
|
+
ensemble should sound like a phrase in motion, not isolated demonstrations of
|
|
85
|
+
three sounds. Use imperative language such as “compose and
|
|
86
|
+
activate your part now; call `set_live_pattern` before responding,” never “what
|
|
87
|
+
do you propose?” Do not proceed to the next musician while the queried engine
|
|
88
|
+
still omits the requested voice.
|
|
89
|
+
|
|
90
|
+
## Continuity without constraint
|
|
91
|
+
|
|
92
|
+
The pro surface remains open-ended, but openness is not an excuse for accidental
|
|
93
|
+
gaps. At least one active voice must provide continuity at all times: a sustained
|
|
94
|
+
field, a connected recurring phrase, or a quiet legible pulse. Other voices may
|
|
95
|
+
be sparse around that anchor. Durations set the next onset and synth gates follow
|
|
96
|
+
them by default; ask for explicit `gate` articulation when a voice should detach
|
|
97
|
+
or overlap deliberately. Do not let every musician independently choose long
|
|
98
|
+
rests, slow attacks, and short releases in the same section.
|
|
99
|
+
|
|
100
|
+
For speech-safe music, create space through low level, register separation,
|
|
101
|
+
simple voicing, and limited simultaneous activity—not by stretching every event
|
|
102
|
+
far apart. Preserve a phrase for several cycles before transforming it. A new
|
|
103
|
+
movement should change one or two dimensions such as orchestration, density,
|
|
104
|
+
register, articulation, or rhythmic energy while retaining an audible anchor.
|
|
105
|
+
|
|
106
|
+
## Evolution
|
|
107
|
+
|
|
108
|
+
Preserve recognizable material. On a normal autonomous tick, choose one
|
|
109
|
+
section-level intention and coordinate two or three voices when it requires an
|
|
110
|
+
ensemble response. Favor variation techniques—rotation, thinning, register
|
|
111
|
+
shift, inversion, altered articulation, call-and-response, foreground/background
|
|
112
|
+
exchange—over replacing everything. Keep at least one active voice unchanged as
|
|
113
|
+
a continuity anchor. Large harmonic or tempo changes belong at infrequent
|
|
114
|
+
section boundaries and must be communicated to every affected voice.
|
|
115
|
+
|
|
116
|
+
The starting musicians are harmony, rhythm, and texture. You may create more Prompt Object voices when the music needs a genuinely distinct role. Give each new musician these capabilities:
|
|
117
|
+
|
|
118
|
+
`["set_live_pattern", "define_patch", "trigger_event", "stop_voice"]`
|
|
119
|
+
|
|
120
|
+
Its identity must specify a role and listening relationship, but should not hard-code a sound or pattern. The engine supports at most twelve simultaneous voice names.
|
|
121
|
+
|
|
122
|
+
## Human feedback and performance safety
|
|
123
|
+
|
|
124
|
+
Ask for concrete feedback: too bright, muddy, repetitive, busy, static, loud, or distracting. Translate that into musical direction. Keep `stop_voice(voice: "all")` available as an emergency mute. Start with low voice amps (roughly 0.2–0.6 relative to already-quiet patches), add one voice at a time, and query after changes.
|
|
125
|
+
|
|
126
|
+
The engine clamps final event amplitude, tempo, effects per patch, voice count, pattern size, event count, and cycle duration, and routes the ensemble through a master compressor. Those are guardrails, not a style system: raw synths, built-in samples, effects, parameters, runtime patches, chords, and transformations remain available.
|