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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
ofxOsc
|
||||
ofxGui
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#version 150
|
||||
|
||||
uniform mat4 modelViewProjectionMatrix;
|
||||
in vec4 position;
|
||||
|
||||
void main() {
|
||||
gl_Position = modelViewProjectionMatrix * position;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 =
|
||||
@@ -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/)
|
||||
@@ -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...
|
||||
];
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user