From 83a0586d260c89f3912ac3667585473e008b4694 Mon Sep 17 00:00:00 2001 From: Tretzi Date: Tue, 10 Mar 2026 22:50:50 -0500 Subject: [PATCH] feat(mindmachine): implement Android MVP app scaffold, session flows, safety gating, and tests --- ANDROID_MVP_SPEC.md | 741 ++++++++++++++++++ ARCHITECTURE.md | 276 +++++++ MVP.md | 160 ++++ REQUIREMENTS.md | 265 +++++++ TASKS.md | 126 +++ USER_MANUAL.md | 181 +++++ app/build.gradle.kts | 76 ++ app/proguard-rules.pro | 1 + app/src/main/AndroidManifest.xml | 25 + .../java/com/mindmachine/mvp/MainActivity.kt | 307 ++++++++ .../mvp/audio/BinauralAudioEngine.kt | 72 ++ .../mindmachine/mvp/audio/HeadsetMonitor.kt | 18 + .../mvp/data/SettingsRepository.kt | 49 ++ .../java/com/mindmachine/mvp/domain/Models.kt | 71 ++ .../mindmachine/mvp/session/MainViewModel.kt | 181 +++++ .../mvp/session/SessionValidator.kt | 17 + app/src/main/res/values/themes.xml | 4 + .../com/mindmachine/mvp/PresetDefaultsTest.kt | 28 + .../mindmachine/mvp/SessionValidatorTest.kt | 46 ++ build.gradle.kts | 5 + gradle.properties | 4 + settings.gradle.kts | 18 + 22 files changed, 2671 insertions(+) create mode 100644 ANDROID_MVP_SPEC.md create mode 100644 ARCHITECTURE.md create mode 100644 MVP.md create mode 100644 REQUIREMENTS.md create mode 100644 TASKS.md create mode 100644 USER_MANUAL.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/mindmachine/mvp/MainActivity.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/audio/BinauralAudioEngine.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/audio/HeadsetMonitor.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/domain/Models.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt create mode 100644 app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/test/java/com/mindmachine/mvp/PresetDefaultsTest.kt create mode 100644 app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 settings.gradle.kts diff --git a/ANDROID_MVP_SPEC.md b/ANDROID_MVP_SPEC.md new file mode 100644 index 0000000..3a174f5 --- /dev/null +++ b/ANDROID_MVP_SPEC.md @@ -0,0 +1,741 @@ +# MindMachine Android MVP Product Specification + +## 1. Product Summary +MindMachine Android MVP is a native Android app that turns a phone into a simple audiovisual session device. The MVP must let a user safely start a short session using built-in presets, deliver synchronized full-screen visual stimulation plus binaural stereo audio, and allow immediate pause/stop at any time. + +This is a prototype for safe, reliable core experience. It is **not** a medical product and must not make medical or therapeutic claims. + +--- + +## 2. MVP Scope + +### 2.1 Goals +The MVP must prove that Android can reliably: +1. present required safety information before use, +2. guide the user through headphone + holder setup, +3. let the user choose a built-in preset, +4. run a timed session with synchronized visual + audio output, +5. handle pause, resume, stop, completion, and interruptions safely, +6. remain usable in low-light, near-eye conditions. + +### 2.2 In Scope +- Android-only native app +- First-run onboarding and mandatory safety acknowledgment +- Home screen with built-in presets: **Relax, Focus, Sleep Prep** +- Session setup screen with limited edits to preset values +- Active session full-screen experience +- Pause/resume/stop controls +- Session completion screen +- Simple settings screen +- Audio-only and visual-only toggles +- Headphone guidance and runtime handling for output changes +- Keep-screen-awake behavior during session +- Persistence of acknowledgment state, last selected preset, and basic settings + +### 2.3 Explicit Non-Goals for MVP +- Accounts, sync, cloud backup +- Custom preset creation/editor +- Ambient sounds +- Guided voice narration +- Printable holder templates +- Advanced visual editor +- Session history/analytics +- Medical/therapeutic workflows +- iOS version +- Social/community features + +### 2.4 Product Decisions for Ambiguities +To make MVP implementation-ready, the following are fixed decisions: +- **Platform:** Android only +- **Presets shipped:** Relax, Focus, Sleep Prep only +- **Custom sessions:** Not in MVP +- **Visual patterns in MVP:** Flash and Pulse/Fade only +- **Alternating patterns:** Nice-to-have, not MVP +- **Color use:** Monochrome white-on-black only for MVP to reduce complexity and risk +- **Brightness control:** App shows recommendation + optional in-app screen-intensity slider that affects in-app render intensity only; app does **not** force system brightness to max +- **Volume control:** App provides guidance to use device volume; no separate app-owned media mixer UI required for MVP +- **Headphone requirement:** Stereo headphones strongly required for binaural mode; if no wired/Bluetooth headset route is detected, app blocks starting audio-enabled session and offers Visual-only fallback +- **Countdown:** Default 5 seconds, user-adjustable in Settings: Off / 5 / 10 seconds +- **Default session durations:** Relax 10 min, Focus 15 min, Sleep Prep 20 min +- **Interruption handling:** Any call, audio focus loss, or headphone disconnect pauses session and shows recovery sheet + +--- + +## 3. Information Architecture + +### 3.1 Primary Screens +1. Welcome / Intro +2. Safety Acknowledgment +3. Home / Preset List +4. Session Setup +5. Active Session +6. Session Paused Overlay +7. Interruption / Headphone Warning Sheet +8. Session Complete +9. Settings +10. Holder Guidance +11. About / Disclaimer (can live inside Settings) + +### 3.2 Navigation Model +- Root stack navigation +- Default landing after first-run completion: **Home** +- Main forward path: `Welcome -> Safety -> Home -> Setup -> Active Session -> Complete` +- Settings and Holder Guidance reachable from Home and Setup +- During Active Session, user should not navigate elsewhere except via pause/stop/OS interruption handling + +### 3.3 Back Navigation Rules +- Welcome/Safety: Back exits app or returns to previous onboarding step +- Home: Back exits app +- Setup: Back returns to Home +- Active Session: system back disabled or mapped to Pause sheet; must not silently exit session +- Complete: Back returns Home +- Settings/Holder Guidance: Back returns to prior screen + +--- + +## 4. User Flows + +### 4.1 First-Run Flow +1. User opens app +2. Welcome explains what the app does in plain language +3. User taps Continue +4. Safety screen shows warnings and disclaimers +5. User must check acknowledgment box and tap **I Understand** +6. App stores acknowledgment timestamp/version locally +7. User lands on Home + +If user declines acknowledgment, app remains unusable for sessions and can only show info screens. + +### 4.2 Returning User Quick Start +1. User opens app to Home +2. User selects preset card +3. Setup screen opens with preset defaults +4. User confirms mode and duration +5. User taps Start +6. If preflight passes, countdown begins +7. Active Session starts full-screen + +### 4.3 Preflight Check Flow +Triggered when user taps Start. +Checks: +- safety acknowledgment completed +- at least one stimulation mode enabled (audio or visual) +- if audio enabled, stereo headphone route available +- session duration valid +- app not already in active session + +Outcomes: +- Pass -> countdown +- Recoverable issue -> inline warning or modal with action +- Blocking issue -> Start disabled until fixed + +### 4.4 Session Flow +1. Countdown shows 5..1 or selected duration +2. Session enters full-screen mode +3. Visual pattern runs if enabled +4. Audio runs if enabled +5. Time remaining updates once per second +6. User may tap screen to reveal controls +7. User may Pause or Stop any time +8. If timer reaches zero, session ends automatically and safely + +### 4.5 Pause / Resume Flow +1. User taps screen during session +2. Minimal controls appear +3. User taps Pause +4. Audio and visual engines pause immediately +5. Paused overlay appears with remaining time +6. User taps Resume -> 3-second resume countdown -> session restarts +7. User taps Stop -> confirmation sheet + +### 4.6 Stop Flow +1. User taps Stop from controls or paused state +2. Confirmation sheet: Stop Session / Cancel +3. If confirmed, audio stops, visuals stop, wake behavior released, completion screen shown as “Session ended early” + +### 4.7 Interruption Flow +Triggers include call, alarm takeover, audio focus loss, app backgrounding, screen off intent, headphone disconnect. +1. Session auto-pauses +2. Stimulation stops immediately +3. Interruption sheet explains reason +4. User options: + - Resume when safe + - End session + - Switch to Visual-only if headphone route lost +5. Session never continues silently after interruption + +### 4.8 Completion Flow +1. Timer reaches zero +2. Audio/visual output stop immediately +3. Completion screen shows preset name and session status +4. Actions: Repeat Session / Return Home + +### 4.9 Settings Flow +User can modify: +- countdown preference (Off/5/10) +- default mode preference (Audio+Visual, Audio-only, Visual-only) +- keep screen on during session (default on, locked on for session regardless) +- show holder guidance before every session (default off) +- theme behavior if needed (dark only for MVP preferred) +- about/disclaimer access + +--- + +## 5. Screen-by-Screen Requirements + +## 5.1 Welcome / Intro +Purpose: orient first-time user. + +Content: +- app name +- one-sentence explanation +- short bullet list: blinking light, binaural audio, stereo headphones, not medical +- Continue button + +Behavior: +- shown only before safety acknowledgment +- dark theme +- no session actions available + +States: +- default only + +Acceptance notes: +- readable at standard phone sizes +- content fits without scroll on common devices where possible + +## 5.2 Safety Acknowledgment +Purpose: mandatory risk disclosure. + +Required warnings: +- flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers +- do not use while driving, walking, cycling, or operating machinery +- stop immediately if discomfort, dizziness, headache, nausea, anxiety, or eye strain occurs +- binaural mode requires stereo headphones +- app is not a medical device + +Components: +- scrollable warning text +- acknowledgment checkbox: “I understand the risks and will stop if I feel discomfort.” +- primary button: I Understand +- secondary link: Holder Guidance + +Behavior: +- primary button disabled until checkbox checked +- acknowledgment persisted locally with content version + +States: +- unchecked +- checked/enabled + +## 5.3 Home / Preset Selection +Purpose: launch point for returning users. + +Components: +- top app bar: title + Settings +- optional info action: Holder Guidance +- three preset cards: Relax, Focus, Sleep Prep +- each card shows duration, pattern type, beat frequency summary, short description +- optional footer disclaimer: not medical / use safely + +Preset copy guidance: +- Relax: gentle pulse, slower beat +- Focus: steady flash, alert but conservative beat +- Sleep Prep: slow pulse, longest duration, low intensity + +Behavior: +- tap card -> Session Setup for selected preset +- last used preset may show subtle “Last used” badge + +States: +- default loaded +- empty state not applicable +- error state only if preset load fails: inline retry + fallback to bundled defaults + +## 5.4 Session Setup +Purpose: confirm and lightly customize before start. + +Components: +- selected preset header +- editable duration control +- visual mode section +- audio mode section +- headphone status row +- brightness recommendation row +- holder guidance link +- Start button + +Editable fields for MVP: +- duration: preset default, editable within safe range +- mode toggle: Audio+Visual / Audio-only / Visual-only +- visual pattern: only if visual enabled and only within preset-supported options +- blink frequency: adjustable within limited safe range +- carrier frequency: adjustable within limited range +- binaural beat difference: adjustable within limited range + +Validation ranges (product-level; engineering may refine but must enforce equivalent guardrails): +- duration: 1 to 30 minutes +- blink frequency: 1.0 to 20.0 Hz +- carrier frequency: 80 to 400 Hz +- binaural beat difference: 0.5 to 20 Hz + +Recommended preset defaults: +- Relax: 10 min, Pulse, 6 Hz visual, 200 Hz carrier, 6 Hz binaural difference +- Focus: 15 min, Flash, 10 Hz visual, 220 Hz carrier, 10 Hz binaural difference +- Sleep Prep: 20 min, Pulse, 3 Hz visual, 180 Hz carrier, 3 Hz binaural difference + +Behavior: +- Start disabled if invalid config or no enabled mode +- if audio is enabled and no headset detected, show blocking warning and CTA: Continue as Visual-only +- brightness row is advisory text, not a system-permission flow +- optional “Show holder guidance” interstitial before countdown if setting enabled + +Field interaction details: +- use sliders or stepper controls rather than free-form numeric input where possible +- if text input is used, numeric keyboard only +- invalid edits revert or clamp on blur with helper text + +States: +- default +- invalid field state +- blocked by missing headphones +- loading not needed beyond quick preset load + +## 5.5 Holder Guidance +Purpose: give practical safe setup help. + +Content blocks: +- use a simple cardboard visor/holder +- keep phone stable and hands-free +- do not press device against eyes/face +- allow airflow and comfort +- test fit before session +- sit or lie down in a safe place + +Behavior: +- accessible from onboarding, Home, Setup +- dismiss returns to previous screen + +## 5.6 Countdown +Purpose: prepare user before stimulation starts. + +Components: +- full-screen dark background +- large numeric countdown +- short text: “Get comfortable. Session starting…” +- Cancel action + +Behavior: +- start only after preflight passes +- cancellation returns to Setup +- audio/visual stimulation does not start until countdown completes + +## 5.7 Active Session +Purpose: core session experience. + +Default UI: +- full-screen stimulation surface +- overlay hidden by default +- tapping anywhere reveals controls for 3 seconds, then auto-hides + +Overlay controls: +- Pause/Resume +- Stop +- remaining time +- preset/session name + +Visual requirements: +- black background baseline +- white flash or white pulse/fade full-screen +- no extra text while overlay hidden +- no decorative animation unrelated to session timing + +Audio requirements: +- continuous stereo tone generation when audio enabled +- no speaker fallback for binaural mode + +Behavior: +- keep screen awake +- enter immersive full-screen mode +- prevent accidental exit +- stop means immediate cessation of stimulation +- if user locks screen or app backgrounds, auto-pause + +States: +- running with overlay hidden +- running with overlay shown +- paused +- interrupted +- ending/completing + +## 5.8 Paused Overlay +Components: +- Paused title +- remaining time +- Resume button +- Stop button + +Behavior: +- stimulation off while paused +- resume uses 3-second countdown + +## 5.9 Interruption / Headphone Warning Sheet +Triggers: +- headphones disconnected +- audio route changed from stereo headset to speaker/unknown +- incoming call / audio focus loss +- app backgrounded + +Copy must explain exactly what happened and what session modes remain safe. + +Actions: +- Resume Audio+Visual (only if requirements restored) +- Resume Visual-only +- End Session + +## 5.10 Session Complete +Components: +- completion title +- preset name +- status message: Completed or Ended early +- Repeat Session button +- Return Home button + +Behavior: +- Repeat restarts from Setup with prior values preserved +- no autoplay into another session + +## 5.11 Settings +MVP contents: +- countdown preference +- default session mode +- show holder guidance before session +- re-open safety information +- about/disclaimer + +Out of scope in Settings for MVP: +- account +- analytics +- session history +- downloadable presets + +--- + +## 6. UI Component Specifications + +### 6.1 Buttons +- Minimum touch target: 48x48 dp +- Primary buttons high contrast +- Disabled state clearly visible but readable +- Dangerous action button style for Stop/End Session + +### 6.2 Preset Cards +- Entire card tappable +- Show title, short description, duration, mode summary +- Pressed state visible +- Support accessibility focus order and spoken summary + +### 6.3 Toggles / Segmented Controls +Used for mode selection. +- Exactly one of: Audio+Visual / Audio-only / Visual-only +- state change updates dependent controls immediately +- disabling audio hides or disables headphone validation row + +### 6.4 Sliders / Steppers +Used for duration and frequency values. +- live value label always visible +- changes preview in text only, not active stimulation +- values snap to safe increments + +### 6.5 Warnings / Inline Errors +- concise, direct language +- use inline messaging near affected control +- blocking warnings also summarized above Start button + +### 6.6 Confirmation Sheets +Used for Stop and risky state transitions. +- must not obscure primary action meaning +- destructive choice clearly labeled + +--- + +## 7. States, Validation, Empty/Error/Loading + +### 7.1 Global Loading +MVP should avoid heavy loading states. All built-in presets ship locally. +Only acceptable loaders: +- app startup while loading local preferences +- brief transition while preparing session engines + +### 7.2 Validation Rules +- at least one stimulation mode must be on +- duration must be within 1–30 min +- blink frequency must remain within allowed range +- carrier frequency and beat difference must remain in allowed range +- if audio enabled, stereo headset route required before Start and Resume +- safety acknowledgment required before any session start + +### 7.3 Empty States +- none for Home in normal operation because presets are bundled +- if preset load fails, show fallback state with Retry and use embedded defaults automatically + +### 7.4 Error States +Handle explicitly: +- audio engine init failure -> cannot start audio-enabled session; offer Visual-only +- visual renderer failure -> cannot start visual-enabled session; offer Audio-only +- headphone disconnect during session -> auto-pause + warning sheet +- interruption / focus loss -> auto-pause + recovery sheet +- unexpected session engine failure -> stop safely + show non-technical error message and return options + +### 7.5 Recovery Behavior +- users should always have a safe path to end session +- app should never continue flashing or playing audio after fatal error + +--- + +## 8. Accessibility Requirements +1. All non-stimulation screens must support Android screen readers. +2. Interactive elements must have descriptive labels, roles, and state announcements. +3. Minimum touch target: 48 dp. +4. Text contrast must meet WCAG AA on non-session UI. +5. Text should support dynamic type/font scaling up to at least 200% without loss of critical actions. +6. Motion outside the stimulation experience should be minimal. +7. The app must provide **Audio-only** and **Visual-only** session modes. +8. Safety content must use plain language and short sentences. +9. Active session controls must remain discoverable and operable with screen reader focus when overlay is shown. +10. Session must never rely on color alone for meaning. +11. Any icon-only controls must include accessible text labels. +12. For users unable to tolerate flashing, app must make Visual-only off-state easy to find before session start. + +Note: the stimulation content itself is intentionally flashing/pulsing; accessibility work applies to surrounding UI and availability of safe alternatives. + +--- + +## 9. Data Model and Data Needs + +## 9.1 Core Product Models + +### SessionPreset +- id +- name +- description +- defaultDurationSec +- visualEnabledByDefault +- audioEnabledByDefault +- visualPatternType +- blinkFrequencyHz +- recommendedIntensityPercent +- carrierFrequencyHz +- binauralDifferenceHz +- cautionNote +- isBuiltIn +- sortOrder + +### SessionConfig +- presetId +- durationSec +- mode (audioVisual | audioOnly | visualOnly) +- visualPatternType +- blinkFrequencyHz +- intensityPercent +- carrierFrequencyHz +- binauralDifferenceHz + +### SessionRuntimeState +- sessionId +- presetId +- state (idle | countdown | running | paused | interrupted | completed | stopped | error) +- startTimeMonotonic +- elapsedSec +- remainingSec +- interruptionReason +- audioRouteState + +### AppSettings +- safetyAcknowledged +- safetyAcknowledgedVersion +- safetyAcknowledgedAt +- countdownPreference +- defaultModePreference +- showHolderGuidanceBeforeSession +- lastPresetId + +## 9.2 Local Data Storage +Persist locally only: +- safety acknowledgment state/version +- app settings +- last selected preset +- last-used session values for quick repeat +- built-in presets (bundled in app, optionally mirrored into local DB/preferences) + +No remote backend required for MVP. + +## 9.3 Remote Data Needs +None for MVP. +If crash reporting or analytics is later added, it must be explicitly reviewed and must not block MVP. + +--- + +## 10. Must-Have vs Nice-to-Have + +### 10.1 Must-Have for MVP +- Android native app +- first-run intro + mandatory safety acknowledgment +- built-in presets: Relax, Focus, Sleep Prep +- Home, Setup, Active Session, Completion, Settings, Holder Guidance +- audio+visual synchronized session engine +- pause/resume/stop +- countdown +- remaining time display +- full-screen flash and pulse patterns +- stereo binaural audio generation +- headphone-required handling for audio mode +- interruption-safe auto-pause behavior +- keep-screen-awake during session +- local persistence for acknowledgment + settings + last preset +- audio-only and visual-only modes + +### 10.2 Nice-to-Have After MVP +- alternating visual pattern +- custom preset editor +- ambient sound layer +- session history +- printable holder template +- advanced color patterns +- spoken guidance +- richer brightness calibration + +--- + +## 11. Risks and Concrete Mitigations + +### 11.1 Audio Latency / Instability +Risk: glitches or startup delay. +Decision: prioritize stable continuous generation over complex audio features; no ambient mixing in MVP. + +### 11.2 Timing Drift Between Audio and Visual +Risk: UI-driven timing may drift. +Decision: session timing derived from monotonic clock; visual/audio consume same session timeline. + +### 11.3 Device Brightness Variability +Risk: inconsistent perceived intensity. +Decision: do not promise exact brightness; expose conservative recommendation and in-app intensity only. + +### 11.4 Unsafe Use Without Headphones +Risk: speaker playback breaks binaural assumptions. +Decision: block start/resume for audio-enabled session unless stereo headset route is detected; offer Visual-only fallback. + +### 11.5 Interruptions +Risk: call/notification/backgrounding may leave stimulation running. +Decision: any significant interruption forces immediate pause and explicit resume. + +### 11.6 User Comfort / Holder Confusion +Risk: uncomfortable or unsafe placement. +Decision: ship concise holder guidance accessible in onboarding and setup; no printable template in MVP. + +### 11.7 Scope Creep +Risk: custom sessions and advanced patterns delay MVP. +Decision: lock MVP to built-in presets plus limited per-session edits. + +--- + +## 12. Technical Acceptance Criteria +Engineering should be able to test these directly. + +### 12.1 Onboarding / Safety +1. On first launch, app shows Welcome then Safety before Home. +2. User cannot access session start flow until safety acknowledgment is completed. +3. Safety acknowledgment persists across app relaunch. +4. If safety content version changes, user is asked to acknowledge again. + +### 12.2 Presets / Setup +5. Home displays exactly 3 built-in presets: Relax, Focus, Sleep Prep. +6. Tapping a preset opens Setup with correct default values. +7. Setup prevents Start when all stimulation modes are off. +8. Setup prevents Start when values are outside allowed ranges. +9. Setup allows duration changes only within 1–30 minutes. + +### 12.3 Audio / Headphones +10. If audio mode is selected and no supported headset route is detected, Start is blocked. +11. If user chooses Visual-only, session can start without headphones. +12. During an audio-enabled session, headset disconnect causes session to pause within 1 second and shows recovery UI. +13. Resuming Audio+Visual is blocked until a valid headset route is restored. + +### 12.4 Session Runtime +14. Starting a valid session enters countdown, then active full-screen mode. +15. Active session keeps screen awake until pause/stop/completion. +16. Remaining time updates correctly and reaches zero without negative values. +17. Pause stops stimulation and preserves remaining time. +18. Resume restarts from paused remaining time after resume countdown. +19. Stop ends stimulation immediately and does not resume automatically. +20. On natural completion, app shows completion screen with Repeat and Return Home. + +### 12.5 Interruptions / Safety +21. App backgrounding during session pauses stimulation immediately. +22. Incoming call or audio focus loss pauses session immediately. +23. After interruption, session never resumes without user action. +24. Fatal engine error stops all stimulation and presents user-safe recovery messaging. + +### 12.6 Accessibility / UI +25. All interactive controls outside active stimulation have accessible labels. +26. Primary controls meet 48 dp minimum target size. +27. Non-session text supports system font scaling without clipping primary actions. +28. Audio-only and Visual-only are available from Setup. + +### 12.7 Persistence +29. Last selected preset persists across app relaunch. +30. Settings persist across app relaunch. +31. Completion -> Repeat preloads the just-finished config. + +--- + +## 13. Recommended Content Defaults +Use concise, plain copy. Avoid wellness hype and all medical claims. + +### 13.1 Safety Screen Headline +“Read before using MindMachine” + +### 13.2 Safety Checkbox Copy +“I understand the risks and will stop immediately if I feel discomfort.” + +### 13.3 Headphone Warning +“Stereo headphones are required for binaural audio. Connect headphones or switch to Visual-only.” + +### 13.4 Completion Copy +- Completed: “Session complete.” +- Ended early: “Session ended.” + +--- + +## 14. Implementation Notes for Product/UX Alignment +- Keep non-session UI dark, minimal, and calm. +- Prefer step-based controls over free text. +- Avoid deep customization in MVP. +- During active session, every interaction should reduce cognitive load, not add it. +- Safety text should be direct, not alarmist. +- The fastest valid path for a returning user should be: Home -> preset tap -> Start. + +--- + +## 15. Handoff for Andy + +### Explicit Implementation Checklist +- [ ] Build Android-only MVP with screens: Welcome, Safety, Home, Setup, Active Session, Completion, Settings, Holder Guidance +- [ ] Implement bundled presets: Relax, Focus, Sleep Prep with defaults from this spec +- [ ] Persist safety acknowledgment version, settings, last preset, and repeat-session config locally +- [ ] Implement preflight validation for mode selection, parameter ranges, and headphone route +- [ ] Implement session countdown (Off/5/10 setting; default 5) +- [ ] Implement full-screen Active Session with hidden overlay controls and tap-to-reveal behavior +- [ ] Implement Flash and Pulse/Fade monochrome visual patterns only +- [ ] Implement stereo binaural tone engine with carrier + binaural difference config +- [ ] Use shared session timeline / monotonic timing source for audio + visual coordination +- [ ] Implement pause, resume, stop, and natural completion flows +- [ ] Auto-pause on backgrounding, call/audio focus loss, and headphone disconnect +- [ ] Block/resume Audio+Visual only when valid stereo headset route is present +- [ ] Offer Visual-only fallback when audio requirements fail +- [ ] Keep screen awake during active session and release correctly afterward +- [ ] Add accessibility labels, minimum touch targets, scalable text, and safe alternative modes +- [ ] Add test coverage for onboarding gating, setup validation, session transitions, interruption handling, and persistence +- [ ] Keep all copy non-medical and aligned to the safety wording in this spec + +If engineering must cut scope further, preserve in this order: safety gating -> stable session timing -> reliable audio -> full-screen visuals -> interruption handling -> polish. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..3ebcb36 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,276 @@ +# MindMachine - Architecture + +## 1. Overview + +MindMachine should be implemented as a modular mobile application with clearly separated concerns. The core architecture should isolate session timing, audio generation, visual rendering, preset management, and safety/setup flows so the app remains maintainable and easy to evolve. + +For the first implementation, a native Android architecture is recommended. + +--- + +## 2. Architecture Goals + +The system should: +- remain readable and maintainable, +- separate user interface from stimulation logic, +- support deterministic session behavior, +- allow new presets and stimulation modes to be added safely, +- make safety-critical logic explicit and testable. + +--- + +## 3. High-Level Components + +## 3.1 Presentation layer +Responsible for: +- onboarding UI, +- safety warnings, +- preset selection, +- session setup, +- active session controls, +- completion screens. + +Suggested responsibilities: +- render UI state, +- receive user input, +- dispatch commands to application logic, +- avoid direct low-level audio or rendering logic. + +## 3.2 Session orchestration layer +Responsible for: +- starting, pausing, resuming, and stopping sessions, +- tracking elapsed and remaining time, +- coordinating visual and audio parameters, +- responding to interruptions, +- exposing session state to the UI. + +This is the central control layer for the app. + +## 3.3 Audio engine +Responsible for: +- generating stereo tones, +- applying left/right frequency differences, +- keeping playback stable, +- handling headphone/output changes, +- exposing playback status and error states. + +## 3.4 Visual engine +Responsible for: +- rendering full-screen stimulation patterns, +- applying brightness/pulse/flash timing, +- switching between pattern modes, +- syncing to session state. + +## 3.5 Preset and configuration layer +Responsible for: +- built-in presets, +- user-created session settings, +- loading/saving preferences, +- validating parameter ranges. + +## 3.6 Safety layer +Responsible for: +- first-run acknowledgment state, +- pre-session warnings, +- session guardrails, +- interruption safety behavior, +- validation of risky configurations. + +--- + +## 4. Suggested Package / Module Structure + +```text +MindMachine/ +├── app/ +│ ├── ui/ +│ │ ├── onboarding/ +│ │ ├── home/ +│ │ ├── session/ +│ │ ├── settings/ +│ │ └── completion/ +│ ├── domain/ +│ │ ├── session/ +│ │ ├── preset/ +│ │ ├── safety/ +│ │ └── audio/ +│ ├── data/ +│ │ ├── presets/ +│ │ ├── settings/ +│ │ └── storage/ +│ ├── engine/ +│ │ ├── audio/ +│ │ ├── visual/ +│ │ └── timing/ +│ └── platform/ +│ ├── audio/ +│ ├── display/ +│ └── power/ +├── docs/ +└── tests/ +``` + +--- + +## 5. Core Domain Objects + +Suggested core models: +- `SessionPreset` +- `SessionConfig` +- `SessionState` +- `AudioConfig` +- `VisualConfig` +- `SafetyAcknowledgmentState` +- `SessionTimerState` + +Example concepts: +- preset name +- duration +- blink frequency +- visual pattern type +- carrier frequency left/right +- binaural beat difference +- brightness level recommendation +- pause/running/completed/stopped state + +--- + +## 6. Runtime Flow + +### Start flow +1. UI gathers selected preset/config +2. Session orchestrator validates settings +3. Safety layer checks acknowledgment state +4. Audio engine prepares playback +5. Visual engine prepares rendering +6. Session timer starts +7. Active session begins + +### During session +1. Session timer emits progress +2. Audio engine maintains output +3. Visual engine renders active frame state +4. UI observes reduced session state +5. Interruptions/errors are routed back to orchestrator + +### Stop flow +1. User or timer ends session +2. Session orchestrator issues stop commands +3. Audio engine halts playback +4. Visual engine returns to safe idle state +5. UI shows completion state + +--- + +## 7. State Management + +The app should use a unidirectional state model where practical: +- UI emits intents/actions +- orchestration/domain logic computes next state +- UI renders derived state + +This keeps behavior easier to test and reason about. + +--- + +## 8. Timing Strategy + +Timing should not rely on the UI render loop alone. + +Recommended approach: +- maintain a dedicated session timer/controller, +- compute session progress from monotonic time when possible, +- let visual and audio engines consume a stable timing source, +- avoid coupling timing precision to screen redraw alone. + +--- + +## 9. Audio Strategy + +The audio engine should: +- generate left/right channels independently, +- support continuous tone generation, +- maintain stable playback buffers, +- surface output-device changes immediately. + +Preferred properties: +- low glitch risk, +- deterministic playback, +- clear lifecycle methods: prepare, start, pause, resume, stop, release. + +--- + +## 10. Visual Strategy + +The visual engine should: +- use a dedicated full-screen rendering surface, +- support simple pattern strategies, +- derive pattern state from current session time, +- allow future expansion to more complex patterns. + +Pattern implementations should be strategy-based, for example: +- `FlashPatternRenderer` +- `PulsePatternRenderer` +- `AlternatingPatternRenderer` + +--- + +## 11. Safety Architecture + +Safety checks should exist in more than one place: +- onboarding acknowledgment, +- pre-session checks, +- runtime interruption handling, +- parameter validation. + +Examples: +- block session start if warnings not acknowledged, +- warn if stereo output is unavailable, +- stop or pause on critical runtime state changes, +- constrain dangerous parameter combinations. + +--- + +## 12. Persistence + +Persist only what is needed for version 1: +- onboarding/safety acknowledgment, +- recent preset selection, +- user-created presets, +- basic settings. + +Do not overcomplicate storage in version 1. + +--- + +## 13. Testing Strategy + +Test at multiple levels: + +### Unit tests +- preset validation +- session timing calculations +- safety rule evaluation +- parameter transformation logic + +### Integration tests +- session start/stop flow +- audio engine lifecycle behavior +- interruption handling +- persistence loading/saving + +### UI tests +- onboarding flow +- preset selection flow +- active session controls +- completion flow + +--- + +## 14. Key Design Principles + +- Keep components small and explicit +- Keep safety logic centralized and testable +- Avoid hidden coupling between UI and engines +- Prefer predictable configuration over clever automation +- Build for extension, but do not overengineer version 1 diff --git a/MVP.md b/MVP.md new file mode 100644 index 0000000..763dd80 --- /dev/null +++ b/MVP.md @@ -0,0 +1,160 @@ +# MindMachine - MVP Definition + +## 1. MVP Goal + +The MVP shall prove that a smartphone can function as a simple personal mind machine by: +- displaying controlled blinking light patterns on-screen, +- outputting binaural audio through stereo headphones, +- guiding the user through safe setup and session use, +- supporting short, usable sessions with minimal configuration. + +The MVP is a working prototype focused on core experience, safety, and reliability rather than breadth. + +--- + +## 2. MVP Success Criteria + +The MVP is successful if a user can: +1. open the app, +2. read and acknowledge the safety warnings, +3. choose a preset, +4. connect headphones, +5. start a session, +6. experience synchronized screen blinking and binaural audio, +7. pause or stop the session at any time, +8. finish the session without confusion or instability. + +--- + +## 3. Included in MVP + +### 3.1 Core session engine +- Start, pause, resume, and stop a session +- Configurable session duration +- Countdown before start +- Remaining time display + +### 3.2 Visual stimulation +- Full-screen flashing mode +- Full-screen pulse/fade mode +- Adjustable blink frequency +- Adjustable screen brightness recommendation +- Keep screen awake during session + +### 3.3 Audio stimulation +- Stereo binaural tone generation +- Adjustable carrier frequency +- Adjustable binaural beat difference frequency +- In-app volume control or clear system-volume guidance +- Headphone-required guidance + +### 3.4 Presets +The MVP shall include at least 3 built-in presets: +- Relax +- Focus +- Sleep Prep + +Each preset includes: +- duration +- blink rate +- light pattern +- carrier frequency +- binaural difference + +### 3.5 Safety +- First-run warning and acknowledgment flow +- Session-start reminder to use stereo headphones +- Explicit warning for seizure/photo-sensitivity risks +- Stop immediately guidance for discomfort + +### 3.6 Setup guidance +- Brief instructions for how to place the phone in front of the eyes +- Basic explanation of a DIY cardboard holder +- Comfort and fit cautions + +### 3.7 Completion flow +- Session finished screen +- Option to repeat session +- Option to return to home screen + +--- + +## 4. Excluded from MVP + +The MVP does not need: +- accounts or cloud sync +- ambient background sounds +- downloadable content +- spoken voice guidance +- printable holder templates +- advanced animation editor +- adaptive or biometric feedback +- social/community features +- analytics dashboard for users +- medical claims or therapeutic workflows + +--- + +## 5. MVP User Flow + +1. User opens app +2. User reads onboarding + warnings +3. User selects preset +4. User verifies headphones and setup +5. User starts countdown +6. App enters full-screen session mode +7. App runs synchronized light + audio session +8. User pauses/stops or reaches session end +9. App shows completion screen + +--- + +## 6. MVP Screens + +The MVP should include these screens: +- Welcome / onboarding +- Safety warning acknowledgment +- Home / preset selection +- Session setup +- Active session screen +- Session complete screen +- Simple settings screen + +--- + +## 7. MVP Technical Priorities + +Implementation priority order: +1. Stable session timing +2. Reliable stereo audio generation +3. Full-screen light rendering +4. Safe interruption handling +5. Clear UI for start/pause/stop +6. Preset persistence + +--- + +## 8. MVP Risks + +Main risks for the MVP: +- audio latency or instability, +- timing drift between audio and visual patterns, +- inconsistent brightness behavior across devices, +- interruptions from calls/notifications, +- unsafe use without proper warnings, +- poor comfort if holder guidance is unclear. + +--- + +## 9. Recommended First Platform + +The MVP should target **Android first** because: +- native Android gives more direct control over audio/session behavior, +- Android is more practical for experimental device-style applications, +- screen/audio/session APIs are generally more flexible for this use case. + +--- + +## 10. MVP Deliverable + +The MVP deliverable is a native mobile app prototype that safely delivers a basic mind machine session experience with a small set of presets and a clean, minimal interface. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..4343a70 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,265 @@ +# MindMachine - Requirements + +## 1. Purpose + +MindMachine is a mobile phone application that turns a smartphone into a simple audiovisual mind machine. The phone display presents controlled blinking light patterns while stereo audio playback generates binaural beats through headphones. The device is intended to be placed directly in front of the user's eyes using a lightweight cardboard holder, similar in concept to simple glasses or a visor, so the phone can remain positioned comfortably and hands-free. + +The application is intended for relaxation, focus sessions, meditation support, and guided sensory sessions. It is not a medical device and must not make medical or therapeutic claims. + +--- + +## 2. Product Goals + +The application shall: +- provide synchronized visual blinking patterns on the display, +- provide synchronized binaural audio through stereo headphones, +- allow the user to start sessions quickly with minimal setup, +- support safe and comfortable hands-free use with a simple DIY cardboard holder, +- provide configurable sessions for different intended outcomes such as relaxation, focus, and sleep preparation, +- remain simple, readable, and reliable during use in dark or eyes-closed/near-eye conditions. + +--- + +## 3. Core User Scenario + +A user launches the app, selects a session preset, connects stereo headphones, places the phone into a cardboard holder positioned in front of the eyes, and starts the session. During the session: +- the screen displays flashing or pulsing light patterns, +- the audio engine outputs binaural tones independently to the left and right channels, +- the app keeps the screen awake, +- the user may pause, resume, or stop the session easily. + +--- + +## 4. Target Users + +### Primary users +- people interested in meditation or relaxation, +- users experimenting with audiovisual entrainment, +- hobbyists building a simple DIY mind machine with a smartphone. + +### Secondary users +- developers and designers evaluating guided sensory experiences, +- technically curious users wanting to customize frequencies and session behavior. + +--- + +## 5. Functional Requirements + +### 5.1 Session management +The application shall: +- allow the user to start a session from a preset list, +- allow the user to create custom sessions, +- allow the user to pause, resume, and stop a running session, +- display remaining session time, +- support session durations configurable by the user, +- optionally support a short countdown before the session begins. + +### 5.2 Visual stimulation +The application shall: +- display full-screen blinking or pulsing visual patterns, +- support left/right symmetric visual output suitable for near-eye viewing, +- support multiple pattern types, including at minimum: + - full-screen flash, + - pulse/fade, + - alternating brightness patterns, + - color-based patterns, +- allow configuration of blink frequency, +- allow configuration of brightness/intensity within safe device limits, +- provide a dark screen or dim idle state between pulses when required by the selected pattern, +- keep the display active for the duration of the session. + +### 5.3 Audio stimulation +The application shall: +- generate stereo audio suitable for headphones, +- generate binaural beat output by sending different frequencies to the left and right channels, +- allow configuration of carrier frequency, +- allow configuration of binaural beat difference frequency, +- allow adjustment of output volume within the app, +- optionally mix ambient background sound such as rain, noise, or soft drones, +- continue audio playback reliably during the active session unless the user stops it. + +### 5.4 Synchronization +The application shall: +- synchronize visual stimulation timing with the active session timeline, +- allow session presets where light frequency and binaural frequency are coordinated, +- minimize drift between visual events and audio timing as much as practical on the device. + +### 5.5 Presets +The application shall include built-in presets such as: +- Relaxation, +- Meditation, +- Focus, +- Sleep preparation, +- Custom. + +Each preset should define at minimum: +- session length, +- visual pattern type, +- visual blink rate, +- audio carrier frequencies, +- binaural beat difference, +- brightness recommendation, +- safety notes if relevant. + +### 5.6 DIY holder guidance +The application shall: +- include a brief guide explaining how to build or fold a simple cardboard holder, +- explain how the phone should be positioned relative to the eyes, +- explain how to keep the phone stable without hand use, +- explain that the holder should avoid pressure on the eyes or face, +- explain that ventilation and comfort matter during longer sessions. + +### 5.7 Safety and warnings +The application shall: +- show a safety warning before first use, +- require the user to acknowledge warnings before starting the first session, +- warn users not to use the app while driving, walking, cycling, or operating machinery, +- warn users with epilepsy, seizure sensitivity, migraines triggered by flashing light, or similar conditions not to use the visual stimulation mode without medical clearance, +- warn users to use stereo headphones for binaural mode, +- warn users to stop immediately if discomfort, dizziness, nausea, eye strain, anxiety, or headache occurs. + +### 5.8 Accessibility and fallback behavior +The application shall: +- allow audio-only sessions, +- allow visual-only sessions, +- provide large, clear controls for start, pause, resume, and stop, +- use simple language for all safety and setup instructions, +- avoid cluttered UI during active sessions. + +--- + +## 6. Non-Functional Requirements + +### 6.1 Performance +The application should: +- launch quickly, +- begin sessions with minimal delay, +- maintain steady audio output without stutter under normal device conditions, +- maintain visually stable timing within practical mobile device limits. + +### 6.2 Reliability +The application shall: +- recover gracefully from interruptions such as audio route changes, +- handle headphone disconnection safely, +- stop or pause the session if audio output becomes invalid for binaural use, +- avoid accidental screen sleep during active sessions. + +### 6.3 Usability +The application shall: +- support one-handed setup before the session starts, +- require very few steps to launch a preset session, +- present a calm, minimal interface appropriate for low-light use, +- make all important settings understandable without technical knowledge. + +### 6.4 Maintainability +The system should be structured so that: +- visual pattern generation, audio generation, session timing, preset management, and safety flows are separate components, +- new presets and stimulation modes can be added without large architectural changes, +- platform-specific media or brightness handling is isolated cleanly. + +--- + +## 7. User Experience Requirements + +### 7.1 First-run experience +On first launch, the application shall: +- explain what the app does in simple terms, +- explain that stereo headphones are required for binaural beats, +- explain how to position the phone using a cardboard holder, +- present the main safety warnings, +- guide the user to a first starter session. + +### 7.2 Session setup screen +The setup screen shall show: +- selected preset, +- session duration, +- visual mode, +- audio mode, +- headphone status if detectable, +- brightness level recommendation, +- a clear Start button. + +### 7.3 Active session screen +The active session screen shall: +- switch to full-screen mode, +- show the active blinking pattern, +- provide minimal overlay controls, +- allow pause and stop actions, +- display remaining time when the overlay is shown, +- avoid distracting text during the running session. + +### 7.4 Session completion +At the end of a session, the application shall: +- stop flashing and audio safely, +- return brightness and audio state as appropriate, +- show a completion screen, +- allow the user to repeat the session, save it, or return home. + +--- + +## 8. Safety-Critical Constraints + +The application must not: +- present itself as a medical, therapeutic, or diagnostic product, +- force maximum brightness without user awareness, +- continue a visual stimulation session after the user presses Stop, +- assume the user is wearing headphones if stereo output is unavailable, +- hide seizure-related flashing-light warnings. + +The application should: +- default to conservative brightness and intensity settings, +- provide extra caution around high-frequency or high-contrast flashing modes, +- make it easy to disable visual stimulation entirely. + +--- + +## 9. Technical Considerations + +The implementation will likely need: +- precise audio generation for separate left/right stereo channels, +- a timing engine for synchronized session events, +- high-brightness full-screen rendering while respecting device limits, +- screen wake lock / keep-awake behavior, +- optional airplane-mode recommendation to reduce interruptions, +- handling for notifications or incoming calls during a session. + +If built natively for Android, the app should use Android audio APIs and full-screen rendering in a way that keeps latency and timing stable. + +--- + +## 10. Out of Scope (Initial Version) + +The first version does not need to include: +- biometric sensors, +- adaptive biofeedback, +- cloud sync, +- social features, +- account creation, +- medical treatment recommendations, +- VR headset integration, +- remote multi-user sessions. + +--- + +## 11. Open Questions + +The following product questions should be answered before implementation: +- What exact session presets should ship in version 1? +- Should the app target Android only, or Android and iPhone? +- Should visual patterns be monochrome, color-based, or both? +- Should ambient sounds be bundled locally or added later? +- Should the cardboard holder guide include printable templates? +- Should the app support spoken guidance during sessions? +- What safety constraints should be enforced around maximum flash rate and brightness? + +--- + +## 12. Success Criteria + +The product will be considered successful for version 1 if: +- a user can start a working mind machine session in a few steps, +- the phone can reliably display blinking patterns while outputting binaural audio, +- the app clearly communicates safe usage, +- the user can comfortably use a simple holder to keep the phone in place, +- preset and custom sessions both work reliably, +- the documentation is clear enough for review, implementation, and end-user use. diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..e153b6d --- /dev/null +++ b/TASKS.md @@ -0,0 +1,126 @@ +# MindMachine - Task List + +## 1. Product Definition +- [ ] Review and approve `REQUIREMENTS.md` +- [ ] Confirm target platform for version 1 +- [ ] Confirm MVP scope +- [ ] Decide final built-in presets for v1 +- [ ] Confirm safety language and disclaimer wording +- [ ] Decide whether version 1 is Android-only + +## 2. UX / Product Design +- [ ] Create user flow diagram +- [ ] Define onboarding flow +- [ ] Define safety warning flow +- [ ] Define preset selection screen +- [ ] Define session setup screen +- [ ] Define active session screen +- [ ] Define completion screen +- [ ] Define simple settings screen +- [ ] Define cardboard-holder guidance content + +## 3. Technical Planning +- [ ] Choose app architecture +- [ ] Define project module/package structure +- [ ] Select audio generation approach +- [ ] Select visual rendering approach +- [ ] Define timing/synchronization strategy +- [ ] Define persistence model for presets/settings +- [ ] Define interruption handling strategy + +## 4. Project Setup +- [ ] Create project repository +- [ ] Initialize native mobile project +- [ ] Configure formatting/linting +- [ ] Configure unit test framework +- [ ] Configure UI/integration test foundation +- [ ] Create initial README + +## 5. Core Domain Models +- [ ] Implement `SessionPreset` +- [ ] Implement `SessionConfig` +- [ ] Implement `SessionState` +- [ ] Implement `AudioConfig` +- [ ] Implement `VisualConfig` +- [ ] Implement safety acknowledgment state + +## 6. Session Engine +- [ ] Implement session timer/orchestrator +- [ ] Implement start session flow +- [ ] Implement pause/resume logic +- [ ] Implement stop logic +- [ ] Implement completion behavior +- [ ] Add tests for timing and session transitions + +## 7. Audio Engine +- [ ] Implement stereo tone generation +- [ ] Implement binaural beat configuration +- [ ] Implement audio lifecycle management +- [ ] Handle headphone/output changes +- [ ] Handle audio interruptions safely +- [ ] Add tests for config and lifecycle behavior + +## 8. Visual Engine +- [ ] Implement full-screen rendering surface +- [ ] Implement flash pattern +- [ ] Implement pulse/fade pattern +- [ ] Implement alternating pattern option +- [ ] Connect pattern timing to session state +- [ ] Add tests for visual timing logic where practical + +## 9. Safety Features +- [ ] Implement first-run safety acknowledgment +- [ ] Block session start until warnings are acknowledged +- [ ] Add seizure / photo-sensitivity warning text +- [ ] Add headphone requirement warning +- [ ] Add runtime stop/pause behavior for unsafe output changes +- [ ] Add tests for safety guardrails + +## 10. UI Implementation +- [ ] Implement onboarding screen +- [ ] Implement home/preset list screen +- [ ] Implement session setup screen +- [ ] Implement active session screen +- [ ] Implement completion screen +- [ ] Implement settings screen +- [ ] Ensure controls are large and clear + +## 11. Presets and Settings +- [ ] Add Relax preset +- [ ] Add Focus preset +- [ ] Add Sleep Prep preset +- [ ] Add custom session editor (if in v1) +- [ ] Persist recent settings +- [ ] Validate user-entered parameter ranges + +## 12. Holder Guidance / Documentation +- [ ] Write concise holder-building instructions +- [ ] Add fit and comfort guidance +- [ ] Add safe positioning guidance +- [ ] Add end-user usage instructions +- [ ] Review for clarity and brevity + +## 13. Testing +- [ ] Unit test session state transitions +- [ ] Unit test preset validation +- [ ] Unit test safety rules +- [ ] Integration test session start/stop +- [ ] Integration test interruption handling +- [ ] UI test onboarding and preset selection +- [ ] UI test active session controls +- [ ] Manual test with real stereo headphones + +## 14. Pre-Release Review +- [ ] Review wording for medical/safety compliance +- [ ] Review usability in low-light conditions +- [ ] Review comfort assumptions around phone holder +- [ ] Review battery/heat impact during longer sessions +- [ ] Review failure modes and recovery behavior + +## 15. Nice-to-Have After MVP +- [ ] Ambient sound mixing +- [ ] Printable holder template +- [ ] More advanced visual patterns +- [ ] Guided voice sessions +- [ ] Session history +- [ ] Expanded preset library diff --git a/USER_MANUAL.md b/USER_MANUAL.md new file mode 100644 index 0000000..2155d9c --- /dev/null +++ b/USER_MANUAL.md @@ -0,0 +1,181 @@ +# MindMachine - User Manual + +## 1. What MindMachine Does + +MindMachine turns your smartphone into a simple audiovisual mind machine. + +During a session: +- the screen displays blinking or pulsing light patterns, +- stereo headphones play binaural audio, +- the phone is placed in front of your eyes using a simple holder. + +MindMachine is intended for relaxation, focus, meditation support, or experimentation. It is not a medical device. + +--- + +## 2. Before You Begin + +You will need: +- a smartphone with the MindMachine app installed, +- stereo headphones, +- a simple cardboard phone holder or face-mounted support, +- a safe, comfortable place to sit or lie down. + +Do not use the app while walking, driving, cycling, or operating machinery. + +--- + +## 3. Important Safety Information + +Do not use visual stimulation mode if: +- you are sensitive to flashing lights, +- you have epilepsy or a seizure disorder, +- flashing lights trigger migraines or discomfort for you, +- you feel dizzy, nauseous, anxious, or unwell. + +Stop the session immediately if you experience: +- eye strain, +- headache, +- dizziness, +- nausea, +- panic, +- discomfort of any kind. + +Use stereo headphones for binaural sound sessions. + +--- + +## 4. Building a Simple Cardboard Holder + +You can create a simple holder from stiff cardboard. + +The holder should: +- keep the phone in front of your eyes without using your hands, +- hold the phone securely, +- avoid pressure on the eyes, +- sit comfortably on the nose or forehead area, +- allow enough airflow and comfort. + +Think of it like very simple cardboard glasses or a visor. + +Important: +- do not press the phone tightly against your face, +- do not block breathing, +- make sure the phone cannot fall into your eyes, +- test comfort before starting a session. + +--- + +## 5. Starting a Session + +1. Open MindMachine. +2. Read the safety information. +3. Connect stereo headphones. +4. Choose a session preset. +5. Review the session duration and settings. +6. Place the phone into the holder. +7. Position the phone in front of your eyes. +8. Press **Start**. + +A short countdown may appear before the session begins. + +--- + +## 6. Choosing a Session + +MindMachine may provide presets such as: +- **Relax** +- **Focus** +- **Meditation** +- **Sleep Prep** + +Each preset may use different: +- blinking speeds, +- light patterns, +- session lengths, +- binaural beat settings. + +If custom sessions are supported, you can adjust these settings yourself. + +--- + +## 7. During a Session + +During a session: +- the screen will blink, flash, or pulse, +- the audio will play through the headphones, +- the app will keep the screen awake, +- the remaining time may be shown. + +You can usually: +- pause the session, +- resume the session, +- stop the session at any time. + +If anything feels uncomfortable, stop immediately. + +--- + +## 8. After a Session + +When the session ends, the app will: +- stop the sound, +- stop the visual pattern, +- show a completion screen. + +You may then: +- repeat the same session, +- return to the home screen, +- choose a different preset. + +--- + +## 9. Tips for Best Results + +- Use the app in a calm environment. +- Start with shorter sessions. +- Use moderate brightness first. +- Make sure your headphones are truly stereo. +- Sit or lie down comfortably before starting. +- Test your cardboard holder before a full session. + +--- + +## 10. Troubleshooting + +### No binaural effect +Possible causes: +- headphones are not connected, +- audio is playing through speaker instead of headphones, +- headphones are mono instead of stereo. + +What to do: +- reconnect headphones, +- verify left/right stereo output, +- restart the session. + +### Screen turns off +What to do: +- reopen the app, +- restart the session, +- check battery-saving settings if needed. + +### The session feels too intense +What to do: +- lower brightness, +- choose a gentler preset, +- shorten the session, +- use audio-only mode if available. + +### Phone holder is uncomfortable +What to do: +- adjust the cardboard shape, +- reduce pressure points, +- increase spacing from the face, +- shorten the session until fit improves. + +--- + +## 11. Best Practice Reminder + +Use MindMachine carefully, start gently, and prioritize comfort and safety over intensity. diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..48eb776 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.mindmachine.mvp" + compileSdk = 35 + + defaultConfig { + applicationId = "com.mindmachine.mvp" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.09.03") + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6") + implementation("androidx.activity:activity-compose:1.9.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.6") + implementation("androidx.navigation:navigation-compose:2.8.2") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") + + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..b7c9678 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# MVP no-op diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..579af80 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt new file mode 100644 index 0000000..cbc1bf3 --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -0,0 +1,307 @@ +package com.mindmachine.mvp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenu +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.mindmachine.mvp.audio.BinauralAudioEngine +import com.mindmachine.mvp.audio.HeadsetMonitor +import com.mindmachine.mvp.data.SettingsRepository +import com.mindmachine.mvp.domain.CountdownPreference +import com.mindmachine.mvp.domain.RuntimeState +import com.mindmachine.mvp.domain.SessionMode +import com.mindmachine.mvp.session.MainViewModel +import kotlin.math.roundToInt + +class MainActivity : ComponentActivity() { + private val vm: MainViewModel by viewModels { + MainViewModel.Factory(SettingsRepository(applicationContext), HeadsetMonitor(applicationContext), BinauralAudioEngine()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { App(vm) } + } +} + +@Composable +fun App(vm: MainViewModel = viewModel()) { + val nav = rememberNavController() + val ui by vm.ui.collectAsStateWithLifecycle() + val lifecycle = LocalLifecycleOwner.current.lifecycle + + androidx.compose.runtime.DisposableEffect(lifecycle) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_STOP && ui.runtimeState == RuntimeState.RUNNING) { + vm.pause("Session paused because app moved to background.") + } + } + lifecycle.addObserver(observer) + onDispose { lifecycle.removeObserver(observer) } + } + + val startRoute = if (ui.settings.safetyAcknowledged) "home" else "welcome" + NavHost(navController = nav, startDestination = startRoute) { + composable("welcome") { + SimpleScreen("MindMachine", "Blinking light + binaural audio. Stereo headphones required for binaural mode. Not a medical device.") { + Button(onClick = { nav.navigate("safety") }) { Text("Continue") } + } + } + composable("safety") { + SafetyScreen( + onAck = { + vm.acknowledgeSafety() + nav.navigate("home") { popUpTo(0) } + }, + onHolder = { nav.navigate("holder") } + ) + } + composable("home") { + Column(Modifier.fillMaxSize()) { + TopAppBar(title = { Text("MindMachine") }, actions = { + TextButton(onClick = { nav.navigate("settings") }) { Text("Settings") } + }) + LazyColumn(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + item { + TextButton(onClick = { nav.navigate("holder") }) { Text("Holder Guidance") } + } + items(ui.presets) { p -> + Card(Modifier.fillMaxWidth().clickable { + vm.choosePreset(p.id) + nav.navigate("setup") + }.padding(4.dp)) { + Column(Modifier.padding(12.dp)) { + Text(p.name, style = MaterialTheme.typography.titleMedium) + Text(p.description) + Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • ${p.binauralDifferenceHz} Hz") + } + } + } + } + } + } + composable("setup") { + SetupScreen(vm = vm, onStart = { + vm.startSession() + nav.navigate("active") + }, onHolder = { nav.navigate("holder") }) + } + composable("active") { + ActiveSessionScreen(vm = vm, onFinish = { nav.navigate("complete") { popUpTo("setup") } }) + } + composable("complete") { + SimpleScreen( + if (ui.endedEarly) "Session ended" else "Session complete.", + ui.selectedPreset.name + ) { + Button(onClick = { + vm.startSession() + nav.navigate("active") + }) { Text("Repeat Session") } + OutlinedButton(onClick = { nav.navigate("home") { popUpTo(0) } }) { Text("Return Home") } + } + } + composable("settings") { + SettingsScreen(vm) { nav.popBackStack() } + } + composable("holder") { + HolderScreen { nav.popBackStack() } + } + } +} + +@Composable +fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnScope.() -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text(title, style = MaterialTheme.typography.headlineMedium) + Text(subtitle) + actions() + } +} + +@Composable +fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) { + var checked by remember { mutableStateOf(false) } + Column(Modifier.fillMaxSize().padding(16.dp)) { + Text("Read before using MindMachine", style = MaterialTheme.typography.headlineSmall) + Spacer(Modifier.height(12.dp)) + Text("Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers.\n\nDo not use while driving, walking, cycling, or operating machinery.\n\nStop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain.\n\nBinaural mode requires stereo headphones.\n\nThis app is not a medical device.") + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = checked, onCheckedChange = { checked = it }) + Text("I understand the risks and will stop immediately if I feel discomfort.") + } + Button(onClick = onAck, enabled = checked, modifier = Modifier.semantics { contentDescription = "I Understand" }) { Text("I Understand") } + TextButton(onClick = onHolder) { Text("Holder Guidance") } + } +} + +@Composable +fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) { + val ui by vm.ui.collectAsStateWithLifecycle() + val cfg = ui.config + Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall) + Text("Duration: ${cfg.durationSec / 60} min") + Slider(value = (cfg.durationSec / 60).toFloat(), onValueChange = { vm.setDurationMin(it.roundToInt()) }, valueRange = 1f..30f) + Text("Mode") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + SessionMode.values().forEach { mode -> + OutlinedButton(onClick = { vm.setMode(mode) }) { Text(mode.name.replace("_", " ")) } + } + } + Text("Blink ${cfg.blinkFrequencyHz} Hz") + Slider(value = cfg.blinkFrequencyHz, onValueChange = vm::setBlinkFrequency, valueRange = 1f..20f) + Text("Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz") + Slider(value = cfg.carrierFrequencyHz, onValueChange = vm::setCarrier, valueRange = 80f..400f) + Text("Difference ${cfg.binauralDifferenceHz} Hz") + Slider(value = cfg.binauralDifferenceHz, onValueChange = vm::setDifference, valueRange = 0.5f..20f) + Text("Brightness recommendation: keep screen comfortable and avoid eye strain.") + TextButton(onClick = onHolder) { Text("Holder Guidance") } + if (ui.error != null) Text(ui.error!!, color = Color.Red) + Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") } + } +} + +@Composable +fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) { + val ui by vm.ui.collectAsStateWithLifecycle() + if (ui.runtimeState == RuntimeState.COMPLETED || ui.runtimeState == RuntimeState.STOPPED) onFinish() + + var showOverlay by remember { mutableStateOf(true) } + val intensity = (ui.config.intensityPercent / 100f) + val flashing = if (ui.config.visualPatternType.name == "FLASH") { + if ((ui.remainingSec % 2) == 0) intensity else 0f + } else intensity * 0.5f + Box( + modifier = Modifier.fillMaxSize().background(Color.White.copy(alpha = if (ui.config.mode == SessionMode.AUDIO_ONLY) 0f else flashing)) + .clickable { showOverlay = !showOverlay } + ) { + if (ui.runtimeState == RuntimeState.COUNTDOWN) { + Text( + "${ui.countdownSec}", + modifier = Modifier.align(Alignment.Center), + style = MaterialTheme.typography.displayLarge, + color = Color.Black + ) + } + if (showOverlay) { + Column(Modifier.align(Alignment.BottomCenter).fillMaxWidth().background(Color.Black.copy(alpha = 0.6f)).padding(16.dp)) { + Text("${ui.selectedPreset.name} • ${ui.remainingSec}s", color = Color.White) + if (ui.runtimeState == RuntimeState.RUNNING) { + Button(onClick = { vm.pause() }, modifier = Modifier.fillMaxWidth()) { Text("Pause") } + } else { + Button(onClick = { vm.resume() }, modifier = Modifier.fillMaxWidth()) { Text("Resume") } + } + OutlinedButton(onClick = { vm.stop() }, modifier = Modifier.fillMaxWidth()) { Text("Stop") } + if (ui.interruptionReason != null) { + Text(ui.interruptionReason!!, color = Color.White) + if (ui.runtimeState == RuntimeState.INTERRUPTED) { + OutlinedButton(onClick = { vm.switchToVisualOnlyAndResume() }, modifier = Modifier.fillMaxWidth()) { + Text("Resume Visual-only") + } + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen(vm: MainViewModel, onBack: () -> Unit) { + val ui by vm.ui.collectAsStateWithLifecycle() + var expanded by remember { mutableStateOf(false) } + Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Settings", style = MaterialTheme.typography.headlineSmall) + Text("Countdown") + ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { + TextButton(onClick = { expanded = true }) { Text(ui.settings.countdownPreference.name) } + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + CountdownPreference.values().forEach { + DropdownMenuItem(text = { Text(it.name) }, onClick = { vm.updateCountdown(it); expanded = false }) + } + } + } + Text("Default mode") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + SessionMode.values().forEach { mode -> + OutlinedButton(onClick = { vm.updateDefaultMode(mode) }) { Text(mode.name) } + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = ui.settings.showHolderGuidanceBeforeSession, onCheckedChange = vm::updateGuidance) + Text("Show holder guidance before session") + } + HorizontalDivider() + Text("About/Disclaimer: MindMachine is a prototype and not a medical device.") + OutlinedButton(onClick = onBack) { Text("Back") } + } +} + +@Composable +fun HolderScreen(onBack: () -> Unit) { + Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Holder Guidance", style = MaterialTheme.typography.headlineSmall) + Text("• Use a simple cardboard visor/holder.") + Text("• Keep phone stable and hands-free.") + Text("• Do not press device against eyes/face.") + Text("• Allow airflow and comfort.") + Text("• Test fit before session.") + Text("• Sit or lie down in a safe place.") + OutlinedButton(onClick = onBack) { Text("Back") } + } +} diff --git a/app/src/main/java/com/mindmachine/mvp/audio/BinauralAudioEngine.kt b/app/src/main/java/com/mindmachine/mvp/audio/BinauralAudioEngine.kt new file mode 100644 index 0000000..9f777fc --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/audio/BinauralAudioEngine.kt @@ -0,0 +1,72 @@ +package com.mindmachine.mvp.audio + +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioTrack +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.math.PI +import kotlin.math.sin + +class BinauralAudioEngine { + private var track: AudioTrack? = null + private var job: Job? = null + private var scope: CoroutineScope? = null + + fun start(carrierHz: Float, differenceHz: Float) { + stop() + val sampleRate = 44100 + val bufferSize = AudioTrack.getMinBufferSize( + sampleRate, + AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.ENCODING_PCM_16BIT + ).coerceAtLeast(4096) + + val audioTrack = AudioTrack( + AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build(), + AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_PCM_16BIT).setSampleRate(sampleRate).setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(), + bufferSize, + AudioTrack.MODE_STREAM, + AudioTrack.AUDIO_SESSION_ID_GENERATE + ) + track = audioTrack + val localScope = CoroutineScope(Dispatchers.Default) + scope = localScope + audioTrack.play() + + job = localScope.launch { + val shorts = ShortArray(bufferSize) + var phaseL = 0.0 + var phaseR = 0.0 + val leftHz = carrierHz - differenceHz / 2f + val rightHz = carrierHz + differenceHz / 2f + while (isActive) { + for (i in shorts.indices step 2) { + phaseL += 2 * PI * leftHz / sampleRate + phaseR += 2 * PI * rightHz / sampleRate + shorts[i] = (sin(phaseL) * Short.MAX_VALUE * 0.15).toInt().toShort() + shorts[i + 1] = (sin(phaseR) * Short.MAX_VALUE * 0.15).toInt().toShort() + } + audioTrack.write(shorts, 0, shorts.size) + } + } + } + + fun stop() { + job?.cancel() + job = null + scope?.cancel() + scope = null + track?.runCatching { + pause() + flush() + stop() + release() + } + track = null + } +} diff --git a/app/src/main/java/com/mindmachine/mvp/audio/HeadsetMonitor.kt b/app/src/main/java/com/mindmachine/mvp/audio/HeadsetMonitor.kt new file mode 100644 index 0000000..dfa1b49 --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/audio/HeadsetMonitor.kt @@ -0,0 +1,18 @@ +package com.mindmachine.mvp.audio + +import android.content.Context +import android.media.AudioDeviceInfo +import android.media.AudioManager + +class HeadsetMonitor(context: Context) { + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + fun isStereoHeadsetAvailable(): Boolean { + return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).any { + (it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES + || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET + || it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP + || it.type == AudioDeviceInfo.TYPE_BLE_HEADSET) + } + } +} diff --git a/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt b/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt new file mode 100644 index 0000000..bdd1f64 --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt @@ -0,0 +1,49 @@ +package com.mindmachine.mvp.data + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.mindmachine.mvp.domain.AppSettings +import com.mindmachine.mvp.domain.CountdownPreference +import com.mindmachine.mvp.domain.SessionMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.dataStore by preferencesDataStore("mindmachine_settings") + +class SettingsRepository(private val context: Context) { + private object Keys { + val safetyAcknowledged = booleanPreferencesKey("safety_ack") + val safetyVersion = intPreferencesKey("safety_version") + val countdownPref = stringPreferencesKey("countdown_pref") + val defaultMode = stringPreferencesKey("default_mode") + val showGuidance = booleanPreferencesKey("show_guidance") + val lastPresetId = stringPreferencesKey("last_preset") + } + + val settings: Flow = context.dataStore.data.map { p -> + AppSettings( + safetyAcknowledged = p[Keys.safetyAcknowledged] ?: false, + safetyAcknowledgedVersion = p[Keys.safetyVersion] ?: 0, + countdownPreference = runCatching { CountdownPreference.valueOf(p[Keys.countdownPref] ?: "FIVE") }.getOrDefault(CountdownPreference.FIVE), + defaultModePreference = runCatching { SessionMode.valueOf(p[Keys.defaultMode] ?: "AUDIO_VISUAL") }.getOrDefault(SessionMode.AUDIO_VISUAL), + showHolderGuidanceBeforeSession = p[Keys.showGuidance] ?: false, + lastPresetId = p[Keys.lastPresetId], + ) + } + + suspend fun acknowledgeSafety(version: Int) { + context.dataStore.edit { + it[Keys.safetyAcknowledged] = true + it[Keys.safetyVersion] = version + } + } + + suspend fun updateCountdown(value: CountdownPreference) = context.dataStore.edit { it[Keys.countdownPref] = value.name } + suspend fun updateDefaultMode(value: SessionMode) = context.dataStore.edit { it[Keys.defaultMode] = value.name } + suspend fun updateGuidance(value: Boolean) = context.dataStore.edit { it[Keys.showGuidance] = value } + suspend fun updateLastPreset(id: String) = context.dataStore.edit { it[Keys.lastPresetId] = id } +} diff --git a/app/src/main/java/com/mindmachine/mvp/domain/Models.kt b/app/src/main/java/com/mindmachine/mvp/domain/Models.kt new file mode 100644 index 0000000..31abed1 --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/domain/Models.kt @@ -0,0 +1,71 @@ +package com.mindmachine.mvp.domain + +enum class VisualPattern { FLASH, PULSE } +enum class SessionMode { AUDIO_VISUAL, AUDIO_ONLY, VISUAL_ONLY } +enum class RuntimeState { IDLE, COUNTDOWN, RUNNING, PAUSED, INTERRUPTED, COMPLETED, STOPPED, ERROR } +enum class CountdownPreference(val seconds: Int) { OFF(0), FIVE(5), TEN(10) } + +data class SessionPreset( + val id: String, + val name: String, + val description: String, + val defaultDurationSec: Int, + val visualPatternType: VisualPattern, + val blinkFrequencyHz: Float, + val intensityPercent: Int, + val carrierFrequencyHz: Float, + val binauralDifferenceHz: Float, + val cautionNote: String, + val sortOrder: Int, +) + +data class SessionConfig( + val presetId: String, + val durationSec: Int, + val mode: SessionMode, + val visualPatternType: VisualPattern, + val blinkFrequencyHz: Float, + val intensityPercent: Int, + val carrierFrequencyHz: Float, + val binauralDifferenceHz: Float, +) + +data class AppSettings( + val safetyAcknowledged: Boolean = false, + val safetyAcknowledgedVersion: Int = 0, + val countdownPreference: CountdownPreference = CountdownPreference.FIVE, + val defaultModePreference: SessionMode = SessionMode.AUDIO_VISUAL, + val showHolderGuidanceBeforeSession: Boolean = false, + val lastPresetId: String? = null, +) + +object Presets { + val builtIn = listOf( + SessionPreset( + "relax", "Relax", "Gentle pulse, slower beat", 10 * 60, + VisualPattern.PULSE, 6f, 60, 200f, 6f, + "Stop if discomfort occurs.", 1 + ), + SessionPreset( + "focus", "Focus", "Steady flash, conservative alert beat", 15 * 60, + VisualPattern.FLASH, 10f, 65, 220f, 10f, + "Use in a safe seated place only.", 2 + ), + SessionPreset( + "sleep", "Sleep Prep", "Slow pulse, low intensity", 20 * 60, + VisualPattern.PULSE, 3f, 45, 180f, 3f, + "Do not use while doing other activities.", 3 + ) + ) +} + +fun SessionPreset.toConfig(defaultMode: SessionMode = SessionMode.AUDIO_VISUAL) = SessionConfig( + presetId = id, + durationSec = defaultDurationSec, + mode = defaultMode, + visualPatternType = visualPatternType, + blinkFrequencyHz = blinkFrequencyHz, + intensityPercent = intensityPercent, + carrierFrequencyHz = carrierFrequencyHz, + binauralDifferenceHz = binauralDifferenceHz, +) diff --git a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt new file mode 100644 index 0000000..ce5c6de --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt @@ -0,0 +1,181 @@ +package com.mindmachine.mvp.session + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.mindmachine.mvp.audio.BinauralAudioEngine +import com.mindmachine.mvp.audio.HeadsetMonitor +import com.mindmachine.mvp.data.SettingsRepository +import com.mindmachine.mvp.domain.AppSettings +import com.mindmachine.mvp.domain.CountdownPreference +import com.mindmachine.mvp.domain.Presets +import com.mindmachine.mvp.domain.RuntimeState +import com.mindmachine.mvp.domain.SessionConfig +import com.mindmachine.mvp.domain.SessionMode +import com.mindmachine.mvp.domain.SessionPreset +import com.mindmachine.mvp.domain.toConfig +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +const val SAFETY_VERSION = 1 + +data class UiState( + val settings: AppSettings = AppSettings(), + val presets: List = Presets.builtIn, + val selectedPreset: SessionPreset = Presets.builtIn.first(), + val config: SessionConfig = Presets.builtIn.first().toConfig(), + val runtimeState: RuntimeState = RuntimeState.IDLE, + val error: String? = null, + val remainingSec: Int = 0, + val countdownSec: Int = 0, + val endedEarly: Boolean = false, + val interruptionReason: String? = null, +) + +class MainViewModel( + private val settingsRepository: SettingsRepository, + private val headsetMonitor: HeadsetMonitor, + private val audioEngine: BinauralAudioEngine, +) : ViewModel() { + private val _ui = MutableStateFlow(UiState()) + val ui: StateFlow = _ui.asStateFlow() + + private var runJob: Job? = null + + init { + viewModelScope.launch { + settingsRepository.settings.collect { s -> + _ui.update { current -> + val preset = current.presets.find { it.id == (s.lastPresetId ?: current.selectedPreset.id) } ?: current.selectedPreset + current.copy(settings = s, selectedPreset = preset, config = current.config.copy(mode = s.defaultModePreference)) + } + } + } + } + + fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) } + fun choosePreset(id: String) = viewModelScope.launch { + val preset = _ui.value.presets.first { it.id == id } + settingsRepository.updateLastPreset(id) + _ui.update { it.copy(selectedPreset = preset, config = preset.toConfig(it.settings.defaultModePreference), error = null) } + } + + fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) } + fun setDurationMin(min: Int) = _ui.update { it.copy(config = it.config.copy(durationSec = (min.coerceIn(1, 30) * 60)), error = null) } + fun setBlinkFrequency(value: Float) = _ui.update { it.copy(config = it.config.copy(blinkFrequencyHz = value.coerceIn(1f, 20f)), error = null) } + fun setCarrier(value: Float) = _ui.update { it.copy(config = it.config.copy(carrierFrequencyHz = value.coerceIn(80f, 400f)), error = null) } + fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) } + + fun updateCountdown(pref: CountdownPreference) = viewModelScope.launch { settingsRepository.updateCountdown(pref) } + fun updateDefaultMode(mode: SessionMode) = viewModelScope.launch { settingsRepository.updateDefaultMode(mode) } + fun updateGuidance(value: Boolean) = viewModelScope.launch { settingsRepository.updateGuidance(value) } + + fun startSession() { + val state = _ui.value + if (!(state.settings.safetyAcknowledged && state.settings.safetyAcknowledgedVersion >= SAFETY_VERSION)) { + _ui.update { it.copy(error = "You must acknowledge safety before starting sessions.") } + return + } + val headset = headsetMonitor.isStereoHeadsetAvailable() + val validation = SessionValidator.validate(state.config, headset) + if (validation != null) { + _ui.update { it.copy(error = validation) } + return + } + runJob?.cancel() + runJob = viewModelScope.launch { + val count = state.settings.countdownPreference.seconds + if (count > 0) { + for (i in count downTo 1) { + _ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i, remainingSec = state.config.durationSec, endedEarly = false, interruptionReason = null) } + delay(1000) + } + } + _ui.update { it.copy(runtimeState = RuntimeState.RUNNING, remainingSec = state.config.durationSec, countdownSec = 0, error = null) } + if (state.config.mode != SessionMode.VISUAL_ONLY) { + audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz) + } + var remaining = state.config.durationSec + while (remaining > 0) { + delay(1000) + if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { + audioEngine.stop() + _ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") } + return@launch + } + remaining -= 1 + _ui.update { it.copy(remainingSec = remaining) } + } + audioEngine.stop() + _ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) } + } + } + + fun pause(reason: String? = null) { + if (_ui.value.runtimeState != RuntimeState.RUNNING) return + runJob?.cancel() + audioEngine.stop() + _ui.update { it.copy(runtimeState = if (reason == null) RuntimeState.PAUSED else RuntimeState.INTERRUPTED, interruptionReason = reason) } + } + + fun resume() { + val state = _ui.value + if (state.runtimeState != RuntimeState.PAUSED && state.runtimeState != RuntimeState.INTERRUPTED) return + if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { + _ui.update { it.copy(error = "Headphones are required to resume audio mode.") } + return + } + runJob = viewModelScope.launch { + for (i in 3 downTo 1) { + _ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i) } + delay(1000) + } + _ui.update { it.copy(runtimeState = RuntimeState.RUNNING, countdownSec = 0) } + if (state.config.mode != SessionMode.VISUAL_ONLY) { + audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz) + } + var remaining = state.remainingSec + while (remaining > 0) { + delay(1000) + if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) { + audioEngine.stop() + _ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") } + return@launch + } + remaining -= 1 + _ui.update { it.copy(remainingSec = remaining) } + } + audioEngine.stop() + _ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) } + } + } + + fun stop() { + runJob?.cancel() + audioEngine.stop() + _ui.update { it.copy(runtimeState = RuntimeState.STOPPED, endedEarly = true) } + } + + fun switchToVisualOnlyAndResume() { + _ui.update { it.copy(config = it.config.copy(mode = SessionMode.VISUAL_ONLY), error = null) } + resume() + } + + override fun onCleared() { + audioEngine.stop() + super.onCleared() + } + + class Factory( + private val settingsRepository: SettingsRepository, + private val headsetMonitor: HeadsetMonitor, + private val audioEngine: BinauralAudioEngine, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T = MainViewModel(settingsRepository, headsetMonitor, audioEngine) as T + } +} diff --git a/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt b/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt new file mode 100644 index 0000000..a4bde1c --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt @@ -0,0 +1,17 @@ +package com.mindmachine.mvp.session + +import com.mindmachine.mvp.domain.SessionConfig +import com.mindmachine.mvp.domain.SessionMode + +object SessionValidator { + fun validate(config: SessionConfig, headsetAvailable: Boolean): String? { + if (config.durationSec !in 60..(30 * 60)) return "Duration must be 1 to 30 minutes." + if (config.blinkFrequencyHz !in 1f..20f) return "Blink frequency must be 1.0 to 20.0 Hz." + if (config.carrierFrequencyHz !in 80f..400f) return "Carrier frequency must be 80 to 400 Hz." + if (config.binauralDifferenceHz !in 0.5f..20f) return "Binaural difference must be 0.5 to 20 Hz." + if (config.mode != SessionMode.VISUAL_ONLY && !headsetAvailable) { + return "Stereo headphones are required for binaural audio. Connect headphones or switch to Visual-only." + } + return null + } +} diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d575ea4 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +