Skip to content

Conversation

@lodsb
Copy link

@lodsb lodsb commented May 8, 2025

Hey,
this is an ongoing but slow-motion development from my side, just wanted to raise awareness. Currently the main improvements are USB audio support, vectorized audio engine for more CPU headroom; quite a bit of refactoring and getting rid of unnecessary/unused code, some stuff was rewritten in C++.

I am working towards:

  • a new config system (json based), so it is a bit more extensible and USB audio can be configured (primitive channel mapping)
  • hid with speech outputs instead of beep messages (likely just some static raw samples)
  • some audio input/loop recording with a predefined max buffer size (hid is necessary so you can navigate the record mode)

Ideally I'd add/look into:

  • multi-channel config with cue/master out
  • some form of CV output via USB audio to use the SC1000 to control synth gear; basically multi-channel audio output, simple low-pass on the sensor data; might be interesting to allow flexible configurations
  • look into the midi impl again; with the audio recording implemented this may be a fun little sampler

cheers!

@rasteri
Copy link
Owner

rasteri commented May 9, 2025

Oh wow this looks great, I'll try it out asap!

@lodsb
Copy link
Author

lodsb commented May 10, 2025

Cool! I forgot that you have to configure buildroot to also build a c++ stdlib which is then linked statically as I was unable to create a new linux image. My current development setup is sadly WSL-based, therefore I can only debug on the real device with serial line; which is quite combersome. I was not able to create/build a buildroot config that uses anything more modern than 2018.X.

I am slowly refactoring some of the action-mapping code (to get to some json compatible enums etc). Do you remember what the edge states meant that are passed around as unsigned char? 0 is FallingEdge, 1 is Raising? But there are also some values like 3 and 4, I haven't groked that part yet. Thanks & cheers!

@mglean
Copy link

mglean commented Jun 3, 2025

Wow, thank you, amazing.

@rasteri
Copy link
Owner

rasteri commented Jan 7, 2026

yeah, there is still some encoder bug in my code :-S The issue is that I have a SC500 and can't have the device closed/working + a serial cable attached. Do you use another approach for debugging/having a usable shell?

Yeah I just omit the rear plate and have some wires dangling out. Not pretty but it works!

You could probably do something neater if you drilled a hole and added some kind of connector for serial, maybe just another 3.5 TRS

@lodsb
Copy link
Author

lodsb commented Jan 10, 2026

The issue was more that the connector to the serial header was physically interfering with the platter attachment/encoder. but I solved it and soldered the cables directly to the board. I should now be able to debug the current issues. Thanks!

lodsb added 23 commits January 10, 2026 09:41
- Add diff sanity check: snap target_position if >0.5s from position
- Fix position wrapping: use fmod instead of single subtraction for
  short loops with high pitch values
- Reset cap_touch on track changes to force angle_offset recalculation
- Document encoder glitch protection chain across affected files
Add the new sc1000-named files to complete the migration:
- S50sc1000 init script
- sc1000-import scripts
- sc1000 binary placeholder
- sc1000-boot updater script
- Replace ActionState static globals with InputState class owned by sc1000
- Update dispatch_event/perform_action_for_deck to take InputState reference
- Convert rt and rig free functions to struct member functions
- Convert sc1000 free functions to struct member functions
- Replace #ifndef/#define include guards with #pragma once
- Remove extern "C" blocks (codebase is now pure C++)
- Replace typedef with using for type aliases
- Update MidiController to access InputState through rt->engine
- Reset player.stopped when loading new track/loop so scratching works
  immediately without needing to press play first
- Fix deploy.sh build output capture (redirect status messages to stderr)
Organize the ~30 fields in player and deck structs into logical groups
for clarity and maintainability:

Player substates (player_state.h):
- PlaybackMode enum (STOPPED, PLAYING, SCRATCHING)
- PositionState (current, target, offset, last_difference)
- PitchState (current, sync, fader, note, bend, motor_speed)
- VolumeState (set, fader_target, fader_current)
- PlatterState (touched, touched_prev with helper methods)
- RecordingState (requested, active, use_loop)
- FeedbackState (beep_type, beep_position)

Deck substates (deck_state.h):
- NavigationState (folder_idx, file_idx, files_present)
- EncoderState (angle, angle_raw, offset)
- LoopState (track pointer)

Each state group has a reset() method for centralized initialization.
Legacy field aliases maintain full backward compatibility while allowing
gradual migration to the new grouped structure.
Refactor player state into distinct input and output ownership:

- Add DeckInput struct (deck_input.h) for all input-thread-written state:
  encoder, transport, pitch, volume, source selection, and requests
- Expand DeckProcessingState (deck_processing_state.h) for audio engine
  output: position, pitch, volume, recording, and feedback state
- Add query API to AudioEngine/AudioHardware for external code to read
  output state (get_position, get_pitch, get_volume, get_deck_state)
- Update actions.cpp, deck.cpp, sc_input.cpp, sc1000.cpp to write to
  player.input.* instead of legacy player.*_state fields
- Update alsa.cpp to use query API for CV output and monitoring volume
- Migrate use_loop flag to input.source (PlaybackSource enum)
- Migrate feedback_state.beep_type to input.beep_request

This establishes clear data flow:
  Input thread -> DeckInput -> AudioEngine -> DeckProcessingState -> Query API

Legacy sync-back to old player fields remains for gradual migration.
Replace recording_state.requested/active toggle with one-shot requests:
- deck::record() now takes engine parameter to query recording state
- Sets input.record_start or input.record_stop based on current state
- State machine in sc1000.cpp processes requests and clears them
- Recording state is queried via AudioHardware::is_recording()

Mark deprecated legacy state structs:
- RecordingState: fields replaced by input.record_start/stop and input.source
- FeedbackState: fields replaced by input.beep_request
Complete the input/output state separation:
- Remove sync-back from audio_engine.cpp (pos_state, pitch_state, etc.)
- Update deck cue methods to take engine parameter and use query API:
  - is_locked(), recue(), clone(), cue(), punch_in(), punch_out()
  - Get elapsed/position from AudioHardware::get_deck_state()
  - Set offset via player.input.position_offset
- Mark player helper functions as deprecated (get_elapsed, is_active, etc.)
- Remove commented-out dead code in sc_input.cpp

External code now exclusively uses query API for output state.
- Remove deprecated RecordingState and FeedbackState structs
- Remove unused player helper functions (get_elapsed, is_active, etc.)
- Add max_volume setting for output level control
- Fix volume_knob initialization from settings->initial_volume
- Apply volume_knob in audio engine volume calculation
- Replace #define macros in sc_input.cpp with proper C++ members
- Add ButtonMachineState enum for button state machine
- Convert fader_open flags to bool type
Test infrastructure for deterministic audio engine testing:
- TestAudioBackend: mock AudioHardware rendering to memory
- InputSequence: scripted input events (encoder, touch, ADC)
- Test sample generators (sine, sweep) with DFT analysis
- WAV export for external analysis (--dump flag)
- Python spectral analysis script with STFT plots
- Automated test runner script

Tests verify pitch scaling, scratching, and MIDI control.
Devices like MPK mini Plus enumerate as ALSA cards but have no audio
output capability (output_channels=0). Previously these would match
by card number or name substring, causing ALSA open errors.

Now check output_channels > 0 before accepting a device match in both
explicit hw:N matching and fallback name substring matching.
Split sc_input.cpp into separate layers:
- sc_hardware.h/cpp: HardwareInput abstract class with SC1000Hardware impl
- midi_input.h/cpp: Generic MIDI device enumeration and event processing
- sc_input.cpp: Thin coordinator using polymorphic hardware interface

All SC1000-specific state (button debounce, PIC readings, blip filter) is now
private to SC1000Hardware. The input thread coordinator is hardware-agnostic,
enabling future support for different platforms (motorized platter, etc.).
- Remove unused deck fields: protect, ncontrol, control[]
- Remove legacy beep macros (BEEP_NONE, etc.) - use BeepType enum
- Convert int→bool: platter_enabled, disable_volume_adc,
  disable_pic_buttons, jog_reverse in sc_settings
- Remove legacy player state structs (PositionState, PitchState,
  VolumeState, PlatterState) - all state now in unified DeckInput
- Delete player_state.h (no longer needed)
- Replace sentinel values with std::optional:
  - deck::punch now std::optional<double>
  - Cues::get() returns std::optional<double>
  - Remove get_or_unset(), rename CUE_UNSET to CUE_FILE_UNSET
- Rename structs to CamelCase: Track, TrackBlock, Player, Deck,
  Sc1000, ScSettings, Mapping, AudioInterface, LoopBuffer, etc.
- Convert track.h/cpp from C to modern C++ (constexpr, remove
  extern "C" blocks, remove #ifdef __cplusplus guards)
- Replace C-style void parameter lists with empty parentheses
- Convert if-else chains to switch statements in actions.cpp
- Update docker build script to use CMake instead of Make
- Add explicit -O3 for Release builds in CMakeLists.txt
- Add -flto to link options for proper LTO
- Remove legacy Makefile-based build system
- Remove obsolete xwax scripts and documentation
- Remove debug/profiling artifacts (gmon.out, csv files)
- Add .gitignore for build dirs and IDE files
- Keep original_version/ as reference for xwax code
Use LINK_FLAGS property instead of target_link_options which
requires CMake 3.13+. The Docker container has CMake 3.10.
Auto-cue mode:
- Divide track into 4/8/16/32 equal parts for beat slicing
- Toggle with Cue 1+2 (scratch deck) or Cue 3+4 (beat deck)
- MIDI cues trigger jumps to calculated positions
- Add parameter field to GPIO mappings for button index

ALSA fixes:
- Call snd_pcm_drop() before snd_pcm_close() for clean release
- Implement AlsaAudio::stop() to properly stop PCM on shutdown
- Add snd_config_update_free_global() on exit

Startup volume:
- Default volume_knob, crossfader, fader_current to 0 (muted)
- player.init() sets initial_volume and opens crossfader
- Prevents loud audio burst before input thread starts

Bug fixes:
- Fix settings file path casing (sc_settings.json)
- Fix log file path (sc1000.log)
- Initialize card_id in ALSA device scanning
@lodsb
Copy link
Author

lodsb commented Jan 12, 2026

Some fixes, updated the updater and added simple "auto cue markers" so you can slice up a sample (the mode toggles between cue markers from file and 4/8/16/32 equal divisions). There is also a bit more hardware abstraction going on, if one may "port" this over.

lodsb added 4 commits January 13, 2026 01:18
- Fix CV crossfader output using audio volume instead of fader position
- Fix SC500 volume squared bug (crossfader was set to volume_knob)
- Handle track wrap in position control mode (shortest path diff)
- Clamp scratch pitch to ±5x to prevent oscillation
- Add diagnostic logging for prolonged low volume conditions
- Fix fader_current reset() consistency
New setting controls maximum pitch multiplier during scratching.
Higher values give snappier response to quick movements.
Default: 10.0 (was hardcoded 5.0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants