Add SoundBuffer type: procedural audio, sfxr synthesis, DSP effects
New SoundBuffer Python type enables procedural audio generation: - Tone synthesis (sine, square, saw, triangle, noise) with ADSR envelopes - sfxr retro sound effect engine (7 presets, 24 params, mutation, seeding) - DSP effects chain: pitch_shift, low/high pass, echo, reverb, distortion, bit_crush, normalize, reverse, slice - Composition: concat (with crossfade overlap) and mix - Sound() now accepts SoundBuffer or filename string - Sound gains pitch property and play_varied() method - Platform stubs for HeadlessTypes and SDL2Types (loadFromSamples, pitch) - Interactive demo: sfxr clone UI + Animalese speech synthesizer - 62 unit tests across 6 test files (all passing) Refs #251 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb72040396
commit
97dbec9106
20 changed files with 4793 additions and 197 deletions
123
src/PySoundBuffer.h
Normal file
123
src/PySoundBuffer.h
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declarations
|
||||
struct SfxrParams;
|
||||
|
||||
// Core audio data container - holds authoritative sample data
|
||||
class SoundBufferData : public std::enable_shared_from_this<SoundBufferData>
|
||||
{
|
||||
public:
|
||||
std::vector<int16_t> samples;
|
||||
unsigned int sampleRate = 44100;
|
||||
unsigned int channels = 1;
|
||||
|
||||
// Optional sfxr params (set when created via sfxr synthesis)
|
||||
std::shared_ptr<SfxrParams> sfxrParams;
|
||||
|
||||
// Lazy sf::SoundBuffer rebuild
|
||||
sf::SoundBuffer sfBuffer;
|
||||
bool sfBufferDirty = true;
|
||||
|
||||
SoundBufferData() = default;
|
||||
SoundBufferData(std::vector<int16_t>&& s, unsigned int rate, unsigned int ch)
|
||||
: samples(std::move(s)), sampleRate(rate), channels(ch), sfBufferDirty(true) {}
|
||||
|
||||
// Rebuild sf::SoundBuffer from samples if dirty
|
||||
sf::SoundBuffer& getSfBuffer() {
|
||||
if (sfBufferDirty && !samples.empty()) {
|
||||
sfBuffer.loadFromSamples(samples.data(), samples.size(), channels, sampleRate);
|
||||
sfBufferDirty = false;
|
||||
}
|
||||
return sfBuffer;
|
||||
}
|
||||
|
||||
float duration() const {
|
||||
if (sampleRate == 0 || channels == 0 || samples.empty()) return 0.0f;
|
||||
return static_cast<float>(samples.size()) / static_cast<float>(channels) / static_cast<float>(sampleRate);
|
||||
}
|
||||
};
|
||||
|
||||
// Python object wrapper
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<SoundBufferData> data;
|
||||
} PySoundBufferObject;
|
||||
|
||||
// Python type methods/getset declarations
|
||||
namespace PySoundBuffer {
|
||||
// tp_init, tp_new, tp_repr
|
||||
int init(PySoundBufferObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
PyObject* repr(PyObject* obj);
|
||||
|
||||
// Class methods (factories)
|
||||
PyObject* from_samples(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
PyObject* tone(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
PyObject* sfxr(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
PyObject* concat(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
PyObject* mix(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Instance methods (DSP - each returns new SoundBuffer)
|
||||
PyObject* pitch_shift(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* low_pass(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* high_pass(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* echo(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* reverb(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* distortion(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* bit_crush(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* normalize(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* reverse(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* slice(PySoundBufferObject* self, PyObject* args);
|
||||
PyObject* sfxr_mutate(PySoundBufferObject* self, PyObject* args);
|
||||
|
||||
// Properties
|
||||
PyObject* get_duration(PySoundBufferObject* self, void* closure);
|
||||
PyObject* get_sample_count(PySoundBufferObject* self, void* closure);
|
||||
PyObject* get_sample_rate(PySoundBufferObject* self, void* closure);
|
||||
PyObject* get_channels(PySoundBufferObject* self, void* closure);
|
||||
PyObject* get_sfxr_params(PySoundBufferObject* self, void* closure);
|
||||
|
||||
extern PyMethodDef methods[];
|
||||
extern PyGetSetDef getsetters[];
|
||||
}
|
||||
|
||||
// Helper: create a new PySoundBufferObject wrapping given data
|
||||
PyObject* PySoundBuffer_from_data(std::shared_ptr<SoundBufferData> data);
|
||||
|
||||
namespace mcrfpydef {
|
||||
inline PyTypeObject PySoundBufferType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.SoundBuffer",
|
||||
.tp_basicsize = sizeof(PySoundBufferObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = PySoundBuffer::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"SoundBuffer(filename: str)\n"
|
||||
"SoundBuffer.from_samples(data: bytes, channels: int, sample_rate: int)\n"
|
||||
"SoundBuffer.tone(frequency: float, duration: float, waveform: str = 'sine', ...)\n"
|
||||
"SoundBuffer.sfxr(preset: str, seed: int = None)\n\n"
|
||||
"Audio sample buffer for procedural audio generation and effects.\n\n"
|
||||
"Holds PCM sample data that can be created from files, raw samples,\n"
|
||||
"tone synthesis, or sfxr presets. Effect methods return new SoundBuffer\n"
|
||||
"instances (copy-modify pattern).\n\n"
|
||||
"Properties:\n"
|
||||
" duration (float, read-only): Duration in seconds.\n"
|
||||
" sample_count (int, read-only): Total number of samples.\n"
|
||||
" sample_rate (int, read-only): Samples per second (e.g. 44100).\n"
|
||||
" channels (int, read-only): Number of audio channels.\n"
|
||||
" sfxr_params (dict or None, read-only): sfxr parameters if sfxr-generated.\n"
|
||||
),
|
||||
.tp_methods = PySoundBuffer::methods,
|
||||
.tp_getset = PySoundBuffer::getsetters,
|
||||
.tp_init = (initproc)PySoundBuffer::init,
|
||||
.tp_new = PySoundBuffer::pynew,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue