v2.3 : correction bug ligne de basse
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
# _chordlab ORC·v2
|
||||
|
||||
Générateur d'accords et de progressions harmoniques dans le navigateur, inspiré de la logique du **Telepathic Instruments Orchid ORC-1** (implémentation indépendante, étendue). Un seul fichier HTML, zéro dépendance, zéro backend.
|
||||
|
||||
> Construis un accord avec un doigt, façonne son voicing, chaîne une progression, envoie-la en MIDI live vers ton studio dawless ou exporte-la en `.mid` pour ton DAW.
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
**Construction d'accords**
|
||||
- Clavier une octave pour la fondamentale (souris, tactile ou clavier PC)
|
||||
- 8 types d'accords : Dim · Min · Maj · Aug · Sus2 · Sus4 · 5 (power chord) · m♭6
|
||||
- 12 modificateurs empilables : 6 · m7 · M7 · 9 · ♭9 · ♯9 · 11 · ♯11 · 13 · ♭5 · ♯5 · no3
|
||||
- Nommage automatique (Cm7, Cdim7, Cø7, C7(♭9)…) affiché sur le LCD
|
||||
|
||||
**Voicing**
|
||||
- Knob de voicing rotatif ±8 crans (inversions en cascade, une note à la fois)
|
||||
- Drop voicings : close · drop 2 · drop 3 · drop 2+4 · spread
|
||||
- Accords slash : basse sur la fondamentale, la tierce, la quinte ou la septième
|
||||
- Moteur basse séparé (−1 ou −2 octaves)
|
||||
|
||||
**Tonalité et intelligence harmonique**
|
||||
- Key Mode : accords diatoniques à un doigt dans 8 gammes (majeur, mineur naturel/harmonique, dorien, phrygien, lydien, mixolydien, locrien)
|
||||
- Voice-leading automatique (VL AUTO) et optimisation globale de la progression
|
||||
- Suggestion d'accord suivant par table d'harmonie fonctionnelle
|
||||
- 8 presets de cadences générés dans la tonalité choisie
|
||||
|
||||
**Progression et lecture**
|
||||
- Rack de slots : durée individuelle réglable de **1 à 16 temps** (boutons −/+), réordonnancement, audition au clic
|
||||
- Modes de performance : block · strum ↑↓ · harp · arp ↑/↓/↕/random (1/8 à 1/32)
|
||||
- **10 rythmiques de basse** (façon Scaler) : tenue · noires · croches · contretemps · octaves · fondamentale–quinte · boogie · 3–3–2 · pompe · aléatoire
|
||||
- Humanisation vélocité/timing
|
||||
|
||||
**Entrées / sorties**
|
||||
- **Web MIDI out live** : accords et basse sur deux canaux séparés, vers n'importe quelle interface USB-MIDI
|
||||
- **Export `.mid`** : format 1, PPQ 480, tempo inclus, performance rendue en option
|
||||
- **Session `.json`** : sauvegarde/chargement complet (réglages + progression)
|
||||
|
||||
---
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
1. Ouvrir `index.html` dans **Chrome, Edge ou Firefox** (Safari ne supporte pas Web MIDI).
|
||||
2. Cliquer une note du clavier → l'accord sonne et s'affiche sur le LCD.
|
||||
3. `ADD` (ou barre espace) pour l'empiler dans la progression, `PLAY` pour l'écouter.
|
||||
4. Pour le MIDI live : brancher une interface USB-MIDI, autoriser l'accès MIDI, sélectionner le périphérique dans le panneau **MIDI out**.
|
||||
|
||||
> ⚠️ Web MIDI exige un **contexte sécurisé** : `https://` ou `http://localhost`. Un fichier ouvert en `file://` fonctionne pour l'audio et l'export, mais pas pour la sortie MIDI live selon les navigateurs. D'où l'intérêt de l'héberger (voir tutoriel de déploiement).
|
||||
|
||||
---
|
||||
|
||||
## Canaux MIDI par défaut
|
||||
|
||||
| Rôle | Canal | Machine cible (setup de référence) |
|
||||
|---|---|---|
|
||||
| Accords | 3 | Behringer Pro-800 (nappes) |
|
||||
| Basse | 5 | TD-3 + K-2 en unisson |
|
||||
|
||||
Modifiables dans le panneau **MIDI out (live)**. L'export `.mid` reprend les mêmes canaux.
|
||||
|
||||
---
|
||||
|
||||
## Structure du dépôt
|
||||
|
||||
```
|
||||
chordlab/
|
||||
├── index.html # l'application complète
|
||||
├── README.md
|
||||
└── docs/
|
||||
├── TUTORIEL_UTILISATION.md # guide utilisateur complet
|
||||
└── TUTORIEL_DEPLOIEMENT_COOLIFY.md # mise en ligne + mises à jour
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibilité
|
||||
|
||||
| Navigateur | Audio | MIDI live | Export .mid |
|
||||
|---|---|---|---|
|
||||
| Chrome / Edge | ✅ | ✅ | ✅ |
|
||||
| Firefox | ✅ | ✅ | ✅ |
|
||||
| Safari | ✅ | ❌ (pas de Web MIDI) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Notes de conception
|
||||
|
||||
- Le moteur de voicing reproduit le principe du dial de l'Orchid : chaque cran fait une **rotation** de l'accord (la note la plus grave remonte d'une octave, ou l'inverse), sur ±8 crans.
|
||||
- Les combinaisons classiques tombent naturellement : `DIM + 6 = dim7`, `DIM + m7 = ø7` (demi-diminué), `MAJ + m7 + ♯9 = accord Hendrix`.
|
||||
- Empiler `m7 + M7` affiche `WTF?` sur le LCD — clin d'œil assumé à l'Orchid.
|
||||
- Le voice-leading minimise la somme des déplacements de voix entre deux accords en testant les rotations de voicing candidates.
|
||||
- Aucune donnée n'est stockée dans le navigateur : la persistance passe exclusivement par les fichiers `.json` de session.
|
||||
|
||||
## Licence & crédit
|
||||
|
||||
Projet personnel underscorelab / leradiologue. La logique d'accords s'inspire du concept de l'Orchid ORC-1 de Telepathic Instruments, sans réutiliser ni code, ni sons, ni assets de l'instrument original.
|
||||
@@ -1,253 +0,0 @@
|
||||
# _chordlab v2 — Tutoriel complet d'utilisation
|
||||
|
||||
Ce guide couvre l'intégralité de l'application, du premier accord jusqu'aux workflows dawless et DAW. Comptez 10 minutes pour tout lire, 2 minutes pour être opérationnel.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vue d'ensemble de l'interface
|
||||
|
||||
L'écran est organisé comme une face avant de machine hardware :
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ LCD — nom de l'accord, notes, état des modes │
|
||||
├──────────────┬──────────────────────┬───────────────┤
|
||||
│ CHORD TYPE │ ROOT │ VOICING │
|
||||
│ TENSIONS │ (clavier 1 oct.) │ drop/slash │
|
||||
│ ALTÉRATIONS │ │ BASS / KEY │
|
||||
├──────────────┴──────────────────────┴───────────────┤
|
||||
│ PROGRESSION — slots, transport, perf, BPM │
|
||||
├──────────────────┬──────────────────┬───────────────┤
|
||||
│ MIDI OUT LIVE │ EXPORT MIDI │ SESSION │
|
||||
└──────────────────┴──────────────────┴───────────────┘
|
||||
```
|
||||
|
||||
Le **LCD** est votre référence permanente : il affiche le nom de l'accord courant, ses notes réelles (avec octaves), et l'état des modes (voicing, drop, key, perf, MIDI, voice-leading).
|
||||
|
||||
---
|
||||
|
||||
## 2. Jouer un accord
|
||||
|
||||
Le principe fondamental, hérité de l'Orchid : **vous ne jouez jamais les notes de l'accord, vous le décrivez**.
|
||||
|
||||
1. **Type** — un seul actif à la fois (LED allumée). MAJ est sélectionné au démarrage.
|
||||
2. **Fondamentale** — cliquez une touche du clavier, ou utilisez le clavier PC : `A W S E D F T G Y H U J` = C C# D D# E F F# G G# A A# B.
|
||||
3. L'accord sonne immédiatement et s'affiche sur le LCD.
|
||||
|
||||
Les huit types :
|
||||
|
||||
| Bouton | Accord | Intervalles |
|
||||
|---|---|---|
|
||||
| DIM | diminué | 1 · ♭3 · ♭5 |
|
||||
| MIN | mineur | 1 · ♭3 · 5 |
|
||||
| MAJ | majeur | 1 · 3 · 5 |
|
||||
| AUG | augmenté | 1 · 3 · ♯5 |
|
||||
| SUS2 | suspendu 2 | 1 · 2 · 5 |
|
||||
| SUS4 | suspendu 4 | 1 · 4 · 5 |
|
||||
| 5 | power chord | 1 · 5 |
|
||||
| m♭6 | mineur sixte bémol | 1 · ♭3 · ♭6 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Tensions et altérations
|
||||
|
||||
Les deux rangées inférieures s'**empilent** librement sur le type (plusieurs LEDs actives simultanément).
|
||||
|
||||
**Tensions** — elles *ajoutent* des notes :
|
||||
|
||||
| Bouton | Ajoute | Exemple avec MAJ + C |
|
||||
|---|---|---|
|
||||
| 6 | sixte majeure | C6 |
|
||||
| m7 | septième mineure | C7 |
|
||||
| M7 | septième majeure | Cmaj7 |
|
||||
| 9 | neuvième | Cadd9 (ou C7(9) si une 7e est là) |
|
||||
| ♭9 / ♯9 | neuvièmes altérées | C7(♭9), C7(♯9) « Hendrix » |
|
||||
| 11 / ♯11 | onzièmes | C7(11), Cmaj7(♯11) |
|
||||
| 13 | treizième | C7(13) |
|
||||
|
||||
**Altérations** — elles *modifient ou retirent* des notes :
|
||||
|
||||
| Bouton | Effet |
|
||||
|---|---|
|
||||
| ♭5 | remplace la quinte juste par une quinte diminuée |
|
||||
| ♯5 | remplace la quinte juste par une quinte augmentée |
|
||||
| no3 | retire la tierce (accords ouverts, parfaits pour l'ambient) |
|
||||
|
||||
**Combinaisons remarquables** :
|
||||
- `DIM + 6` → accord de **septième diminuée** (Cdim7)
|
||||
- `DIM + m7` → **demi-diminué** (Cø7)
|
||||
- `MIN + m7 + 9` → le m9 des nappes deep
|
||||
- `m7 + M7` ensemble → le LCD affiche `WTF?`. Vous êtes prévenu.
|
||||
|
||||
---
|
||||
|
||||
## 4. Le voicing
|
||||
|
||||
C'est le cœur expressif de la machine. Trois outils indépendants qui se cumulent :
|
||||
|
||||
### 4.1 Le knob rotatif (±8)
|
||||
Chaque cran fait une **rotation** de l'accord : à +1, la note la plus grave remonte d'une octave ; à −1, la plus aiguë descend. Exemple sur Cmaj7 :
|
||||
|
||||
```
|
||||
0 : C4 E4 G4 B4
|
||||
+1 : E4 G4 B4 C5
|
||||
+2 : G4 B4 C5 E5
|
||||
−1 : B3 C4 E4 G4
|
||||
```
|
||||
|
||||
Manipulation : glisser verticalement sur le knob, boutons −/0/+, flèches ←/→ du clavier, ou double-clic pour revenir à 0. Le même accord à +3 et à −2 raconte deux histoires complètement différentes — c'est l'outil n°1 pour trouver *la* couleur.
|
||||
|
||||
### 4.2 Drop voicings
|
||||
Appliqués après la rotation, ils écartent les voix comme le ferait un pianiste ou un arrangeur :
|
||||
|
||||
| Mode | Effet | Usage typique |
|
||||
|---|---|---|
|
||||
| close | rien (position serrée) | défaut |
|
||||
| drop 2 | 2e voix depuis le haut → −1 octave | le voicing jazz standard |
|
||||
| drop 3 | 3e voix depuis le haut → −1 octave | grands écarts, guitare |
|
||||
| drop 2+4 | deux voix descendues | très ouvert, cinématique |
|
||||
| spread | une voix sur deux → +1 octave | nappes larges, ambient |
|
||||
|
||||
### 4.3 Accords slash
|
||||
Le sélecteur **basse /** choisit quelle note de l'accord la basse joue : fondamentale, tierce, quinte ou septième. Le LCD affiche alors `C/E`, `C/G`… La détection est intelligente : sur un accord mineur elle prend la tierce mineure, sur un ♭5 la quinte diminuée.
|
||||
|
||||
### 4.4 BASS et ses rythmiques
|
||||
Le bouton **BASS** active le moteur basse (−1 ou −2 octaves). En MIDI live et à l'export, la basse sort sur son propre canal — c'est elle qui pilote ta TD-3 pendant que les accords vont au Pro-800.
|
||||
|
||||
Le sélecteur **rythme B** (inspiré de Scaler) transforme la note tenue en pattern rythmique, appliqué partout — audition, MIDI live et export `.mid` :
|
||||
|
||||
| Rythme | Comportement |
|
||||
|---|---|
|
||||
| tenue | une note sur toute la durée de l'accord (défaut) |
|
||||
| noires | une frappe par temps |
|
||||
| croches | deux frappes par temps |
|
||||
| contretemps | frappes sur les contretemps uniquement (dub, ska) |
|
||||
| octaves | croches alternant fondamentale / octave (synthwave, italo) |
|
||||
| fond.–quinte | noires alternant fondamentale / quinte |
|
||||
| boogie | noires cyclant fondamentale → quinte → octave → quinte |
|
||||
| 3–3–2 | tresillo (positions 1, 2.5, 4 sur un cycle de 4 temps) |
|
||||
| pompe | noires à gate très court (house, techno) |
|
||||
| aléatoire | croches probabilistes sur fondamentale/quinte/octave (IDM) |
|
||||
|
||||
Tous les patterns se tronquent proprement sur les durées impaires : des noires sur un slot de 3 temps donnent exactement 3 frappes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Key Mode — accords diatoniques à un doigt
|
||||
|
||||
Activez **KEY**, choisissez une tonique et une gamme parmi les huit disponibles. À partir de là :
|
||||
|
||||
- Chaque touche du clavier génère automatiquement **l'accord diatonique du degré correspondant** (en Do majeur : C→C, D→Dm, E→Em, F→F, G→G, A→Am, B→Bdim).
|
||||
- Les touches hors tonalité sont **grisées** et n'émettent rien (le LCD affiche `hors tonalité`).
|
||||
- Les tensions 6/7/9/11/13 restent actives et sont harmonisées **dans la gamme** (la 7e de G en Do majeur sera F naturel → G7, pas Gmaj7). Un astérisque dans le nom (`G7*`) signale un accord harmonisé diatoniquement.
|
||||
|
||||
Le mineur harmonique vaut le détour : le degré V y devient un vrai accord de dominante avec sensible — la cadence mineure classique à un doigt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Construire une progression
|
||||
|
||||
### 6.1 Manuellement
|
||||
Jouez un accord, puis **ADD** (ou barre espace). Il apparaît dans un slot avec son nom, ses notes et sa durée.
|
||||
|
||||
Sur chaque slot :
|
||||
- **− / +** autour du badge durée (`3♩`) — règle la durée de **1 à 16 temps**, au pas de 1. Un accord de 3, 5 ou 7 temps est désormais un seul slot, plus besoin de le découper.
|
||||
- **‹ ›** — déplacer le slot dans la séquence
|
||||
- **×** — supprimer
|
||||
- **clic sur le slot** — l'auditionner seul
|
||||
|
||||
### 6.2 Avec les presets
|
||||
Le menu déroulant propose huit cadences classiques (I–V–vi–IV, ii–V–I, cycle des quintes…). **Charger** les génère dans la tonalité et la gamme sélectionnées, avec vos tensions et votre voicing courants. C'est le moyen le plus rapide d'avoir une base à triturer.
|
||||
|
||||
### 6.3 Avec la suggestion harmonique
|
||||
**✦ suggérer** ajoute un accord choisi par une table d'harmonie fonctionnelle (après un I : IV, V, vi… ; après un ii : V ; etc.), dans la tonalité courante. Enchaînez les clics pour laisser la machine dériver, reprenez la main quand ça devient intéressant.
|
||||
|
||||
### 6.4 Voice-leading
|
||||
Deux outils pour des enchaînements fluides :
|
||||
- **VL AUTO** (transport) — chaque accord ajouté est automatiquement re-voicé pour minimiser le mouvement des voix par rapport au précédent.
|
||||
- **⇄ voice-leading** — optimise d'un coup toute la progression existante. À utiliser après un chargement de preset : la différence s'entend immédiatement.
|
||||
|
||||
---
|
||||
|
||||
## 7. Lecture et modes de performance
|
||||
|
||||
**PLAY** lance la progression au BPM affiché ; **LOOP** la fait boucler ; le slot en cours s'illumine.
|
||||
|
||||
Le sélecteur **perf** transforme la restitution de chaque accord :
|
||||
|
||||
| Mode | Rendu |
|
||||
|---|---|
|
||||
| block | accord plaqué |
|
||||
| strum ↑ / ↓ | notes égrenées vers le haut / le bas (façon guitare) |
|
||||
| harp | balayage rapide (glissando) |
|
||||
| arp ↑ / ↓ / ↕ / rnd | arpège à la vitesse du sélecteur **rate** (1/8, 1/16, 1/32) |
|
||||
|
||||
**HUM** humanise vélocités (±10) et timing (±15 ms) — utile pour que les strums et arps respirent.
|
||||
|
||||
> Ces modes s'appliquent partout : à l'audition, en MIDI live, et **dans l'export .mid** (désactivable, voir §9).
|
||||
|
||||
---
|
||||
|
||||
## 8. MIDI live vers le hardware
|
||||
|
||||
1. Branchez votre interface USB-MIDI **avant** d'ouvrir la page (ou rechargez-la après).
|
||||
2. Le navigateur demande l'autorisation d'accès MIDI — acceptez.
|
||||
3. Panneau **MIDI out (live)** : sélectionnez le périphérique.
|
||||
4. Vérifiez les canaux : **accords ch3** (Pro-800), **basse ch5** (TD-3/K-2) par défaut — adaptez à votre routage.
|
||||
5. Jouez : chaque accord cliqué, chaque slot auditionné et la lecture complète partent en MIDI, avec les modes de performance rendus.
|
||||
|
||||
Le LCD affiche `MIDI ON` quand un périphérique est actif. En cas de note bloquée sur une machine, **STOP** envoie un All Notes Off sur les 16 canaux.
|
||||
|
||||
> Contexte sécurisé requis : Web MIDI ne fonctionne qu'en `https://` ou sur `localhost`. C'est la raison principale d'héberger l'app (voir le tutoriel Coolify).
|
||||
|
||||
---
|
||||
|
||||
## 9. Export .mid
|
||||
|
||||
Panneau **Export MIDI** :
|
||||
|
||||
- **pistes** : `accords + basse` (2 pistes, chacune sur son canal) ou `accords seuls`
|
||||
- **perf rendue** : `oui` écrit les strums/arps note par note dans le fichier ; `non` exporte des accords plaqués (préférable si vous voulez arpéger dans le DAW ou le Tracker)
|
||||
|
||||
Le fichier généré (`chordlab_Cm7-Fm7-Ab-Bb_112bpm.mid`, format 1, PPQ 480, tempo inclus) respecte la durée individuelle de chaque slot.
|
||||
|
||||
**Destinations typiques** :
|
||||
- **DAW** — glisser-déposer sur une piste instrument, c'est tout.
|
||||
- **Polyend Tracker Mini** — copier le `.mid` sur la carte SD et l'importer ; chaque piste MIDI devient une piste du pattern.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sessions
|
||||
|
||||
**↓ sauver la session** génère un `.json` contenant tous les réglages (type, tensions, voicing, drop, slash, tonalité, perf, BPM…) et la progression complète. **↑ charger une session** restaure tout.
|
||||
|
||||
L'application ne stocke rien dans le navigateur : le fichier `.json` est l'unique persistance. Versionnez vos sessions dans Gitea comme n'importe quel fichier de projet.
|
||||
|
||||
---
|
||||
|
||||
## 11. Raccourcis clavier
|
||||
|
||||
| Touche | Action |
|
||||
|---|---|
|
||||
| A W S E D F T G Y H U J | fondamentales C → B |
|
||||
| Espace | ADD (ajouter l'accord courant à la progression) |
|
||||
| ← / → | voicing −1 / +1 |
|
||||
| ↑ ↓ ← → (knob focalisé) | voicing fin |
|
||||
| double-clic sur le knob | voicing = 0 |
|
||||
|
||||
---
|
||||
|
||||
## 12. Deux workflows de référence
|
||||
|
||||
### Workflow dawless (session hardware)
|
||||
1. KEY ON, tonique et gamme du morceau.
|
||||
2. Preset ou ✦ suggérer pour poser 4–8 accords ; ⇄ voice-leading.
|
||||
3. MIDI live branché : accords ch3 → Pro-800 (nappe lente), basse ch5 → TD-3.
|
||||
4. LOOP + perf `block`, BPM du morceau : la progression tourne pendant que vous sculptez les sons sur les machines et enregistrez sur le L-12.
|
||||
5. Session sauvée en `.json` pour reprendre la structure plus tard.
|
||||
|
||||
### Workflow DAW / Tracker (composition)
|
||||
1. Construction libre (KEY OFF) : types + tensions + drop voicings pour chercher des couleurs.
|
||||
2. VL AUTO activé, on empile au fil de l'inspiration.
|
||||
3. Export `.mid` avec `perf rendue : non` (accords propres) → import dans le DAW ou le Tracker Mini.
|
||||
4. L'arpégiation et le sound design se font dans la machine de destination.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
# _chordlab ORC·v2
|
||||
|
||||
Générateur d'accords et de progressions harmoniques dans le navigateur, inspiré de la logique du **Telepathic Instruments Orchid ORC-1** (implémentation indépendante, étendue). Un seul fichier HTML, zéro dépendance, zéro backend.
|
||||
|
||||
> Construis un accord avec un doigt, façonne son voicing, chaîne une progression, envoie-la en MIDI live vers ton studio dawless ou exporte-la en `.mid` pour ton DAW.
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
**Construction d'accords**
|
||||
- Clavier une octave pour la fondamentale (souris, tactile ou clavier PC)
|
||||
- 8 types d'accords : Dim · Min · Maj · Aug · Sus2 · Sus4 · 5 (power chord) · m♭6
|
||||
- 12 modificateurs empilables : 6 · m7 · M7 · 9 · ♭9 · ♯9 · 11 · ♯11 · 13 · ♭5 · ♯5 · no3
|
||||
- Nommage automatique (Cm7, Cdim7, Cø7, C7(♭9)…) affiché sur le LCD
|
||||
|
||||
**Voicing**
|
||||
- Knob de voicing rotatif ±8 crans (inversions en cascade, une note à la fois)
|
||||
- Drop voicings : close · drop 2 · drop 3 · drop 2+4 · spread
|
||||
- Accords slash : basse sur la fondamentale, la tierce, la quinte ou la septième
|
||||
- Moteur basse séparé (−1 ou −2 octaves)
|
||||
|
||||
**Tonalité et intelligence harmonique**
|
||||
- Key Mode : accords diatoniques à un doigt dans 8 gammes (majeur, mineur naturel/harmonique, dorien, phrygien, lydien, mixolydien, locrien)
|
||||
- Voice-leading automatique (VL AUTO) et optimisation globale de la progression
|
||||
- Suggestion d'accord suivant par table d'harmonie fonctionnelle
|
||||
- 8 presets de cadences générés dans la tonalité choisie
|
||||
|
||||
**Progression et lecture**
|
||||
- Rack de slots : durée individuelle réglable de **1 à 16 temps** (boutons −/+), réordonnancement, audition au clic
|
||||
- Modes de performance : block · strum ↑↓ · harp · arp ↑/↓/↕/random (1/8 à 1/32)
|
||||
- Humanisation vélocité/timing
|
||||
|
||||
**Entrées / sorties**
|
||||
- **Web MIDI out live** : accords et basse sur deux canaux séparés, vers n'importe quelle interface USB-MIDI
|
||||
- **Export `.mid`** : format 1, PPQ 480, tempo inclus, performance rendue en option
|
||||
- **Session `.json`** : sauvegarde/chargement complet (réglages + progression)
|
||||
|
||||
---
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
1. Ouvrir `index.html` dans **Chrome, Edge ou Firefox** (Safari ne supporte pas Web MIDI).
|
||||
2. Cliquer une note du clavier → l'accord sonne et s'affiche sur le LCD.
|
||||
3. `ADD` (ou barre espace) pour l'empiler dans la progression, `PLAY` pour l'écouter.
|
||||
4. Pour le MIDI live : brancher une interface USB-MIDI, autoriser l'accès MIDI, sélectionner le périphérique dans le panneau **MIDI out**.
|
||||
|
||||
> ⚠️ Web MIDI exige un **contexte sécurisé** : `https://` ou `http://localhost`. Un fichier ouvert en `file://` fonctionne pour l'audio et l'export, mais pas pour la sortie MIDI live selon les navigateurs. D'où l'intérêt de l'héberger (voir tutoriel de déploiement).
|
||||
|
||||
---
|
||||
|
||||
## Canaux MIDI par défaut
|
||||
|
||||
| Rôle | Canal | Machine cible (setup de référence) |
|
||||
|---|---|---|
|
||||
| Accords | 3 | Behringer Pro-800 (nappes) |
|
||||
| Basse | 5 | TD-3 + K-2 en unisson |
|
||||
|
||||
Modifiables dans le panneau **MIDI out (live)**. L'export `.mid` reprend les mêmes canaux.
|
||||
|
||||
---
|
||||
|
||||
## Structure du dépôt
|
||||
|
||||
```
|
||||
chordlab/
|
||||
├── index.html # l'application complète
|
||||
├── README.md
|
||||
└── docs/
|
||||
├── TUTORIEL_UTILISATION.md # guide utilisateur complet
|
||||
└── TUTORIEL_DEPLOIEMENT_COOLIFY.md # mise en ligne + mises à jour
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibilité
|
||||
|
||||
| Navigateur | Audio | MIDI live | Export .mid |
|
||||
|---|---|---|---|
|
||||
| Chrome / Edge | ✅ | ✅ | ✅ |
|
||||
| Firefox | ✅ | ✅ | ✅ |
|
||||
| Safari | ✅ | ❌ (pas de Web MIDI) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Notes de conception
|
||||
|
||||
- Le moteur de voicing reproduit le principe du dial de l'Orchid : chaque cran fait une **rotation** de l'accord (la note la plus grave remonte d'une octave, ou l'inverse), sur ±8 crans.
|
||||
- Les combinaisons classiques tombent naturellement : `DIM + 6 = dim7`, `DIM + m7 = ø7` (demi-diminué), `MAJ + m7 + ♯9 = accord Hendrix`.
|
||||
- Empiler `m7 + M7` affiche `WTF?` sur le LCD — clin d'œil assumé à l'Orchid.
|
||||
- Le voice-leading minimise la somme des déplacements de voix entre deux accords en testant les rotations de voicing candidates.
|
||||
- Aucune donnée n'est stockée dans le navigateur : la persistance passe exclusivement par les fichiers `.json` de session.
|
||||
|
||||
## Licence & crédit
|
||||
|
||||
Projet personnel underscorelab / leradiologue. La logique d'accords s'inspire du concept de l'Orchid ORC-1 de Telepathic Instruments, sans réutiliser ni code, ni sons, ni assets de l'instrument original.
|
||||
@@ -1,96 +0,0 @@
|
||||
# _chordlab ORC·v2
|
||||
|
||||
Générateur d'accords et de progressions harmoniques dans le navigateur, inspiré de la logique du **Telepathic Instruments Orchid ORC-1** (implémentation indépendante, étendue). Un seul fichier HTML, zéro dépendance, zéro backend.
|
||||
|
||||
> Construis un accord avec un doigt, façonne son voicing, chaîne une progression, envoie-la en MIDI live vers ton studio dawless ou exporte-la en `.mid` pour ton DAW.
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
**Construction d'accords**
|
||||
- Clavier une octave pour la fondamentale (souris, tactile ou clavier PC)
|
||||
- 8 types d'accords : Dim · Min · Maj · Aug · Sus2 · Sus4 · 5 (power chord) · m♭6
|
||||
- 12 modificateurs empilables : 6 · m7 · M7 · 9 · ♭9 · ♯9 · 11 · ♯11 · 13 · ♭5 · ♯5 · no3
|
||||
- Nommage automatique (Cm7, Cdim7, Cø7, C7(♭9)…) affiché sur le LCD
|
||||
|
||||
**Voicing**
|
||||
- Knob de voicing rotatif ±8 crans (inversions en cascade, une note à la fois)
|
||||
- Drop voicings : close · drop 2 · drop 3 · drop 2+4 · spread
|
||||
- Accords slash : basse sur la fondamentale, la tierce, la quinte ou la septième
|
||||
- Moteur basse séparé (−1 ou −2 octaves)
|
||||
|
||||
**Tonalité et intelligence harmonique**
|
||||
- Key Mode : accords diatoniques à un doigt dans 8 gammes (majeur, mineur naturel/harmonique, dorien, phrygien, lydien, mixolydien, locrien)
|
||||
- Voice-leading automatique (VL AUTO) et optimisation globale de la progression
|
||||
- Suggestion d'accord suivant par table d'harmonie fonctionnelle
|
||||
- 8 presets de cadences générés dans la tonalité choisie
|
||||
|
||||
**Progression et lecture**
|
||||
- Rack de slots : durée individuelle (1/2/4/8 temps), réordonnancement, audition au clic
|
||||
- Modes de performance : block · strum ↑↓ · harp · arp ↑/↓/↕/random (1/8 à 1/32)
|
||||
- Humanisation vélocité/timing
|
||||
|
||||
**Entrées / sorties**
|
||||
- **Web MIDI out live** : accords et basse sur deux canaux séparés, vers n'importe quelle interface USB-MIDI
|
||||
- **Export `.mid`** : format 1, PPQ 480, tempo inclus, performance rendue en option
|
||||
- **Session `.json`** : sauvegarde/chargement complet (réglages + progression)
|
||||
|
||||
---
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
1. Ouvrir `index.html` dans **Chrome, Edge ou Firefox** (Safari ne supporte pas Web MIDI).
|
||||
2. Cliquer une note du clavier → l'accord sonne et s'affiche sur le LCD.
|
||||
3. `ADD` (ou barre espace) pour l'empiler dans la progression, `PLAY` pour l'écouter.
|
||||
4. Pour le MIDI live : brancher une interface USB-MIDI, autoriser l'accès MIDI, sélectionner le périphérique dans le panneau **MIDI out**.
|
||||
|
||||
> ⚠️ Web MIDI exige un **contexte sécurisé** : `https://` ou `http://localhost`. Un fichier ouvert en `file://` fonctionne pour l'audio et l'export, mais pas pour la sortie MIDI live selon les navigateurs. D'où l'intérêt de l'héberger (voir tutoriel de déploiement).
|
||||
|
||||
---
|
||||
|
||||
## Canaux MIDI par défaut
|
||||
|
||||
| Rôle | Canal | Machine cible (setup de référence) |
|
||||
|---|---|---|
|
||||
| Accords | 3 | Behringer Pro-800 (nappes) |
|
||||
| Basse | 5 | TD-3 + K-2 en unisson |
|
||||
|
||||
Modifiables dans le panneau **MIDI out (live)**. L'export `.mid` reprend les mêmes canaux.
|
||||
|
||||
---
|
||||
|
||||
## Structure du dépôt
|
||||
|
||||
```
|
||||
chordlab/
|
||||
├── index.html # l'application complète
|
||||
├── README.md
|
||||
└── docs/
|
||||
├── TUTORIEL_UTILISATION.md # guide utilisateur complet
|
||||
└── TUTORIEL_DEPLOIEMENT_COOLIFY.md # mise en ligne + mises à jour
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibilité
|
||||
|
||||
| Navigateur | Audio | MIDI live | Export .mid |
|
||||
|---|---|---|---|
|
||||
| Chrome / Edge | ✅ | ✅ | ✅ |
|
||||
| Firefox | ✅ | ✅ | ✅ |
|
||||
| Safari | ✅ | ❌ (pas de Web MIDI) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Notes de conception
|
||||
|
||||
- Le moteur de voicing reproduit le principe du dial de l'Orchid : chaque cran fait une **rotation** de l'accord (la note la plus grave remonte d'une octave, ou l'inverse), sur ±8 crans.
|
||||
- Les combinaisons classiques tombent naturellement : `DIM + 6 = dim7`, `DIM + m7 = ø7` (demi-diminué), `MAJ + m7 + ♯9 = accord Hendrix`.
|
||||
- Empiler `m7 + M7` affiche `WTF?` sur le LCD — clin d'œil assumé à l'Orchid.
|
||||
- Le voice-leading minimise la somme des déplacements de voix entre deux accords en testant les rotations de voicing candidates.
|
||||
- Aucune donnée n'est stockée dans le navigateur : la persistance passe exclusivement par les fichiers `.json` de session.
|
||||
|
||||
## Licence & crédit
|
||||
|
||||
Projet personnel underscorelab / leradiologue. La logique d'accords s'inspire du concept de l'Orchid ORC-1 de Telepathic Instruments, sans réutiliser ni code, ni sons, ni assets de l'instrument original.
|
||||
@@ -1,170 +0,0 @@
|
||||
# _chordlab — Mise en ligne avec Coolify
|
||||
|
||||
Déploiement sur l'infrastructure ghostinthemachine.fr, **sur le même modèle que le site des vacances** (`sudouest.ghostinthemachine.fr`) : dépôt Gitea → ressource Coolify en build pack **Static** → domaine servi par Traefik avec le certificat wildcard existant.
|
||||
|
||||
Cible retenue dans ce tutoriel : `https://chordlab.ghostinthemachine.fr`
|
||||
|
||||
> **Pourquoi héberger une app qui tient dans un fichier ?** Parce que **Web MIDI exige un contexte sécurisé** (`https://` ou `localhost`). En `file://`, la sortie MIDI live vers le hardware est bloquée par le navigateur. Une fois sur ton domaine en HTTPS, tout fonctionne — y compris depuis le portable posé à côté du studio.
|
||||
|
||||
---
|
||||
|
||||
## 0. Prérequis (déjà en place sur ton infra)
|
||||
|
||||
- Serveur Scaleway avec **Coolify 4.x** et **Traefik** opérationnels
|
||||
- **Gitea** self-hosted accessible
|
||||
- Wildcard DNS `*.ghostinthemachine.fr` pointant vers le serveur (Cloudflare)
|
||||
- Certificat wildcard géré par acme.sh/Cloudflare — rien à faire côté SSL
|
||||
|
||||
[PROBABLE] Les libellés exacts de l'interface Coolify peuvent varier légèrement selon la version ; le déroulé ci-dessous correspond au flux validé lors du déploiement du site des vacances.
|
||||
|
||||
---
|
||||
|
||||
## 1. Préparer le dépôt Gitea
|
||||
|
||||
### 1.1 Créer le dépôt
|
||||
|
||||
Dans Gitea : **+ → Nouveau dépôt** → nom `chordlab`. Comme pour le site des vacances, le contenu n'a rien de sensible : **le plus simple est de le créer public** (voir §2.2 pour la raison précise).
|
||||
|
||||
### 1.2 Structurer et pousser
|
||||
|
||||
```bash
|
||||
mkdir chordlab && cd chordlab
|
||||
git init
|
||||
|
||||
# Le fichier de l'app DOIT s'appeler index.html
|
||||
# (le serveur statique sert index.html à la racine du domaine)
|
||||
cp /chemin/vers/chordlab_v2.html index.html
|
||||
|
||||
# Documentation
|
||||
cp /chemin/vers/README.md .
|
||||
mkdir docs
|
||||
cp /chemin/vers/TUTORIEL_UTILISATION.md docs/
|
||||
cp /chemin/vers/TUTORIEL_DEPLOIEMENT_COOLIFY.md docs/
|
||||
|
||||
git add .
|
||||
git commit -m "chordlab v2 — initial"
|
||||
git remote add origin https://git.ghostinthemachine.fr/matthieu/chordlab.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
> Adapte l'URL du remote à ton instance Gitea réelle et à ton utilisateur.
|
||||
|
||||
---
|
||||
|
||||
## 2. Créer la ressource dans Coolify
|
||||
|
||||
### 2.1 Nouvelle ressource
|
||||
|
||||
1. Coolify → ton projet → **+ New → Resource**.
|
||||
2. Choisir **Public Repository** (dépôt public) et coller l'URL HTTPS du dépôt Gitea :
|
||||
`https://git.ghostinthemachine.fr/matthieu/chordlab.git`
|
||||
3. Branche : `main`.
|
||||
4. **Build Pack : Static** — c'est le point clé. Pas de Dockerfile, pas de Nixpacks : Coolify sert simplement les fichiers via un conteneur nginx.
|
||||
5. **Publish Directory : `/`** (l'`index.html` est à la racine du dépôt).
|
||||
|
||||
### 2.2 ⚠️ Le piège du dépôt privé (leçon du site des vacances)
|
||||
|
||||
Si le dépôt Gitea est **privé** et que la ressource est de type « Public Repository », le déploiement échoue avec :
|
||||
|
||||
```
|
||||
exit code: 128
|
||||
fatal: could not read Username for 'https://git.ghostinthemachine.fr'
|
||||
```
|
||||
|
||||
Coolify clone sans credentials, Gitea refuse. Deux solutions, exactement comme la dernière fois :
|
||||
|
||||
| Option | Quand | Comment |
|
||||
|---|---|---|
|
||||
| **A. Rendre le dépôt public** | contenu non sensible (notre cas) | Gitea → dépôt → Paramètres → cocher « Visibilité : public ». Redéployer. C'est la solution retenue pour le site des vacances. |
|
||||
| **B. Deploy Key SSH** | si tu tiens au dépôt privé | Coolify → ressource → Source → générer une clé de déploiement → l'ajouter dans Gitea (dépôt → Paramètres → Clés de déploiement) → utiliser l'URL **SSH** du dépôt (`git@git.ghostinthemachine.fr:matthieu/chordlab.git`) |
|
||||
|
||||
### 2.3 Domaine
|
||||
|
||||
Dans la ressource → **Domains** :
|
||||
|
||||
```
|
||||
https://chordlab.ghostinthemachine.fr
|
||||
```
|
||||
|
||||
- Le wildcard DNS Cloudflare couvre déjà `chordlab.ghostinthemachine.fr` → aucune entrée DNS à créer.
|
||||
- Bien saisir le domaine **avec `https://`** : c'est ce qui pilote la génération des labels Traefik pour la redirection et le TLS.
|
||||
|
||||
### 2.4 Déployer
|
||||
|
||||
Bouton **Deploy**. Le log doit montrer : clone du dépôt → build statique → conteneur démarré. Première mise en ligne : généralement moins d'une minute.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vérifications post-déploiement
|
||||
|
||||
1. **`https://chordlab.ghostinthemachine.fr`** répond avec l'interface (face avant beige, LCD).
|
||||
2. **Cadenas SSL** valide (certificat wildcard).
|
||||
3. **Test Web MIDI** : interface USB-MIDI branchée → recharger la page → le navigateur demande l'autorisation MIDI → le périphérique apparaît dans le panneau *MIDI out (live)* et le LCD affiche `MIDI ON`. Si l'autorisation n'est jamais demandée, vérifier que l'URL est bien en `https://` (c'était le même principe que le micro pour LibreChat : pas de périphérique sans contexte sécurisé).
|
||||
4. **Test export** : deux accords → ADD → `↓ exporter .mid` → le fichier se télécharge.
|
||||
|
||||
---
|
||||
|
||||
## 4. Mise à jour de l'application
|
||||
|
||||
Même mécanique que pour le site des vacances : **le dépôt Git est la source de vérité**, Coolify redéploie depuis le dernier commit.
|
||||
|
||||
### 4.1 Mise à jour manuelle (workflow de base)
|
||||
|
||||
```bash
|
||||
cd chordlab
|
||||
# remplacer index.html par la nouvelle version de l'app
|
||||
cp /chemin/vers/chordlab_v2.html index.html
|
||||
|
||||
git add index.html
|
||||
git commit -m "v2.1 — <description du changement>"
|
||||
git push
|
||||
```
|
||||
|
||||
Puis dans Coolify → ressource `chordlab` → **Redeploy**. Le conteneur est reconstruit avec les nouveaux fichiers ; interruption de service quasi nulle.
|
||||
|
||||
### 4.2 Déploiement automatique au push (optionnel)
|
||||
|
||||
Pour supprimer l'étape manuelle :
|
||||
|
||||
1. Coolify → ressource → **Webhooks** → copier l'URL du webhook de déploiement.
|
||||
2. Gitea → dépôt `chordlab` → **Paramètres → Webhooks → Ajouter un webhook → Gitea**.
|
||||
3. URL cible : l'URL Coolify copiée · Événement : **Push** · branche `main`.
|
||||
4. Tester avec « Test Delivery », puis faire un commit de contrôle : chaque `git push` déclenche désormais le redéploiement.
|
||||
|
||||
### 4.3 Revenir en arrière
|
||||
|
||||
Deux options :
|
||||
- **Git** : `git revert <commit>` puis push (propre, historisé — à préférer) ;
|
||||
- **Coolify** : la liste des déploiements permet de relancer une image précédente. [INCERTAIN] La disponibilité du rollback en un clic dépend de la version de Coolify ; le revert Git fonctionne dans tous les cas.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dépannage
|
||||
|
||||
| Symptôme | Cause probable | Correction |
|
||||
|---|---|---|
|
||||
| `exit code: 128 — could not read Username` | dépôt privé cloné sans credentials | §2.2 : dépôt public ou Deploy Key SSH |
|
||||
| 404 Traefik sur le domaine | domaine mal saisi dans la ressource, ou routeur pas régénéré | vérifier Domains (avec `https://`), redéployer ; en dernier recours contrôler les labels du conteneur (`docker inspect`) |
|
||||
| La page charge mais pas de demande d'autorisation MIDI | contexte non sécurisé ou navigateur Safari | vérifier `https://`, utiliser Chrome/Edge/Firefox |
|
||||
| Ancienne version affichée après redéploiement | cache navigateur (fichier HTML unique) | rechargement forcé (Ctrl+Shift+R) ; si récurrent, ajouter un query string versionné dans les liens partagés (`?v=2.1`) |
|
||||
| Le conteneur tourne mais rien ne répond | l'app n'est pas nommée `index.html` à la racine, ou Publish Directory incorrect | renommer le fichier / corriger le Publish Directory, redéployer |
|
||||
| Périphérique MIDI absent de la liste | interface branchée après le chargement | recharger la page (l'app écoute les changements, mais certains OS n'exposent le port qu'à la connexion) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Récapitulatif du modèle
|
||||
|
||||
```
|
||||
[poste local] [serveur ghostinthemachine.fr]
|
||||
index.html ──git push──▶ Gitea ──clone──▶ Coolify (Static build pack)
|
||||
│
|
||||
▼
|
||||
conteneur nginx statique
|
||||
│
|
||||
Traefik + wildcard SSL
|
||||
│
|
||||
▼
|
||||
https://chordlab.ghostinthemachine.fr
|
||||
```
|
||||
|
||||
Un fichier, un push, un redeploy. Le même modèle resservira pour toute page ou micro-app statique à venir.
|
||||
@@ -1,236 +0,0 @@
|
||||
# _chordlab v2 — Tutoriel complet d'utilisation
|
||||
|
||||
Ce guide couvre l'intégralité de l'application, du premier accord jusqu'aux workflows dawless et DAW. Comptez 10 minutes pour tout lire, 2 minutes pour être opérationnel.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vue d'ensemble de l'interface
|
||||
|
||||
L'écran est organisé comme une face avant de machine hardware :
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ LCD — nom de l'accord, notes, état des modes │
|
||||
├──────────────┬──────────────────────┬───────────────┤
|
||||
│ CHORD TYPE │ ROOT │ VOICING │
|
||||
│ TENSIONS │ (clavier 1 oct.) │ drop/slash │
|
||||
│ ALTÉRATIONS │ │ BASS / KEY │
|
||||
├──────────────┴──────────────────────┴───────────────┤
|
||||
│ PROGRESSION — slots, transport, perf, BPM │
|
||||
├──────────────────┬──────────────────┬───────────────┤
|
||||
│ MIDI OUT LIVE │ EXPORT MIDI │ SESSION │
|
||||
└──────────────────┴──────────────────┴───────────────┘
|
||||
```
|
||||
|
||||
Le **LCD** est votre référence permanente : il affiche le nom de l'accord courant, ses notes réelles (avec octaves), et l'état des modes (voicing, drop, key, perf, MIDI, voice-leading).
|
||||
|
||||
---
|
||||
|
||||
## 2. Jouer un accord
|
||||
|
||||
Le principe fondamental, hérité de l'Orchid : **vous ne jouez jamais les notes de l'accord, vous le décrivez**.
|
||||
|
||||
1. **Type** — un seul actif à la fois (LED allumée). MAJ est sélectionné au démarrage.
|
||||
2. **Fondamentale** — cliquez une touche du clavier, ou utilisez le clavier PC : `A W S E D F T G Y H U J` = C C# D D# E F F# G G# A A# B.
|
||||
3. L'accord sonne immédiatement et s'affiche sur le LCD.
|
||||
|
||||
Les huit types :
|
||||
|
||||
| Bouton | Accord | Intervalles |
|
||||
|---|---|---|
|
||||
| DIM | diminué | 1 · ♭3 · ♭5 |
|
||||
| MIN | mineur | 1 · ♭3 · 5 |
|
||||
| MAJ | majeur | 1 · 3 · 5 |
|
||||
| AUG | augmenté | 1 · 3 · ♯5 |
|
||||
| SUS2 | suspendu 2 | 1 · 2 · 5 |
|
||||
| SUS4 | suspendu 4 | 1 · 4 · 5 |
|
||||
| 5 | power chord | 1 · 5 |
|
||||
| m♭6 | mineur sixte bémol | 1 · ♭3 · ♭6 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Tensions et altérations
|
||||
|
||||
Les deux rangées inférieures s'**empilent** librement sur le type (plusieurs LEDs actives simultanément).
|
||||
|
||||
**Tensions** — elles *ajoutent* des notes :
|
||||
|
||||
| Bouton | Ajoute | Exemple avec MAJ + C |
|
||||
|---|---|---|
|
||||
| 6 | sixte majeure | C6 |
|
||||
| m7 | septième mineure | C7 |
|
||||
| M7 | septième majeure | Cmaj7 |
|
||||
| 9 | neuvième | Cadd9 (ou C7(9) si une 7e est là) |
|
||||
| ♭9 / ♯9 | neuvièmes altérées | C7(♭9), C7(♯9) « Hendrix » |
|
||||
| 11 / ♯11 | onzièmes | C7(11), Cmaj7(♯11) |
|
||||
| 13 | treizième | C7(13) |
|
||||
|
||||
**Altérations** — elles *modifient ou retirent* des notes :
|
||||
|
||||
| Bouton | Effet |
|
||||
|---|---|
|
||||
| ♭5 | remplace la quinte juste par une quinte diminuée |
|
||||
| ♯5 | remplace la quinte juste par une quinte augmentée |
|
||||
| no3 | retire la tierce (accords ouverts, parfaits pour l'ambient) |
|
||||
|
||||
**Combinaisons remarquables** :
|
||||
- `DIM + 6` → accord de **septième diminuée** (Cdim7)
|
||||
- `DIM + m7` → **demi-diminué** (Cø7)
|
||||
- `MIN + m7 + 9` → le m9 des nappes deep
|
||||
- `m7 + M7` ensemble → le LCD affiche `WTF?`. Vous êtes prévenu.
|
||||
|
||||
---
|
||||
|
||||
## 4. Le voicing
|
||||
|
||||
C'est le cœur expressif de la machine. Trois outils indépendants qui se cumulent :
|
||||
|
||||
### 4.1 Le knob rotatif (±8)
|
||||
Chaque cran fait une **rotation** de l'accord : à +1, la note la plus grave remonte d'une octave ; à −1, la plus aiguë descend. Exemple sur Cmaj7 :
|
||||
|
||||
```
|
||||
0 : C4 E4 G4 B4
|
||||
+1 : E4 G4 B4 C5
|
||||
+2 : G4 B4 C5 E5
|
||||
−1 : B3 C4 E4 G4
|
||||
```
|
||||
|
||||
Manipulation : glisser verticalement sur le knob, boutons −/0/+, flèches ←/→ du clavier, ou double-clic pour revenir à 0. Le même accord à +3 et à −2 raconte deux histoires complètement différentes — c'est l'outil n°1 pour trouver *la* couleur.
|
||||
|
||||
### 4.2 Drop voicings
|
||||
Appliqués après la rotation, ils écartent les voix comme le ferait un pianiste ou un arrangeur :
|
||||
|
||||
| Mode | Effet | Usage typique |
|
||||
|---|---|---|
|
||||
| close | rien (position serrée) | défaut |
|
||||
| drop 2 | 2e voix depuis le haut → −1 octave | le voicing jazz standard |
|
||||
| drop 3 | 3e voix depuis le haut → −1 octave | grands écarts, guitare |
|
||||
| drop 2+4 | deux voix descendues | très ouvert, cinématique |
|
||||
| spread | une voix sur deux → +1 octave | nappes larges, ambient |
|
||||
|
||||
### 4.3 Accords slash
|
||||
Le sélecteur **basse /** choisit quelle note de l'accord la basse joue : fondamentale, tierce, quinte ou septième. Le LCD affiche alors `C/E`, `C/G`… La détection est intelligente : sur un accord mineur elle prend la tierce mineure, sur un ♭5 la quinte diminuée.
|
||||
|
||||
### 4.4 BASS
|
||||
Le bouton **BASS** active le moteur basse (−1 ou −2 octaves). En MIDI live et à l'export, la basse sort sur son propre canal — c'est elle qui pilote ta TD-3 pendant que les accords vont au Pro-800.
|
||||
|
||||
---
|
||||
|
||||
## 5. Key Mode — accords diatoniques à un doigt
|
||||
|
||||
Activez **KEY**, choisissez une tonique et une gamme parmi les huit disponibles. À partir de là :
|
||||
|
||||
- Chaque touche du clavier génère automatiquement **l'accord diatonique du degré correspondant** (en Do majeur : C→C, D→Dm, E→Em, F→F, G→G, A→Am, B→Bdim).
|
||||
- Les touches hors tonalité sont **grisées** et n'émettent rien (le LCD affiche `hors tonalité`).
|
||||
- Les tensions 6/7/9/11/13 restent actives et sont harmonisées **dans la gamme** (la 7e de G en Do majeur sera F naturel → G7, pas Gmaj7). Un astérisque dans le nom (`G7*`) signale un accord harmonisé diatoniquement.
|
||||
|
||||
Le mineur harmonique vaut le détour : le degré V y devient un vrai accord de dominante avec sensible — la cadence mineure classique à un doigt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Construire une progression
|
||||
|
||||
### 6.1 Manuellement
|
||||
Jouez un accord, puis **ADD** (ou barre espace). Il apparaît dans un slot avec son nom, ses notes et sa durée.
|
||||
|
||||
Sur chaque slot :
|
||||
- **badge durée** (`4♩`) — cliquer fait tourner 4 → 8 → 2 → 1 temps
|
||||
- **‹ ›** — déplacer le slot dans la séquence
|
||||
- **×** — supprimer
|
||||
- **clic sur le slot** — l'auditionner seul
|
||||
|
||||
### 6.2 Avec les presets
|
||||
Le menu déroulant propose huit cadences classiques (I–V–vi–IV, ii–V–I, cycle des quintes…). **Charger** les génère dans la tonalité et la gamme sélectionnées, avec vos tensions et votre voicing courants. C'est le moyen le plus rapide d'avoir une base à triturer.
|
||||
|
||||
### 6.3 Avec la suggestion harmonique
|
||||
**✦ suggérer** ajoute un accord choisi par une table d'harmonie fonctionnelle (après un I : IV, V, vi… ; après un ii : V ; etc.), dans la tonalité courante. Enchaînez les clics pour laisser la machine dériver, reprenez la main quand ça devient intéressant.
|
||||
|
||||
### 6.4 Voice-leading
|
||||
Deux outils pour des enchaînements fluides :
|
||||
- **VL AUTO** (transport) — chaque accord ajouté est automatiquement re-voicé pour minimiser le mouvement des voix par rapport au précédent.
|
||||
- **⇄ voice-leading** — optimise d'un coup toute la progression existante. À utiliser après un chargement de preset : la différence s'entend immédiatement.
|
||||
|
||||
---
|
||||
|
||||
## 7. Lecture et modes de performance
|
||||
|
||||
**PLAY** lance la progression au BPM affiché ; **LOOP** la fait boucler ; le slot en cours s'illumine.
|
||||
|
||||
Le sélecteur **perf** transforme la restitution de chaque accord :
|
||||
|
||||
| Mode | Rendu |
|
||||
|---|---|
|
||||
| block | accord plaqué |
|
||||
| strum ↑ / ↓ | notes égrenées vers le haut / le bas (façon guitare) |
|
||||
| harp | balayage rapide (glissando) |
|
||||
| arp ↑ / ↓ / ↕ / rnd | arpège à la vitesse du sélecteur **rate** (1/8, 1/16, 1/32) |
|
||||
|
||||
**HUM** humanise vélocités (±10) et timing (±15 ms) — utile pour que les strums et arps respirent.
|
||||
|
||||
> Ces modes s'appliquent partout : à l'audition, en MIDI live, et **dans l'export .mid** (désactivable, voir §9).
|
||||
|
||||
---
|
||||
|
||||
## 8. MIDI live vers le hardware
|
||||
|
||||
1. Branchez votre interface USB-MIDI **avant** d'ouvrir la page (ou rechargez-la après).
|
||||
2. Le navigateur demande l'autorisation d'accès MIDI — acceptez.
|
||||
3. Panneau **MIDI out (live)** : sélectionnez le périphérique.
|
||||
4. Vérifiez les canaux : **accords ch3** (Pro-800), **basse ch5** (TD-3/K-2) par défaut — adaptez à votre routage.
|
||||
5. Jouez : chaque accord cliqué, chaque slot auditionné et la lecture complète partent en MIDI, avec les modes de performance rendus.
|
||||
|
||||
Le LCD affiche `MIDI ON` quand un périphérique est actif. En cas de note bloquée sur une machine, **STOP** envoie un All Notes Off sur les 16 canaux.
|
||||
|
||||
> Contexte sécurisé requis : Web MIDI ne fonctionne qu'en `https://` ou sur `localhost`. C'est la raison principale d'héberger l'app (voir le tutoriel Coolify).
|
||||
|
||||
---
|
||||
|
||||
## 9. Export .mid
|
||||
|
||||
Panneau **Export MIDI** :
|
||||
|
||||
- **pistes** : `accords + basse` (2 pistes, chacune sur son canal) ou `accords seuls`
|
||||
- **perf rendue** : `oui` écrit les strums/arps note par note dans le fichier ; `non` exporte des accords plaqués (préférable si vous voulez arpéger dans le DAW ou le Tracker)
|
||||
|
||||
Le fichier généré (`chordlab_Cm7-Fm7-Ab-Bb_112bpm.mid`, format 1, PPQ 480, tempo inclus) respecte la durée individuelle de chaque slot.
|
||||
|
||||
**Destinations typiques** :
|
||||
- **DAW** — glisser-déposer sur une piste instrument, c'est tout.
|
||||
- **Polyend Tracker Mini** — copier le `.mid` sur la carte SD et l'importer ; chaque piste MIDI devient une piste du pattern.
|
||||
|
||||
---
|
||||
|
||||
## 10. Sessions
|
||||
|
||||
**↓ sauver la session** génère un `.json` contenant tous les réglages (type, tensions, voicing, drop, slash, tonalité, perf, BPM…) et la progression complète. **↑ charger une session** restaure tout.
|
||||
|
||||
L'application ne stocke rien dans le navigateur : le fichier `.json` est l'unique persistance. Versionnez vos sessions dans Gitea comme n'importe quel fichier de projet.
|
||||
|
||||
---
|
||||
|
||||
## 11. Raccourcis clavier
|
||||
|
||||
| Touche | Action |
|
||||
|---|---|
|
||||
| A W S E D F T G Y H U J | fondamentales C → B |
|
||||
| Espace | ADD (ajouter l'accord courant à la progression) |
|
||||
| ← / → | voicing −1 / +1 |
|
||||
| ↑ ↓ ← → (knob focalisé) | voicing fin |
|
||||
| double-clic sur le knob | voicing = 0 |
|
||||
|
||||
---
|
||||
|
||||
## 12. Deux workflows de référence
|
||||
|
||||
### Workflow dawless (session hardware)
|
||||
1. KEY ON, tonique et gamme du morceau.
|
||||
2. Preset ou ✦ suggérer pour poser 4–8 accords ; ⇄ voice-leading.
|
||||
3. MIDI live branché : accords ch3 → Pro-800 (nappe lente), basse ch5 → TD-3.
|
||||
4. LOOP + perf `block`, BPM du morceau : la progression tourne pendant que vous sculptez les sons sur les machines et enregistrez sur le L-12.
|
||||
5. Session sauvée en `.json` pour reprendre la structure plus tard.
|
||||
|
||||
### Workflow DAW / Tracker (composition)
|
||||
1. Construction libre (KEY OFF) : types + tensions + drop voicings pour chercher des couleurs.
|
||||
2. VL AUTO activé, on empile au fil de l'inspiration.
|
||||
3. Export `.mid` avec `perf rendue : non` (accords propres) → import dans le DAW ou le Tracker Mini.
|
||||
4. L'arpégiation et le sound design se font dans la machine de destination.
|
||||
Binary file not shown.
@@ -1,958 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>_chordlab — générateur d'accords</title>
|
||||
<style>
|
||||
:root{
|
||||
--panel:#E0D6C3; /* face avant beige */
|
||||
--panel-edge:#C9BCA4;
|
||||
--module:#26231D; /* modules charbon */
|
||||
--module-2:#332F27;
|
||||
--silk:#403A2F; /* sérigraphie sur beige */
|
||||
--silk-light:#D8CFBB; /* sérigraphie sur charbon */
|
||||
--lcd-bg:#141A12;
|
||||
--lcd-amber:#FFB454;
|
||||
--lcd-dim:#7A6A3F;
|
||||
--led-on:#FFAE33;
|
||||
--led-off:#4A4436;
|
||||
--led-red:#E5482B;
|
||||
--key-w:#EFE9DB;
|
||||
--key-b:#1C1A16;
|
||||
--accent:#E5482B;
|
||||
--mono:ui-monospace,"SF Mono","Cascadia Mono","JetBrains Mono",Menlo,Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{font-size:15px}
|
||||
body{
|
||||
font-family:var(--mono);
|
||||
background:#17150F;
|
||||
color:var(--silk);
|
||||
min-height:100vh;
|
||||
display:flex;justify-content:center;
|
||||
padding:1.2rem;
|
||||
}
|
||||
.unit{
|
||||
width:100%;max-width:1060px;
|
||||
background:linear-gradient(178deg,var(--panel) 0%,#D6CBB5 100%);
|
||||
border-radius:14px;
|
||||
border:1px solid var(--panel-edge);
|
||||
box-shadow:0 18px 50px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.5);
|
||||
padding:1.4rem 1.6rem 1.6rem;
|
||||
}
|
||||
header{
|
||||
display:flex;align-items:baseline;justify-content:space-between;
|
||||
border-bottom:2px solid var(--silk);
|
||||
padding-bottom:.6rem;margin-bottom:1rem;
|
||||
}
|
||||
header h1{font-size:1.15rem;font-weight:800;letter-spacing:.06em}
|
||||
header h1 span{color:var(--accent)}
|
||||
header .sub{font-size:.62rem;letter-spacing:.18em;text-transform:uppercase;opacity:.7}
|
||||
|
||||
/* ---------- LCD ---------- */
|
||||
.lcd{
|
||||
background:var(--lcd-bg);
|
||||
border-radius:8px;
|
||||
border:3px solid #0B0E0A;
|
||||
box-shadow:inset 0 0 24px rgba(0,0,0,.8), 0 1px 0 rgba(255,255,255,.35);
|
||||
padding:.8rem 1.1rem;
|
||||
display:grid;grid-template-columns:1fr auto;gap:.4rem 1rem;align-items:center;
|
||||
margin-bottom:1.1rem;
|
||||
min-height:86px;
|
||||
}
|
||||
.lcd .chord-name{
|
||||
font-size:2.1rem;font-weight:700;color:var(--lcd-amber);
|
||||
text-shadow:0 0 12px rgba(255,180,84,.45);
|
||||
line-height:1.1;
|
||||
min-height:2.4rem;
|
||||
}
|
||||
.lcd .chord-notes{font-size:.8rem;color:var(--lcd-dim);letter-spacing:.12em}
|
||||
.lcd .lcd-right{text-align:right;font-size:.68rem;color:var(--lcd-dim);line-height:1.7}
|
||||
.lcd .lcd-right b{color:var(--lcd-amber);font-weight:600}
|
||||
|
||||
/* ---------- Rangée principale ---------- */
|
||||
.deck{
|
||||
display:grid;
|
||||
grid-template-columns:250px 1fr 190px;
|
||||
gap:1rem;
|
||||
margin-bottom:1.1rem;
|
||||
}
|
||||
.mod{
|
||||
background:var(--module);
|
||||
border-radius:10px;
|
||||
padding:.8rem;
|
||||
box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 2px 6px rgba(0,0,0,.25);
|
||||
}
|
||||
.mod h2{
|
||||
font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;
|
||||
color:var(--silk-light);opacity:.65;margin-bottom:.6rem;
|
||||
}
|
||||
.btn-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem}
|
||||
.hwbtn{
|
||||
background:var(--module-2);
|
||||
border:1px solid #4A4436;
|
||||
border-radius:7px;
|
||||
color:var(--silk-light);
|
||||
font-family:var(--mono);
|
||||
font-size:.72rem;font-weight:600;
|
||||
padding:.75rem 0 .55rem;
|
||||
cursor:pointer;
|
||||
display:flex;flex-direction:column;align-items:center;gap:.4rem;
|
||||
transition:transform .05s;
|
||||
}
|
||||
.hwbtn:active{transform:translateY(1px)}
|
||||
.hwbtn .led{
|
||||
width:8px;height:8px;border-radius:50%;
|
||||
background:var(--led-off);
|
||||
box-shadow:inset 0 1px 2px rgba(0,0,0,.6);
|
||||
}
|
||||
.hwbtn.on .led{
|
||||
background:var(--led-on);
|
||||
box-shadow:0 0 8px var(--led-on), 0 0 2px var(--led-on);
|
||||
}
|
||||
.hwbtn:focus-visible{outline:2px solid var(--led-on);outline-offset:2px}
|
||||
.row-label{font-size:.55rem;letter-spacing:.16em;color:var(--silk-light);opacity:.45;margin:.6rem 0 .35rem;text-transform:uppercase}
|
||||
|
||||
/* ---------- Clavier ---------- */
|
||||
.kbd-mod{display:flex;flex-direction:column}
|
||||
.kbd{position:relative;flex:1;min-height:170px;display:flex;border-radius:6px;overflow:hidden}
|
||||
.wkey{
|
||||
flex:1;background:var(--key-w);
|
||||
border:1px solid #B9AE97;border-top:none;
|
||||
cursor:pointer;position:relative;
|
||||
display:flex;align-items:flex-end;justify-content:center;
|
||||
padding-bottom:.4rem;font-size:.6rem;color:#8A8070;
|
||||
}
|
||||
.wkey:active,.wkey.pressed{background:#D9CFBA}
|
||||
.wkey.outkey{background:#DDD6C8;color:#B5AC98}
|
||||
.bkey{
|
||||
position:absolute;top:0;width:9%;height:58%;
|
||||
background:var(--key-b);
|
||||
border-radius:0 0 4px 4px;
|
||||
cursor:pointer;z-index:2;
|
||||
box-shadow:0 3px 5px rgba(0,0,0,.4);
|
||||
}
|
||||
.bkey:active,.bkey.pressed{background:#3A362C}
|
||||
.bkey.outkey{background:#4B4739}
|
||||
.kbd-hint{font-size:.55rem;color:var(--silk-light);opacity:.4;margin-top:.5rem;letter-spacing:.08em}
|
||||
|
||||
/* ---------- Voicing / bass / key ---------- */
|
||||
.side{display:flex;flex-direction:column;gap:.8rem}
|
||||
.knob-wrap{display:flex;flex-direction:column;align-items:center;gap:.3rem}
|
||||
.knob{width:74px;height:74px;cursor:ns-resize;touch-action:none}
|
||||
.knob circle.body{fill:#3A362C;stroke:#4A4436;stroke-width:2}
|
||||
.knob line.ind{stroke:var(--led-on);stroke-width:3;stroke-linecap:round}
|
||||
.knob-val{font-size:.75rem;color:var(--silk-light)}
|
||||
.mini-row{display:flex;gap:.4rem;align-items:center;justify-content:center}
|
||||
.mini-btn{
|
||||
background:var(--module-2);border:1px solid #4A4436;border-radius:5px;
|
||||
color:var(--silk-light);font-family:var(--mono);font-size:.7rem;
|
||||
padding:.25rem .55rem;cursor:pointer;
|
||||
}
|
||||
select,input[type=number]{
|
||||
background:var(--module-2);border:1px solid #4A4436;border-radius:5px;
|
||||
color:var(--silk-light);font-family:var(--mono);font-size:.7rem;
|
||||
padding:.3rem .35rem;width:100%;
|
||||
}
|
||||
.toggle-line{display:flex;align-items:center;gap:.5rem;font-size:.65rem;color:var(--silk-light)}
|
||||
.toggle-line .hwbtn{flex-direction:row;padding:.4rem .6rem;gap:.4rem}
|
||||
|
||||
/* ---------- Progression ---------- */
|
||||
.prog-mod{margin-bottom:1.1rem}
|
||||
.prog-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem}
|
||||
.prog-slots{display:flex;gap:.5rem;flex-wrap:wrap;min-height:64px}
|
||||
.slot{
|
||||
background:var(--module-2);
|
||||
border:1px solid #4A4436;border-radius:7px;
|
||||
padding:.45rem .6rem;min-width:96px;
|
||||
color:var(--silk-light);cursor:pointer;position:relative;
|
||||
font-size:.72rem;
|
||||
}
|
||||
.slot.playing{border-color:var(--led-on);box-shadow:0 0 10px rgba(255,174,51,.35)}
|
||||
.slot .sname{font-weight:700;color:var(--lcd-amber);font-size:.85rem}
|
||||
.slot .snotes{opacity:.55;font-size:.58rem;margin-top:.15rem}
|
||||
.slot .del{
|
||||
position:absolute;top:2px;right:5px;color:var(--silk-light);
|
||||
opacity:.4;font-size:.7rem;cursor:pointer;background:none;border:none;font-family:var(--mono);
|
||||
}
|
||||
.slot .del:hover{opacity:1;color:var(--led-red)}
|
||||
.slot-empty{
|
||||
border:1px dashed #4A4436;border-radius:7px;min-width:96px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
color:var(--silk-light);opacity:.35;font-size:.62rem;padding:.45rem .6rem;
|
||||
}
|
||||
.transport{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.7rem}
|
||||
.transport .hwbtn{flex-direction:row;padding:.5rem .8rem;gap:.45rem}
|
||||
.transport .hwbtn.rec .led{background:var(--led-off)}
|
||||
.transport .hwbtn.playing .led{background:var(--led-red);box-shadow:0 0 8px var(--led-red)}
|
||||
.bpm-wrap{display:flex;align-items:center;gap:.35rem;color:var(--silk-light);font-size:.68rem}
|
||||
.bpm-wrap input{width:64px}
|
||||
.preset-wrap{margin-left:auto;display:flex;gap:.4rem;align-items:center}
|
||||
.preset-wrap select{width:auto}
|
||||
|
||||
/* ---------- IO ---------- */
|
||||
.io{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.io-line{display:grid;grid-template-columns:auto 1fr;gap:.5rem;align-items:center;font-size:.65rem;color:var(--silk-light);margin-bottom:.45rem}
|
||||
.io-line label{opacity:.6;white-space:nowrap}
|
||||
.status{font-size:.6rem;color:var(--silk-light);opacity:.55;margin-top:.4rem;min-height:1em}
|
||||
.export-btn{
|
||||
width:100%;background:var(--accent);border:none;border-radius:7px;
|
||||
color:#FFF3E4;font-family:var(--mono);font-weight:700;font-size:.8rem;
|
||||
padding:.7rem;cursor:pointer;letter-spacing:.05em;margin-top:.3rem;
|
||||
}
|
||||
.export-btn:active{transform:translateY(1px)}
|
||||
|
||||
footer{margin-top:1rem;font-size:.55rem;opacity:.5;letter-spacing:.1em;text-align:right}
|
||||
|
||||
@media (max-width:860px){
|
||||
.deck{grid-template-columns:1fr}
|
||||
.io{grid-template-columns:1fr}
|
||||
.kbd{min-height:140px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
*{transition:none !important}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="unit">
|
||||
<header>
|
||||
<h1>_chordlab <span>ORC</span></h1>
|
||||
<div class="sub">chord ideas machine · underscorelab / leradiologue</div>
|
||||
</header>
|
||||
|
||||
<!-- LCD -->
|
||||
<div class="lcd" role="status" aria-live="polite">
|
||||
<div>
|
||||
<div class="chord-name" id="lcdName">—</div>
|
||||
<div class="chord-notes" id="lcdNotes">joue une note</div>
|
||||
</div>
|
||||
<div class="lcd-right">
|
||||
<div>VOICING <b id="lcdVoicing">0</b></div>
|
||||
<div>KEY <b id="lcdKey">OFF</b></div>
|
||||
<div>MIDI <b id="lcdMidi">—</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rangée principale -->
|
||||
<div class="deck">
|
||||
|
||||
<!-- Types + extensions -->
|
||||
<div class="mod">
|
||||
<h2>Chord type</h2>
|
||||
<div class="btn-grid" id="typeBtns">
|
||||
<button class="hwbtn" data-type="dim"><span class="led"></span>DIM</button>
|
||||
<button class="hwbtn" data-type="min"><span class="led"></span>MIN</button>
|
||||
<button class="hwbtn on" data-type="maj"><span class="led"></span>MAJ</button>
|
||||
<button class="hwbtn" data-type="sus"><span class="led"></span>SUS</button>
|
||||
</div>
|
||||
<div class="row-label">Modifiers</div>
|
||||
<div class="btn-grid" id="extBtns">
|
||||
<button class="hwbtn" data-ext="6"><span class="led"></span>6</button>
|
||||
<button class="hwbtn" data-ext="m7"><span class="led"></span>m7</button>
|
||||
<button class="hwbtn" data-ext="M7"><span class="led"></span>M7</button>
|
||||
<button class="hwbtn" data-ext="9"><span class="led"></span>9</button>
|
||||
</div>
|
||||
<div class="row-label" style="opacity:.3">combos : dim+maj=aug · min+sus=sus2</div>
|
||||
</div>
|
||||
|
||||
<!-- Clavier -->
|
||||
<div class="mod kbd-mod">
|
||||
<h2>Root</h2>
|
||||
<div class="kbd" id="kbd"></div>
|
||||
<div class="kbd-hint">clavier PC : A W S E D F T G Y H U J · ␣ = ajouter à la progression</div>
|
||||
</div>
|
||||
|
||||
<!-- Voicing / bass / key -->
|
||||
<div class="mod side">
|
||||
<h2>Voicing</h2>
|
||||
<div class="knob-wrap">
|
||||
<svg class="knob" id="voicingKnob" viewBox="0 0 74 74" role="slider" aria-label="Voicing" aria-valuemin="-8" aria-valuemax="8" aria-valuenow="0" tabindex="0">
|
||||
<circle class="body" cx="37" cy="37" r="33"/>
|
||||
<line class="ind" id="knobInd" x1="37" y1="37" x2="37" y2="10"/>
|
||||
</svg>
|
||||
<div class="knob-val" id="knobVal">0</div>
|
||||
<div class="mini-row">
|
||||
<button class="mini-btn" id="voicDown">−</button>
|
||||
<button class="mini-btn" id="voicReset">0</button>
|
||||
<button class="mini-btn" id="voicUp">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-line">
|
||||
<button class="hwbtn" id="bassBtn"><span class="led"></span>BASS</button>
|
||||
<select id="bassOct" title="Octave basse">
|
||||
<option value="-24">−2 oct</option>
|
||||
<option value="-12" selected>−1 oct</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toggle-line">
|
||||
<button class="hwbtn" id="keyBtn"><span class="led"></span>KEY</button>
|
||||
<select id="keyRoot"></select>
|
||||
<select id="keyMode">
|
||||
<option value="major">maj</option>
|
||||
<option value="minor">min</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progression -->
|
||||
<div class="mod prog-mod">
|
||||
<div class="prog-head">
|
||||
<h2 style="margin:0">Progression</h2>
|
||||
<div class="preset-wrap">
|
||||
<select id="presetSel">
|
||||
<option value="">— preset (dans la tonalité KEY) —</option>
|
||||
<option value="1,5,6,4">I – V – vi – IV</option>
|
||||
<option value="2,5,1">ii – V – I</option>
|
||||
<option value="1,6,4,5">I – vi – IV – V</option>
|
||||
<option value="1,4,6,5">I – IV – vi – V</option>
|
||||
<option value="6,4,1,5">vi – IV – I – V</option>
|
||||
<option value="1,7,6,7">i – VII – VI – VII (min)</option>
|
||||
<option value="1,3,7,6">i – III – VII – VI (min)</option>
|
||||
</select>
|
||||
<button class="mini-btn" id="presetLoad">charger</button>
|
||||
<button class="mini-btn" id="progClear">vider</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prog-slots" id="progSlots"></div>
|
||||
<div class="transport">
|
||||
<button class="hwbtn" id="addBtn"><span class="led"></span>ADD (␣)</button>
|
||||
<button class="hwbtn" id="playBtn"><span class="led"></span>PLAY</button>
|
||||
<button class="hwbtn" id="loopBtn"><span class="led"></span>LOOP</button>
|
||||
<div class="bpm-wrap">BPM <input type="number" id="bpm" value="112" min="30" max="300"></div>
|
||||
<div class="bpm-wrap">durée/accord
|
||||
<select id="chordLen" style="width:auto">
|
||||
<option value="4">1 mesure</option>
|
||||
<option value="2">1/2 mesure</option>
|
||||
<option value="8">2 mesures</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IO -->
|
||||
<div class="io">
|
||||
<div class="mod">
|
||||
<h2>MIDI out (live)</h2>
|
||||
<div class="io-line"><label>périphérique</label><select id="midiDev"><option value="">— désactivé —</option></select></div>
|
||||
<div class="io-line"><label>canal accords</label><select id="chChord"></select></div>
|
||||
<div class="io-line"><label>canal basse</label><select id="chBass"></select></div>
|
||||
<div class="status" id="midiStatus">Web MIDI : en attente…</div>
|
||||
</div>
|
||||
<div class="mod">
|
||||
<h2>Export</h2>
|
||||
<div class="io-line"><label>nom du fichier</label><input type="number" style="display:none"><select id="exportSplit">
|
||||
<option value="split">accords + basse (2 pistes)</option>
|
||||
<option value="chords">accords seuls</option>
|
||||
</select></div>
|
||||
<button class="export-btn" id="exportBtn">↓ exporter .mid</button>
|
||||
<div class="status" id="exportStatus"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>_chordlab v1.0 · logique d'accords inspirée de l'Orchid ORC-1 (implémentation indépendante)</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
/* =========================================================
|
||||
_chordlab — générateur d'accords façon Orchid
|
||||
Moteur : root + type + extensions → set d'intervalles
|
||||
Voicing : rotation des notes (inversions en cascade)
|
||||
========================================================= */
|
||||
|
||||
const NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];
|
||||
const BASE_OCTAVE_MIDI = 60; // C4
|
||||
|
||||
/* ---------- État ---------- */
|
||||
const state = {
|
||||
types: ["maj"], // ordre de sélection
|
||||
exts: new Set(),
|
||||
voicing: 0, // -8..+8 rotations
|
||||
bass: false,
|
||||
bassOct: -12,
|
||||
keyMode: false,
|
||||
keyRoot: 0, // 0 = C
|
||||
keyScale: "major",
|
||||
lastRoot: null, // dernière note jouée (0..11)
|
||||
progression: [], // [{label, notes[], bass, rootPc}]
|
||||
playing: false,
|
||||
loop: false,
|
||||
playIdx: -1,
|
||||
playTimer: null,
|
||||
liveNotes: [], // notes MIDI actuellement tenues (live audition)
|
||||
};
|
||||
|
||||
/* ---------- Moteur d'accords ---------- */
|
||||
const TYPE_INTERVALS = {
|
||||
dim:[0,3,6], min:[0,3,7], maj:[0,4,7], sus:[0,5,7],
|
||||
aug:[0,4,8], sus2:[0,2,7],
|
||||
};
|
||||
const EXT_INTERVALS = { "6":9, "m7":10, "M7":11, "9":14 };
|
||||
|
||||
function resolveType(types){
|
||||
const s = new Set(types);
|
||||
if (s.has("dim") && s.has("maj")) return "aug";
|
||||
if (s.has("min") && s.has("sus")) return "sus2";
|
||||
return types[types.length-1] || "maj";
|
||||
}
|
||||
|
||||
function chordName(rootPc, type, exts){
|
||||
const base = {maj:"", min:"m", dim:"dim", sus:"sus4", aug:"aug", sus2:"sus2"}[type];
|
||||
let name = NOTE_NAMES[rootPc] + base;
|
||||
const e = new Set(exts);
|
||||
// cas nommés proprement
|
||||
if (type==="dim" && e.has("6") && !e.has("m7") && !e.has("M7")) { name = NOTE_NAMES[rootPc]+"dim7"; e.delete("6"); }
|
||||
else if (type==="dim" && e.has("m7")) { name = NOTE_NAMES[rootPc]+"ø7"; e.delete("m7"); }
|
||||
if (e.has("m7") && e.has("M7")) return name + " WTF?"; // hommage
|
||||
if (e.has("m7")) { name += "7"; e.delete("m7"); }
|
||||
if (e.has("M7")) { name += "maj7"; e.delete("M7"); }
|
||||
if (e.has("6")) { name += "6"; e.delete("6"); }
|
||||
if (e.has("9")) { name += (name===NOTE_NAMES[rootPc]+ "" || /7|6/.test(name)) ? "9".replace("9", name.match(/7|6/)?"(9)":"add9") : "add9"; }
|
||||
return name;
|
||||
}
|
||||
|
||||
/* Construit les notes MIDI (avant voicing) */
|
||||
function buildChord(rootPc, type, exts){
|
||||
const intervals = new Set(TYPE_INTERVALS[type]);
|
||||
exts.forEach(e => intervals.add(EXT_INTERVALS[e]));
|
||||
const root = BASE_OCTAVE_MIDI + rootPc;
|
||||
return [...intervals].sort((a,b)=>a-b).map(i => root + i);
|
||||
}
|
||||
|
||||
/* Voicing = rotation : +1 → la note la plus basse monte d'une octave */
|
||||
function applyVoicing(notes, steps){
|
||||
let n = [...notes].sort((a,b)=>a-b);
|
||||
const k = Math.abs(steps);
|
||||
for (let i=0;i<k;i++){
|
||||
if (steps>0){ n[0]+=12; } else { n[n.length-1]-=12; }
|
||||
n.sort((a,b)=>a-b);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------- Key Mode : accords diatoniques ---------- */
|
||||
const SCALES = { major:[0,2,4,5,7,9,11], minor:[0,2,3,5,7,8,10] };
|
||||
|
||||
function diatonicChord(notePc, keyRoot, scaleName, exts){
|
||||
const scale = SCALES[scaleName];
|
||||
const rel = (notePc - keyRoot + 12) % 12;
|
||||
const deg = scale.indexOf(rel);
|
||||
if (deg === -1) return null; // hors tonalité
|
||||
const pick = j => keyRoot + scale[(deg+j)%7] + 12*Math.floor((deg+j)/7);
|
||||
const semis = [pick(0), pick(2), pick(4)]; // triade en tierces
|
||||
if (exts.has("6")) semis.push(pick(5));
|
||||
if (exts.has("m7") || exts.has("M7")) semis.push(pick(6)); // 7e diatonique
|
||||
if (exts.has("9")) semis.push(pick(8));
|
||||
const root = BASE_OCTAVE_MIDI + notePc;
|
||||
const notes = semis.map(s => BASE_OCTAVE_MIDI + s);
|
||||
// normalisation : ancrer la fondamentale sur la note jouée
|
||||
const shift = root - notes[0];
|
||||
const final = notes.map(n => n + shift).sort((a,b)=>a-b);
|
||||
// nom : détecter la qualité de la triade
|
||||
const i1 = final[1]-final[0], i2 = final[2]-final[0];
|
||||
let q = "";
|
||||
if (i1===4 && i2===7) q=""; else if (i1===3 && i2===7) q="m";
|
||||
else if (i1===3 && i2===6) q="dim"; else if (i1===4 && i2===8) q="aug";
|
||||
let name = NOTE_NAMES[notePc] + q;
|
||||
if (exts.has("m7")||exts.has("M7")) name += "7*";
|
||||
if (exts.has("6")) name += "6*";
|
||||
if (exts.has("9")) name += "(9)*";
|
||||
return {notes: final, name};
|
||||
}
|
||||
|
||||
/* ---------- Accord courant ---------- */
|
||||
function currentChord(rootPc){
|
||||
if (state.keyMode){
|
||||
const d = diatonicChord(rootPc, state.keyRoot, state.keyScale, state.exts);
|
||||
if (!d) return null;
|
||||
return {name:d.name, notes:applyVoicing(d.notes, state.voicing), rootPc};
|
||||
}
|
||||
const type = resolveType(state.types);
|
||||
const notes = applyVoicing(buildChord(rootPc, type, [...state.exts]), state.voicing);
|
||||
return {name:chordName(rootPc, type, [...state.exts]), notes, rootPc};
|
||||
}
|
||||
|
||||
function bassNote(rootPc){ return BASE_OCTAVE_MIDI + rootPc + state.bassOct; }
|
||||
|
||||
/* =========================================================
|
||||
AUDIO — petit poly Web Audio (2 saws désaccordées + LP)
|
||||
========================================================= */
|
||||
let AC = null, master = null;
|
||||
function ensureAudio(){
|
||||
if (AC) return;
|
||||
AC = new (window.AudioContext||window.webkitAudioContext)();
|
||||
master = AC.createGain(); master.gain.value = 0.5;
|
||||
const comp = AC.createDynamicsCompressor();
|
||||
master.connect(comp); comp.connect(AC.destination);
|
||||
}
|
||||
function playNote(midi, dur, vel=0.8, isBass=false){
|
||||
ensureAudio();
|
||||
const t = AC.currentTime;
|
||||
const f = 440 * Math.pow(2,(midi-69)/12);
|
||||
const g = AC.createGain();
|
||||
const lp = AC.createBiquadFilter();
|
||||
lp.type="lowpass";
|
||||
lp.frequency.value = isBass ? 700 : 2200;
|
||||
lp.Q.value = 0.7;
|
||||
const oscs = [];
|
||||
const detunes = isBass ? [0] : [-6, 6];
|
||||
detunes.forEach(d=>{
|
||||
const o = AC.createOscillator();
|
||||
o.type = isBass ? "triangle" : "sawtooth";
|
||||
o.frequency.value = f;
|
||||
o.detune.value = d;
|
||||
o.connect(lp); oscs.push(o);
|
||||
});
|
||||
lp.connect(g); g.connect(master);
|
||||
const peak = (isBass?0.28:0.14) * vel;
|
||||
g.gain.setValueAtTime(0, t);
|
||||
g.gain.linearRampToValueAtTime(peak, t+0.015);
|
||||
g.gain.setTargetAtTime(peak*0.7, t+0.05, 0.25);
|
||||
g.gain.setTargetAtTime(0, t+dur, 0.12);
|
||||
oscs.forEach(o=>{ o.start(t); o.stop(t+dur+0.8); });
|
||||
}
|
||||
function playChordAudio(chord, dur){
|
||||
chord.notes.forEach(n => playNote(n, dur, 0.8, false));
|
||||
if (state.bass) playNote(bassNote(chord.rootPc), dur, 0.9, true);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
WEB MIDI — sortie live
|
||||
========================================================= */
|
||||
let midiAccess = null, midiOut = null;
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
function midiInit(){
|
||||
if (!navigator.requestMIDIAccess){
|
||||
$("midiStatus").textContent = "Web MIDI non supporté (utilise Chrome/Edge/Firefox).";
|
||||
$("lcdMidi").textContent = "N/A";
|
||||
return;
|
||||
}
|
||||
navigator.requestMIDIAccess({sysex:false}).then(acc=>{
|
||||
midiAccess = acc;
|
||||
refreshMidiDevices();
|
||||
acc.onstatechange = refreshMidiDevices;
|
||||
$("midiStatus").textContent = "Web MIDI prêt.";
|
||||
}).catch(()=>{ $("midiStatus").textContent = "Accès MIDI refusé."; });
|
||||
}
|
||||
function refreshMidiDevices(){
|
||||
const sel = $("midiDev");
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">— désactivé —</option>';
|
||||
midiAccess.outputs.forEach(out=>{
|
||||
const o = document.createElement("option");
|
||||
o.value = out.id; o.textContent = out.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.value = cur;
|
||||
selectMidiDevice();
|
||||
}
|
||||
function selectMidiDevice(){
|
||||
const id = $("midiDev").value;
|
||||
midiOut = id ? midiAccess.outputs.get(id) : null;
|
||||
$("lcdMidi").textContent = midiOut ? "ON" : "—";
|
||||
}
|
||||
function midiSend(chord, durMs){
|
||||
if (!midiOut) return;
|
||||
const chC = parseInt($("chChord").value)-1;
|
||||
const chB = parseInt($("chBass").value)-1;
|
||||
const now = performance.now();
|
||||
chord.notes.forEach(n=>{
|
||||
midiOut.send([0x90|chC, n, 100]);
|
||||
midiOut.send([0x80|chC, n, 0], now+durMs);
|
||||
});
|
||||
if (state.bass){
|
||||
const b = bassNote(chord.rootPc);
|
||||
midiOut.send([0x90|chB, b, 110]);
|
||||
midiOut.send([0x80|chB, b, 0], now+durMs);
|
||||
}
|
||||
}
|
||||
function midiPanic(){
|
||||
if (!midiOut) return;
|
||||
for (let ch=0; ch<16; ch++) midiOut.send([0xB0|ch, 123, 0]);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
EXPORT .mid — writer minimal (format 1, PPQ 480)
|
||||
========================================================= */
|
||||
function vlq(n){
|
||||
const bytes=[n&0x7F];
|
||||
while(n>>=7) bytes.unshift((n&0x7F)|0x80);
|
||||
return bytes;
|
||||
}
|
||||
function trackChunk(events){
|
||||
// events: [{dt, data:[...]}]
|
||||
const body=[];
|
||||
events.forEach(e=>{ body.push(...vlq(e.dt), ...e.data); });
|
||||
body.push(0x00,0xFF,0x2F,0x00); // end of track
|
||||
const len = body.length;
|
||||
return [0x4D,0x54,0x72,0x6B,(len>>>24)&255,(len>>>16)&255,(len>>>8)&255,len&255,...body];
|
||||
}
|
||||
function exportMidi(){
|
||||
if (!state.progression.length){
|
||||
$("exportStatus").textContent = "Progression vide — ajoute des accords d'abord.";
|
||||
return;
|
||||
}
|
||||
const PPQ = 480;
|
||||
const bpm = Math.max(30, Math.min(300, parseInt($("bpm").value)||112));
|
||||
const beats = parseInt($("chordLen").value);
|
||||
const ticksPerChord = beats*PPQ;
|
||||
const gate = Math.round(ticksPerChord*0.97);
|
||||
const tempo = Math.round(60000000/bpm);
|
||||
const split = $("exportSplit").value;
|
||||
const chC = parseInt($("chChord").value)-1;
|
||||
const chB = parseInt($("chBass").value)-1;
|
||||
|
||||
const meta = [
|
||||
{dt:0, data:[0xFF,0x51,0x03,(tempo>>16)&255,(tempo>>8)&255,tempo&255]},
|
||||
{dt:0, data:[0xFF,0x58,0x04,4,2,24,8]},
|
||||
];
|
||||
|
||||
const chordEv = [];
|
||||
const bassEv = [];
|
||||
state.progression.forEach((c,i)=>{
|
||||
const at = i*ticksPerChord;
|
||||
// note-ons groupés à dt calculé depuis la fin du précédent gate
|
||||
c.notes.forEach((n,j)=>{
|
||||
chordEv.push({t:at, on:true, n, ch:chC});
|
||||
chordEv.push({t:at+gate, on:false, n, ch:chC});
|
||||
});
|
||||
if (c.bass !== null && c.bass !== undefined){
|
||||
bassEv.push({t:at, on:true, n:c.bass, ch:chB});
|
||||
bassEv.push({t:at+gate, on:false, n:c.bass, ch:chB});
|
||||
}
|
||||
});
|
||||
const toTrack = evs=>{
|
||||
evs.sort((a,b)=> a.t-b.t || (a.on===b.on?0:(a.on?1:-1)));
|
||||
let last=0; const out=[];
|
||||
evs.forEach(e=>{
|
||||
out.push({dt:e.t-last, data:[(e.on?0x90:0x80)|e.ch, e.n, e.on?100:0]});
|
||||
last=e.t;
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
const tracks = [trackChunk([...meta, ...toTrack(chordEv)])];
|
||||
if (split==="split" && bassEv.length) tracks.push(trackChunk(toTrack(bassEv)));
|
||||
|
||||
const nTracks = tracks.length;
|
||||
const header = [0x4D,0x54,0x68,0x64,0,0,0,6, 0,1, 0,nTracks, (PPQ>>8)&255, PPQ&255];
|
||||
const bytes = new Uint8Array([...header, ...tracks.flat()]);
|
||||
const blob = new Blob([bytes], {type:"audio/midi"});
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
const labels = state.progression.map(c=>c.label.replace(/[^\w#]/g,"")).slice(0,4).join("-");
|
||||
a.download = `chordlab_${labels}_${bpm}bpm.mid`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
$("exportStatus").textContent = `Exporté : ${a.download}`;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
UI
|
||||
========================================================= */
|
||||
|
||||
/* ----- clavier ----- */
|
||||
const WHITE = [0,2,4,5,7,9,11];
|
||||
const BLACK = [{pc:1,pos:0},{pc:3,pos:1},{pc:6,pos:3},{pc:8,pos:4},{pc:10,pos:5}];
|
||||
const kbdEl = $("kbd");
|
||||
const keyEls = {};
|
||||
function buildKeyboard(){
|
||||
WHITE.forEach(pc=>{
|
||||
const k = document.createElement("div");
|
||||
k.className="wkey"; k.dataset.pc=pc;
|
||||
k.textContent = NOTE_NAMES[pc];
|
||||
k.setAttribute("role","button");
|
||||
kbdEl.appendChild(k); keyEls[pc]=k;
|
||||
});
|
||||
BLACK.forEach(({pc,pos})=>{
|
||||
const k = document.createElement("div");
|
||||
k.className="bkey"; k.dataset.pc=pc;
|
||||
k.style.left = `calc(${(pos+1)*(100/7)}% - 4.5%)`;
|
||||
kbdEl.appendChild(k); keyEls[pc]=k;
|
||||
});
|
||||
}
|
||||
function refreshKeyDimming(){
|
||||
const scale = SCALES[state.keyScale];
|
||||
Object.entries(keyEls).forEach(([pc,el])=>{
|
||||
const inKey = !state.keyMode || scale.includes((pc - state.keyRoot + 12)%12);
|
||||
el.classList.toggle("outkey", !inKey);
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- déclenchement d'accord ----- */
|
||||
function triggerChord(rootPc, fromPlayback=false){
|
||||
const chord = currentChord(rootPc);
|
||||
if (!chord){
|
||||
$("lcdName").textContent = "×";
|
||||
$("lcdNotes").textContent = "hors tonalité";
|
||||
return null;
|
||||
}
|
||||
state.lastRoot = rootPc;
|
||||
$("lcdName").textContent = chord.name;
|
||||
$("lcdNotes").textContent = chord.notes.map(n=>NOTE_NAMES[n%12]+(Math.floor(n/12)-1)).join(" · ")
|
||||
+ (state.bass ? " | bass "+NOTE_NAMES[bassNote(rootPc)%12] : "");
|
||||
if (!fromPlayback){
|
||||
const durS = 1.4;
|
||||
playChordAudio(chord, durS);
|
||||
midiSend(chord, durS*1000);
|
||||
}
|
||||
return chord;
|
||||
}
|
||||
|
||||
/* ----- boutons type/ext ----- */
|
||||
document.querySelectorAll("#typeBtns .hwbtn").forEach(btn=>{
|
||||
btn.addEventListener("click", ()=>{
|
||||
const t = btn.dataset.type;
|
||||
const i = state.types.indexOf(t);
|
||||
if (i>=0){
|
||||
if (state.types.length>1) state.types.splice(i,1);
|
||||
} else {
|
||||
const test = [...state.types, t];
|
||||
const s = new Set(test);
|
||||
const combo = (s.has("dim")&&s.has("maj")) || (s.has("min")&&s.has("sus"));
|
||||
state.types = (test.length<=1 || (test.length===2 && combo)) ? test : [t];
|
||||
}
|
||||
document.querySelectorAll("#typeBtns .hwbtn").forEach(b=>
|
||||
b.classList.toggle("on", state.types.includes(b.dataset.type)));
|
||||
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#extBtns .hwbtn").forEach(btn=>{
|
||||
btn.addEventListener("click", ()=>{
|
||||
const e = btn.dataset.ext;
|
||||
state.exts.has(e) ? state.exts.delete(e) : state.exts.add(e);
|
||||
btn.classList.toggle("on", state.exts.has(e));
|
||||
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
||||
});
|
||||
});
|
||||
|
||||
/* ----- clavier souris + PC ----- */
|
||||
kbdEl.addEventListener("pointerdown", ev=>{
|
||||
const pc = ev.target.dataset.pc;
|
||||
if (pc===undefined) return;
|
||||
ev.target.classList.add("pressed");
|
||||
triggerChord(parseInt(pc));
|
||||
});
|
||||
kbdEl.addEventListener("pointerup", ev=>{
|
||||
ev.target.classList && ev.target.classList.remove("pressed");
|
||||
});
|
||||
const PCKEYS = {a:0,w:1,s:2,e:3,d:4,f:5,t:6,g:7,y:8,h:9,u:10,j:11};
|
||||
window.addEventListener("keydown", ev=>{
|
||||
if (ev.repeat || ev.target.tagName==="INPUT" || ev.target.tagName==="SELECT") return;
|
||||
const k = ev.key.toLowerCase();
|
||||
if (k in PCKEYS){
|
||||
const pc = PCKEYS[k];
|
||||
keyEls[pc].classList.add("pressed");
|
||||
triggerChord(pc);
|
||||
} else if (ev.code==="Space"){
|
||||
ev.preventDefault();
|
||||
addToProgression();
|
||||
}
|
||||
});
|
||||
window.addEventListener("keyup", ev=>{
|
||||
const k = ev.key.toLowerCase();
|
||||
if (k in PCKEYS) keyEls[PCKEYS[k]].classList.remove("pressed");
|
||||
});
|
||||
|
||||
/* ----- knob voicing ----- */
|
||||
const knob = $("voicingKnob");
|
||||
function setVoicing(v){
|
||||
state.voicing = Math.max(-8, Math.min(8, v));
|
||||
const angle = state.voicing * 16; // ±128°
|
||||
const rad = (angle-90)*Math.PI/180;
|
||||
$("knobInd").setAttribute("x2", 37 + 26*Math.cos(rad));
|
||||
$("knobInd").setAttribute("y2", 37 + 26*Math.sin(rad));
|
||||
$("knobVal").textContent = state.voicing>0 ? "+"+state.voicing : state.voicing;
|
||||
$("lcdVoicing").textContent = $("knobVal").textContent;
|
||||
knob.setAttribute("aria-valuenow", state.voicing);
|
||||
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
||||
}
|
||||
let dragY=null, dragStart=0;
|
||||
knob.addEventListener("pointerdown", ev=>{ dragY=ev.clientY; dragStart=state.voicing; knob.setPointerCapture(ev.pointerId); });
|
||||
knob.addEventListener("pointermove", ev=>{
|
||||
if (dragY===null) return;
|
||||
const dv = Math.round((dragY-ev.clientY)/14);
|
||||
if (dragStart+dv !== state.voicing) setVoicing(dragStart+dv);
|
||||
});
|
||||
knob.addEventListener("pointerup", ()=>{ dragY=null; });
|
||||
knob.addEventListener("dblclick", ()=>setVoicing(0));
|
||||
knob.addEventListener("keydown", ev=>{
|
||||
if (ev.key==="ArrowUp"||ev.key==="ArrowRight"){ setVoicing(state.voicing+1); ev.preventDefault(); }
|
||||
if (ev.key==="ArrowDown"||ev.key==="ArrowLeft"){ setVoicing(state.voicing-1); ev.preventDefault(); }
|
||||
});
|
||||
$("voicUp").addEventListener("click", ()=>setVoicing(state.voicing+1));
|
||||
$("voicDown").addEventListener("click", ()=>setVoicing(state.voicing-1));
|
||||
$("voicReset").addEventListener("click", ()=>setVoicing(0));
|
||||
|
||||
/* ----- bass & key ----- */
|
||||
$("bassBtn").addEventListener("click", function(){
|
||||
state.bass = !state.bass;
|
||||
this.classList.toggle("on", state.bass);
|
||||
if (state.lastRoot!==null) triggerChord(state.lastRoot);
|
||||
});
|
||||
$("bassOct").addEventListener("change", function(){ state.bassOct = parseInt(this.value); });
|
||||
NOTE_NAMES.forEach((n,i)=>{
|
||||
const o = document.createElement("option");
|
||||
o.value=i; o.textContent=n;
|
||||
$("keyRoot").appendChild(o);
|
||||
});
|
||||
$("keyBtn").addEventListener("click", function(){
|
||||
state.keyMode = !state.keyMode;
|
||||
this.classList.toggle("on", state.keyMode);
|
||||
updateKeyLcd(); refreshKeyDimming();
|
||||
});
|
||||
$("keyRoot").addEventListener("change", function(){ state.keyRoot=parseInt(this.value); updateKeyLcd(); refreshKeyDimming(); });
|
||||
$("keyMode").addEventListener("change", function(){ state.keyScale=this.value; updateKeyLcd(); refreshKeyDimming(); });
|
||||
function updateKeyLcd(){
|
||||
$("lcdKey").textContent = state.keyMode
|
||||
? NOTE_NAMES[state.keyRoot] + (state.keyScale==="major"?" maj":" min")
|
||||
: "OFF";
|
||||
}
|
||||
|
||||
/* ----- progression ----- */
|
||||
function addToProgression(){
|
||||
if (state.lastRoot===null) return;
|
||||
const chord = currentChord(state.lastRoot);
|
||||
if (!chord) return;
|
||||
state.progression.push({
|
||||
label: chord.name,
|
||||
notes: [...chord.notes],
|
||||
bass: state.bass ? bassNote(chord.rootPc) : null,
|
||||
rootPc: chord.rootPc,
|
||||
});
|
||||
renderProgression();
|
||||
}
|
||||
$("addBtn").addEventListener("click", addToProgression);
|
||||
$("progClear").addEventListener("click", ()=>{ stopPlayback(); state.progression=[]; renderProgression(); });
|
||||
|
||||
function renderProgression(){
|
||||
const el = $("progSlots");
|
||||
el.innerHTML="";
|
||||
if (!state.progression.length){
|
||||
const e = document.createElement("div");
|
||||
e.className="slot-empty";
|
||||
e.textContent="vide — joue un accord puis ADD";
|
||||
el.appendChild(e);
|
||||
return;
|
||||
}
|
||||
state.progression.forEach((c,i)=>{
|
||||
const s = document.createElement("div");
|
||||
s.className="slot"+(i===state.playIdx?" playing":"");
|
||||
s.innerHTML = `<button class="del" title="supprimer">×</button>
|
||||
<div class="sname">${c.label}</div>
|
||||
<div class="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div>`;
|
||||
s.querySelector(".del").addEventListener("click", ev=>{
|
||||
ev.stopPropagation();
|
||||
state.progression.splice(i,1);
|
||||
renderProgression();
|
||||
});
|
||||
s.addEventListener("click", ()=>{
|
||||
const durS = 1.2;
|
||||
c.notes.forEach(n=>playNote(n,durS));
|
||||
if (c.bass!==null) playNote(c.bass,durS,0.9,true);
|
||||
midiSendRaw(c, durS*1000);
|
||||
});
|
||||
el.appendChild(s);
|
||||
});
|
||||
}
|
||||
function midiSendRaw(c, durMs){
|
||||
if (!midiOut) return;
|
||||
const chC = parseInt($("chChord").value)-1;
|
||||
const chB = parseInt($("chBass").value)-1;
|
||||
const now = performance.now();
|
||||
c.notes.forEach(n=>{ midiOut.send([0x90|chC,n,100]); midiOut.send([0x80|chC,n,0],now+durMs); });
|
||||
if (c.bass!==null){ midiOut.send([0x90|chB,c.bass,110]); midiOut.send([0x80|chB,c.bass,0],now+durMs); }
|
||||
}
|
||||
|
||||
/* ----- presets diatoniques ----- */
|
||||
$("presetLoad").addEventListener("click", ()=>{
|
||||
const val = $("presetSel").value;
|
||||
if (!val) return;
|
||||
const degrees = val.split(",").map(Number);
|
||||
const scale = SCALES[state.keyScale];
|
||||
stopPlayback();
|
||||
state.progression = [];
|
||||
degrees.forEach(d=>{
|
||||
const pc = (state.keyRoot + scale[(d-1)%7]) % 12;
|
||||
const chord = (()=>{
|
||||
const dChord = diatonicChord(pc, state.keyRoot, state.keyScale, state.exts);
|
||||
if (!dChord) return null;
|
||||
return {name:dChord.name, notes:applyVoicing(dChord.notes, state.voicing), rootPc:pc};
|
||||
})();
|
||||
if (chord) state.progression.push({
|
||||
label: chord.name, notes:[...chord.notes],
|
||||
bass: state.bass ? bassNote(pc) : null, rootPc: pc,
|
||||
});
|
||||
});
|
||||
renderProgression();
|
||||
});
|
||||
|
||||
/* ----- lecture ----- */
|
||||
function stopPlayback(){
|
||||
state.playing=false; state.playIdx=-1;
|
||||
clearTimeout(state.playTimer);
|
||||
$("playBtn").classList.remove("playing","on");
|
||||
$("playBtn").innerHTML = '<span class="led"></span>PLAY';
|
||||
midiPanic();
|
||||
renderProgression();
|
||||
}
|
||||
function startPlayback(){
|
||||
if (!state.progression.length) return;
|
||||
state.playing=true;
|
||||
$("playBtn").classList.add("playing","on");
|
||||
$("playBtn").innerHTML = '<span class="led"></span>STOP';
|
||||
let i=0;
|
||||
const step = ()=>{
|
||||
if (!state.playing) return;
|
||||
if (i>=state.progression.length){
|
||||
if (state.loop){ i=0; } else { stopPlayback(); return; }
|
||||
}
|
||||
const bpm = Math.max(30, Math.min(300, parseInt($("bpm").value)||112));
|
||||
const beats = parseInt($("chordLen").value);
|
||||
const durMs = beats*60000/bpm;
|
||||
const c = state.progression[i];
|
||||
state.playIdx=i;
|
||||
renderProgression();
|
||||
$("lcdName").textContent = c.label;
|
||||
$("lcdNotes").textContent = c.notes.map(n=>NOTE_NAMES[n%12]+(Math.floor(n/12)-1)).join(" · ");
|
||||
const gateS = durMs/1000*0.95;
|
||||
c.notes.forEach(n=>playNote(n,gateS));
|
||||
if (c.bass!==null) playNote(c.bass,gateS,0.9,true);
|
||||
midiSendRaw(c, durMs*0.95);
|
||||
i++;
|
||||
state.playTimer = setTimeout(step, durMs);
|
||||
};
|
||||
step();
|
||||
}
|
||||
$("playBtn").addEventListener("click", ()=> state.playing ? stopPlayback() : startPlayback());
|
||||
$("loopBtn").addEventListener("click", function(){
|
||||
state.loop=!state.loop;
|
||||
this.classList.toggle("on", state.loop);
|
||||
});
|
||||
|
||||
/* ----- MIDI UI ----- */
|
||||
for (let i=1;i<=16;i++){
|
||||
const o1=document.createElement("option"); o1.value=i; o1.textContent="ch "+i;
|
||||
const o2=o1.cloneNode(true);
|
||||
$("chChord").appendChild(o1); $("chBass").appendChild(o2);
|
||||
}
|
||||
$("chChord").value=3; // Pro-800 — nappes
|
||||
$("chBass").value=5; // TD-3 + K-2 unison
|
||||
$("midiDev").addEventListener("change", selectMidiDevice);
|
||||
|
||||
/* ----- export ----- */
|
||||
$("exportBtn").addEventListener("click", exportMidi);
|
||||
|
||||
/* ----- init ----- */
|
||||
buildKeyboard();
|
||||
renderProgression();
|
||||
refreshKeyDimming();
|
||||
setVoicing(0);
|
||||
midiInit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
-1268
File diff suppressed because it is too large
Load Diff
+26
-19
@@ -230,12 +230,11 @@
|
||||
*{transition:none !important}
|
||||
}
|
||||
</style>
|
||||
<script defer src="https://umami.ghostinthemachine.fr/script.js" data-website-id="0e5510f0-0521-4e70-b43e-6cc7d97df7a3"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="unit">
|
||||
<header>
|
||||
<h1>_chordlab <span>ORC·v2</span></h1>
|
||||
<h1><span>_chordlab.v2</span></h1>
|
||||
<div class="sub">chord ideas machine · underscorelab / leradiologue</div>
|
||||
</header>
|
||||
|
||||
@@ -436,7 +435,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>_chordlab v2.2 · logique d'accords inspirée de l'Orchid ORC-1 (implémentation indépendante étendue)</footer>
|
||||
<footer>_chordlab v2.3 · logique d'accords</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -536,17 +535,20 @@ function applyDrop(notes, mode){
|
||||
return n.sort((a,b)=>a-b);
|
||||
}
|
||||
|
||||
/* Slash bass : choisit la note de basse parmi les tons de l'accord */
|
||||
function slashInterval(rootPc, type, exts, which){
|
||||
const rel = buildChord(rootPc, type, exts).map(n=>n-(BASE+rootPc));
|
||||
/* Slash bass : choisit la note de basse parmi les notes réelles de l'accord.
|
||||
Travaille sur les notes construites (avant voicing), donc valable
|
||||
aussi bien en mode libre qu'en Key Mode. */
|
||||
function slashIntervalFrom(rawNotes, which){
|
||||
const root = rawNotes[0];
|
||||
const rel = rawNotes.map(n => (n - root) % 12).map(i => i<0 ? i+12 : i);
|
||||
const pickFrom = arr => arr.find(i=>rel.includes(i));
|
||||
if (which==="third") return pickFrom([4,3,5,2]) ?? 0;
|
||||
if (which==="fifth") return pickFrom([7,6,8]) ?? 0;
|
||||
if (which==="seventh") return pickFrom([10,11,9]) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
function bassNoteFor(rootPc, type, exts){
|
||||
const iv = slashInterval(rootPc, type, exts, state.slash);
|
||||
function bassFromRaw(rawNotes, rootPc){
|
||||
const iv = slashIntervalFrom(rawNotes, state.slash);
|
||||
return {midi: BASE + rootPc + iv + state.bassOct, iv};
|
||||
}
|
||||
|
||||
@@ -604,9 +606,7 @@ function currentChord(rootPc){
|
||||
// slash dans le nom
|
||||
let bass = null;
|
||||
if (state.bass){
|
||||
const b = state.keyMode
|
||||
? {midi: BASE + rootPc + state.bassOct, iv:0}
|
||||
: bassNoteFor(rootPc, state.type, state.exts);
|
||||
const b = bassFromRaw(raw, rootPc);
|
||||
bass = b.midi;
|
||||
if (b.iv !== 0) name += "/" + NOTE_NAMES[(rootPc + b.iv) % 12];
|
||||
}
|
||||
@@ -651,10 +651,15 @@ function suggestNext(){
|
||||
let notes = applyDrop(applyVoicing(d.notes, state.voicing), state.drop);
|
||||
if (state.vlAuto && state.progression.length)
|
||||
notes = bestVoicingAgainst(notes, state.progression[state.progression.length-1].notes);
|
||||
let label = d.name;
|
||||
let bass = null;
|
||||
if (state.bass){
|
||||
const b = bassFromRaw(d.notes, pc);
|
||||
bass = b.midi;
|
||||
if (b.iv !== 0) label += "/" + NOTE_NAMES[(pc + b.iv) % 12];
|
||||
}
|
||||
state.progression.push({
|
||||
label: d.name, notes,
|
||||
bass: state.bass ? BASE + pc + state.bassOct : null,
|
||||
rootPc: pc, beats: 4,
|
||||
label, notes, bass, rootPc: pc, beats: 4,
|
||||
});
|
||||
renderProgression();
|
||||
}
|
||||
@@ -1191,11 +1196,13 @@ $("presetLoad").addEventListener("click",()=>{
|
||||
let notes=applyDrop(applyVoicing(dc.notes,state.voicing),state.drop);
|
||||
if (state.vlAuto && prev) notes=bestVoicingAgainst(notes,prev);
|
||||
prev=notes;
|
||||
state.progression.push({
|
||||
label:dc.name,notes,
|
||||
bass:state.bass?BASE+pc+state.bassOct:null,
|
||||
rootPc:pc,beats:4,
|
||||
});
|
||||
let label=dc.name, bass=null;
|
||||
if (state.bass){
|
||||
const b=bassFromRaw(dc.notes,pc);
|
||||
bass=b.midi;
|
||||
if (b.iv!==0) label+="/"+NOTE_NAMES[(pc+b.iv)%12];
|
||||
}
|
||||
state.progression.push({label,notes,bass,rootPc:pc,beats:4});
|
||||
});
|
||||
renderProgression();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user