Skip to content

Commit b891902

Browse files
committed
add FaustProcessor compileFromBitcode for pickling
1 parent fcade6d commit b891902

File tree

6 files changed

+290
-84
lines changed

6 files changed

+290
-84
lines changed

CLAUDE.md

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,22 @@ DawDreamer is a Digital Audio Workstation (DAW) framework for Python enabling pr
88

99
---
1010

11-
## Quick Install (Library Already Built)
11+
## Quick Install
1212

13-
If `dawdreamer/dawdreamer.so` exists:
13+
`setup.py` handles the full flow: building C++ (if needed), copying the `.so`, and installing. It detects source changes and skips recompilation when up-to-date.
1414

1515
```bash
16-
# Minimal output (recommended for LLMs)
17-
python3 setup.py develop --quiet && echo "✓ Installed" || echo "✗ Failed"
16+
pip install -e .
1817

1918
# Verify
20-
python3 -c "import dawdreamer; dawdreamer.RenderEngine(44100, 512)" && echo "✓ Working"
19+
python3 -c "import dawdreamer; dawdreamer.RenderEngine(44100, 512)"
2120
```
2221

23-
**WSL2 Note**: Takes 1-2 minutes (normal - processing Faust libraries).
22+
**WSL2 Note**: First install takes several minutes (C++ build + processing Faust libraries). Subsequent installs skip the C++ build if sources haven't changed.
2423

2524
---
2625

27-
## Quick Build (Library Doesn't Exist)
26+
## First-Time Setup (Prerequisites)
2827

2928
### Linux
3029

@@ -34,22 +33,12 @@ sudo apt-get install -yq build-essential clang cmake git python3-dev \
3433
libboost-all-dev libfreetype6-dev libncurses-dev \
3534
libx11-dev libxrandr-dev libasound2-dev libxcomposite-dev
3635

37-
# Setup and build
36+
# Submodules and Faust
3837
git submodule update --init --recursive
3938
cd thirdparty/libfaust && python3 download_libfaust.py && cd ../..
4039

41-
export PYTHONLIBPATH=/usr/lib/python3.12
42-
export PYTHONINCLUDEPATH=/usr/include/python3.12
43-
44-
cd thirdparty/libsamplerate && \
45-
cmake -DCMAKE_BUILD_TYPE=Release -Bbuild_release && \
46-
cmake --build build_release && cd ../..
47-
48-
cd Builds/LinuxMakefile && \
49-
make CONFIG=Release CXXFLAGS="-I$PYTHONINCLUDEPATH" LDFLAGS="-L$PYTHONLIBPATH" && \
50-
cd ../..
51-
52-
python3 setup.py develop --quiet && echo "✓ Done"
40+
# Build and install (builds C++, copies .so, installs package)
41+
pip install -e .
5342
```
5443

5544
### macOS
@@ -74,7 +63,8 @@ python setup.py develop
7463

7564
| Issue | Solution |
7665
|-------|----------|
77-
| Slow install (WSL2) | Normal - wait 1-2 min |
66+
| Slow first install (WSL2) | Normal - C++ build + Faust libs |
67+
| C++ changes not detected (WSL2) | `rm -rf Builds/LinuxMakefile/build` then reinstall |
7868
| Missing libtinfo (Linux) | `sudo apt-get install libncurses-dev` |
7969
| Import fails | Check Python 3.11-3.14: `python3 --version` |
8070
| Arch mismatch (macOS) | Set `ARCHS=arm64` or `x86_64` |

DEVELOPER.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,19 +166,27 @@ cmake --build build_release --config Release
166166
cd ../..
167167
```
168168

169-
#### Build DawDreamer:
169+
#### Build, Copy, and Install:
170+
171+
`setup.py` handles the full flow: building C++ via `make`, copying the `.so`, and installing the Python package. If the C++ sources haven't changed since the last build, it skips recompilation.
172+
173+
```bash
174+
pip install -e .
175+
```
176+
177+
To build manually (e.g., for debugging):
170178
```bash
171179
cd Builds/LinuxMakefile
172180
make CONFIG=Release CXXFLAGS="-I$PYTHONINCLUDEPATH" LDFLAGS="-L$PYTHONLIBPATH"
173181
cd ../..
174182
```
175183

176-
Output: `dawdreamer/dawdreamer.so`
184+
Build output: `Builds/LinuxMakefile/build/libdawdreamer.so`
185+
`setup.py` copies this to `dawdreamer/dawdreamer.so` automatically.
177186

178-
#### Install Python Package:
187+
**WSL2 Note**: The `make` build system may not detect source changes across the Windows/Linux filesystem boundary due to timestamp caching. If changes aren't picked up, delete the build directory first:
179188
```bash
180-
python3 setup.py develop
181-
# Or for wheel: python3 -m build --wheel
189+
rm -rf Builds/LinuxMakefile/build
182190
```
183191

184192
### macOS
@@ -262,11 +270,12 @@ See [Issue #82](https://github.com/DBraun/DawDreamer/issues/82#issuecomment-1097
262270
### Modifying C++ Source
263271

264272
1. Edit source files in `Source/`
265-
2. Open `DawDreamer.jucer` with [JUCE Projucer](https://juce.com/get-juce)
273+
2. If adding/removing files, open `DawDreamer.jucer` with [JUCE Projucer](https://juce.com/get-juce)
266274
- Projucer regenerates platform-specific build files
267275
- **DO NOT** edit .xcodeproj/.sln files directly
268-
3. Rebuild via platform-specific build system
269-
4. Reinstall: `python3 setup.py develop` (fast, just updates the .so)
276+
3. Rebuild and reinstall: `pip install -e .`
277+
- Detects source changes, rebuilds C++, copies `.so`, installs package
278+
- On WSL2, if changes aren't detected: `rm -rf Builds/LinuxMakefile/build` first
270279

271280
**Adding a New Processor:**
272281
1. Create `Source/MyProcessor.h` (header-only or with .cpp)
@@ -414,6 +423,10 @@ python -m pip install dist/dawdreamer-*.whl
414423
- Normal on WSL2 due to cross-filesystem I/O (1-2 minutes)
415424
- Processing ~200+ Faust library files
416425

426+
**C++ changes not detected (WSL2)**
427+
- WSL2 cross-filesystem timestamps may be stale, causing `make` to skip rebuilds
428+
- Fix: `rm -rf Builds/LinuxMakefile/build` then run `pip install -e .` again
429+
417430
### Runtime Issues
418431

419432
**ImportError: cannot import dawdreamer**

Source/FaustProcessor.cpp

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -543,62 +543,76 @@ bool FaustProcessor::compile()
543543
pathToFaustLibraries);
544544
}
545545

546-
//// print where faustlib is looking for stdfaust.lib and the other lib files.
547-
// auto pathnames = m_factory->getIncludePathnames();
548-
// std::cout << "pathnames:\n" << std::endl;
549-
// for (auto name : pathnames) {
550-
// std::cout << name << "\n" << std::endl;
551-
//}
552-
// std::cout << "library list:\n" << std::endl;
553-
// auto librarylist = m_factory->getLibraryList();
554-
// for (auto name : librarylist) {
555-
// std::cout << name << "\n" << std::endl;
556-
//}
546+
return initFromFactory();
547+
}
548+
549+
bool FaustProcessor::compileFromBitcode(const std::string& bitcode)
550+
{
551+
m_compileState = kNotCompiled;
552+
clear();
553+
554+
std::string error_msg;
555+
m_factory = readDSPFactoryFromBitcode(bitcode, getTarget(), error_msg, m_llvmOptLevel);
556+
557+
if (!m_factory)
558+
{
559+
throw std::runtime_error("FaustProcessor::compileFromBitcode(): Failed to restore factory "
560+
"from bitcode: " +
561+
error_msg);
562+
}
563+
564+
return initFromFactory();
565+
}
566+
567+
bool FaustProcessor::initFromFactory()
568+
{
569+
// Create DSP instance from the factory (either mono or poly).
570+
// Called by compile() after creating the factory from source code,
571+
// and by setPickleState() after restoring the factory from bitcode.
572+
bool is_polyphonic = m_nvoices > 0;
557573

558574
try
559575
{
560576
if (is_polyphonic)
561577
{
562-
// (false, true) works
563578
m_dsp_poly =
564579
m_poly_factory->createPolyDSPInstance(m_nvoices, m_dynamicVoices, m_groupVoices);
565580
if (!m_dsp_poly)
566581
{
567582
clear();
568583
throw std::runtime_error(
569-
"FaustProcessor::compile(): Cannot create Poly DSP instance.");
584+
"FaustProcessor::initFromFactory(): Cannot create Poly DSP instance.");
570585
}
571-
// m_dsp_poly->setReleaseLength(m_releaseLengthSec); // Removed in Faust 2.81.10
572586
}
573587
else
574588
{
575-
// create DSP instance
576589
m_dsp = m_factory->createDSPInstance();
577590
if (!m_dsp)
578591
{
579592
clear();
580-
throw std::runtime_error("FaustProcessor::compile(): Cannot create DSP instance.");
593+
throw std::runtime_error(
594+
"FaustProcessor::initFromFactory(): Cannot create DSP instance.");
581595
}
582596
}
583597
}
584598
catch (const std::exception& e)
585599
{
586600
clear();
587601
throw std::runtime_error(
588-
std::string("FaustProcessor::compile(): Exception creating DSP instance: ") + e.what());
602+
std::string("FaustProcessor::initFromFactory(): Exception creating DSP instance: ") +
603+
e.what());
589604
}
590605
catch (...)
591606
{
592607
clear();
593608
throw std::runtime_error(
594-
"FaustProcessor::compile(): Unknown exception creating DSP instance");
609+
"FaustProcessor::initFromFactory(): Unknown exception creating DSP instance");
595610
}
596611

597612
dsp* theDsp = is_polyphonic ? m_dsp_poly : m_dsp;
598613

599614
try
600615
{
601-
// get channels
602616
int inputs = theDsp->getNumInputs();
603617
int outputs = theDsp->getNumOutputs();
604618

@@ -607,7 +621,6 @@ bool FaustProcessor::compile()
607621

608622
setMainBusInputsAndOutputs(inputs, outputs);
609623

610-
// make new UI
611624
if (is_polyphonic)
612625
{
613626
m_midi_handler = rt_midi("my_midi");
@@ -625,7 +638,6 @@ bool FaustProcessor::compile()
625638
m_soundUI = new MySoundUI(&m_SoundfileMap, m_faustAssetsPaths, sr);
626639
theDsp->buildUserInterface(m_soundUI);
627640

628-
// init
629641
theDsp->init(sr);
630642

631643
createParameterLayout();
@@ -636,14 +648,15 @@ bool FaustProcessor::compile()
636648
{
637649
clear();
638650
throw std::runtime_error(
639-
std::string("FaustProcessor::compile(): Exception during DSP initialization: ") +
651+
std::string(
652+
"FaustProcessor::initFromFactory(): Exception during DSP initialization: ") +
640653
e.what());
641654
}
642655
catch (...)
643656
{
644657
clear();
645658
throw std::runtime_error(
646-
"FaustProcessor::compile(): Unknown exception during DSP initialization");
659+
"FaustProcessor::initFromFactory(): Unknown exception during DSP initialization");
647660
}
648661

649662
return true;

Source/FaustProcessor.h

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ class FaustProcessor : public ProcessorBase
198198
// faust stuff
199199
void clear();
200200
bool compile();
201+
bool compileFromBitcode(const std::string& bitcode);
202+
bool initFromFactory();
201203
bool setDSPString(const std::string& code);
202204
bool setDSPFile(const std::string& path);
203205
bool setParamWithIndex(const int index, float p);
@@ -309,6 +311,14 @@ class FaustProcessor : public ProcessorBase
309311
state["midi_qn"] = MidiSerialization::serializeMidiBuffer(myMidiBufferQN);
310312
state["midi_sec"] = MidiSerialization::serializeMidiBuffer(myMidiBufferSec);
311313

314+
// Serialize compiled LLVM bitcode to avoid recompilation on unpickle.
315+
// Only supported for mono (non-polyphonic) factories.
316+
// Polyphonic factories fall back to recompilation from source.
317+
if (m_factory)
318+
{
319+
state["bitcode"] = writeDSPFactoryToBitcode(m_factory);
320+
}
321+
312322
return state;
313323
}
314324

@@ -368,8 +378,23 @@ class FaustProcessor : public ProcessorBase
368378
m_llvmOptLevel = nb::cast<int>(state["llvm_opt_level"]);
369379
m_releaseLengthSec = nb::cast<double>(state["release_length"]);
370380

371-
// Compile the Faust code
372-
if (!m_code.empty())
381+
// Restore from compiled LLVM bitcode if available (fast path),
382+
// otherwise fall back to full recompilation from source (slow path).
383+
if (state.contains("bitcode"))
384+
{
385+
std::string bitcode = nb::cast<std::string>(state["bitcode"]);
386+
std::string error_msg;
387+
m_factory = readDSPFactoryFromBitcode(bitcode, getTarget(), error_msg, m_llvmOptLevel);
388+
if (!m_factory)
389+
{
390+
throw std::runtime_error(
391+
"FaustProcessor::setPickleState(): Failed to restore factory "
392+
"from bitcode: " +
393+
error_msg);
394+
}
395+
initFromFactory();
396+
}
397+
else if (!m_code.empty())
373398
{
374399
compile();
375400
}
@@ -583,6 +608,10 @@ inline void create_bindings_for_faust_processor(nb::module_& m)
583608
.def("compile", &FaustProcessor::compile,
584609
"Compile the FAUST object. You must have already set a dsp file "
585610
"path or dsp string.")
611+
.def("compile_from_bitcode", &FaustProcessor::compileFromBitcode, arg("bitcode"),
612+
"Restore a compiled FAUST object from LLVM bitcode, avoiding "
613+
"recompilation from source. Bitcode can be obtained from the "
614+
"processor's pickle state dict.")
586615
.def_prop_rw("auto_import", &FaustProcessor::getAutoImport, &FaustProcessor::setAutoImport,
587616
"The auto import string. Default is `import(\"stdfaust.lib\");`")
588617
.def("get_parameters_description", &FaustProcessor::getPluginParametersDescription,

Source/RenderEngine.h

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,16 @@ class RenderEngine : public AudioPlayHead
122122
state["bpm_automation"] = nb::ndarray<nb::numpy, float>(array_data, 1, shape, capsule);
123123
}
124124

125-
// Save processors with their types and states
125+
// Save all registered processors (not just connected ones)
126126
nb::list processors_list;
127-
for (auto* proc : m_connectedProcessors)
127+
for (const auto& [proc_name, nodeID] : m_UniqueNameToNodeID)
128128
{
129+
auto node = m_mainProcessorGraph->getNodeForId(nodeID);
130+
if (!node)
131+
continue;
132+
auto* proc = dynamic_cast<ProcessorBase*>(node->getProcessor());
133+
if (!proc)
134+
continue;
129135
nb::dict proc_dict;
130136

131137
// Helper lambda to save automation for any processor
@@ -400,15 +406,25 @@ class RenderEngine : public AudioPlayHead
400406
faust_proc->setLLVMOpt(nb::cast<int>(proc_state["llvm_opt_level"]));
401407
faust_proc->setReleaseLength(nb::cast<double>(proc_state["release_length"]));
402408

403-
// Compile the Faust code
404-
faust_proc->compile();
405-
406-
// Restore soundfiles
409+
// Restore soundfiles BEFORE compilation because
410+
// setSoundfiles resets m_compileState.
407411
if (proc_state.contains("soundfiles"))
408412
{
409413
faust_proc->setSoundfiles(nb::cast<nb::dict>(proc_state["soundfiles"]));
410414
}
411415

416+
// Restore from LLVM bitcode if available (fast path),
417+
// otherwise fall back to full recompilation from source.
418+
if (proc_state.contains("bitcode"))
419+
{
420+
faust_proc->compileFromBitcode(
421+
nb::cast<std::string>(proc_state["bitcode"]));
422+
}
423+
else
424+
{
425+
faust_proc->compile();
426+
}
427+
412428
// Save parameters for later restoration (after graph is loaded)
413429
if (proc_state.contains("parameters"))
414430
{

0 commit comments

Comments
 (0)