chore: import oscope-of as monorepo subtree

Context: AV-Live monorepo unifies the SuperCollider sound engine
(sound_algo) and the openFrameworks oscilloscope visualizer (oscope-of)
under one roof, driven by a SwiftUI menubar launcher. oscope-of was a
separate private repo until now.

Approach: imported via 'git fetch + read-tree --prefix=oscope-of/'
(pre-subtree-script merge) so the entire 1-commit history of the
source repo is grafted under the oscope-of/ subdirectory. The standard
git-subtree trailers below let 'git subtree pull/push' sync changes
back and forth.

Changes:
- Add oscope-of/ tree (33 files, 2126 lines) :
  - src/main.cpp + src/ofApp.{h,cpp} : oF entry point
  - src/HantekDevice.{h,cpp} : Hantek 6022BL USB driver
  - src/RingBuffer.h : lock-free SPSC ringbuffer USB->UI thread
  - src/FFT.{h,cpp} : Cooley-Tukey FFT with Hann windowing
  - src/OscClient.{h,cpp} : OSC bridge to SuperCollider
  - src/visualizers/{Lissajous,Spectrogram,Reactive}Vis.{h,cpp} :
    3 GPU-accelerated visualization modes
  - bin/data/shaders/{crt,hex_grid}.{vert,frag} : CRT phosphor +
    procedural hex grid shaders
  - Makefile, addons.make, config.make : oF build config
  - docs/{HANTEK_FIRMWARE,OSC_PROTOCOL}.md : integration docs

Impact: enables Phase 4 (SwiftUI launcher) to spawn the oscope-of
binary alongside scsynth/sclang as part of one cohesive AV stack,
and removes the need to maintain a second private repository.

git-subtree-dir: oscope-of
git-subtree-mainline: de035a8596
git-subtree-split: 6f51177920219e03b025119eef648217df0885b6
This commit is contained in:
L'électron rare
2026-05-07 11:57:10 +02:00
parent de035a8596
commit e7edd7a6e7
33 changed files with 2126 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# openFrameworks build artifacts
obj/
bin/oscope-of
bin/oscope-of_debug
bin/oscope-of.app/
bin/oscope-of.app
bin/libs/
*.o
*.d
# Xcode
*.xcuserdata/
*.xcuserstate
xcuserdata/
*.xcodeproj/project.xcworkspace/xcuserdata/
DerivedData/
# macOS
.DS_Store
# Editor
.vscode/
.idea/
*.swp
*~
# Generated project files (keep handwritten config.make/addons.make)
oscope-of.xcodeproj/
Makefile.bak
openFrameworks-Info.plist
+14
View File
@@ -0,0 +1,14 @@
# Délègue au Makefile générique d'openFrameworks.
# Suppose que le projet est cloné dans <OF_ROOT>/apps/myApps/oscope-of/.
ifndef PROJECT_ROOT
PROJECT_ROOT := $(realpath ./)
endif
include $(PROJECT_ROOT)/config.make
ifndef OF_ROOT
OF_ROOT = ../../..
endif
include $(OF_ROOT)/libs/openFrameworksCompiled/project/makefileCommon/compile.project.mk
+165
View File
@@ -0,0 +1,165 @@
# oscope-of
Visualizer openFrameworks pour le système de live coding [`sound_algo`](https://github.com/electron-rare/sound_algo) couplé à un oscilloscope USB Hantek 6022BL.
Trois modes superposés :
1. **Lissajous XY** — CH1=X, CH2=Y, persistance type CRT phosphor (vert `#00FF7F`), shader glow gaussien
2. **Spectrogramme FFT** — 1024 bins, scrolling horizontal, palette bleu→violet→rouge
3. **Layer réactif sound_algo** — grille hexagonale, gradient HSV, modulé par BPM/beat/`amp.*` reçus en OSC
## Pré-requis
- macOS Apple Silicon (M1/M2/M3/M5) — testé en cible, fonctionne aussi sur Intel macs avec ajustement de `config.make`
- [openFrameworks 0.12+](https://openframeworks.cc/download/) avec template Xcode
- Xcode Command Line Tools : `xcode-select --install`
- libusb-1.0 : `brew install libusb`
- Pour charger le firmware Hantek : `brew install fxload` ou compilation depuis [OpenHantek6022](https://github.com/OpenHantek/OpenHantek6022)
## Installation
### Étape 1 — Cloner dans l'arbre openFrameworks
Le projet doit vivre dans `<OF_ROOT>/apps/myApps/oscope-of` pour que le Makefile générique fonctionne :
```bash
cd <OF_ROOT>/apps/myApps/
git clone git@github.com:electron-rare/oscope-of.git
cd oscope-of
```
Si vous le placez ailleurs, ajustez `OF_ROOT` dans `config.make`.
### Étape 2 — Régénérer le projet Xcode
```bash
<OF_ROOT>/projectGenerator -u apps/myApps/oscope-of
```
Cela crée `oscope-of.xcodeproj/` à partir des fichiers `addons.make` et `config.make`.
### Étape 3 — Charger le firmware Hantek
Le 6022BL est un Cypress FX2 sans firmware persistant : il faut uploader le firmware OpenHantek6022 à chaque branchement USB.
Voir le guide complet : [docs/HANTEK_SETUP.md](docs/HANTEK_SETUP.md).
Résumé express :
```bash
# Cloner et builder le firmware OpenHantek6022 une seule fois.
git clone https://github.com/OpenHantek/OpenHantek6022.git
cd OpenHantek6022/openhantek/res/firmware/
# (lire les instructions de build du repo pour produire les .hex)
# Charger à chaque branchement (PID 0x6022 = sans firmware) :
fxload -t fx2lp -I firmware.hex -D /dev/bus/usb/<bus>/<dev>
# ou via libusb-helper sur macOS — voir docs/HANTEK_SETUP.md
```
Une fois le firmware chargé, le device réénumère en `0x04B5:0x602A` et `oscope-of` peut le claim.
### Étape 4 — Lancer sound_algo et son bridge
Depuis votre arbre `sound_algo` :
```supercollider
// Dans SuperCollider
"web_bridge.scd".loadRelative;
~startBridge.value;
```
Le bridge écoute sur `127.0.0.1:57121` (SC) et `127.0.0.1:57122` (Node→SC) ; `oscope-of` se branche sur ces deux ports.
### Étape 5 — Build et run
Ouvrir `oscope-of.xcodeproj` dans Xcode, sélectionner la scheme `oscope-of Release`, ⌘+R.
Ou en CLI :
```bash
make Release
make RunRelease
```
## Modes et contrôles clavier
| Touche | Action |
|--------|--------|
| `1` | Mode Lissajous (plein écran XY) |
| `2` | Mode Spectrogram (plein écran FFT) |
| `3` | Mode Reactive (layer sound_algo seul) |
| `4` | Mode Hybrid (défaut, les 3 superposés) |
| `f` | Toggle fullscreen |
| `g` | Toggle GUI ofxPanel |
| `r` | Reload shaders (utile pour l'édition live) |
## Configuration
`bin/data/settings.json` :
```json
{
"scope": { "sample_rate": 48000000, "gain_ch1": 0.5, "gain_ch2": 0.5, "buffer_size": 4096 },
"osc": { "listen_port": 57122, "send_host": "127.0.0.1", "send_port": 57121 },
"display": { "fullscreen": false, "width": 1920, "height": 1080, "default_mode": "hybrid" }
}
```
## Protocole OSC
Voir [docs/OSC_PROTOCOL.md](docs/OSC_PROTOCOL.md) pour la liste complète des messages reçus depuis sound_algo et envoyés vers SC.
## Troubleshooting
### "Hantek 6022BL trouvé MAIS firmware non chargé"
Le device énumère en `0x04B5:0x6022` au lieu de `0x602A`. Charger le firmware (voir étape 3) puis relancer.
### "claim_interface failed: LIBUSB_ERROR_ACCESS"
macOS bloque l'accès USB direct par défaut. Aller dans **Réglages Système → Confidentialité et sécurité → Surveillance des entrées** et autoriser le binaire `oscope-of`. Sinon, lancer en root pour test : `sudo bin/oscope-of_debug`.
### Aucun signal en Lissajous
- Vérifier que les sondes sont branchées sur CH1 et CH2 et que le couplage DC/AC est compatible avec votre source
- Vérifier les V/div dans la GUI (slider `CH1 V/div`, `CH2 V/div`)
- Le statut "Scope" dans la GUI doit afficher "OK" et un nombre de samples > 0
### Conflit avec OpenHantek lui-même
Si OpenHantek6022 est lancé en parallèle, il claim le device et `oscope-of` ne pourra pas l'ouvrir. Quitter OpenHantek avant.
### Pas de messages OSC
Vérifier dans la GUI que le port d'écoute est bien `57122`, et que `sound_algo`'s bridge a démarré (`~startBridge.value` dans SC). Tester avec `oscchief` ou `OSCdef.trace`.
## Architecture
```
src/
main.cpp Entry point OF, fenêtre 1920x1080 MSAA 8x
ofApp.h/.cpp Orchestrateur, dispatch des modes
HantekDevice.h/.cpp libusb wrapper, thread bulk-transfer
OscClient.h/.cpp ofxOsc receiver/sender pour sound_algo
ScopeData.h Ringbuffer SPSC lock-free
FFT.h/.cpp Cooley-Tukey radix-2 in-place
visualizers/
Visualizer.h Interface abstraite
LissajousVis.h/.cpp XY scope avec FBO trail
SpectrogramVis.h/.cpp FFT scrolling
ReactiveVis.h/.cpp Layer sound_algo (shader bg.frag)
bin/data/
settings.json
shaders/
glow.vert/frag 9-tap Gaussian + threshold
bloom.vert/frag Bloom single-pass (réservé futur)
bg.frag Layer réactif sound_algo
docs/
HANTEK_SETUP.md Guide firmware + permissions USB macOS
OSC_PROTOCOL.md Référence des messages OSC échangés
```
## Licence
MIT — voir le projet sound_algo pour la licence côté SuperCollider.
+2
View File
@@ -0,0 +1,2 @@
ofxOsc
ofxGui
+19
View File
@@ -0,0 +1,19 @@
{
"scope": {
"sample_rate": 48000000,
"gain_ch1": 0.5,
"gain_ch2": 0.5,
"buffer_size": 4096
},
"osc": {
"listen_port": 57123,
"send_host": "127.0.0.1",
"send_port": 57121
},
"display": {
"fullscreen": false,
"width": 1920,
"height": 1080,
"default_mode": "hybrid"
}
}
+76
View File
@@ -0,0 +1,76 @@
#version 150
// Layer arrière-plan réactif sound_algo.
// Grille hexagonale qui pulse sur kick, gradient HSV qui tourne au beat,
// distortion modulée par la mélodie. Subtil par défaut (alpha global ~0.6).
uniform vec2 uResolution;
uniform float uTime;
uniform float uBpm;
uniform int uBeat;
uniform float uKick;
uniform float uHat;
uniform float uSnare;
uniform float uClap;
uniform float uPerc;
uniform float uMelody;
uniform float uAcid;
uniform float uHarmony;
out vec4 fragColor;
// HSV -> RGB.
vec3 hsv2rgb(vec3 c) {
vec3 p = abs(fract(c.xxx + vec3(0.0, 2.0/3.0, 1.0/3.0)) * 6.0 - 3.0);
return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y);
}
// Distance hexagonale (vector display style).
float hexDist(vec2 p) {
p = abs(p);
return max(p.x * 0.866 + p.y * 0.5, p.y);
}
vec4 hexCoords(vec2 uv) {
vec2 r = vec2(1.0, 1.732);
vec2 h = r * 0.5;
vec2 a = mod(uv, r) - h;
vec2 b = mod(uv - h, r) - h;
vec2 gv = (dot(a, a) < dot(b, b)) ? a : b;
float d = hexDist(gv);
return vec4(gv, d, 1.0);
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / min(uResolution.x, uResolution.y);
// Distortion mélodique.
uv += 0.05 * uMelody * vec2(sin(uv.y * 6.0 + uTime * 1.3),
cos(uv.x * 5.0 + uTime * 0.9));
// Grille hexagonale.
float scale = 6.0 + 2.0 * uHarmony - 1.5 * uKick;
vec4 hex = hexCoords(uv * scale);
float ring = smoothstep(0.5, 0.49, hex.z);
float pulse = smoothstep(0.4 - 0.3 * uKick, 0.5, hex.z);
// Couleur HSV qui tourne avec beat.
float hue = fract(0.55 + 0.02 * float(uBeat) + 0.05 * uAcid);
vec3 col = hsv2rgb(vec3(hue, 0.6 + 0.3 * uClap, 0.35 + 0.5 * pulse));
// Vignette radiale.
float r = length(uv);
col *= smoothstep(1.4, 0.2, r);
// Boost kick.
col += 0.15 * uKick * vec3(0.6, 0.2, 1.0);
// Trame snare (lignes horizontales).
col += 0.08 * uSnare * sin(uv.y * 200.0);
// Hat = grain léger.
float n = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
col += 0.06 * uHat * (n - 0.5);
fragColor = vec4(col, 0.85);
}
+8
View File
@@ -0,0 +1,8 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
void main() {
gl_Position = modelViewProjectionMatrix * position;
}
+27
View File
@@ -0,0 +1,27 @@
#version 150
// Bloom single-pass simplifié : downsample 5x5 + threshold.
// Réservé pour usage futur ; LissajousVis utilise actuellement glow.frag.
uniform sampler2DRect uTex;
uniform vec2 uTexel;
uniform float uThreshold;
in vec2 vUV;
out vec4 fragColor;
void main() {
vec3 acc = vec3(0.0);
float wsum = 0.0;
for (int dy = -2; dy <= 2; ++dy) {
for (int dx = -2; dx <= 2; ++dx) {
vec2 q = vUV + vec2(float(dx), float(dy)) * 1.5;
vec3 c = texture(uTex, q).rgb;
float l = dot(c, vec3(0.299, 0.587, 0.114));
float k = smoothstep(uThreshold, uThreshold + 0.2, l);
acc += c * k;
wsum += 1.0;
}
}
fragColor = vec4(acc / wsum, 1.0);
}
+11
View File
@@ -0,0 +1,11 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
in vec2 texcoord;
out vec2 vUV;
void main() {
vUV = texcoord;
gl_Position = modelViewProjectionMatrix * position;
}
+41
View File
@@ -0,0 +1,41 @@
#version 150
// Glow CRT phosphor : 9-tap Gaussian + threshold + additive.
// Préserve la couleur d'origine et ajoute un halo vert pondéré.
uniform sampler2DRect uTex;
uniform vec2 uTexel; // 1.0 / textureSize
uniform float uIntensity; // boost global (1.0 = neutre)
in vec2 vUV;
out vec4 fragColor;
void main() {
// ofTexture par défaut sur of macOS = sampler2DRect (coords pixel).
vec2 p = vUV;
vec4 base = texture(uTex, p);
// 9-tap (3x3) gaussian.
float w[9] = float[9](
1.0, 2.0, 1.0,
2.0, 4.0, 2.0,
1.0, 2.0, 1.0
);
vec4 sum = vec4(0.0);
int idx = 0;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
vec2 q = p + vec2(float(dx), float(dy));
sum += texture(uTex, q) * w[idx];
idx++;
}
}
sum /= 16.0;
// Threshold : seuil sur la luminance pour ne booster que le tracé.
float l = dot(sum.rgb, vec3(0.299, 0.587, 0.114));
float bloom = smoothstep(0.05, 0.6, l);
vec3 glowCol = vec3(0.0, 1.0, 0.5) * bloom * uIntensity;
fragColor = vec4(base.rgb + glowCol, 1.0);
}
+11
View File
@@ -0,0 +1,11 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
in vec2 texcoord;
out vec2 vUV;
void main() {
vUV = texcoord;
gl_Position = modelViewProjectionMatrix * position;
}
+50
View File
@@ -0,0 +1,50 @@
################################################################################
# CONFIGURE PROJECT MAKEFILE (optional)
# This file is where we make project specific configurations.
################################################################################
################################################################################
# OF ROOT
################################################################################
OF_ROOT = ../../..
################################################################################
# PROJECT EXCLUSIONS
################################################################################
# PROJECT_EXCLUSIONS =
################################################################################
# LIBUSB AUTO-DETECT
# Préfère pkg-config (Apple Silicon + Intel + Linux), fallback à
# `brew --prefix libusb` si pkg-config n'est pas installé.
################################################################################
HAS_PKGCONFIG := $(shell command -v pkg-config 2>/dev/null)
ifneq ($(HAS_PKGCONFIG),)
LIBUSB_CFLAGS := $(shell pkg-config --cflags libusb-1.0)
LIBUSB_LDFLAGS := $(shell pkg-config --libs libusb-1.0)
else
HAS_BREW := $(shell command -v brew 2>/dev/null)
ifneq ($(HAS_BREW),)
LIBUSB_PREFIX := $(shell brew --prefix libusb)
LIBUSB_CFLAGS := -I$(LIBUSB_PREFIX)/include/libusb-1.0
LIBUSB_LDFLAGS := -L$(LIBUSB_PREFIX)/lib -lusb-1.0
else
# Fallback générique
LIBUSB_CFLAGS := -I/usr/local/include/libusb-1.0 -I/opt/homebrew/include/libusb-1.0
LIBUSB_LDFLAGS := -L/usr/local/lib -L/opt/homebrew/lib -lusb-1.0
endif
endif
PROJECT_CFLAGS = $(LIBUSB_CFLAGS)
PROJECT_LDFLAGS = $(LIBUSB_LDFLAGS)
################################################################################
# PROJECT CPPFLAGS
################################################################################
PROJECT_CPPFLAGS = -std=c++17
################################################################################
# PROJECT OPTIMIZATION CFLAGS
################################################################################
# PROJECT_OPTIMIZATION_CFLAGS_RELEASE =
# PROJECT_OPTIMIZATION_CFLAGS_DEBUG =
+135
View File
@@ -0,0 +1,135 @@
# Hantek 6022BL — setup macOS Apple Silicon
Le Hantek 6022BL est un oscilloscope USB 2 voies, 8-bit, basé sur le microcontrôleur Cypress FX2LP (CY7C68013A). Le FX2 n'a pas de mémoire flash : à chaque branchement USB, un firmware doit être uploadé en RAM. Tant que ce n'est pas fait, le device énumère avec un descripteur générique (PID `0x6022`) et n'a pas d'endpoint bulk fonctionnel.
Le firmware open-source de référence est celui du projet [OpenHantek6022](https://github.com/OpenHantek/OpenHantek6022) (GPL-3.0). Une fois chargé, le device réénumère en `0x04B5:0x602A` avec un endpoint bulk IN sur `0x86`.
## 1. Installer libusb et fxload
```bash
brew install libusb fxload
```
Vérifier que `fxload` est bien sous `/opt/homebrew/bin/fxload` (Apple Silicon) ou `/usr/local/bin/fxload` (Intel).
## 2. Récupérer ou builder le firmware
### Option A — pré-built depuis OpenHantek6022
Le repo upstream fournit des `.hex` pré-buildés dans `openhantek/res/firmware/` :
```bash
git clone https://github.com/OpenHantek/OpenHantek6022.git
ls OpenHantek6022/openhantek/res/firmware/
# Hantek6022BE.hex Hantek6022BL.hex ...
```
Pour le 6022BL spécifiquement, utiliser `Hantek6022BL.hex`.
### Option B — build from source
Le firmware est en C8051, buildable avec `sdcc` :
```bash
brew install sdcc
cd OpenHantek6022/openhantek/res/firmware/build
make
# produit .hex et .iic
```
## 3. Identifier le bus/device USB
Brancher le scope, puis :
```bash
system_profiler SPUSBDataType | grep -A 6 "Hantek\|6022\|04b5"
```
Vous devriez voir une entrée avec `Vendor ID: 0x04b5` et `Product ID: 0x6022` (avant firmware).
Pour avoir bus/device au format requis par fxload, sur macOS la convention `/dev/bus/usb/` n'existe pas — il faut utiliser libusb directement. Une option simple :
```bash
# Lister les devices avec libusb (depuis brew)
ioreg -p IOUSB -l -w 0 | grep -i hantek
```
## 4. Charger le firmware
### Méthode A — fxload (Linux-style, peut nécessiter adaptation)
Sur Linux : `fxload -t fx2lp -I Hantek6022BL.hex -D /dev/bus/usb/001/005`.
Sur macOS, `fxload` Homebrew accepte `-D` au format `vid:pid` ou via libusb device path. Tester :
```bash
sudo fxload -t fx2lp -I Hantek6022BL.hex
# (sans -D, scanne tous les FX2 et upload sur le premier)
```
### Méthode B — utilitaire Python avec libusb
Plus fiable sur macOS, le repo OpenHantek6022 fournit un script ou vous pouvez utiliser `pyusb` :
```python
# upload_firmware.py
import usb.core, time
from intelhex import IntelHex
dev = usb.core.find(idVendor=0x04B5, idProduct=0x6022)
ih = IntelHex("Hantek6022BL.hex")
# Stop CPU
dev.ctrl_transfer(0x40, 0xA0, 0xE600, 0, [0x01])
# Upload
for start, end in ih.segments():
chunk = ih.tobinarray(start=start, size=end-start)
dev.ctrl_transfer(0x40, 0xA0, start, 0, chunk.tobytes())
# Start CPU
dev.ctrl_transfer(0x40, 0xA0, 0xE600, 0, [0x00])
time.sleep(1)
print("Firmware uploaded.")
```
```bash
pip install pyusb intelhex
sudo python upload_firmware.py
```
### Méthode C — passer par OpenHantek6022 lui-même
Si vous installez OpenHantek6022 en .app sur macOS, son lancement charge automatiquement le firmware. Vous pouvez le lancer une fois (qui charge le firmware), le quitter, puis lancer `oscope-of` qui trouvera le device en mode firmware.
## 5. Vérifier le succès du chargement
Après upload, le device doit réénumérer en quelques secondes :
```bash
system_profiler SPUSBDataType | grep -A 6 "04b5"
# Product ID: 0x602a <-- firmware chargé
```
## 6. Permissions USB sur macOS
macOS Sequoia/Sonoma demande une autorisation explicite pour qu'une application non signée Apple accède aux devices USB.
- **Réglages Système → Confidentialité et sécurité → Périphériques USB / Surveillance des entrées** : ajouter le binaire `oscope-of` à la liste autorisée.
- En cas d'échec persistant : `sudo /Users/<vous>/.../oscope-of.app/Contents/MacOS/oscope-of` pour test (lancer en root contourne les ACL).
## 7. Diagnostic
Le binaire `oscope-of` log les statuts au démarrage :
- `Hantek 6022BL trouvé MAIS firmware non chargé` → revenir à l'étape 4
- `claim_interface failed` → permissions USB ou device claimé par une autre app
- `bulk_transfer: LIBUSB_ERROR_TIMEOUT` répétés → le firmware est chargé mais ne stream pas ; vérifier sample rate et gain
## 8. Re-uploader à chaque branchement
Le firmware vit en RAM : débrancher = perdre le firmware. Pour automatiser, créer un service launchd ou un script `usb_arrived.sh` qui détecte le PID `0x6022` et lance l'upload.
## Liens
- [OpenHantek6022 firmware source](https://github.com/OpenHantek/OpenHantek6022/tree/main/openhantek/res/firmware)
- [Cypress FX2LP datasheet](https://www.infineon.com/dgdl/Infineon-CY7C68013A_CY7C68014A_CY7C68015A_CY7C68016A_EZ-USB_FX2LP_USB_Microcontroller_High_Speed_USB_Peripheral_Controller-DataSheet-v17_00-EN.pdf)
- [libusb-1.0 API doc](https://libusb.sourceforge.io/api-1.0/)
+124
View File
@@ -0,0 +1,124 @@
# Protocole OSC entre oscope-of et sound_algo
Référence des messages OSC échangés entre les deux projets. Sert aussi de spec pour toute évolution du bridge `web_bridge.scd` côté sound_algo.
## Vue d'ensemble
```
+---------------------+
| sclang (SC IDE) |
+----------+----------+
| :57110 (réception SC interne)
|
+-----------v-----------+ :3000 ws
| web_bridge.scd | <----------------> browser (control panel)
| (Node + sclang OSC) |
+-----+-------------+---+
:57121 | | | :57122
(SC<-) | | | (->SC, broadcast WS)
v v v
+-----+--------------------+
| oscope-of |
| ofxOscReceiver :57122 |
| ofxOscSender :57121 |
+--------------------------+
```
`oscope-of` :
- **Écoute** sur `127.0.0.1:57122` les `/sync/*`
- **Envoie** sur `127.0.0.1:57121` des `/control/*` interprétés par sound_algo
## Messages reçus (sound_algo → oscope-of)
### `/sync/bpm <float>`
Tempo courant en BPM. Émis à chaque changement (`~bpm = X` dans une track ou via `~tempo.()`).
```
/sync/bpm 132.5
```
### `/sync/beat <int>`
Compteur de beats incrémenté à chaque pulsation par le clock SC. Reset à 0 sur `~jumpTo` ou changement de track.
```
/sync/beat 1247
```
### `/sync/amp <string voice> <float val>`
Amplitude RMS (0..1) de l'une des 8 voies. Mis à jour ~30 Hz par les RMS analyzers de sound_algo.
Voix possibles : `kick`, `hat`, `snare`, `clap`, `perc`, `melody`, `acid`, `harmony`.
```
/sync/amp "kick" 0.78
/sync/amp "melody" 0.22
```
### `/sync/rms <float master>`
RMS du bus master (post-FX). Utile pour le ducking visuel global.
```
/sync/rms 0.41
```
### `/sync/album <string>`
Nom de l'album courant (ex `acid_journey`, `vietnam_hard`).
### `/sync/melody <string>`
Nom de la mélodie active (ex `epic_long_001`, `short_modal_007`).
### `/sync/synthdef <string>`
SynthDef courante en mélodie (ex `\fmLead`, `\acidBass`).
## Messages envoyés (oscope-of → sound_algo)
oscope-of est un client OSC actif : il peut piloter sound_algo, mais cette feature est minoritaire dans l'usage par défaut (la GUI principale reste le browser web).
### `/control/kk <string>`
Joue un kick via le live API `~kk.<name>`.
```
/control/kk "tek"
```
### `/control/setMelody <string>`
Change la mélodie active.
```
/control/setMelody "epic_long_001"
```
### `/control/setAlbum <string>`
Change l'album / la track.
```
/control/setAlbum "acid_journey"
```
### `/control/<param> <float>`
Tout autre paramètre exposé par sound_algo via OSCdef. La nomenclature suit `~p.<param>.()` ou `~ff.<fxname>.<param>`.
## Notes d'implémentation
- Le bridge sound_algo broadcast les `/sync/*` à la fois en UDP direct (vers `oscope-of` et tout autre client OSC sur :57122) et en WebSocket (vers les browsers). oscope-of utilise UNIQUEMENT le canal UDP.
- Si vous lancez plusieurs visualizers en parallèle (par ex `oscope-of` + un autre client OF), augmenter le buffer UDP socket ou utiliser des ports distincts.
- Le `web_bridge.scd` côté sound_algo doit avoir oscope-of dans sa liste de subscribers UDP. Vérifier dans le bridge :
```supercollider
~oscRelays = [
NetAddr("127.0.0.1", 57122), // oscope-of
// autres clients OSC...
];
```
+60
View File
@@ -0,0 +1,60 @@
#include "FFT.h"
#include <cmath>
namespace oscope {
FFT::FFT(std::size_t size) : size_(size), window_(size), work_(size) {
// Fenêtre de Hann pré-calculée.
for (std::size_t i = 0; i < size_; ++i) {
window_[i] = 0.5f * (1.0f - std::cos(2.0f * M_PI * i / (size_ - 1)));
}
}
void FFT::hannWindow(std::vector<float>& buf) const {
for (std::size_t i = 0; i < size_; ++i) buf[i] *= window_[i];
}
void FFT::fftInPlace(std::vector<std::complex<float>>& x) const {
const std::size_t N = x.size();
// Bit-reversal permutation.
std::size_t j = 0;
for (std::size_t i = 1; i < N; ++i) {
std::size_t bit = N >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if (i < j) std::swap(x[i], x[j]);
}
// Butterflies.
for (std::size_t len = 2; len <= N; len <<= 1) {
const float ang = -2.0f * M_PI / static_cast<float>(len);
const std::complex<float> wlen(std::cos(ang), std::sin(ang));
for (std::size_t i = 0; i < N; i += len) {
std::complex<float> w(1.0f, 0.0f);
for (std::size_t k = 0; k < len / 2; ++k) {
const auto u = x[i + k];
const auto v = x[i + k + len / 2] * w;
x[i + k] = u + v;
x[i + k + len / 2] = u - v;
w *= wlen;
}
}
}
}
void FFT::magnitude(const std::vector<float>& in, std::vector<float>& outMag) {
const std::size_t N = size_;
std::vector<float> windowed(N, 0.0f);
const std::size_t copy = std::min(N, in.size());
for (std::size_t i = 0; i < copy; ++i) windowed[i] = in[i];
hannWindow(windowed);
for (std::size_t i = 0; i < N; ++i) work_[i] = std::complex<float>(windowed[i], 0.0f);
fftInPlace(work_);
outMag.resize(N / 2);
const float invN = 1.0f / static_cast<float>(N);
for (std::size_t i = 0; i < N / 2; ++i) {
outMag[i] = std::abs(work_[i]) * invN;
}
}
} // namespace oscope
+32
View File
@@ -0,0 +1,32 @@
#pragma once
// FFT Cooley-Tukey radix-2 in-place, sans dépendance externe.
// Format : entrée temporelle réelle (N samples, N puissance de 2),
// sortie magnitude (N/2 bins, dernier sample = Nyquist).
#include <complex>
#include <cstddef>
#include <vector>
namespace oscope {
class FFT {
public:
explicit FFT(std::size_t size);
/// Calcule la FFT du signal `in` (taille = size_) et écrit les magnitudes
/// dans `outMag` (taille = size_/2).
void magnitude(const std::vector<float>& in, std::vector<float>& outMag);
std::size_t size() const { return size_; }
private:
void hannWindow(std::vector<float>& buf) const;
void fftInPlace(std::vector<std::complex<float>>& x) const;
std::size_t size_;
std::vector<float> window_;
std::vector<std::complex<float>> work_;
};
} // namespace oscope
+277
View File
@@ -0,0 +1,277 @@
#include "HantekDevice.h"
#include <libusb.h>
#include <chrono>
#include <cstring>
#include <vector>
#include "ofLog.h"
namespace oscope {
namespace {
// Identifiants USB du Hantek 6022BL.
// 0x04B5 / 0x6022 : appareil avec firmware OEM, ou aucun firmware (énumération
// minimale, pas de bulk endpoint actif).
// 0x04B5 / 0x602A : appareil avec firmware OpenHantek6022 chargé.
constexpr uint16_t kVendorId = 0x04B5;
constexpr uint16_t kProductIdRaw = 0x6022;
constexpr uint16_t kProductIdFirmware = 0x602A;
constexpr int kInterface = 0;
constexpr int kAltSetting = 0; // alt 0 = bulk EP 0x86 (firmware OpenHantek6022)
constexpr unsigned char kEpIn = 0x86;
// Vendor requests OpenHantek6022.
constexpr uint8_t kReqSetSampleRate = 0xE2;
constexpr uint8_t kReqSetCh1Gain = 0xE0;
constexpr uint8_t kReqSetCh2Gain = 0xE1;
constexpr uint8_t kReqSetNumChannels= 0xE4;
// Codes de gain (registre du FX2).
uint8_t gainCode(HantekGain g) {
switch (g) {
case HantekGain::G_5V: return 0x01;
case HantekGain::G_2_5V: return 0x02;
case HantekGain::G_1V: return 0x05;
case HantekGain::G_500mV: return 0x0a;
case HantekGain::G_250mV: return 0x14;
}
return 0x05;
}
// Code de sample rate (cf. firmware OpenHantek6022).
uint8_t sampleRateCode(uint32_t hz) {
if (hz >= 48000000) return 48;
if (hz >= 24000000) return 30; // dual-channel max ~16 MS/s, 30=24M single
if (hz >= 16000000) return 16;
if (hz >= 8000000) return 8;
if (hz >= 4000000) return 4;
if (hz >= 2000000) return 2;
return 1;
}
constexpr int kBulkTimeoutMs = 200;
constexpr int kBulkXferBytes = 8192; // 4096 samples par canal (2 canaux entrelacés)
} // namespace
HantekDevice::HantekDevice()
: ctx_(nullptr), handle_(nullptr), running_(false),
status_(HantekStatus::NotFound),
sampleRateHz_(8000000),
gainCh1_(static_cast<int>(HantekGain::G_1V)),
gainCh2_(static_cast<int>(HantekGain::G_1V)) {}
HantekDevice::~HantekDevice() {
stop();
}
HantekStatus HantekDevice::start() {
if (running_.load()) {
status_.store(HantekStatus::AlreadyRunning);
return HantekStatus::AlreadyRunning;
}
int rc = libusb_init(&ctx_);
if (rc != 0) {
ofLogError("HantekDevice") << "libusb_init failed: " << libusb_error_name(rc);
status_.store(HantekStatus::UsbError);
return HantekStatus::UsbError;
}
// Recherche prioritaire du device avec firmware chargé.
handle_ = libusb_open_device_with_vid_pid(ctx_, kVendorId, kProductIdFirmware);
if (handle_ == nullptr) {
// Fallback : device sans firmware.
handle_ = libusb_open_device_with_vid_pid(ctx_, kVendorId, kProductIdRaw);
if (handle_ != nullptr) {
ofLogWarning("HantekDevice")
<< "Hantek 6022BL trouvé MAIS firmware non chargé (PID 0x6022). "
<< "Charger le firmware OpenHantek6022 via fxload, puis relancer. "
<< "Voir docs/HANTEK_SETUP.md.";
libusb_close(handle_);
handle_ = nullptr;
libusb_exit(ctx_);
ctx_ = nullptr;
status_.store(HantekStatus::FirmwareNeeded);
return HantekStatus::FirmwareNeeded;
}
ofLogError("HantekDevice") << "Aucun Hantek 6022BL détecté.";
libusb_exit(ctx_);
ctx_ = nullptr;
status_.store(HantekStatus::NotFound);
return HantekStatus::NotFound;
}
// Sur macOS, AppleUSBHostLegacyClient claim l'interface 0 par defaut
// pour les devices vendor-specific. On force un re-configure (0->1) pour
// detacher le client legacy, puis reset, puis claim.
libusb_set_auto_detach_kernel_driver(handle_, 1);
(void)libusb_detach_kernel_driver(handle_, kInterface);
(void)libusb_set_configuration(handle_, 0);
(void)libusb_set_configuration(handle_, 1);
rc = libusb_claim_interface(handle_, kInterface);
if (rc != 0) {
ofLogError("HantekDevice") << "claim_interface failed: " << libusb_error_name(rc);
libusb_close(handle_);
handle_ = nullptr;
libusb_exit(ctx_);
ctx_ = nullptr;
status_.store(HantekStatus::InterfaceClaimFailed);
return HantekStatus::InterfaceClaimFailed;
}
rc = libusb_set_interface_alt_setting(handle_, kInterface, kAltSetting);
ofLogNotice("HantekDevice") << "set_alt_setting(" << kInterface << ", " << kAltSetting
<< ") = " << rc << " (" << libusb_error_name(rc) << ")";
if (rc != 0) {
ofLogWarning("HantekDevice") << "alt_setting failed (continuing)";
}
libusb_clear_halt(handle_, kEpIn);
if (!configureDevice()) {
libusb_release_interface(handle_, kInterface);
libusb_close(handle_);
handle_ = nullptr;
libusb_exit(ctx_);
ctx_ = nullptr;
status_.store(HantekStatus::UsbError);
return HantekStatus::UsbError;
}
running_.store(true);
status_.store(HantekStatus::Ok);
worker_ = std::thread([this] { streamLoop(); });
return HantekStatus::Ok;
}
void HantekDevice::stop() {
running_.store(false);
if (worker_.joinable()) worker_.join();
if (handle_ != nullptr) {
libusb_release_interface(handle_, kInterface);
libusb_close(handle_);
handle_ = nullptr;
}
if (ctx_ != nullptr) {
libusb_exit(ctx_);
ctx_ = nullptr;
}
}
void HantekDevice::setSampleRate(uint32_t hz) {
sampleRateHz_.store(hz);
if (handle_ != nullptr && running_.load()) {
const uint8_t code = sampleRateCode(hz);
sendVendorControl(kReqSetSampleRate, 0, 0, &code, 1);
}
}
void HantekDevice::setGain(int channel, HantekGain gain) {
const uint8_t code = gainCode(gain);
if (channel == 1) {
gainCh1_.store(static_cast<int>(gain));
if (handle_) sendVendorControl(kReqSetCh1Gain, 0, 0, &code, 1);
} else if (channel == 2) {
gainCh2_.store(static_cast<int>(gain));
if (handle_) sendVendorControl(kReqSetCh2Gain, 0, 0, &code, 1);
}
}
bool HantekDevice::sendVendorControl(uint8_t request, uint16_t value,
uint16_t index, const uint8_t* data,
uint16_t length) {
const uint8_t bmRequestType =
LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_OUT;
int rc = libusb_control_transfer(handle_, bmRequestType, request, value, index,
const_cast<uint8_t*>(data), length, 1000);
if (rc < 0) {
ofLogError("HantekDevice") << "control_transfer 0x" << std::hex << int(request)
<< " failed: " << libusb_error_name(rc);
return false;
}
return true;
}
bool HantekDevice::configureDevice() {
const uint8_t numChannels = 2;
if (!sendVendorControl(kReqSetNumChannels, 0, 0, &numChannels, 1)) return false;
const uint8_t srCode = sampleRateCode(sampleRateHz_.load());
if (!sendVendorControl(kReqSetSampleRate, 0, 0, &srCode, 1)) return false;
const uint8_t g1 = gainCode(static_cast<HantekGain>(gainCh1_.load()));
if (!sendVendorControl(kReqSetCh1Gain, 0, 0, &g1, 1)) return false;
const uint8_t g2 = gainCode(static_cast<HantekGain>(gainCh2_.load()));
if (!sendVendorControl(kReqSetCh2Gain, 0, 0, &g2, 1)) return false;
// E3 = trigger / start sampling (firmware fx2lafw / OpenHantek6022)
const uint8_t startTrig = 0x01;
if (!sendVendorControl(0xE3, 0, 0, &startTrig, 1)) {
ofLogWarning("HantekDevice") << "vendor 0xE3 (start) failed (continuing)";
}
ofLogNotice("HantekDevice") << "configureDevice: numCh=2 sr=" << (int)srCode
<< " g1=" << (int)g1 << " g2=" << (int)g2 << " trig=0x01";
return true;
}
void HantekDevice::streamLoop() {
std::vector<uint8_t> raw(kBulkXferBytes);
std::vector<float> ch1, ch2;
ch1.reserve(kBulkXferBytes / 2);
ch2.reserve(kBulkXferBytes / 2);
while (running_.load()) {
int actual = 0;
int rc = libusb_bulk_transfer(handle_, kEpIn, raw.data(),
static_cast<int>(raw.size()),
&actual, kBulkTimeoutMs);
if (rc == LIBUSB_ERROR_TIMEOUT) {
continue;
}
if (rc != 0) {
ofLogWarning("HantekDevice") << "bulk_transfer: " << libusb_error_name(rc);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
static std::atomic<uint64_t> totalBytes{0};
static auto lastLog = std::chrono::steady_clock::now();
totalBytes += actual;
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastLog).count() >= 2) {
ofLogNotice("HantekDevice") << "stream " << totalBytes.load() << " bytes received";
lastLog = now;
}
// Format OpenHantek6022 : octets entrelacés [CH1, CH2, CH1, CH2, ...].
// Chaque échantillon est un uint8_t centré autour de 128 (offset binary).
const std::size_t pairs = static_cast<std::size_t>(actual) / 2;
ch1.resize(pairs);
ch2.resize(pairs);
for (std::size_t i = 0; i < pairs; ++i) {
const uint8_t a = raw[2 * i];
const uint8_t b = raw[2 * i + 1];
ch1[i] = (static_cast<float>(a) - 128.0f) / 128.0f;
ch2[i] = (static_cast<float>(b) - 128.0f) / 128.0f;
}
ring_.push(ch1.data(), ch2.data(), pairs);
}
}
std::string HantekDevice::statusString() const {
switch (status_.load()) {
case HantekStatus::Ok: return "OK";
case HantekStatus::NotFound: return "Aucun Hantek 6022BL detecte";
case HantekStatus::FirmwareNeeded: return "Firmware Cypress non charge (voir docs/HANTEK_SETUP.md)";
case HantekStatus::OpenFailed: return "libusb_open echec";
case HantekStatus::InterfaceClaimFailed: return "claim_interface echec";
case HantekStatus::AlreadyRunning: return "deja en cours";
case HantekStatus::UsbError: return "erreur USB";
}
return "?";
}
} // namespace oscope
+99
View File
@@ -0,0 +1,99 @@
#pragma once
// Wrapper libusb-1.0 pour l'oscilloscope Hantek 6022BL (Cypress FX2-based).
//
// Références protocolaires :
// - https://github.com/OpenHantek/OpenHantek6022 (code firmware open-source
// Cypress FX2 + commandes vendor)
// - VID 0x04B5 / PID 0x6022 (firmware non chargé) ou 0x602A (variante)
// - Endpoint bulk IN 0x86 sur l'interface 0, alt setting 1
//
// Important : le 6022BL démarre en "device générique" tant que le firmware
// Cypress n'est pas uploadé. Cette classe détecte ce cas et retourne
// Status::FirmwareNeeded au lieu de tenter un upload (le user doit utiliser
// fxload externe — voir docs/HANTEK_SETUP.md).
#include <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include "ScopeData.h"
struct libusb_context;
struct libusb_device_handle;
namespace oscope {
enum class HantekStatus {
Ok,
NotFound,
FirmwareNeeded,
OpenFailed,
InterfaceClaimFailed,
AlreadyRunning,
UsbError
};
enum class HantekGain {
G_5V = 0, ///< +/- 5 V (0x01)
G_2_5V = 1, ///< +/- 2.5 V (0x02)
G_1V = 2, ///< +/- 1 V (0x05)
G_500mV = 3, ///< +/- 500 mV (0x0a)
G_250mV = 4 ///< +/- 250 mV (0x14)
};
class HantekDevice {
public:
HantekDevice();
~HantekDevice();
HantekDevice(const HantekDevice&) = delete;
HantekDevice& operator=(const HantekDevice&) = delete;
/// Ouvre le device, claim l'interface, configure sample rate / gains.
/// Démarre le thread bulk-transfer et alimente le ringbuffer.
HantekStatus start();
/// Stoppe le thread, libère l'interface, ferme libusb.
void stop();
/// Sample rate desired (Hz). Codes valides : 1e6, 2e6, 4e6, 8e6, 16e6,
/// 24e6, 48e6 (24 et 48 MS/s ne sont disponibles qu'avec un seul canal
/// actif sur le firmware OpenHantek6022).
void setSampleRate(uint32_t hz);
/// Gain par canal (1 ou 2).
void setGain(int channel, HantekGain gain);
/// Accesseur vers le ringbuffer partagé.
ScopeRing& ring() { return ring_; }
/// Statut courant (Ok ou dernier code d'erreur).
HantekStatus status() const { return status_.load(); }
/// Description humaine du dernier statut.
std::string statusString() const;
/// Indique si un firmware doit être chargé (renvoyé par start()).
bool firmwareNeeded() const { return status_.load() == HantekStatus::FirmwareNeeded; }
private:
void streamLoop();
bool sendVendorControl(uint8_t request, uint16_t value, uint16_t index,
const uint8_t* data, uint16_t length);
bool configureDevice();
libusb_context* ctx_;
libusb_device_handle* handle_;
std::thread worker_;
std::atomic<bool> running_;
std::atomic<HantekStatus> status_;
std::atomic<uint32_t> sampleRateHz_;
std::atomic<int> gainCh1_;
std::atomic<int> gainCh2_;
ScopeRing ring_;
};
} // namespace oscope
+82
View File
@@ -0,0 +1,82 @@
#include "OscClient.h"
#include "ofLog.h"
namespace oscope {
void OscClient::setup(int listenPort, const std::string& sendHost, int sendPort) {
receiver_.setup(listenPort);
sender_.setup(sendHost, sendPort);
// Pré-init des 8 voies connues.
for (const auto& v : {"kick", "hat", "snare", "clap", "perc",
"melody", "acid", "harmony"}) {
amp_[v] = 0.0f;
}
ofLogNotice("OscClient") << "listen :" << listenPort
<< " send -> " << sendHost << ":" << sendPort;
}
void OscClient::update() {
while (receiver_.hasWaitingMessages()) {
ofxOscMessage m;
receiver_.getNextMessage(m);
const std::string& a = m.getAddress();
if (a == "/sync/bpm" && m.getNumArgs() >= 1) {
bpm_ = m.getArgAsFloat(0);
} else if (a == "/sync/beat" && m.getNumArgs() >= 1) {
beat_ = m.getArgAsInt32(0);
} else if (a == "/sync/amp" && m.getNumArgs() >= 2) {
amp_[m.getArgAsString(0)] = m.getArgAsFloat(1);
} else if (a == "/sync/rms" && m.getNumArgs() >= 1) {
rms_ = m.getArgAsFloat(0);
} else if (a == "/sync/album" && m.getNumArgs() >= 1) {
album_ = m.getArgAsString(0);
} else if (a == "/sync/melody" && m.getNumArgs() >= 1) {
melody_ = m.getArgAsString(0);
} else if (a == "/sync/synthdef" && m.getNumArgs() >= 1) {
synthdef_ = m.getArgAsString(0);
}
}
}
float OscClient::amp(const std::string& voice) const {
auto it = amp_.find(voice);
return (it == amp_.end()) ? 0.0f : it->second;
}
bool OscClient::beatPulse() {
if (beat_ != lastBeatPulsed_) {
lastBeatPulsed_ = beat_;
return true;
}
return false;
}
void OscClient::sendKick(const std::string& name) {
sendControl("/control/kk", name);
}
void OscClient::sendMelody(const std::string& name) {
sendControl("/control/setMelody", name);
}
void OscClient::sendAlbum(const std::string& name) {
sendControl("/control/setAlbum", name);
}
void OscClient::sendControl(const std::string& addr, float value) {
ofxOscMessage m;
m.setAddress(addr);
m.addFloatArg(value);
sender_.sendMessage(m, false);
}
void OscClient::sendControl(const std::string& addr, const std::string& value) {
ofxOscMessage m;
m.setAddress(addr);
m.addStringArg(value);
sender_.sendMessage(m, false);
}
} // namespace oscope
+63
View File
@@ -0,0 +1,63 @@
#pragma once
// Client OSC pour le pont sound_algo.
//
// Reçoit sur 127.0.0.1:57122 les messages /sync/* émis par le bridge :
// /sync/bpm <float>
// /sync/beat <int>
// /sync/amp <string voice> <float val> // 8 voies
// /sync/rms <float master>
// /sync/album <string>
// /sync/melody <string>
// /sync/synthdef <string>
//
// Envoie sur 127.0.0.1:57121 les messages /control/* compris par sclang
// (kicks, mélodies, FX, etc.).
#include "ofxOsc.h"
#include <string>
#include <unordered_map>
#include <vector>
namespace oscope {
class OscClient {
public:
void setup(int listenPort, const std::string& sendHost, int sendPort);
void update();
// Etat courant (lu par les visualizers).
float bpm() const { return bpm_; }
int beat() const { return beat_; }
float amp(const std::string& voice) const;
float rms() const { return rms_; }
const std::string& album() const { return album_; }
const std::string& melody() const { return melody_; }
const std::string& synthdef() const { return synthdef_; }
/// Beat impulse : retourne true une seule fois quand le beat change.
bool beatPulse();
/// Helpers d'envoi.
void sendKick(const std::string& name);
void sendMelody(const std::string& name);
void sendAlbum(const std::string& name);
void sendControl(const std::string& addr, float value);
void sendControl(const std::string& addr, const std::string& value);
private:
ofxOscReceiver receiver_;
ofxOscSender sender_;
float bpm_ = 120.0f;
int beat_ = 0;
int lastBeatPulsed_ = -1;
float rms_ = 0.0f;
std::unordered_map<std::string, float> amp_;
std::string album_;
std::string melody_;
std::string synthdef_;
};
} // namespace oscope
+69
View File
@@ -0,0 +1,69 @@
#pragma once
// Structure partagée entre le thread USB Hantek et le thread principal OF.
// Ringbuffer SPSC (single-producer / single-consumer) lock-free basé sur
// std::atomic. Le producteur est le thread bulk-transfer libusb, le consommateur
// est ofApp::update().
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace oscope {
// Capacité du ringbuffer en échantillons par canal. Doit être une puissance de 2
// pour permettre le masquage modulo via (idx & (kCapacity - 1)).
static constexpr std::size_t kRingCapacity = 1u << 18; // 262144 samples
/// Buffer SPSC partagé pour les échantillons CH1/CH2 normalisés [-1, 1].
class ScopeRing {
public:
ScopeRing() : write_(0), read_(0) {
ch1_.resize(kRingCapacity, 0.0f);
ch2_.resize(kRingCapacity, 0.0f);
}
/// Producteur : écrit n échantillons. Si le buffer est plein, écrase
/// les plus anciens (overwrite policy, on préfère perdre du passé que
/// bloquer le thread USB).
void push(const float* ch1, const float* ch2, std::size_t n) {
const std::size_t mask = kRingCapacity - 1;
std::size_t w = write_.load(std::memory_order_relaxed);
for (std::size_t i = 0; i < n; ++i) {
ch1_[(w + i) & mask] = ch1[i];
ch2_[(w + i) & mask] = ch2[i];
}
write_.store(w + n, std::memory_order_release);
}
/// Consommateur : lit jusqu'à `nrequested` échantillons les plus récents.
/// Retourne le nombre effectivement copié.
std::size_t readLatest(std::vector<float>& outCh1,
std::vector<float>& outCh2,
std::size_t nrequested) {
const std::size_t mask = kRingCapacity - 1;
const std::size_t w = write_.load(std::memory_order_acquire);
const std::size_t available = (w >= nrequested) ? nrequested : w;
outCh1.resize(available);
outCh2.resize(available);
const std::size_t start = w - available;
for (std::size_t i = 0; i < available; ++i) {
outCh1[i] = ch1_[(start + i) & mask];
outCh2[i] = ch2_[(start + i) & mask];
}
read_.store(w, std::memory_order_release);
return available;
}
std::size_t writeIndex() const { return write_.load(std::memory_order_acquire); }
private:
std::vector<float> ch1_;
std::vector<float> ch2_;
std::atomic<std::size_t> write_;
std::atomic<std::size_t> read_;
};
} // namespace oscope
+19
View File
@@ -0,0 +1,19 @@
// oscope-of — main entry point.
// Fenêtre 1920x1080 avec MSAA 8x, OpenGL 3.2 core profile pour les shaders modernes.
#include "ofMain.h"
#include "ofApp.h"
int main() {
ofGLFWWindowSettings settings;
settings.setGLVersion(3, 2);
settings.setSize(1920, 1080);
settings.numSamples = 8;
settings.windowMode = OF_WINDOW;
settings.title = "oscope-of";
auto window = ofCreateWindow(settings);
ofRunApp(window, std::make_shared<ofApp>());
ofRunMainLoop();
return 0;
}
+173
View File
@@ -0,0 +1,173 @@
#include "ofApp.h"
#include "ofxOsc.h"
#include <fstream>
#include <sstream>
namespace {
// Mini-parseur JSON minimaliste pour settings.json — suffisant pour les clés
// plates qu'on utilise (pas de vraie dépendance à ofxJSON).
std::string extractString(const std::string& src, const std::string& key,
const std::string& fallback) {
auto p = src.find("\"" + key + "\"");
if (p == std::string::npos) return fallback;
p = src.find(':', p);
if (p == std::string::npos) return fallback;
auto q = src.find('"', p);
if (q == std::string::npos) return fallback;
auto r = src.find('"', q + 1);
if (r == std::string::npos) return fallback;
return src.substr(q + 1, r - q - 1);
}
float extractNumber(const std::string& src, const std::string& key, float fallback) {
auto p = src.find("\"" + key + "\"");
if (p == std::string::npos) return fallback;
p = src.find(':', p);
if (p == std::string::npos) return fallback;
std::size_t i = p + 1;
while (i < src.size() && (src[i] == ' ' || src[i] == '\n' || src[i] == '\t')) ++i;
std::size_t j = i;
while (j < src.size() && (std::isdigit(src[j]) || src[j] == '.' || src[j] == '-')) ++j;
if (j == i) return fallback;
try { return std::stof(src.substr(i, j - i)); } catch (...) { return fallback; }
}
} // namespace
void ofApp::setup() {
ofSetVerticalSync(true);
ofSetFrameRate(60);
ofBackground(0);
ofSetCircleResolution(72);
loadSettings();
osc_.setup(oscListenPort_, oscSendHost_, oscSendPort_);
auto st = scope_.start();
if (st == oscope::HantekStatus::FirmwareNeeded) {
ofLogWarning("ofApp") << "Hantek firmware non charge — fonctionnement degrade.";
}
const int W = ofGetWidth();
const int H = ofGetHeight();
lissajous_ = std::make_unique<oscope::LissajousVis>();
spectro_ = std::make_unique<oscope::SpectrogramVis>();
reactive_ = std::make_unique<oscope::ReactiveVis>();
waveform_ = std::make_unique<oscope::WaveformVis>();
lissajous_->setup(W, H);
spectro_->setup(W, H / 4);
reactive_->setup(W, H);
waveform_->setup(W, H);
gui_.setup("oscope-of");
gui_.add(modeLabel_.setup("Mode", "Hybrid"));
gui_.add(scopeStatusLabel_.setup("Scope", scope_.statusString()));
gui_.add(oscStatusLabel_.setup("OSC", "listening"));
gui_.add(gainCh1_.setup("CH1 V/div", 1.0f, 0.25f, 5.0f));
gui_.add(gainCh2_.setup("CH2 V/div", 1.0f, 0.25f, 5.0f));
gui_.add(trailFade_.setup("Trail fade", 0.07f, 0.0f, 1.0f));
gui_.add(sampleRateHz_.setup("Sample rate", 8000000, 1000000, 48000000));
ch1_.reserve(bufferSize_);
ch2_.reserve(bufferSize_);
}
void ofApp::loadSettings() {
std::ifstream f(ofToDataPath("settings.json"));
if (!f.is_open()) return;
std::stringstream ss; ss << f.rdbuf();
const std::string s = ss.str();
oscListenPort_ = static_cast<int>(extractNumber(s, "listen_port", oscListenPort_));
oscSendPort_ = static_cast<int>(extractNumber(s, "send_port", oscSendPort_));
oscSendHost_ = extractString(s, "send_host", oscSendHost_);
bufferSize_ = static_cast<int>(extractNumber(s, "buffer_size", bufferSize_));
const std::string m = extractString(s, "default_mode", "hybrid");
if (m == "lissajous") mode_ = Mode::Lissajous;
else if (m == "spectro" || m == "spectrogram") mode_ = Mode::Spectrogram;
else if (m == "reactive") mode_ = Mode::Reactive;
else if (m == "waveform" || m == "scope") mode_ = Mode::Waveform;
else mode_ = Mode::Hybrid;
}
void ofApp::update() {
osc_.update();
scope_.ring().readLatest(ch1_, ch2_, static_cast<std::size_t>(bufferSize_));
oscope::VisFrame frame{ch1_, ch2_, osc_};
if (mode_ == Mode::Lissajous || mode_ == Mode::Hybrid) lissajous_->update(frame);
if (mode_ == Mode::Spectrogram || mode_ == Mode::Hybrid) spectro_->update(frame);
if (mode_ == Mode::Reactive || mode_ == Mode::Hybrid) reactive_->update(frame);
if (mode_ == Mode::Waveform || mode_ == Mode::Hybrid) waveform_->update(frame);
scopeStatusLabel_ = "Scope: " + scope_.statusString() + " (" +
ofToString(ch1_.size()) + " s)";
oscStatusLabel_ = "OSC bpm=" + ofToString(osc_.bpm(), 1) +
" beat=" + ofToString(osc_.beat()) +
" kick=" + ofToString(osc_.amp("kick"), 2);
}
void ofApp::draw() {
const int W = ofGetWidth();
const int H = ofGetHeight();
if (mode_ == Mode::Reactive || mode_ == Mode::Hybrid) {
reactive_->draw(0, 0, W, H);
} else {
ofBackground(0);
}
if (mode_ == Mode::Waveform) {
waveform_->draw(0, 0, W, H);
} else if (mode_ == Mode::Lissajous || mode_ == Mode::Hybrid) {
lissajous_->draw(0, 0, W, H);
}
if (mode_ == Mode::Spectrogram || mode_ == Mode::Hybrid) {
const int sh = (mode_ == Mode::Spectrogram) ? H : H / 4;
const int sy = (mode_ == Mode::Spectrogram) ? 0 : H - sh;
spectro_->draw(0, sy, W, sh);
}
if (showGui_) {
gui_.draw();
drawHud();
}
}
void ofApp::drawHud() {
ofPushStyle();
ofSetColor(180, 220, 200, 220);
ofDrawBitmapString("FPS: " + ofToString(ofGetFrameRate(), 1), 12, ofGetHeight() - 28);
ofDrawBitmapString("Mode: " +
std::string(mode_ == Mode::Lissajous ? "Lissajous" :
mode_ == Mode::Spectrogram ? "Spectrogram" :
mode_ == Mode::Reactive ? "Reactive" :
mode_ == Mode::Waveform ? "Waveform" : "Hybrid"),
12, ofGetHeight() - 12);
ofPopStyle();
}
void ofApp::exit() {
scope_.stop();
}
void ofApp::keyPressed(int key) {
switch (key) {
case '1': mode_ = Mode::Lissajous; modeLabel_ = "Lissajous"; break;
case '2': mode_ = Mode::Spectrogram; modeLabel_ = "Spectrogram"; break;
case '3': mode_ = Mode::Reactive; modeLabel_ = "Reactive"; break;
case '4': mode_ = Mode::Hybrid; modeLabel_ = "Hybrid"; break;
case '5': mode_ = Mode::Waveform; modeLabel_ = "Waveform"; break;
case 'f':
fullscreen_ = !fullscreen_;
ofSetFullscreen(fullscreen_);
break;
case 'g': showGui_ = !showGui_; break;
case 'r':
lissajous_->reloadShaders();
reactive_->reloadShaders();
ofLogNotice("ofApp") << "Shaders rechargés";
break;
default: break;
}
}
+62
View File
@@ -0,0 +1,62 @@
#pragma once
// Orchestrateur principal :
// - démarre le scope Hantek + thread USB
// - démarre le client OSC
// - gère le mode courant (Lissajous / Spectrogram / Reactive / Hybrid)
// - assemble les visualizers et la GUI ofxPanel
#include "ofMain.h"
#include "ofxGui.h"
#include "HantekDevice.h"
#include "OscClient.h"
#include "visualizers/LissajousVis.h"
#include "visualizers/ReactiveVis.h"
#include "visualizers/SpectrogramVis.h"
#include "visualizers/WaveformVis.h"
#include <memory>
class ofApp : public ofBaseApp {
public:
enum class Mode { Lissajous, Spectrogram, Reactive, Waveform, Hybrid };
void setup() override;
void update() override;
void draw() override;
void exit() override;
void keyPressed(int key) override;
private:
void loadSettings();
void drawHud();
oscope::HantekDevice scope_;
oscope::OscClient osc_;
std::unique_ptr<oscope::LissajousVis> lissajous_;
std::unique_ptr<oscope::SpectrogramVis> spectro_;
std::unique_ptr<oscope::ReactiveVis> reactive_;
std::unique_ptr<oscope::WaveformVis> waveform_;
std::vector<float> ch1_, ch2_;
Mode mode_ = Mode::Hybrid;
bool showGui_ = true;
bool fullscreen_ = false;
// GUI
ofxPanel gui_;
ofxFloatSlider gainCh1_, gainCh2_;
ofxFloatSlider trailFade_;
ofxIntSlider sampleRateHz_;
ofxLabel scopeStatusLabel_;
ofxLabel oscStatusLabel_;
ofxLabel modeLabel_;
// Settings persistants.
int oscListenPort_ = 57122;
std::string oscSendHost_ = "127.0.0.1";
int oscSendPort_ = 57121;
int bufferSize_ = 4096;
};
@@ -0,0 +1,74 @@
#include "LissajousVis.h"
namespace oscope {
void LissajousVis::setup(int w, int h) {
w_ = w; h_ = h;
ofFboSettings s;
s.width = w; s.height = h;
s.internalformat = GL_RGBA16F;
s.numSamples = 0;
s.useDepth = false;
trail_.allocate(s);
trail_.begin(); ofClear(0, 0, 0, 255); trail_.end();
reloadShaders();
}
void LissajousVis::reloadShaders() {
glow_.load("shaders/glow");
}
void LissajousVis::update(const VisFrame& frame) {
// Détection beat -> flash kick (utilise l'amplitude kick).
const float k = frame.osc.amp("kick");
if (k > kickFlash_) kickFlash_ = k;
kickFlash_ *= 0.92f;
// Dessin dans le FBO trail avec fade.
trail_.begin();
ofPushStyle();
ofEnableAlphaBlending();
// Fade : rectangle noir semi-transparent.
ofSetColor(0, 0, 0, 18);
ofDrawRectangle(0, 0, w_, h_);
// Trace CH1/CH2 en LINE_STRIP centré dans le FBO.
const auto& ch1 = frame.ch1;
const auto& ch2 = frame.ch2;
const std::size_t n = std::min(ch1.size(), ch2.size());
if (n >= 2) {
const float cx = w_ * 0.5f;
const float cy = h_ * 0.5f;
const float scale = 0.45f * std::min(w_, h_);
ofMesh mesh;
mesh.setMode(OF_PRIMITIVE_LINE_STRIP);
const float intensity = 0.6f + 0.4f * kickFlash_;
ofFloatColor base(0.0f, 1.0f, 0.5f, intensity); // vert phosphor #00FF7F
for (std::size_t i = 0; i < n; ++i) {
mesh.addVertex(glm::vec3(cx + ch1[i] * scale,
cy - ch2[i] * scale, 0.0f));
mesh.addColor(base);
}
mesh.draw();
}
ofPopStyle();
trail_.end();
}
void LissajousVis::draw(int x, int y, int w, int h) {
ofPushStyle();
ofEnableBlendMode(OF_BLENDMODE_ADD);
if (glow_.isLoaded()) {
glow_.begin();
glow_.setUniformTexture("uTex", trail_.getTexture(), 0);
glow_.setUniform2f("uTexel", 1.0f / w_, 1.0f / h_);
glow_.setUniform1f("uIntensity", 1.2f + 0.6f * kickFlash_);
trail_.draw(x, y, w, h);
glow_.end();
} else {
trail_.draw(x, y, w, h);
}
ofPopStyle();
}
} // namespace oscope
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Lissajous XY scope avec persistance type CRT phosphor.
// FBO en feedback : à chaque frame on dessine un fade noir alpha-bas par-dessus
// puis on trace les segments (CH1, CH2). Un shader glow (Gaussian blur 9-tap)
// est appliqué en sortie.
#include "Visualizer.h"
#include "ofMain.h"
namespace oscope {
class LissajousVis : public Visualizer {
public:
void setup(int w, int h) override;
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
void reloadShaders() override;
private:
ofFbo trail_;
ofShader glow_;
int w_ = 0;
int h_ = 0;
float kickFlash_ = 0.0f; // 0..1, décrémenté à chaque frame
int lastBeat_ = -1;
};
} // namespace oscope
+56
View File
@@ -0,0 +1,56 @@
#include "ReactiveVis.h"
namespace oscope {
void ReactiveVis::setup(int w, int h) {
w_ = w; h_ = h;
reloadShaders();
}
void ReactiveVis::reloadShaders() {
bg_.load("shaders/bg");
}
void ReactiveVis::update(const VisFrame& frame) {
time_ += ofGetLastFrameTime();
auto& osc = frame.osc;
bpm_ = osc.bpm();
beat_ = osc.beat();
// Lissage exponentiel pour éviter les jumps brutaux.
auto smooth = [](float& s, float target) { s += (target - s) * 0.18f; };
smooth(kick_, osc.amp("kick"));
smooth(hat_, osc.amp("hat"));
smooth(snare_, osc.amp("snare"));
smooth(clap_, osc.amp("clap"));
smooth(perc_, osc.amp("perc"));
smooth(melody_, osc.amp("melody"));
smooth(acid_, osc.amp("acid"));
smooth(harmony_, osc.amp("harmony"));
}
void ReactiveVis::draw(int x, int y, int w, int h) {
if (!bg_.isLoaded()) {
ofPushStyle();
ofSetColor(8, 6, 16);
ofDrawRectangle(x, y, w, h);
ofPopStyle();
return;
}
bg_.begin();
bg_.setUniform2f("uResolution", w, h);
bg_.setUniform1f("uTime", time_);
bg_.setUniform1f("uBpm", bpm_);
bg_.setUniform1i("uBeat", beat_);
bg_.setUniform1f("uKick", kick_);
bg_.setUniform1f("uHat", hat_);
bg_.setUniform1f("uSnare", snare_);
bg_.setUniform1f("uClap", clap_);
bg_.setUniform1f("uPerc", perc_);
bg_.setUniform1f("uMelody", melody_);
bg_.setUniform1f("uAcid", acid_);
bg_.setUniform1f("uHarmony", harmony_);
ofDrawRectangle(x, y, w, h);
bg_.end();
}
} // namespace oscope
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Layer arrière-plan piloté par sound_algo : grille hexagonale qui pulse au
// kick, gradient HSV qui tourne au beat, distortion modulée par la mélodie.
// Tout est calculé dans bg.frag, on n'envoie que des uniforms.
#include "Visualizer.h"
#include "ofMain.h"
namespace oscope {
class ReactiveVis : public Visualizer {
public:
void setup(int w, int h) override;
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
void reloadShaders() override;
private:
ofShader bg_;
int w_ = 0, h_ = 0;
float time_ = 0.0f;
float kick_ = 0.0f, hat_ = 0.0f, snare_ = 0.0f, clap_ = 0.0f;
float perc_ = 0.0f, melody_ = 0.0f, acid_ = 0.0f, harmony_ = 0.0f;
float bpm_ = 120.0f;
int beat_ = 0;
};
} // namespace oscope
@@ -0,0 +1,71 @@
#include "SpectrogramVis.h"
#include <algorithm>
#include <cmath>
namespace oscope {
namespace {
constexpr std::size_t kFftSize = 1024;
}
SpectrogramVis::SpectrogramVis() : fft_(kFftSize) {}
void SpectrogramVis::setup(int w, int h) {
w_ = w; h_ = h;
ofFboSettings s;
s.width = w; s.height = h;
s.internalformat = GL_RGBA8;
s.useDepth = false;
fbo_.allocate(s);
scratch_.allocate(s);
fbo_.begin(); ofClear(0, 0, 0, 255); fbo_.end();
scratch_.begin(); ofClear(0, 0, 0, 255); scratch_.end();
mono_.resize(kFftSize, 0.0f);
}
void SpectrogramVis::update(const VisFrame& frame) {
const auto& ch1 = frame.ch1;
const auto& ch2 = frame.ch2;
const std::size_t n = std::min({ch1.size(), ch2.size(), kFftSize});
if (n == 0) return;
// Mixe mono = (CH1 + CH2) / 2, prend les n derniers samples, padde avec 0.
std::fill(mono_.begin(), mono_.end(), 0.0f);
for (std::size_t i = 0; i < n; ++i) mono_[i] = 0.5f * (ch1[i] + ch2[i]);
fft_.magnitude(mono_, mag_);
// Décale fbo_ d'1 px vers la gauche en blittant dans scratch_, puis dessine
// la nouvelle colonne tout à droite.
scratch_.begin();
ofClear(0, 0, 0, 255);
fbo_.draw(-1, 0);
// Nouvelle colonne : magnitude log-mappée -> hue bleu→violet→rouge.
const std::size_t bins = mag_.size();
for (int py = 0; py < h_; ++py) {
// Mapping log-fréquence : bins du bas = basses, haut = aigus.
const float t = 1.0f - static_cast<float>(py) / h_;
const std::size_t bin = static_cast<std::size_t>(std::pow(t, 3.0f) * (bins - 1));
const float magdb = 20.0f * std::log10(std::max(mag_[bin], 1e-6f)) + 60.0f;
const float v = ofClamp(magdb / 60.0f, 0.0f, 1.0f);
const float hue = 170.0f - 170.0f * v; // bleu (170) -> rouge (0)
ofColor c; c.setHsb(hue, 200.0f * (0.4f + 0.6f * v), 255.0f * v);
ofSetColor(c);
ofDrawRectangle(w_ - 1, py, 1, 1);
}
scratch_.end();
// Swap.
fbo_.begin();
ofClear(0, 0, 0, 255);
scratch_.draw(0, 0);
fbo_.end();
}
void SpectrogramVis::draw(int x, int y, int w, int h) {
ofPushStyle();
ofSetColor(255, 255, 255, 200);
fbo_.draw(x, y, w, h);
ofPopStyle();
}
} // namespace oscope
@@ -0,0 +1,29 @@
#pragma once
// Spectrogramme FFT scrollant. Une nouvelle colonne est ajoutée à droite à
// chaque update, le contenu de la texture se décale d'1 pixel vers la gauche.
#include "../FFT.h"
#include "Visualizer.h"
#include "ofMain.h"
namespace oscope {
class SpectrogramVis : public Visualizer {
public:
SpectrogramVis();
void setup(int w, int h) override;
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
private:
ofFbo fbo_;
ofFbo scratch_;
int w_ = 0;
int h_ = 0;
FFT fft_;
std::vector<float> mono_;
std::vector<float> mag_;
};
} // namespace oscope
+27
View File
@@ -0,0 +1,27 @@
#pragma once
// Interface abstraite pour les 3 modes de visualisation.
#include "../OscClient.h"
#include "../ScopeData.h"
#include <vector>
namespace oscope {
struct VisFrame {
const std::vector<float>& ch1;
const std::vector<float>& ch2;
OscClient& osc;
};
class Visualizer {
public:
virtual ~Visualizer() = default;
virtual void setup(int w, int h) = 0;
virtual void update(const VisFrame& frame) = 0;
virtual void draw(int x, int y, int w, int h) = 0;
virtual void reloadShaders() {}
};
} // namespace oscope
+132
View File
@@ -0,0 +1,132 @@
#include "WaveformVis.h"
#include <cmath>
#include <algorithm>
namespace oscope {
void WaveformVis::setup(int w, int h) {
w_ = w;
h_ = h;
trace1_.assign(syntheticSize_, 0.0f);
trace2_.assign(syntheticSize_, 0.0f);
}
void WaveformVis::update(const VisFrame& frame) {
const auto& osc = frame.osc;
// Pulse de beat : déclenche un envelope decay quand /sync/beat tick
int b = osc.beat();
if (b != prevBeat_) {
beatPhase_ = 1.0f;
prevBeat_ = b;
}
beatPhase_ *= 0.92f;
// Si Hantek a fourni des samples, on les copie tels quels
if (frame.ch1.size() > 16) {
std::size_t n = std::min(frame.ch1.size(), trace1_.size());
for (std::size_t i = 0; i < n; ++i) {
trace1_[i] = frame.ch1[i];
trace2_[i] = (i < frame.ch2.size()) ? frame.ch2[i] : 0.0f;
}
if (n < trace1_.size()) {
std::fill(trace1_.begin() + n, trace1_.end(), 0.0f);
std::fill(trace2_.begin() + n, trace2_.end(), 0.0f);
}
return;
}
// Sinon : synthèse à partir des audio meters reçus en OSC
const float kick = osc.amp("kick");
const float snare = osc.amp("snare");
const float melody = osc.amp("melody");
const float hat = osc.amp("hat");
const float bpm = std::max(60.0f, std::min(240.0f, osc.bpm()));
// Fréquence porteuse modulée par melody
const float carrierHz = 220.0f + melody * 1800.0f;
const float sampleRate = 48000.0f;
const float dphi = ofWrap(2.0f * PI * carrierHz / sampleRate, 0.0f, 1e9f);
// Sub-bass kick
const float subHz = 60.0f;
const float dsub = 2.0f * PI * subHz / sampleRate;
for (int i = 0; i < syntheticSize_; ++i) {
phase_ += dphi;
if (phase_ > 1e6f) phase_ = std::fmod(phase_, 2.0f * PI);
const float t = static_cast<float>(i) / static_cast<float>(syntheticSize_);
const float carrier = std::sin(phase_) * (0.3f + melody * 0.6f);
const float sub = std::sin(t * dsub * syntheticSize_) * kick * (0.4f + beatPhase_ * 0.6f);
const float noise = (ofRandom(-1.0f, 1.0f)) * (snare * 0.4f + hat * 0.25f);
const float ch1 = (carrier + sub + noise) * (0.6f + beatPhase_ * 0.4f);
const float carrier2 = std::sin(phase_ * 1.5f + 0.7f) * (0.25f + melody * 0.5f);
const float ch2 = carrier2 + sub * 0.7f + noise * 0.6f;
trace1_[i] = std::tanh(ch1);
trace2_[i] = std::tanh(ch2 * 0.9f);
}
}
void WaveformVis::draw(int x, int y, int w, int h) {
ofPushStyle();
ofPushMatrix();
// Fond noir + grille verte CRT
ofFill();
ofSetColor(8, 12, 8);
ofDrawRectangle(x, y, w, h);
ofNoFill();
ofSetLineWidth(1);
ofSetColor(0, 80, 50, 80);
for (int i = 0; i <= divX_; ++i) {
const float gx = x + (w / divX_) * i;
ofDrawLine(gx, y, gx, y + h);
}
for (int j = 0; j <= divY_; ++j) {
const float gy = y + (h / divY_) * j;
ofDrawLine(x, gy, x + w, gy);
}
// Centre lines, plus marqués
ofSetColor(0, 140, 80, 130);
ofDrawLine(x, y + h * 0.5f, x + w, y + h * 0.5f);
ofDrawLine(x + w * 0.5f, y, x + w * 0.5f, y + h);
auto plot = [&](const std::vector<float>& trace, ofColor color, float yOff) {
ofSetColor(color);
ofSetLineWidth(2);
ofBeginShape();
const int n = static_cast<int>(trace.size());
for (int i = 0; i < n; ++i) {
const float px = x + (static_cast<float>(i) / (n - 1)) * w;
const float py = y + yOff + trace[i] * (h * 0.22f);
ofVertex(px, py);
}
ofEndShape(false);
};
// Glow phosphor : 3 passes décalées en alpha
for (int pass = 3; pass >= 1; --pass) {
const float a = 30.0f * pass;
plot(trace1_, ofColor(0, 255, 140, static_cast<int>(a)), h * 0.30f);
plot(trace2_, ofColor(255, 200, 60, static_cast<int>(a)), h * 0.70f);
}
// HUD
ofSetColor(160, 220, 180);
const float vdiv1 = 0.5f;
const float vdiv2 = 0.5f;
const float tdiv = 5.0f; // ms/div approximatif
ofDrawBitmapString("CH1 " + ofToString(vdiv1, 2) + " V/div", x + 10, y + 16);
ofDrawBitmapString("CH2 " + ofToString(vdiv2, 2) + " V/div", x + 10, y + 32);
ofDrawBitmapString("Time " + ofToString(tdiv, 1) + " ms/div", x + 10, y + 48);
ofDrawBitmapString("Trig auto", x + 10, y + 64);
ofPopMatrix();
ofPopStyle();
}
} // namespace oscope
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// Time-domain oscilloscope view (CRT-style) for ch1/ch2 buffers.
// Falls back to synthesized signal driven by OscClient telemetry when
// the Hantek scope buffer is empty (Big Sur libusb limitation).
#include "Visualizer.h"
#include "ofMain.h"
namespace oscope {
class WaveformVis : public Visualizer {
public:
void setup(int w, int h) override;
void update(const VisFrame& frame) override;
void draw(int x, int y, int w, int h) override;
private:
int w_ = 0, h_ = 0;
float phase_ = 0.0f;
float beatPhase_ = 0.0f;
int prevBeat_ = 0;
std::vector<float> trace1_;
std::vector<float> trace2_;
int syntheticSize_ = 2048;
float divX_ = 8.0f; // grid divisions
float divY_ = 8.0f;
};
} // namespace oscope