Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch all kv values on initialization #29

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
# and commit this file to your remote git repository to share the goodness with others.

# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart

tasks:
- init: |
npm install
npm run build
command: npm run dev
env:
DISABLE_IO: true
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,20 @@ When playing music with others and using this program:
- Utilize several entrypoints of user interactivity to make the most out of every situation.
- Create your own light shows to interact with during performance
- Extend the system by adding your own modules

### Development

In order to develop on the project, first clone the project and run the following in the root of the repository:

```sh
# Install dependencies
npm install

# Create builds for all platforms
npm run build

# Watch for file changes, and continuously build and run all platforms
npm run dev
```

Then navigate to http://localhost:1337 to access the UI. Note that this will spin up a Springboard app with all Jam Tools modules registered, including prototype features which are not in a finished state.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion docs/content/getting-started/configuration/index.md

This file was deleted.

12 changes: 5 additions & 7 deletions docs/content/index.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
Developer Documentation
# Springboard

- [ModuleAPI](typedoc_docs/module_api/classes/ModuleAPI)
- [StatesAPI](typedoc_docs/module_api/classes/StatesAPI)
Springboard is a highly flexible cross-platform application framework based on React and JSON-RPC. The framework embraces isomorphism as much as possible, with support for dependency injection for each deployment context, which allows the application to easily pivot how things work in the different environments.

{%
include-markdown "typedoc_docs/README.md"
heading-offset=1
%}
👈
To learn more, check out the documentation
👈
87 changes: 87 additions & 0 deletions docs/content/jamtools/macro-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
The Macro module exposes functionality to allow the user to configure their own MIDI devices to use a given feature of your application. This means there are no hardcoded MIDI device names in your code, so your application can be used by any user. The Macro module uses the [MIDI IO module](./midi-io-module.md) to interact with MIDI devices.

Macros can be used as primitives to compose simple or complex features. They provide a way to interact with MIDI devices, while having the feature-level code be agnostic to the MIDI device being used.

The following macro types are exposed by the Macro module:

- Inputs:
- `musical_keyboard_input` - Receive MIDI "note" events for a MIDI keyboard chosen by the user. This is useful for receiving tonal input from a MIDI keyboard.
- `midi_control_change_input` - Receive MIDI events for a specific [controller change](https://cmtext.indiana.edu/MIDI/chapter3_controller_change.php) input chosen by the user, like a slider or knob. This is useful for receiving a spectrum of data from one control.
- `midi_button_input` - Receive MIDI events for a specific MIDI controller button or note chosen by the user. This is useful for running a specific action when the given button is pressed.
- Outputs
- `musical_keyboard_output` - Send MIDI events to a MIDI keyboard chosen by the user. This is useful for sending tonal information to a DAW or hardware synth.
- `midi_control_change_output` - Send MIDI events for a controller change output chosen by the user. This is useful for changing things on a DAW effect like the dry/wet or intensity setting.
- `midi_button_output` - Send MIDI events for a specific button/note chosen by the user. This is useful to send discrete messages to a toggleable setting in a DAW. (note this is one is actually not implemented as of the time of writing this)

## Examples

Here is a basic "MIDI thru" example, where we simply proxy MIDI events between two MIDI keyboards chosen by the user:

```jsx
import springboard from 'springboard';

import '@jamtools/core/modules/macro_module/macro_module';

springboard.registerModule('MidiThru', {}, async (moduleAPI) => {
// Get the Macro module
const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro');

// Create macros
const inputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Input', 'musical_keyboard_input', {});
const outputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Output', 'musical_keyboard_output', {});

// Subscribe to events from the MIDI keyboard(s) chosen by the user for the "My MIDI Input" macro defined above
inputmacro.subject.subscribe(evt => {
outputMacro.send(evt.event); // Send the same MIDI event to the configured MIDI output device
});

moduleAPI.registerRoute('', {}, () => {
return (
<div>
<inputMacro.edit/>
<outputMacro.edit/>
</div>
);
});
});
```

We would normally do something more useful here like an arpeggio or chord extension, but for this example we are just doing a "MIDI Thru" by proxying the events from one MIDI device to another.

Instead of explicitly calling `input.subject.subscribe`, we can also use the shorthand `onTrigger` option to listen for events:

```jsx
const outputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Output', 'musical_keyboard_output', {});

const inputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Input', 'musical_keyboard_input', {
onTrigger: (evt) => {
outputMacro.send(evt.event);
},
});
```

---

Here is an example where we extend the note pressed to be a major triad:

```jsx
const outputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Output', 'musical_keyboard_output', {});

const inputMacro = await macroModule.createMacro(moduleAPI, 'My MIDI Input', 'musical_keyboard_input', {
onTrigger: (evt) => {
// Send 3 MIDI events. One for each note of the triad.

outputMacro.send(evt.event); // Send the original note pressed

outputMacro.send({
...evt.event,
number: evt.event.number + 4, // Major third interval
});

outputMacro.send({
...evt.event,
number: evt.event.number + 7, // Perfect fifth interval
});
},
});
```
36 changes: 36 additions & 0 deletions docs/content/jamtools/midi-io-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
The MIDI IO module exposes functionality for listening to MIDI input devices and communicating to MIDI output devices. Using this module, the application needs no boilerplate code for MIDI device initialization or polling.

Example of using the MIDI IO module's `midiInputSubject` to subscribe to all incoming midi events for any plugged in MIDI devices.

```jsx
import React from 'react';

import springboard from 'springboard';
import {MidiEventFull} from '@jamtools/core/modules/macro_module/macro_module_types';

import '@jamtools/core/modules/io/io_module';

springboard.registerModule('Main', {}, async (moduleAPI) => {
const mostRecentMidiEvent = await moduleAPI.statesAPI.createSharedState<MidiEventFull | null>('mostRecentMidiEvent', null);
const ioModule = moduleAPI.deps.module.moduleRegistry.getModule('io');

// Subscribe to all midi input events
ioModule.midiInputSubject.subscribe(event => {
mostRecentMidiEvent.setState(event);
});

moduleAPI.registerRoute('', {}, () => {
const midiEvent = mostRecentMidiEvent.useState();

return (
<div>
{midiEvent && (
<pre>
{JSON.stringify(midiEvent, null, 2)}
</pre>
)}
</div>
);
});
});
```
12 changes: 12 additions & 0 deletions docs/content/jamtools/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Springboard is heavily influenced by the requirements of Jam Tools, which entails:
- Easily interact with MIDI instruments
- Configure the MIDI instruments from a device other than the device with the MIDI instruments connected
- Be able to run the application as a fullstack app or as a standalone browser application
- Have the feature-level and UI code "right next" to the related MIDI functionality (ideally it's possible to have them in the same file)

The modules provided by the [`@jamtools/core`](https://npmjs.com/package/@jamtools/core) package contain the relevant functionality develop your MIDI application. In particular:

- [MIDI IO module](./midi-io-module.md) - Exposes functionality to listen to MIDI input devices and communicate to MIDI output devices. This module makes it so the application needs no MIDI device initialization or polling.
- [Macro module](./macro-module.md) - Exposes functionality to allow the user to configure their own MIDI devices to use a given feature of your application. The Macro module uses the MIDI IO module to interact with MIDI devices.

Coming soon is the "Snacks" module, which will allow MIDI-related features to scale better. At the moment, a module has to implement its whole feature once, whereas with the Snack system the user will be able to compose their own features using smaller pieces implemented by modules. The current system is analogous to having an effect rack hardcoded, where as the Snack system allows the user to construct their own effect racks, and make several effect racks arbitrarily.
4 changes: 4 additions & 0 deletions docs/content/springboard/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Springboard has first-class support for dependency injection.

TODO: describe the process of comment-based platform-specific compile-time conditionals
and also implement the proper dependency injection stuff
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Deployment contexts

A Springboard application can be deployed and run in multiple ways. The framework abstracts this away, so that feature-level code can be agnostic to the deployment context.

An application deployment can be single-player-only, multi-player-only, or a hybrid where the user swaps between contexts.

### Multi-player

The framework helps facilitate realtime communication between clients behind-the-scenes using [WebSockets](https://en.wikipedia.org/wiki/WebSocket) and [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC#Version_2.0). By defining shared actions and states in your application, user actions are sent to the correct device to process the action, and any shared state that changes as a consequence from the action is automatically synchronized across devices in realtime.

![Multi-player deployment](../../assets/deployment-diagram-multiplayer.png)

### Single-player

In the single-player (or local-only, offline) mode, all code runs locally, and any data storage happens locally.

When the user chooses to go local-only, the browser is refreshed to process the change. This may not be a requirement in the future.

![Single-player deployment](../../assets/deployment-diagram-singleplayer.png)
4 changes: 4 additions & 0 deletions docs/content/springboard/deployment-contexts/desktop-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Springboard's desktop app deployment uses [Tauri](https://v2.tauri.app) as the application shell, and packages the app with:

- A Hono server sidecar to host the application
- A separate Maestro node script to make it so IO operations (like MIDI events) can happen with minimal latency
3 changes: 3 additions & 0 deletions docs/content/springboard/deployment-contexts/mobile-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Springboard's mobile app deployment uses [React Native](https://reactnative.dev) as the application shell, and runs the frontend Springboard app in a webview.

In the offline mode, the webview communicates to the React Native process for file and data management. In the online mode, the webview loads the UI from the server so the frontend code is guaranteed to be in line with the server code version, and communicates to a remote server directly.
41 changes: 41 additions & 0 deletions docs/content/springboard/module-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
A Springboard application is typically comprised of several modules. A given module uses the [ModuleAPI](../typedoc_docs/module_api/classes/ModuleAPI.md) to register components, actions, and states with the application.

The ModuleAPI allows modules to:

- Register routes and render React components
- Create remote actions and shared states
- Receive dependencies through dependency injection
- Expose functions and utilties for other modules to use. This typcially includes actions, states, or reusable React components.
- Interact with other modules by consuming their exposed functions/properties

Modules can play a few different types of roles:

- Utility - A utility module exposes things for other modules to use, and likely does not register any routes. An example of this kind of module is the [MIDI IO Module](https://github.com/jamtools/jamtools/blob/main/packages/jamtools/core/modules/io/io_module.tsx), which currently just exposes a way for other modules to interact with MIDI devices and qwerty keyboards.
- Feature - A feature-level module implements certain features for your application. These modules typically register routes and interact with other modules to faciliate cohesion and "getting shit done".
- Initializer - An initializer module does some initial setup for an application. This kind of module is typically not needed for small applications, though these modules become useful when deployment use cases are complicated. You may want to have some specific initialization on a mobile app versus desktop app, for instance.

---

## Writing a feature module

When registering a module, the module provides a callback to run on app initialization. The callback the module provides essentially _is_ the module. The callback receives an instance of the `ModuleAPI`. The namespacing of states and actions for this particular module are automatically managed by the framework. Some useful methods/properties from the `ModuleAPI` are documented in the [ModuleAPI docs](../typedoc_docs/module_api/classes/ModuleAPI.md).

(explain some methods briefly here, a link to individual api reference methods. probably good to have youtube videos walking through code too, though you can do the same in text form probably)

## Writing a utility module

By default it is assumed a module is a feature or initializer module, meaning it is assumed that the module does not expose anything for other modules to use. In order for other modules to be aware of any exposed functions/properties, we need to perform [interface merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) to register the module's return value with the Springboard framework's module system.

Here's an [example](
https://github.com/jamtools/jamtools/blob/cea35258c6d7e495a68148c4a9e61ac06dcca609/packages/jamtools/core/modules/macro_module/macro_module.tsx#L31-L35) of the Macro module declaring its return type:


```ts
declare module 'springboard/module_registry/module_registry' {
interface AllModules {
macro: MacroModule;
}
}
```

This makes it so any other module that interacts with this module knows what is available from that module, and typescript can provide the relevant autocompletions and type checking for consuming this module. When the module is registered with the framework, the type checker will ensure that the return value matches what is defined in the `AllModules` interface.
10 changes: 10 additions & 0 deletions docs/content/springboard/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Springboard was built out of the necessity of creating MIDI applications deployed in a fullstack web application context, and simultaneously allowing the application to run standalone in the browser for maximum portability. This dual requirement has shaped how the Springboard framework works, by maximizing the amount of code reuse across the different platforms.

Springboard uses the concept of [modules](./module-development.md) to encapsulate responsibilities of different pieces of code. A new Springboard project contains no modules by default. There are some predefined modules that you can import into your code, namely the modules defined by the [`@jamtools/core`](https://github.com/jamtools/jamtools/tree/main/packages/jamtools/core/modules) package at the time of writing.

---

More information:

- [Developing a module](./module-development.md)
- [Deployment contexts](./deployment-contexts/deployment-contexts.md) - Single-player and multi-player
3 changes: 3 additions & 0 deletions docs/content/springboard/server-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You can hook into the framework's Hono server using the server module system.

Most business logic should go in the application/Maestro portion, though if http is ever directly required, you'll need an http server, which is where this comes in handy.
35 changes: 30 additions & 5 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
site_name: Jam Tools
site_name: Springboard
docs_dir: content
theme:
name: material
highlightjs: true
palette:
scheme: slate
# primary: indigo
features:
- navigation.expand

- navigation.footer
markdown_extensions:
- pymdownx.highlight
# anchor_linenums: true
# line_spans: __span
# pygments_lang_class: true
# - pymdownx.inlinehilite
# - pymdownx.snippets
- pymdownx.superfences

extra_css:
- stylesheets/custom.css

nav:
- Home: ./index.md
- Developer Documentation:
- Module API: ./typedoc_docs/module_api/classes/ModuleAPI.md
- States API: ./typedoc_docs/module_api/classes/StatesAPI.md
- Springboard:
- Overview: ./springboard/overview.md
- Developing a module: ./springboard/module-development.md
- Deployment Contexts:
- Single/Multi-player: ./springboard/deployment-contexts/deployment-contexts.md
- Desktop App: ./springboard/deployment-contexts/desktop-app.md
- Mobile App: ./springboard/deployment-contexts/mobile-app.md
- API reference:
- Module API: ./typedoc_docs/module_api/classes/ModuleAPI.md
- States API: ./typedoc_docs/module_api/classes/StatesAPI.md

- Jam Tools:
- Overview: ./jamtools/overview.md
- Modules:
- Macro module: ./jamtools/macro-module.md
- MIDI IO module: ./jamtools/midi-io-module.md

plugins:
- search
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"typedoc": "./scripts/patch_typedoc-plugin-markdown.sh && npx typedoc --out content/typedoc_docs",
"typedoc:watch": "./scripts/patch_typedoc-plugin-markdown.sh && nodemon --watch typedoc.json --exec \"npm run typedoc -- -watch\"",
"mkdocs": "mkdocs build",
"mkdocs:watch": "mkdocs serve --dev-addr=0.0.0.0:8000 --watch typedoc_docs",
"mkdocs:watch": "mkdocs serve --dev-addr=0.0.0.0:8000 --watch content/typedoc_docs",
"serve": "concurrently \"npm run typedoc:watch\" \"npm run mkdocs:watch\"",
"build": "npm run typedoc && npm run mkdocs",
"install-deps": "npm i && pip install mkdocs mkdocs-material mkdocs-include-markdown-plugin"
Expand Down
1 change: 1 addition & 0 deletions docs/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"../packages/springboard/core/engine/module_api.ts",
"../packages/springboard/core/engine/register.ts",
],
"disableSources": true,
"entryPointStrategy": "expand",
"tsconfig": "../packages/springboard/core/tsconfig.json",
// "entryPoints": ["src/index.ts"], // Adjust to your entry point files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,6 @@ jamtools.registerModule('chord_families', {}, async (moduleAPI) => {
});
};

console.log('running snack: root mode');

moduleAPI.registerRoute('', {}, () => {
const state = rootModeState.useState();

Expand Down
2 changes: 0 additions & 2 deletions packages/jamtools/features/snacks/midi_thru_cc_snack.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {jamtools} from 'springboard/engine/register';

jamtools.registerModule('midi_thru_cc', {}, async (moduleAPI) => {
console.log('running snack: midi thru cc');

const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro');

const [input, output] = await Promise.all([
Expand Down
2 changes: 0 additions & 2 deletions packages/jamtools/features/snacks/midi_thru_snack.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {jamtools} from 'springboard/engine/register';

jamtools.registerModule('midi_thru', {}, async (moduleAPI) => {
console.log('running snack: midi thru');

const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro');

const [input, output] = await Promise.all([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ jamtools.registerModule('root_mode_module', {}, async (moduleAPI) => {
});
};

console.log('running snack: root mode');

moduleAPI.registerRoute('', {}, () => {
const state = rootModeState.useState();

Expand Down
Loading
Loading