anima-core 0.2.1 → 1.0.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,396 @@
1
+ # Audio
2
+
3
+ ## Two Audio Systems
4
+
5
+ DragonRuby provides two distinct audio interfaces:
6
+
7
+ | System | Purpose | Format | Cleanup |
8
+ |--------|---------|--------|---------|
9
+ | `args.outputs.sounds` | One-shot effects | WAV | Automatic |
10
+ | `args.audio[:key]` | Continuous/looping | OGG/MP3 | Manual |
11
+
12
+ ## One-Shot Sound Effects
13
+
14
+ Fire-and-forget sounds via `args.outputs.sounds`.
15
+
16
+ ```ruby
17
+ # Simple path string
18
+ args.outputs.sounds << "sounds/coin.wav"
19
+
20
+ # With volume control
21
+ args.outputs.sounds << { path: "sounds/explosion.wav", gain: 0.5 }
22
+ ```
23
+
24
+ **Event-driven triggers:**
25
+
26
+ ```ruby
27
+ # On input
28
+ if args.inputs.keyboard.key_down.space
29
+ args.outputs.sounds << "sounds/jump.wav"
30
+ end
31
+
32
+ # On collision
33
+ if Geometry.intersect_rect?(bullet, enemy)
34
+ args.outputs.sounds << "sounds/hit.wav"
35
+ enemy.dead = true
36
+ end
37
+
38
+ # On timer event
39
+ if args.state.timer == 0
40
+ args.outputs.sounds << "sounds/game-over.wav"
41
+ end
42
+ ```
43
+
44
+ ## Continuous Audio (args.audio)
45
+
46
+ Managed audio with full control via hash keys.
47
+
48
+ ```ruby
49
+ # Start background music
50
+ args.audio[:music] = {
51
+ input: "sounds/theme.ogg",
52
+ looping: true,
53
+ gain: 0.8
54
+ }
55
+
56
+ # Stop music
57
+ args.audio[:music] = nil
58
+ # OR
59
+ args.audio.delete(:music)
60
+ ```
61
+
62
+ **Initialize once pattern:**
63
+
64
+ ```ruby
65
+ def tick(args)
66
+ if Kernel.tick_count == 0
67
+ args.audio[:music] = { input: "sounds/theme.ogg", looping: true }
68
+ end
69
+ # Game logic...
70
+ end
71
+ ```
72
+
73
+ ## Audio Properties
74
+
75
+ All configurable properties for `args.audio`:
76
+
77
+ ```ruby
78
+ args.audio[:track] = {
79
+ input: "sounds/music.ogg", # File path (required)
80
+ gain: 1.0, # Volume: 0.0 to 1.0 (MUST be float)
81
+ pitch: 1.0, # Pitch: 1.0 = normal (MUST be float)
82
+ looping: true, # Loop continuously
83
+ paused: false, # Pause playback
84
+ x: 0.0, y: 0.0, z: 0.0 # 3D position: -1.0 to 1.0
85
+ }
86
+ ```
87
+
88
+ **Dynamic properties (read-only, added by engine):**
89
+
90
+ ```ruby
91
+ track = args.audio[:track]
92
+ track.playtime # Current position in seconds
93
+ track.playlength # Total duration in seconds
94
+ ```
95
+
96
+ ## Volume Control
97
+
98
+ ### Per-Track Volume (gain)
99
+
100
+ ```ruby
101
+ # Set volume (0.0 = silent, 1.0 = full)
102
+ args.audio[:music].gain = 0.5
103
+
104
+ # Fade out over time
105
+ args.audio[:music].gain -= 0.01 if args.audio[:music].gain > 0
106
+ ```
107
+
108
+ ### Global Volume
109
+
110
+ ```ruby
111
+ # Adjust master volume
112
+ args.audio.volume += 0.1 if args.inputs.up
113
+ args.audio.volume -= 0.1 if args.inputs.down
114
+ ```
115
+
116
+ ## Pausing and Resuming
117
+
118
+ ```ruby
119
+ # Pause
120
+ args.audio[:music].paused = true
121
+
122
+ # Resume
123
+ args.audio[:music].paused = false
124
+
125
+ # Toggle
126
+ args.audio[:music].paused = !args.audio[:music].paused
127
+ ```
128
+
129
+ **Common pattern - pause on game over:**
130
+
131
+ ```ruby
132
+ if args.state.timer == 0
133
+ args.audio[:music].paused = true
134
+ args.outputs.sounds << "sounds/game-over.wav"
135
+ end
136
+ ```
137
+
138
+ ## Seeking (Playback Position)
139
+
140
+ ```ruby
141
+ # Jump to specific time (seconds)
142
+ args.audio[:music].playtime = 30.0
143
+
144
+ # Jump to 50% of track
145
+ args.audio[:music].playtime = args.audio[:music].playlength * 0.5
146
+
147
+ # Restart from beginning
148
+ args.audio[:music].playtime = 0
149
+ ```
150
+
151
+ ## Audio Formats
152
+
153
+ | Format | Use Case | Notes |
154
+ |--------|----------|-------|
155
+ | WAV | Sound effects | Uncompressed, high quality, max 44.1kHz |
156
+ | OGG | Music/loops | Compressed, cross-platform |
157
+ | MP3 | Music/loops | Compressed, widely supported |
158
+
159
+ **Convert WAV to OGG:**
160
+ ```bash
161
+ ffmpeg -i sound.wav -ac 2 -b:a 160k -ar 44100 -acodec libvorbis sound.ogg
162
+ ```
163
+
164
+ **Re-encode problematic OGG:**
165
+ ```bash
166
+ ffmpeg -i sound.ogg -ac 2 -b:a 160k -ar 44100 -acodec libvorbis sound-fixed.ogg
167
+ ```
168
+
169
+ ## Multi-Track Management
170
+
171
+ ```ruby
172
+ # Initialize multiple tracks
173
+ args.audio[:music] = { input: "sounds/music.ogg", looping: true }
174
+ args.audio[:ambient] = { input: "sounds/wind.ogg", looping: true, gain: 0.3 }
175
+
176
+ # Iterate all active audio
177
+ args.audio.each do |key, track|
178
+ puts "#{key}: #{track.playtime}/#{track.playlength}s"
179
+ end
180
+
181
+ # Stop all audio
182
+ args.audio.each_key { |k| args.audio.delete(k) }
183
+ ```
184
+
185
+ ## Crossfading
186
+
187
+ Smooth transition between tracks:
188
+
189
+ ```ruby
190
+ def start_crossfade(args, new_track)
191
+ # Capture current track for fadeout
192
+ current = args.audio[:music]
193
+ args.audio[:music_fade] = {
194
+ input: current[:input],
195
+ looping: true,
196
+ gain: current[:gain],
197
+ playtime: current[:playtime]
198
+ }
199
+
200
+ # Start new track at zero volume
201
+ args.audio[:music] = {
202
+ input: new_track,
203
+ looping: true,
204
+ gain: 0.0
205
+ }
206
+ end
207
+
208
+ def update_crossfade(args)
209
+ # Fade in new track
210
+ if args.audio[:music] && args.audio[:music].gain < 1.0
211
+ args.audio[:music].gain = (args.audio[:music].gain + 0.01).clamp(0, 1)
212
+ end
213
+
214
+ # Fade out old track
215
+ if args.audio[:music_fade]
216
+ args.audio[:music_fade].gain -= 0.01
217
+ if args.audio[:music_fade].gain <= 0
218
+ args.audio.delete(:music_fade)
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## 3D Audio Positioning
225
+
226
+ Position audio in stereo field:
227
+
228
+ ```ruby
229
+ # x: -1.0 (left) to 1.0 (right)
230
+ # y: -1.0 (back) to 1.0 (front)
231
+ # z: distance (typically 0.0)
232
+
233
+ args.audio[:enemy_sound] = {
234
+ input: "sounds/growl.ogg",
235
+ looping: true,
236
+ x: (enemy.x / 640.0) - 1.0, # Scale screen x to -1..1
237
+ y: 0.0,
238
+ z: 0.0
239
+ }
240
+ ```
241
+
242
+ ## Sound Synthesis (Advanced)
243
+
244
+ Generate audio procedurally:
245
+
246
+ ```ruby
247
+ def generate_sine_wave(frequency:, duration:)
248
+ sample_rate = 48000
249
+ samples_per_period = (sample_rate / frequency).ceil
250
+ sample_count = (sample_rate * duration).floor
251
+
252
+ sample_count.times.map do |i|
253
+ Math.sin(2 * Math::PI * i / samples_per_period)
254
+ end
255
+ end
256
+
257
+ # Play generated sound
258
+ wave = generate_sine_wave(frequency: 440, duration: 0.5)
259
+ args.audio[:beep] = {
260
+ input: [1, 48000, wave] # [channels, sample_rate, samples]
261
+ }
262
+ ```
263
+
264
+ **Wave types:**
265
+ - Sine wave - smooth tone
266
+ - Square wave - harsh, retro (use gain: 0.3)
267
+ - Sawtooth wave - buzzy, synth-like (use gain: 0.3)
268
+ - Triangle wave - soft, mellow
269
+
270
+ ## Common Antipatterns
271
+
272
+ ### Re-initializing Music Every Frame
273
+
274
+ ```ruby
275
+ # WRONG - restarts music every frame
276
+ def tick(args)
277
+ args.audio[:music] = { input: "sounds/theme.ogg", looping: true }
278
+ end
279
+
280
+ # CORRECT - initialize once
281
+ def tick(args)
282
+ if Kernel.tick_count == 0
283
+ args.audio[:music] = { input: "sounds/theme.ogg", looping: true }
284
+ end
285
+ end
286
+ ```
287
+
288
+ ### Integer for gain/pitch
289
+
290
+ ```ruby
291
+ # WRONG - must be float
292
+ args.audio[:music].gain = 1
293
+ args.audio[:music].pitch = 2
294
+
295
+ # CORRECT
296
+ args.audio[:music].gain = 1.0
297
+ args.audio[:music].pitch = 2.0
298
+ ```
299
+
300
+ ### Forgetting to Stop Looping Audio
301
+
302
+ ```ruby
303
+ # WRONG - music plays forever even after scene change
304
+ def change_scene(args)
305
+ args.state.scene = :menu
306
+ end
307
+
308
+ # CORRECT - stop or pause music on transition
309
+ def change_scene(args)
310
+ args.audio[:gameplay_music] = nil
311
+ args.state.scene = :menu
312
+ end
313
+ ```
314
+
315
+ ### Playing Same Sound Multiple Times Per Frame
316
+
317
+ ```ruby
318
+ # WRONG - multiple bullets fire same frame, overlapping sounds
319
+ args.state.bullets.each do |bullet|
320
+ if bullet.just_fired
321
+ args.outputs.sounds << "sounds/shoot.wav"
322
+ end
323
+ end
324
+
325
+ # CORRECT - play once per frame
326
+ if args.state.bullets.any?(&:just_fired)
327
+ args.outputs.sounds << "sounds/shoot.wav"
328
+ end
329
+ ```
330
+
331
+ ### Not Handling Missing Audio
332
+
333
+ ```ruby
334
+ # WRONG - crashes if audio was deleted
335
+ args.audio[:music].paused = true
336
+
337
+ # CORRECT - guard against nil
338
+ args.audio[:music]&.paused = true
339
+ # OR
340
+ if args.audio[:music]
341
+ args.audio[:music].paused = true
342
+ end
343
+ ```
344
+
345
+ ## Best Practices
346
+
347
+ 1. **Use WAV for short effects, OGG/MP3 for music**
348
+ 2. **Initialize looping audio on tick 0 or in boot**
349
+ 3. **Pause music on game over, don't delete** (allows resume)
350
+ 4. **Use descriptive keys** - `:gameplay_music`, `:menu_ambient`
351
+ 5. **Clean up audio on scene transitions**
352
+ 6. **Restart DragonRuby after changing sound files** (hot reload limitation)
353
+
354
+ ## Decision Tree
355
+
356
+ ```
357
+ Playing a sound?
358
+ ├── One-shot effect (explosion, coin, jump)
359
+ │ └── examples/audio/sound_effects.rb
360
+ │ └── Need volume control? → { path: "...", gain: 0.5 }
361
+
362
+ ├── Background music/ambient loops
363
+ │ └── examples/audio/background_music.rb
364
+ │ ├── Need pause/volume/seek? → examples/audio/music_controls.rb
365
+ │ ├── Need to stop? → args.audio[:key] = nil
366
+ │ └── Event-driven? → examples/audio/audio_events.rb
367
+
368
+ └── Transitioning between tracks?
369
+ └── examples/audio/crossfade.rb
370
+
371
+ Managing audio state?
372
+ ├── Initialize on game start → examples/audio/background_music.rb
373
+ ├── Pause/resume controls → examples/audio/music_controls.rb
374
+ ├── Stop on scene change → args.audio[:key] = nil
375
+ └── Volume adjustment → examples/audio/music_controls.rb
376
+
377
+ Triggering sounds?
378
+ ├── On input (jump, shoot) → examples/audio/audio_events.rb
379
+ ├── On collision → examples/audio/audio_events.rb
380
+ └── On timer/periodic → examples/audio/audio_events.rb
381
+
382
+ Need advanced audio?
383
+ ├── 3D positioning → x, y, z properties (-1.0 to 1.0)
384
+ ├── Track progress → .playtime / .playlength
385
+ ├── Multiple tracks → different keys (:music, :sfx, :ambient)
386
+ └── Procedural audio → [channels, sample_rate, samples_array]
387
+ ```
388
+
389
+ ## Examples
390
+
391
+ | File | Demonstrates |
392
+ |------|--------------|
393
+ | `examples/audio/sound_effects.rb` | One-shot sounds with args.outputs.sounds |
394
+ | `examples/audio/background_music.rb` | Looping music with args.audio |
395
+ | `examples/audio/music_controls.rb` | Pause, resume, volume, seeking |
396
+ | `examples/audio/audio_events.rb` | Event-driven sound triggers |