feat: improve ai instrument editors
This commit is contained in:
@@ -37,7 +37,7 @@ Ces instruments tournent dans le browser via AudioWorklet — pas de serveur né
|
||||
| **Glitch** | Générateur de textures glitch | Buffer repeat, stutter, bit crush, downsample, reverse, pitch shift |
|
||||
| **Circus** | Orgue de barbarie / calliope | 4 registres (Flute 8', Flute 4', Principal, Mixture), tremolo, wobble, air |
|
||||
| **Honk** | Klaxon / sirène / corne | 3 modes (Klaxon, Siren, Horn), sweep paramétrique, harmoniques |
|
||||
| **Magenta** | AI MIDI generator | Magenta.js browser-side (MelodyRNN, DrumsRNN, ImprovRNN), temperature, steps |
|
||||
| **Magenta** | Local algorithmic MIDI generator | Browser-side heuristic generation modes (Melody, Drums, Improv), temperature, steps |
|
||||
|
||||
## Instruments AI-backed
|
||||
|
||||
@@ -45,7 +45,7 @@ Ces instruments génèrent de l'audio via le serveur AI Bridge (`:8301`).
|
||||
|
||||
| Instrument | Backend | Description |
|
||||
|---|---|---|
|
||||
| **AceStep** | ACE-Step | Génération musicale IA par prompt (style + bpm + durée) |
|
||||
| **AceStep** | ACE-Step / AI Bridge | Génération audio par prompt, import automatique en sample, local-first puis fallback bridge |
|
||||
| **KokoroTTS** | Kokoro | Synthèse vocale rapide — 12 voix (af_heart, am_adam, bf_emma...) |
|
||||
| **Piper** | Piper/Chatterbox | Synthèse vocale TTS avec personas |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# IA AceStep
|
||||
|
||||
An AI music generator that creates audio from text prompts.
|
||||
An AI audio generator that turns text prompts into playable samples.
|
||||
|
||||
**Track type: Audio** — generated samples appear as regions on the timeline.
|
||||
|
||||
@@ -8,7 +8,7 @@ An AI music generator that creates audio from text prompts.
|
||||
|
||||
## 0. Overview
|
||||
|
||||
_IA AceStep_ generates music from text descriptions using the ACE-Step or MusicGen AI backend. The generated WAV is loaded as a playable sample. Use the AI panel to describe the music you want.
|
||||
_IA AceStep_ generates music from text descriptions and imports the rendered WAV into the device as a playable sample. The editor prefers a local ACE-Step backend when available, then falls back to the shared AI Bridge workflow.
|
||||
|
||||
Example uses:
|
||||
|
||||
@@ -54,13 +54,19 @@ Select from 14 presets: ambient, electronic, experimental, classical, jazz, indu
|
||||
|
||||
### 2.3 Composer
|
||||
|
||||
Click to start generation. A progress bar shows the status. Generation takes 20-60 seconds depending on duration.
|
||||
Click to start generation. The UI reports provider detection, rendering progress and sample import state.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend
|
||||
|
||||
Uses ACE-Step (port 9200) when available, falls back to MusicGen (port 9000) via the AI Bridge (port 8301).
|
||||
Preferred path:
|
||||
- Local ACE-Step backend (`127.0.0.1:9200`)
|
||||
|
||||
Fallback path:
|
||||
- AI Bridge (`:8301`), which itself can route to ACE-Step or other backends
|
||||
|
||||
The resulting audio is imported into the project and attached to the device sample slot automatically.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# IA Magenta
|
||||
|
||||
An algorithmic MIDI sequence generator with built-in polyphonic synthesizer.
|
||||
A local algorithmic MIDI sequence generator with built-in polyphonic synthesizer.
|
||||
|
||||
---
|
||||
|
||||
## 0. Overview
|
||||
|
||||
_IA Magenta_ generates MIDI note sequences using algorithmic composition and plays them through an integrated synthesizer. Three models offer different musical styles. No external connection required — all generation runs locally.
|
||||
_IA Magenta_ generates MIDI note sequences using local algorithmic composition and plays them through an integrated synthesizer. Three generation modes offer different musical behaviors. No external connection is required.
|
||||
|
||||
Example uses:
|
||||
|
||||
@@ -35,12 +35,12 @@ Synth amplitude envelope.
|
||||
|
||||
Low-pass filter frequency.
|
||||
|
||||
### 1.5 Model
|
||||
### 1.5 Mode
|
||||
|
||||
Generation algorithm:
|
||||
- **Melody** — minor scale patterns
|
||||
- **Drums** — kick, snare, hihat, ride, crash, tom
|
||||
- **Improv** — pentatonic with chromatic variations
|
||||
Generation mode:
|
||||
- **Melody** — melodic patterns
|
||||
- **Drums** — simple drum hits and accents
|
||||
- **Improv** — freer melodic motion with more variation
|
||||
|
||||
### 1.6 Temperature
|
||||
|
||||
@@ -62,13 +62,17 @@ Stereo panning spread based on note pitch.
|
||||
|
||||
## 2. Generation
|
||||
|
||||
1. Set Model, Temperature, Steps and Density
|
||||
1. Set Mode, Temperature, Steps and Density
|
||||
2. Click **Generate** to create a sequence
|
||||
3. Click **Play** to listen
|
||||
4. Click **Stop** to halt playback
|
||||
|
||||
MIDI notes also trigger the built-in synth directly.
|
||||
|
||||
## 3. Execution Model
|
||||
|
||||
Generation is local and heuristic. This device does not depend on an external AI bridge or a remote model backend in its current form.
|
||||
|
||||
---
|
||||
|
||||
_IA instrument — openDIAW.be / KXKM_
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
export type AceStepGenerationState =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "generating"
|
||||
| "importing"
|
||||
| "ready"
|
||||
| "error"
|
||||
|
||||
export type AceStepGenerationRequest = {
|
||||
prompt: string
|
||||
style: string
|
||||
durationSec: number
|
||||
bpm: number
|
||||
}
|
||||
|
||||
export type AceStepGenerationStatus = {
|
||||
state: AceStepGenerationState
|
||||
message: string
|
||||
detail: string
|
||||
progress: number
|
||||
provider?: "ace-step-local" | "ai-bridge"
|
||||
}
|
||||
|
||||
export type AceStepGenerationResult = {
|
||||
audio: Blob
|
||||
durationSec: number
|
||||
provider: "ace-step-local" | "ai-bridge"
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT = "ambient electronic"
|
||||
const AI_BRIDGE = `${location.protocol === "https:" ? "https:" : "http:"}//${location.hostname}:8301`
|
||||
const ACE_STEP_URL = "http://127.0.0.1:9200"
|
||||
|
||||
export class AceStepGenerationService {
|
||||
async generateMusic(
|
||||
request: AceStepGenerationRequest,
|
||||
report: (status: AceStepGenerationStatus) => void
|
||||
): Promise<AceStepGenerationResult> {
|
||||
const prompt = request.prompt.trim() || DEFAULT_PROMPT
|
||||
const durationSec = Math.round(request.durationSec)
|
||||
|
||||
report({
|
||||
state: "checking",
|
||||
message: "Verification du moteur local",
|
||||
detail: "Recherche d'un backend ACE-Step local...",
|
||||
progress: 10
|
||||
})
|
||||
|
||||
const local = await this.#generateFromLocal({
|
||||
prompt,
|
||||
style: request.style,
|
||||
durationSec,
|
||||
bpm: request.bpm
|
||||
}, report)
|
||||
if (local !== null) {
|
||||
return local
|
||||
}
|
||||
|
||||
report({
|
||||
state: "generating",
|
||||
message: "Generation via AI Bridge",
|
||||
detail: "Fallback vers le bridge audio partage.",
|
||||
progress: 25,
|
||||
provider: "ai-bridge"
|
||||
})
|
||||
|
||||
const bridgeResponse = await fetch(`${AI_BRIDGE}/generate/music`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
prompt: `${prompt}, ${request.style}`,
|
||||
duration: durationSec,
|
||||
style: request.style,
|
||||
bpm: request.bpm
|
||||
}),
|
||||
signal: AbortSignal.timeout(300000)
|
||||
})
|
||||
if (!bridgeResponse.ok) {
|
||||
throw new Error(`AI Bridge HTTP ${bridgeResponse.status}`)
|
||||
}
|
||||
|
||||
report({
|
||||
state: "generating",
|
||||
message: "Reception de l'audio",
|
||||
detail: "Le rendu audio est pret a etre importe.",
|
||||
progress: 90,
|
||||
provider: "ai-bridge"
|
||||
})
|
||||
|
||||
return {
|
||||
audio: await bridgeResponse.blob(),
|
||||
durationSec,
|
||||
provider: "ai-bridge"
|
||||
}
|
||||
}
|
||||
|
||||
async #generateFromLocal(
|
||||
request: AceStepGenerationRequest,
|
||||
report: (status: AceStepGenerationStatus) => void
|
||||
): Promise<AceStepGenerationResult | null> {
|
||||
const health = await fetch(`${ACE_STEP_URL}/health`, {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
}).then(async response => response.ok ? response.json() : null).catch(() => null)
|
||||
if (health?.ok !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
report({
|
||||
state: "generating",
|
||||
message: "Generation locale",
|
||||
detail: `ACE-Step local • ${request.bpm} BPM • ${request.durationSec}s`,
|
||||
progress: 25,
|
||||
provider: "ace-step-local"
|
||||
})
|
||||
|
||||
const localResponse = await fetch(`${ACE_STEP_URL}/generate`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
prompt: `${request.prompt}, ${request.style}`,
|
||||
duration: request.durationSec,
|
||||
steps: 8,
|
||||
bpm: request.bpm
|
||||
}),
|
||||
signal: AbortSignal.timeout(300000)
|
||||
})
|
||||
if (!localResponse.ok) {
|
||||
throw new Error(`ACE-Step local HTTP ${localResponse.status}`)
|
||||
}
|
||||
|
||||
report({
|
||||
state: "generating",
|
||||
message: "Reception de l'audio",
|
||||
detail: "Le rendu local est pret a etre importe.",
|
||||
progress: 90,
|
||||
provider: "ace-step-local"
|
||||
})
|
||||
|
||||
return {
|
||||
audio: await localResponse.blob(),
|
||||
durationSec: request.durationSec,
|
||||
provider: "ace-step-local"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import {RouteLocation} from "@opendaw/lib-jsx"
|
||||
import {PPQN} from "@opendaw/lib-dsp"
|
||||
import {AnimationFrame, Browser, ConsoleCommands, Dragging, Files} from "@opendaw/lib-dom"
|
||||
import {Promises} from "@opendaw/lib-runtime"
|
||||
import {ExportStemsConfiguration, InstrumentFactories, PresetDecoder} from "@opendaw/studio-adapters"
|
||||
import {ExportStemsConfiguration, InstrumentFactories, PresetDecoder, Sample} from "@opendaw/studio-adapters"
|
||||
import {Address} from "@opendaw/lib-box"
|
||||
import {
|
||||
AudioContentFactory,
|
||||
@@ -71,6 +71,7 @@ import {SoftwareMIDIPanel} from "@/ui/software-midi/SoftwareMIDIPanel"
|
||||
import {Mixdowns} from "@/service/Mixdowns"
|
||||
import {ShadertoyState} from "@/ui/shadertoy/ShadertoyState"
|
||||
import {CodeEditorState} from "@/ui/werkstatt-editor/CodeEditorState"
|
||||
import {AceStepGenerationService} from "@/service/AceStepGenerationService"
|
||||
|
||||
/**
|
||||
* I am just piling stuff after stuff in here to boot the environment.
|
||||
@@ -106,6 +107,7 @@ export class StudioService implements ProjectEnv {
|
||||
readonly panelLayout = new PanelContents(createPanelFactory(this))
|
||||
readonly spotlightDataSupplier = new SpotlightDataSupplier()
|
||||
readonly samplePlayback: SamplePlayback
|
||||
readonly aceStepGeneration = new AceStepGenerationService()
|
||||
readonly recovery = new Recovery(() => this.#projectProfileService.getValue(), this)
|
||||
readonly engine = new EngineFacade()
|
||||
|
||||
@@ -130,12 +132,12 @@ export class StudioService implements ProjectEnv {
|
||||
readonly cloudAuthManager: CloudAuthManager,
|
||||
readonly buildInfo: BuildInfo) {
|
||||
this.#sampleService = new SampleService(audioContext)
|
||||
this.#sampleService.subscribe(([sample, _]) => this.#signals.notify({
|
||||
this.#sampleService.subscribe(([sample]) => this.#signals.notify({
|
||||
type: "import-sample",
|
||||
sample
|
||||
}))
|
||||
this.#soundfontService = new SoundfontService()
|
||||
this.#soundfontService.subscribe(([soundfont, _]) => this.#signals.notify({
|
||||
this.#soundfontService.subscribe(([soundfont]) => this.#signals.notify({
|
||||
type: "import-soundfont",
|
||||
soundfont
|
||||
}))
|
||||
@@ -222,6 +224,11 @@ export class StudioService implements ProjectEnv {
|
||||
})
|
||||
}
|
||||
|
||||
async importGeneratedSample(name: string, audio: Blob): Promise<Sample> {
|
||||
const arrayBuffer = await audio.arrayBuffer()
|
||||
return this.#sampleService.importFile({name, arrayBuffer})
|
||||
}
|
||||
|
||||
async exportStems() {
|
||||
return this.#projectProfileService.getValue()
|
||||
.ifSome(async (profile) => {
|
||||
@@ -562,4 +569,4 @@ export class StudioService implements ProjectEnv {
|
||||
StudioPreferences.catchupAndSubscribe(value =>
|
||||
document.body.classList.toggle("scrollbar-padding", value), "visibility", "scrollbar-padding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ export const Dashboard = ({lifecycle, service}: Construct) => {
|
||||
href="https://github.com/andremichelle/openDAW" target="upstream">openDAW</a> cree
|
||||
pour le spectacle <em>3615 J'ai pete</em> de la compagnie <a
|
||||
href="https://kxkm.net" target="kxkm">KXKM</a>.
|
||||
Il ajoute <strong>9 instruments AI</strong> (Drone, Grain, Glitch, Circus, Honk, Magenta,
|
||||
Il ajoute <strong>9 instruments IA / generatifs</strong> (Drone, Grain, Glitch, Circus, Honk, Magenta,
|
||||
AceStep, KokoroTTS, Piper) et un <strong>AI Bridge</strong> avec 19 backends audio.
|
||||
</p>
|
||||
<p style={{margin: "0.5em 0", fontSize: "11px"}}>
|
||||
Instruments de clown | Generation IA | TTS 12 voix | MIDI Magenta.js | 19 backends audio
|
||||
Instruments de clown | Generation locale et IA assistee | TTS 12 voix | MIDI algorithmique | 19 backends audio
|
||||
</p>
|
||||
<div className="columns">
|
||||
<DemoProjects lifecycle={lifecycle} service={service}/>
|
||||
|
||||
@@ -1,9 +1,91 @@
|
||||
@use "@/mixins"
|
||||
|
||||
component
|
||||
@include mixins.ControlLayout(1)
|
||||
@include mixins.Control
|
||||
display: grid
|
||||
grid-template-columns: repeat(2, minmax(0, auto))
|
||||
gap: 0.5em
|
||||
align-content: start
|
||||
|
||||
> div.sample-drop
|
||||
> section.hero,
|
||||
> section.section
|
||||
background: rgba(white, 0.03)
|
||||
box-shadow: inset 0 0 0 1px rgba(white, 0.04)
|
||||
border-radius: 0.35em
|
||||
padding: 0.45em
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.35em
|
||||
min-width: 0
|
||||
pointer-events: all
|
||||
|
||||
> section.hero
|
||||
grid-column: 1 / -1
|
||||
|
||||
> span.eyebrow
|
||||
font-size: 0.45em
|
||||
letter-spacing: 0.12em
|
||||
text-transform: uppercase
|
||||
color: rgba(white, 0.45)
|
||||
|
||||
> h3
|
||||
margin: 0
|
||||
font-size: 0.9em
|
||||
color: var(--color-text)
|
||||
|
||||
> p
|
||||
margin: 0
|
||||
font-size: 0.5em
|
||||
line-height: 1.4
|
||||
color: rgba(white, 0.55)
|
||||
|
||||
> section.compact
|
||||
grid-column: span 1
|
||||
|
||||
> section.sample-section,
|
||||
> section.ai-panel,
|
||||
> section.note
|
||||
grid-column: 1 / -1
|
||||
|
||||
> section.note > p
|
||||
margin: 0
|
||||
font-size: 0.5em
|
||||
line-height: 1.4
|
||||
color: rgba(white, 0.55)
|
||||
|
||||
> section.section
|
||||
> div.section-header
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.15em
|
||||
|
||||
> h4
|
||||
margin: 0
|
||||
font-size: 0.55em
|
||||
text-transform: uppercase
|
||||
letter-spacing: 0.08em
|
||||
color: var(--color-cream)
|
||||
|
||||
> span
|
||||
font-size: 0.45em
|
||||
color: rgba(white, 0.4)
|
||||
|
||||
> div.control-row
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 0.25em
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.15em
|
||||
font-size: 0.45em
|
||||
color: rgba(white, 0.55)
|
||||
|
||||
> section.ai-panel
|
||||
min-width: 14em
|
||||
|
||||
> section.sample-section > div.sample-drop
|
||||
border-radius: 50%
|
||||
color: var(--color-shadow)
|
||||
display: flex
|
||||
@@ -11,10 +93,12 @@ component
|
||||
align-items: center
|
||||
justify-content: center
|
||||
outline: 1px dashed rgba(white, 0.1)
|
||||
margin: 0.375em
|
||||
position: relative
|
||||
cursor: pointer
|
||||
pointer-events: all
|
||||
min-width: 3.25em
|
||||
min-height: 3.25em
|
||||
align-self: flex-start
|
||||
|
||||
> svg
|
||||
width: 2em
|
||||
@@ -39,17 +123,14 @@ component
|
||||
color: var(--color-black)
|
||||
background-color: var(--color-blue)
|
||||
|
||||
> div.ai-panel
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.25em
|
||||
margin: 0.375em
|
||||
padding: 0.25em
|
||||
pointer-events: all
|
||||
min-width: 10em
|
||||
border-left: 1px solid rgba(white, 0.08)
|
||||
> section.sample-section > span.detail
|
||||
font-size: 0.4em
|
||||
color: rgba(white, 0.45)
|
||||
min-height: 1em
|
||||
|
||||
> textarea, > input[type="text"]
|
||||
> section.ai-panel
|
||||
textarea,
|
||||
input[type="text"]
|
||||
font-size: 0.5em
|
||||
font-family: inherit
|
||||
width: 100%
|
||||
@@ -68,7 +149,7 @@ component
|
||||
&::placeholder
|
||||
color: rgba(white, 0.3)
|
||||
|
||||
> select
|
||||
select
|
||||
font-size: 0.5em
|
||||
font-family: inherit
|
||||
padding: 0.2em 0.3em
|
||||
@@ -107,10 +188,23 @@ component
|
||||
opacity: 0.35
|
||||
cursor: default
|
||||
|
||||
> span.status
|
||||
font-size: 0.4em
|
||||
color: rgba(white, 0.4)
|
||||
min-height: 1em
|
||||
> div.status-block
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.1em
|
||||
padding: 0.2em 0.25em
|
||||
border-radius: 0.25em
|
||||
background: rgba(0, 0, 0, 0.18)
|
||||
|
||||
> span.status
|
||||
font-size: 0.45em
|
||||
color: var(--color-text)
|
||||
min-height: 1em
|
||||
|
||||
> span.detail
|
||||
font-size: 0.4em
|
||||
color: rgba(white, 0.45)
|
||||
min-height: 1em
|
||||
|
||||
> div.progress-bar
|
||||
height: 2px
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import css from "./AceStepDeviceEditor.sass?inline"
|
||||
import {asInstanceOf, Lifecycle} from "@opendaw/lib-std"
|
||||
import {asInstanceOf, Lifecycle, UUID} from "@opendaw/lib-std"
|
||||
import {createElement} from "@opendaw/lib-jsx"
|
||||
import {DeviceEditor} from "@/ui/devices/DeviceEditor.tsx"
|
||||
import {MenuItems} from "@/ui/devices/menu-items.ts"
|
||||
@@ -12,10 +12,11 @@ import {AudioFileBox} from "@opendaw/studio-boxes"
|
||||
import {Icon} from "@/ui/components/Icon"
|
||||
import {SampleSelector, SampleSelectStrategy} from "@/ui/devices/SampleSelector"
|
||||
import {StudioService} from "@/service/StudioService"
|
||||
import {AceStepGenerationState} from "@/service/AceStepGenerationService"
|
||||
|
||||
const className = Html.adoptStyleSheet(css, "AceStepDeviceEditor")
|
||||
const AI_BRIDGE = (location.protocol === "https:" ? "https:" : "http:") + "//" + location.hostname + ":8301"
|
||||
const STYLES = ["ambient","electronic","experimental","classical","jazz","industrial","drone","glitch","techno","house","drum-n-bass","lo-fi","cinematic","world"] as const
|
||||
const STYLES = ["ambient", "electronic", "experimental", "classical", "jazz", "industrial", "drone", "glitch", "techno", "house", "drum-n-bass", "lo-fi", "cinematic", "world"] as const
|
||||
void createElement
|
||||
|
||||
type Construct = { lifecycle: Lifecycle; service: StudioService; adapter: AceStepDeviceBoxAdapter; deviceHost: DeviceHost }
|
||||
|
||||
@@ -25,63 +26,146 @@ export const AceStepDeviceEditor = ({lifecycle, service, adapter, deviceHost}: C
|
||||
const {editing, midiLearning} = project
|
||||
const knob = (parameter: typeof volume) => ControlBuilder.createKnob({lifecycle, editing, midiLearning, adapter, parameter})
|
||||
const sampleDropZone: HTMLElement = (<div className="sample-drop"><Icon symbol={IconSymbol.Waveform}/></div>)
|
||||
const sampleMetaEl: HTMLElement = <span className="detail">Aucun sample genere pour l'instant.</span>
|
||||
const sampleSelector = new SampleSelector(service, SampleSelectStrategy.forPointerField(adapter.box.file))
|
||||
lifecycle.ownAll(
|
||||
adapter.box.file.catchupAndSubscribe(pointer => pointer.targetVertex.match({
|
||||
none: () => sampleDropZone.removeAttribute("sample"),
|
||||
some: ({box}) => sampleDropZone.setAttribute("sample", asInstanceOf(box, AudioFileBox).fileName.getValue())
|
||||
none: () => {
|
||||
sampleDropZone.removeAttribute("sample")
|
||||
sampleMetaEl.textContent = "Aucun sample genere pour l'instant."
|
||||
},
|
||||
some: ({box}) => {
|
||||
const fileName = asInstanceOf(box, AudioFileBox).fileName.getValue()
|
||||
sampleDropZone.setAttribute("sample", fileName)
|
||||
sampleMetaEl.textContent = `Cible actuelle: ${fileName}`
|
||||
}
|
||||
})),
|
||||
sampleSelector.configureBrowseClick(sampleDropZone),
|
||||
sampleSelector.configureContextMenu(sampleDropZone),
|
||||
sampleSelector.configureDrop(sampleDropZone)
|
||||
)
|
||||
|
||||
const promptArea: HTMLTextAreaElement = <textarea rows={3} placeholder="Prompt musical... Ex: dark ambient with metallic textures and slow drones"/> as HTMLTextAreaElement
|
||||
const styleSelect: HTMLSelectElement = (<select>{STYLES.map(s => <option value={s}>{s}</option>)}</select>) as HTMLSelectElement
|
||||
const statusEl: HTMLElement = <span className="status"/>
|
||||
const styleSelect: HTMLSelectElement = (<select>{STYLES.map(style => <option value={style}>{style}</option>)}</select>) as HTMLSelectElement
|
||||
styleSelect.value = STYLES[0]
|
||||
const statusEl: HTMLElement = <span className="status">Pret a composer</span>
|
||||
const detailEl: HTMLElement = <span className="detail">Generation locale en priorite, puis AI Bridge si necessaire.</span>
|
||||
const progressEl: HTMLElement = <div className="progress-bar"><div className="progress-fill"/></div>
|
||||
const genBtn: HTMLButtonElement = <button>Composer</button> as HTMLButtonElement
|
||||
const providerEl: HTMLElement = <span className="detail">Provider: en attente</span>
|
||||
const genBtn: HTMLButtonElement = <button>Composer une boucle</button> as HTMLButtonElement
|
||||
let busy = false
|
||||
const fill = progressEl.querySelector(".progress-fill") as HTMLElement
|
||||
|
||||
const setUiState = (state: AceStepGenerationState, status: string, detail: string, progress: number, provider?: string) => {
|
||||
statusEl.textContent = status
|
||||
detailEl.textContent = detail
|
||||
providerEl.textContent = `Provider: ${provider ?? "en attente"}`
|
||||
fill.style.width = `${progress}%`
|
||||
genBtn.classList.toggle("generating", state === "checking" || state === "generating" || state === "importing")
|
||||
}
|
||||
|
||||
genBtn.addEventListener("click", async () => {
|
||||
if (busy) return
|
||||
busy = true; genBtn.disabled = true; genBtn.classList.add("generating"); genBtn.textContent = "Composition..."
|
||||
statusEl.textContent = ""
|
||||
const fill = progressEl.querySelector(".progress-fill") as HTMLElement
|
||||
fill.style.width = "10%"
|
||||
busy = true
|
||||
genBtn.disabled = true
|
||||
genBtn.textContent = "Composition..."
|
||||
setUiState("checking", "Generation en cours", "Verification des providers disponibles...", 10)
|
||||
try {
|
||||
const prompt = promptArea.value.trim() || "ambient electronic"
|
||||
const style = styleSelect.value
|
||||
const bpm = Math.round(tempo.getValue())
|
||||
const dur = Math.round(duration.getValue())
|
||||
fill.style.width = "20%"
|
||||
const resp = await fetch(`${AI_BRIDGE}/generate/music`, {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({prompt: `${prompt}, ${style}`, duration: dur, style, bpm}),
|
||||
signal: AbortSignal.timeout(300000)
|
||||
const generation = await service.aceStepGeneration.generateMusic({
|
||||
prompt,
|
||||
style,
|
||||
bpm,
|
||||
durationSec: dur
|
||||
}, update => {
|
||||
setUiState(update.state, update.message, update.detail, update.progress, update.provider)
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
fill.style.width = "90%"
|
||||
const blob = await resp.blob()
|
||||
statusEl.textContent = `OK — ${style} ${bpm}bpm (${(blob.size / 176400).toFixed(0)}s)`
|
||||
fill.style.width = "100%"
|
||||
setUiState("importing", "Import du sample", `${style} • ${bpm} BPM • ${generation.durationSec}s`, 95, generation.provider)
|
||||
const sample = await service.importGeneratedSample(
|
||||
`AceStep ${style} ${new Date().toISOString().slice(11, 19).replace(/:/g, "-")}`,
|
||||
generation.audio
|
||||
)
|
||||
service.project.trackUserCreatedSample(UUID.parse(sample.uuid))
|
||||
sampleSelector.newSample(sample)
|
||||
setUiState("ready", "Composition terminee", `${style} • ${bpm} BPM • ${sample.duration.toFixed(1)}s d'audio`, 100, generation.provider)
|
||||
} catch (err) {
|
||||
statusEl.textContent = `Erreur: ${err instanceof Error ? err.message : "echec"}`
|
||||
fill.style.width = "0%"
|
||||
setUiState("error", "Echec de la composition", err instanceof Error ? err.message : "Generation interrompue", 0)
|
||||
} finally {
|
||||
busy = false; genBtn.disabled = false; genBtn.classList.remove("generating"); genBtn.textContent = "Composer"
|
||||
busy = false
|
||||
genBtn.disabled = false
|
||||
genBtn.textContent = "Composer une boucle"
|
||||
setTimeout(() => { fill.style.width = "0%" }, 3000)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<DeviceEditor lifecycle={lifecycle} project={project} adapter={adapter}
|
||||
populateMenu={parent => MenuItems.forAudioUnitInput(parent, service, deviceHost)}
|
||||
populateControls={() => (
|
||||
<div className={className}>
|
||||
{knob(volume)}{knob(release)}{knob(tempo)}{knob(duration)}{sampleDropZone}
|
||||
<div className="ai-panel">
|
||||
{promptArea}{styleSelect}
|
||||
<section className="hero">
|
||||
<span className="eyebrow">Generation audio IA</span>
|
||||
<h3>AceStep</h3>
|
||||
<p>Generez un sample musical depuis un prompt, en priorite avec un moteur local puis via AI Bridge si besoin.</p>
|
||||
</section>
|
||||
<section className="section compact">
|
||||
<div className="section-header">
|
||||
<h4>Sortie</h4>
|
||||
<span>niveau et relache</span>
|
||||
</div>
|
||||
<div className="control-row">
|
||||
{knob(volume)}
|
||||
{knob(release)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section compact">
|
||||
<div className="section-header">
|
||||
<h4>Cadence</h4>
|
||||
<span>tempo et duree</span>
|
||||
</div>
|
||||
<div className="control-row">
|
||||
{knob(tempo)}
|
||||
{knob(duration)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section sample-section">
|
||||
<div className="section-header">
|
||||
<h4>Destination audio</h4>
|
||||
<span>sample charge ou depot glisse</span>
|
||||
</div>
|
||||
{sampleDropZone}
|
||||
{sampleMetaEl}
|
||||
</section>
|
||||
<section className="section ai-panel">
|
||||
<div className="section-header">
|
||||
<h4>Brief musical</h4>
|
||||
<span>prompt, style, provider et lancement</span>
|
||||
</div>
|
||||
<label>
|
||||
<span>Prompt</span>
|
||||
{promptArea}
|
||||
</label>
|
||||
<label>
|
||||
<span>Style</span>
|
||||
{styleSelect}
|
||||
</label>
|
||||
<div className="ai-controls">{genBtn}</div>
|
||||
{progressEl}{statusEl}
|
||||
</div>
|
||||
{progressEl}
|
||||
<div className="status-block">
|
||||
{statusEl}
|
||||
{detailEl}
|
||||
{providerEl}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section note">
|
||||
<div className="section-header">
|
||||
<h4>Usage</h4>
|
||||
</div>
|
||||
<p>La generation essaye d'abord ACE-Step local, puis bascule vers AI Bridge. Le rendu importe ensuite automatiquement un sample dans le device.</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
populateMeter={() => (<DevicePeakMeter lifecycle={lifecycle} receiver={project.liveStreamReceiver} address={adapter.address}/>)}
|
||||
|
||||
@@ -1,18 +1,83 @@
|
||||
@use "@/mixins"
|
||||
|
||||
component
|
||||
@include mixins.ControlLayout(2)
|
||||
@include mixins.Control
|
||||
display: grid
|
||||
grid-template-columns: repeat(2, minmax(0, auto))
|
||||
gap: 0.5em
|
||||
align-content: start
|
||||
|
||||
> div.magenta-panel
|
||||
> section.hero,
|
||||
> section.section
|
||||
background: rgba(white, 0.03)
|
||||
box-shadow: inset 0 0 0 1px rgba(white, 0.04)
|
||||
border-radius: 0.35em
|
||||
padding: 0.45em
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
gap: 0.25em
|
||||
padding: 0.25em
|
||||
margin: 0.375em
|
||||
gap: 0.35em
|
||||
min-width: 0
|
||||
pointer-events: all
|
||||
|
||||
> button
|
||||
> section.hero
|
||||
grid-column: 1 / -1
|
||||
|
||||
> span.eyebrow
|
||||
font-size: 0.45em
|
||||
letter-spacing: 0.12em
|
||||
text-transform: uppercase
|
||||
color: rgba(white, 0.45)
|
||||
|
||||
> h3
|
||||
margin: 0
|
||||
font-size: 0.9em
|
||||
color: var(--color-text)
|
||||
|
||||
> p
|
||||
margin: 0
|
||||
font-size: 0.5em
|
||||
line-height: 1.4
|
||||
color: rgba(white, 0.55)
|
||||
|
||||
> section.section
|
||||
> div.section-header
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.15em
|
||||
|
||||
> h4
|
||||
margin: 0
|
||||
font-size: 0.55em
|
||||
text-transform: uppercase
|
||||
letter-spacing: 0.08em
|
||||
color: var(--color-cream)
|
||||
|
||||
> span
|
||||
font-size: 0.45em
|
||||
color: rgba(white, 0.4)
|
||||
|
||||
> div.control-row
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 0.25em
|
||||
|
||||
> section.magenta-panel,
|
||||
> section.note
|
||||
grid-column: 1 / -1
|
||||
|
||||
> section.note > p
|
||||
margin: 0
|
||||
font-size: 0.5em
|
||||
line-height: 1.4
|
||||
color: rgba(white, 0.55)
|
||||
|
||||
> section.magenta-panel
|
||||
> div.actions
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 0.25em
|
||||
|
||||
> div.actions > button
|
||||
font-size: 0.6em
|
||||
padding: 0.2em 0.6em
|
||||
border-radius: 0.3em
|
||||
@@ -26,16 +91,24 @@ component
|
||||
&:hover
|
||||
background: rgba(white, 0.15)
|
||||
|
||||
&.active
|
||||
background: var(--color-blue)
|
||||
border-color: var(--color-blue)
|
||||
color: var(--color-black)
|
||||
|
||||
&:disabled
|
||||
opacity: 0.4
|
||||
cursor: default
|
||||
|
||||
> span.status
|
||||
font-size: 0.45em
|
||||
color: rgba(white, 0.5)
|
||||
white-space: nowrap
|
||||
> div.status-block
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.1em
|
||||
padding: 0.2em 0.25em
|
||||
border-radius: 0.25em
|
||||
background: rgba(0, 0, 0, 0.18)
|
||||
|
||||
> span.status
|
||||
font-size: 0.45em
|
||||
color: var(--color-text)
|
||||
min-height: 1em
|
||||
|
||||
> span.detail
|
||||
font-size: 0.4em
|
||||
color: rgba(white, 0.45)
|
||||
min-height: 1em
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Html} from "@opendaw/lib-dom"
|
||||
import {StudioService} from "@/service/StudioService"
|
||||
|
||||
const className = Html.adoptStyleSheet(css, "MagentaDeviceEditor")
|
||||
|
||||
void createElement
|
||||
type Construct = {
|
||||
lifecycle: Lifecycle
|
||||
service: StudioService
|
||||
@@ -18,7 +18,6 @@ type Construct = {
|
||||
deviceHost: DeviceHost
|
||||
}
|
||||
|
||||
// Built-in algorithmic MIDI generation (no external CDN needed)
|
||||
const SCALES: Record<string, number[]> = {
|
||||
minor: [0, 2, 3, 5, 7, 8, 10],
|
||||
major: [0, 2, 4, 5, 7, 9, 11],
|
||||
@@ -29,6 +28,17 @@ const SCALES: Record<string, number[]> = {
|
||||
const DRUM_PITCHES = [36, 38, 42, 46, 49, 51]
|
||||
const MODEL_NAMES = ["Melody", "Drums", "Improv"]
|
||||
|
||||
const noteToFrequency = (pitch: number): number => 440.0 * Math.pow(2.0, (pitch - 69.0) / 12.0)
|
||||
|
||||
const waveformTypeAt = (value: number): OscillatorType => ([
|
||||
"sine",
|
||||
"triangle",
|
||||
"sawtooth",
|
||||
"square"
|
||||
] as const)[Math.max(0, Math.min(3, Math.round(value)))]
|
||||
|
||||
const dbToGain = (db: number): number => Math.pow(10.0, db / 20.0)
|
||||
|
||||
function generateSequence(modelIdx: number, numSteps: number, temp: number, densityVal: number): {notes: Array<{pitch: number, quantizedStartStep: number, quantizedEndStep: number}>, totalQuantizedSteps: number} {
|
||||
const notes: Array<{pitch: number, quantizedStartStep: number, quantizedEndStep: number}> = []
|
||||
const isDrums = modelIdx === 1
|
||||
@@ -47,8 +57,8 @@ function generateSequence(modelIdx: number, numSteps: number, temp: number, dens
|
||||
pitch = Math.max(36, Math.min(84, octave * 12 + scale[scaleIdx]))
|
||||
prevPitch = pitch
|
||||
}
|
||||
const duration = isDrums ? 1 : Math.max(1, Math.floor(Math.random() * temp * 3) + 1)
|
||||
notes.push({pitch, quantizedStartStep: step, quantizedEndStep: Math.min(step + duration, numSteps)})
|
||||
const noteDuration = isDrums ? 1 : Math.max(1, Math.floor(Math.random() * temp * 3) + 1)
|
||||
notes.push({pitch, quantizedStartStep: step, quantizedEndStep: Math.min(step + noteDuration, numSteps)})
|
||||
}
|
||||
return {notes, totalQuantizedSteps: numSteps}
|
||||
}
|
||||
@@ -59,71 +69,179 @@ export const MagentaDeviceEditor = ({lifecycle, service, adapter, deviceHost}: C
|
||||
const {editing, midiLearning} = project
|
||||
const knob = (parameter: typeof volume) => ControlBuilder.createKnob({lifecycle, editing, midiLearning, adapter, parameter})
|
||||
|
||||
const statusEl: HTMLElement = <span className="status">Ready</span>
|
||||
const generateBtn: HTMLButtonElement = <button>Generate</button> as HTMLButtonElement
|
||||
const playBtn: HTMLButtonElement = <button disabled>Play</button> as HTMLButtonElement
|
||||
const statusEl: HTMLElement = <span className="status">Pret a generer</span>
|
||||
const detailEl: HTMLElement = <span className="detail">Choisissez un mode algorithmique puis lancez une sequence locale.</span>
|
||||
const generateBtn: HTMLButtonElement = <button>Generer une sequence</button> as HTMLButtonElement
|
||||
const playBtn: HTMLButtonElement = <button disabled>Ecouter</button> as HTMLButtonElement
|
||||
const stopBtn: HTMLButtonElement = <button disabled>Stop</button> as HTMLButtonElement
|
||||
const noteCountEl: HTMLElement = <span className="status"/>
|
||||
let generatedSequence: ReturnType<typeof generateSequence> | null = null
|
||||
let playTimeouts: Array<number> = []
|
||||
let activePreviewStops: Array<() => void> = []
|
||||
let isPlaying = false
|
||||
|
||||
const stopPreview = () => {
|
||||
playTimeouts.forEach(timeout => clearTimeout(timeout))
|
||||
playTimeouts = []
|
||||
activePreviewStops.forEach(stop => stop())
|
||||
activePreviewStops = []
|
||||
isPlaying = false
|
||||
playBtn.textContent = "Ecouter"
|
||||
stopBtn.disabled = true
|
||||
}
|
||||
|
||||
const previewNote = (pitch: number, startMs: number, durationMs: number) => {
|
||||
const audioContext = service.audioContext
|
||||
const now = audioContext.currentTime
|
||||
const startTime = now + startMs / 1000.0
|
||||
const durationSec = Math.max(durationMs / 1000.0, 0.04)
|
||||
const stopTime = startTime + durationSec + Math.max(release.getValue(), 0.02) + 0.05
|
||||
const attackSec = Math.max(attack.getValue(), 0.003)
|
||||
const releaseSec = Math.max(release.getValue(), 0.02)
|
||||
const oscillator = audioContext.createOscillator()
|
||||
const filterNode = audioContext.createBiquadFilter()
|
||||
const gainNode = audioContext.createGain()
|
||||
const panNode = audioContext.createStereoPanner()
|
||||
const modeIdx = Math.round(model.getValue())
|
||||
const oscType = modeIdx === 1 ? "square" : waveformTypeAt(waveform.getValue())
|
||||
const gain = Math.min(0.35, dbToGain(volume.getValue()) * 0.18)
|
||||
const pan = Math.max(-1.0, Math.min(1.0, ((pitch - 60.0) / 24.0) * stereoWidth.getValue()))
|
||||
|
||||
oscillator.type = oscType
|
||||
oscillator.frequency.setValueAtTime(noteToFrequency(pitch), startTime)
|
||||
filterNode.type = "lowpass"
|
||||
filterNode.frequency.setValueAtTime(cutoff.getValue(), startTime)
|
||||
filterNode.Q.setValueAtTime(modeIdx === 1 ? 0.5 : 1.2, startTime)
|
||||
panNode.pan.setValueAtTime(pan, startTime)
|
||||
|
||||
gainNode.gain.setValueAtTime(0.0001, startTime)
|
||||
gainNode.gain.linearRampToValueAtTime(gain, startTime + attackSec)
|
||||
gainNode.gain.setValueAtTime(gain, startTime + durationSec)
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.0001, startTime + durationSec + releaseSec)
|
||||
|
||||
oscillator.connect(filterNode)
|
||||
filterNode.connect(gainNode)
|
||||
gainNode.connect(panNode)
|
||||
panNode.connect(audioContext.destination)
|
||||
|
||||
oscillator.start(startTime)
|
||||
oscillator.stop(stopTime)
|
||||
|
||||
const teardown = () => {
|
||||
try { oscillator.stop() } catch (_error) { void _error }
|
||||
oscillator.disconnect()
|
||||
filterNode.disconnect()
|
||||
gainNode.disconnect()
|
||||
panNode.disconnect()
|
||||
}
|
||||
oscillator.onended = () => teardown()
|
||||
activePreviewStops.push(teardown)
|
||||
}
|
||||
|
||||
generateBtn.addEventListener("click", () => {
|
||||
const modelIdx = Math.round(model.getValue())
|
||||
const temp = temperature.getValue()
|
||||
const numSteps = Math.round(steps.getValue())
|
||||
const dens = density.getValue()
|
||||
statusEl.textContent = `Generating ${MODEL_NAMES[modelIdx]}...`
|
||||
statusEl.textContent = `Generation ${MODEL_NAMES[modelIdx]}`
|
||||
generatedSequence = generateSequence(modelIdx, numSteps, temp, dens)
|
||||
statusEl.textContent = `${MODEL_NAMES[modelIdx]}: ${generatedSequence.notes.length} notes`
|
||||
noteCountEl.textContent = `${numSteps} steps, temp ${temp.toFixed(1)}`
|
||||
statusEl.textContent = `${generatedSequence.notes.length} notes creees`
|
||||
detailEl.textContent = `${MODEL_NAMES[modelIdx]} • ${numSteps} steps • generation locale`
|
||||
playBtn.disabled = false
|
||||
})
|
||||
|
||||
playBtn.addEventListener("click", () => {
|
||||
if (!generatedSequence || isPlaying) return
|
||||
void service.audioContext.resume()
|
||||
stopPreview()
|
||||
isPlaying = true
|
||||
playBtn.textContent = "..."
|
||||
playBtn.textContent = "Lecture..."
|
||||
stopBtn.disabled = false
|
||||
const bpm = 120
|
||||
statusEl.textContent = "Lecture en cours"
|
||||
const bpm = project.timelineBox.bpm.getValue()
|
||||
const noteCount = generatedSequence.notes.length
|
||||
detailEl.textContent = `${noteCount} evenements • ${Math.round(bpm)} BPM`
|
||||
const msPerStep = (60000 / bpm) / 4
|
||||
for (const note of generatedSequence.notes) {
|
||||
const startMs = note.quantizedStartStep * msPerStep
|
||||
const durationMs = Math.max((note.quantizedEndStep - note.quantizedStartStep) * msPerStep, msPerStep * 0.8)
|
||||
previewNote(note.pitch, startMs, durationMs)
|
||||
playTimeouts.push(window.setTimeout(() => {
|
||||
statusEl.textContent = `Note ${note.pitch}`
|
||||
detailEl.textContent = `Note ${note.pitch}`
|
||||
}, startMs))
|
||||
}
|
||||
const totalMs = generatedSequence.totalQuantizedSteps * msPerStep
|
||||
playTimeouts.push(window.setTimeout(() => {
|
||||
isPlaying = false
|
||||
playBtn.textContent = "Play"
|
||||
stopBtn.disabled = true
|
||||
statusEl.textContent = "Done"
|
||||
stopPreview()
|
||||
statusEl.textContent = "Lecture terminee"
|
||||
detailEl.textContent = `${noteCount} evenements • ${Math.round(bpm)} BPM`
|
||||
}, totalMs))
|
||||
})
|
||||
|
||||
stopBtn.addEventListener("click", () => {
|
||||
playTimeouts.forEach(t => clearTimeout(t))
|
||||
playTimeouts = []
|
||||
isPlaying = false
|
||||
playBtn.textContent = "Play"
|
||||
stopBtn.disabled = true
|
||||
statusEl.textContent = "Stopped"
|
||||
stopPreview()
|
||||
statusEl.textContent = "Lecture arretee"
|
||||
detailEl.textContent = "La sequence reste disponible pour une nouvelle ecoute."
|
||||
})
|
||||
|
||||
lifecycle.ownAll({terminate: () => { playTimeouts.forEach(t => clearTimeout(t)) }})
|
||||
lifecycle.ownAll({terminate: () => stopPreview()})
|
||||
|
||||
return (
|
||||
<DeviceEditor lifecycle={lifecycle} project={project} adapter={adapter}
|
||||
populateMenu={parent => MenuItems.forAudioUnitInput(parent, service, deviceHost)}
|
||||
populateControls={() => (
|
||||
<div className={className}>
|
||||
{knob(volume)}{knob(waveform)}{knob(attack)}{knob(release)}{knob(cutoff)}
|
||||
{knob(model)}{knob(temperature)}{knob(steps)}{knob(density)}{knob(stereoWidth)}
|
||||
<div className="magenta-panel">
|
||||
{generateBtn}{playBtn}{stopBtn}
|
||||
{statusEl}{noteCountEl}
|
||||
</div>
|
||||
<section className="hero">
|
||||
<span className="eyebrow">MIDI algorithmique local</span>
|
||||
<h3>Magenta</h3>
|
||||
<p>Generez des sequences MIDI localement, puis jouez-les avec le synth integre. Ici, l'accent est sur l'algorithmique plutot que sur l'inference d'un modele externe.</p>
|
||||
</section>
|
||||
<section className="section">
|
||||
<div className="section-header">
|
||||
<h4>Son</h4>
|
||||
<span>timbre et envelope</span>
|
||||
</div>
|
||||
<div className="control-row">
|
||||
{knob(volume)}
|
||||
{knob(waveform)}
|
||||
{knob(cutoff)}
|
||||
{knob(attack)}
|
||||
{knob(release)}
|
||||
{knob(stereoWidth)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section">
|
||||
<div className="section-header">
|
||||
<h4>Generation</h4>
|
||||
<span>mode algorithmique et densite</span>
|
||||
</div>
|
||||
<div className="control-row">
|
||||
{knob(model)}
|
||||
{knob(temperature)}
|
||||
{knob(steps)}
|
||||
{knob(density)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section magenta-panel">
|
||||
<div className="section-header">
|
||||
<h4>Transport</h4>
|
||||
<span>generation, preview audio locale et retour d'etat</span>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{generateBtn}
|
||||
{playBtn}
|
||||
{stopBtn}
|
||||
</div>
|
||||
<div className="status-block">
|
||||
{statusEl}
|
||||
{detailEl}
|
||||
</div>
|
||||
</section>
|
||||
<section className="section note">
|
||||
<div className="section-header">
|
||||
<h4>Usage</h4>
|
||||
</div>
|
||||
<p>Les memes parametres restent exposes. Le bouton d'ecoute declenche maintenant une vraie preview audio locale synchronisee sur le tempo du projet.</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
populateMeter={() => (<DevicePeakMeter lifecycle={lifecycle} receiver={project.liveStreamReceiver} address={adapter.address}/>)}
|
||||
|
||||
@@ -69,7 +69,7 @@ export class MagentaDeviceBoxAdapter implements InstrumentDeviceBoxAdapter {
|
||||
StringMapping.numeric({unit: "Hz", fractionDigits: 0, unitPrefix: true}), "Cutoff"),
|
||||
model: this.#parametric.createParameter(
|
||||
box.model, ValueMapping.linearInteger(0, 2),
|
||||
StringMapping.indices("", ["MelodyRNN", "DrumsRNN", "ImprovRNN"]), "Model"),
|
||||
StringMapping.indices("", ["Melody", "Drums", "Improv"]), "Mode"),
|
||||
temperature: this.#parametric.createParameter(
|
||||
box.temperature, ValueMapping.linear(0.1, 2.0),
|
||||
StringMapping.numeric({unit: "", fractionDigits: 2}), "Temp."),
|
||||
|
||||
@@ -323,7 +323,7 @@ export namespace InstrumentFactories {
|
||||
export const Magenta: InstrumentFactory<void, MagentaDeviceBox> = {
|
||||
defaultName: "IA Magenta",
|
||||
defaultIcon: IconSymbol.Robot,
|
||||
description: "[AI] MIDI generator (algorithmic)",
|
||||
description: "[Local] Algorithmic MIDI generator",
|
||||
manualPage: DeviceManualUrls.Magenta,
|
||||
trackType: TrackType.Notes,
|
||||
create: (boxGraph: BoxGraph,
|
||||
@@ -342,7 +342,7 @@ export namespace InstrumentFactories {
|
||||
export const AceStep: InstrumentFactory<AudioFileBox, AceStepDeviceBox> = {
|
||||
defaultName: "IA AceStep",
|
||||
defaultIcon: IconSymbol.Flask,
|
||||
description: "[AI] Music generator (ACE-Step)",
|
||||
description: "[AI Audio] Prompt-to-sample generator",
|
||||
manualPage: DeviceManualUrls.AceStep,
|
||||
trackType: TrackType.Audio,
|
||||
create: (boxGraph: BoxGraph,
|
||||
@@ -450,4 +450,4 @@ export namespace InstrumentFactories {
|
||||
const useSoundfontFile = (boxGraph: BoxGraph, fileUUID: UUID.Bytes, name: string) =>
|
||||
boxGraph.findBox<SoundfontFileBox>(fileUUID)
|
||||
.unwrapOrElse(() => SoundfontFileBox.create(boxGraph, fileUUID, box => box.fileName.setValue(name)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user