diff --git a/.github/workflows/plugins_build.yml b/.github/workflows/plugins_build.yml
index 881e27e..2c00e51 100644
--- a/.github/workflows/plugins_build.yml
+++ b/.github/workflows/plugins_build.yml
@@ -139,6 +139,7 @@ jobs:
run: |
mkdir -p package
cp build/src/chua/*.so \
+ build/src/chips/*.so \
build/src/linear_algebra/*.so \
build/src/fluidOpcodes/*.so \
build/src/dssi4cs/*.so \
@@ -285,6 +286,7 @@ jobs:
run: |
mkdir -p package
cp build/src/chua/*.dylib \
+ build/src/chips/*.dylib \
build/src/linear_algebra/*.dylib \
build/src/fluidOpcodes/*.dylib \
build/src/image/*.dylib \
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4de3201..8154200 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -322,6 +322,7 @@ endif()
enable_testing()
add_subdirectory(./src/chua)
+add_subdirectory(./src/chips)
add_subdirectory(./src/linear_algebra)
add_subdirectory(./src/fluidOpcodes)
add_subdirectory(./src/dssi4cs)
diff --git a/README.md b/README.md
index 77b62be..5cdb458 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,9 @@ Currently, the plugins available in this tree are
- chua (requires the Eigen library, header-only)
**chuap**
+- chips (uses the floooh/chips `m6581.h` SID 6581 emulator, header-only; vendored under this directory)
+**sid6581 sid6581adsr sid6581voice**
+
- fluidOpcodes (requires fluidsynth)
**fluidEngine fluidLoad fluidNote fluidCCi fluidCCk fluidControl fluidProgramSelect fluidAllOut fluidOut fluidInfo fluidSetInterpMethod**
@@ -217,6 +220,55 @@ if set, overrides the built-in default location; otherwise the installed
Windows release packages.
+chips opcodes
+---
+
+The `chips` plugin's `sid6581` opcode wraps the
+[floooh/chips](https://github.com/floooh/chips) MOS 6581 "SID" emulator
+(`m6581.h`), which is **vendored under this directory** (header-only), so no
+external dependency is required. A companion `sid6581adsr` opcode builds a
+`Sid6581ADSR` user-defined type (attack/decay/release in seconds, sustain 0..1),
+whose `.att`, `.dec`, `.sus` and `.rel` members can be read or written with
+dot notation, and a `sid6581voice` opcode builds a `Sid6581Voice` type whose
+`.freq`, `.wave` and `.pw` members describe a voice's tone:
+
+```
+iEnv:Sid6581ADSR sid6581adsr kA, kD, kS, kR
+iVoice:Sid6581Voice sid6581voice kFreq, kWave, kPw
+
+aOut sid6581 iVoice1, kGate1, iEnv1, \
+ iVoice2, kGate2, iEnv2, \
+ iVoice3, kGate3, iEnv3, \
+ kCutoff, kRes, iFilterMode, iRouteMask
+```
+
+Where:
+
+| Argument | Rate | Meaning |
+| --- | --- | --- |
+| `aOut` | a | output signal (single channel) |
+| `kFreq` | k | voice frequency in Hz (`.freq`) |
+| `kWave` | k | waveform bitmask: 1 = triangle, 2 = sawtooth, 4 = pulse, 8 = noise (values combine) (`.wave`) |
+| `kPw` | k | pulse-width register (0..4095); used only by the pulse waveform (`.pw`) |
+| `iVoice1..3` | Sid6581Voice | per-voice tone (from `sid6581voice`) |
+| `kGate1..3` | k | envelope gate (>= 0.5 on, otherwise off) |
+| `iEnv1..3` | Sid6581ADSR | per-voice envelope (from `sid6581adsr`) |
+| `kCutoff` | k | filter cutoff register (0..2047) |
+| `kRes` | k | filter resonance (0..15) |
+| `iFilterMode` | i | filter mode bitmask: 1 = low-pass, 2 = band-pass, 4 = high-pass, 8 = voice 3 off (no effect if voice 3 is in `iRouteMask`; e.g. `iRouteMask = 7` still plays voice 3) |
+| `iRouteMask` | i | filter routing bitmask: 1 = voice 1, 2 = voice 2, 4 = voice 3 |
+
+The SID clock is fixed at the PAL rate (985248 Hz) and the master volume at
+maximum (15). Each voice's ADSR is latched on a gate transition (note-on
+latches A/D/S/R, note-off refreshes R), so k-rate envelope changes apply to
+the next note.
+The emulator is licensed under zlib/libpng (the SID implementation derives
+from tedplay, Unlicense); see the notices in `src/chips/m6581.h`.
+
+It is built by default; configure with `-DBUILD_CHIPS_OPCODES=OFF` to
+exclude it.
+
+
Faust opcodes
---
@@ -399,7 +451,7 @@ $ make
By default, all the plugins are built. If one wants to exclude a
plugin from the build process, one can pass an option to the cmake command.
-The options are `BUILD_CHUA_OPCODES`, `BUILD_CUDA_OPCODES`,
+The options are `BUILD_CHUA_OPCODES`, `BUILD_CHIPS_OPCODES`, `BUILD_CUDA_OPCODES`,
`BUILD_DSSI_OPCODES`, `BUILD_OPENCL_OPCODES`, `BUILD_FAUST_OPCODES`,
`BUILD_FLUID_OPCODES`, `BUILD_IMAGE_OPCODES`, `BUILD_HDF5_OPCODES`,
`BUILD_LINEAR_ALGEBRA_OPCODES`, `BUILD_P5GLOVE_OPCODES`, `BUILD_STK_OPCODES`,
diff --git a/cmake/Modules/FindCSOUND.cmake b/cmake/Modules/FindCSOUND.cmake
index a0dec67..50e4a68 100644
--- a/cmake/Modules/FindCSOUND.cmake
+++ b/cmake/Modules/FindCSOUND.cmake
@@ -5,50 +5,87 @@
# CSOUND_LIBRARIES - The libraries needed to use the Csound library.
if(APPLE)
+ # Hints are tried in order: explicit overrides, local builds in ~/Library,
+ # the Csound 7 installer, then the system framework.
+ set(CMAKE_FIND_FRAMEWORK LAST)
find_path(CSOUND_INCLUDE_DIR csound.h
HINTS
+ ${CSOUND_INCLUDE_DIR_HINT}
"$ENV{HOME}/Library/Frameworks/CsoundLib64.framework/Headers"
"$ENV{HOME}/Library/Frameworks/CsoundLib.framework/Headers"
+ /Applications/csound/CsoundLib64.framework/Headers
/Library/Frameworks/CsoundLib64.framework/Headers
/Library/Frameworks/CsoundLib.framework/Headers
- ${CSOUND_INCLUDE_DIR_HINT}
)
find_path(CSOUND_FRAMEWORK CsoundLib64
HINTS
+ ${CSOUND_FRAMEWORK_DIR_HINT}
"$ENV{HOME}/Library/Frameworks/CsoundLib64.framework"
+ /Applications/csound/CsoundLib64.framework
/Library/Frameworks/CsoundLib64.framework
- ${CSOUND_FRAMEWORK_DIR_HINT}
)
find_path(CSOUND_FRAMEWORK32 CsoundLib
HINTS
+ ${CSOUND_FRAMEWORK32_DIR_HINT}
"$ENV{HOME}/Library/Frameworks/CsoundLib64.framework"
"$ENV{HOME}/Library/Frameworks/CsoundLib.framework"
+ /Applications/csound/CsoundLib64.framework
/Library/Frameworks/CsoundLib64.framework
/Library/Frameworks/CsoundLib.framework
- ${CSOUND_FRAMEWORK32_DIR_HINT}
)
else()
- find_path(CSOUND_INCLUDE_DIR csound.h
- PATH_SUFFIXES csound
- HINTS ${CSOUND_INCLUDE_DIR_HINT}
- )
+ if(WIN32)
+ find_path(CSOUND_INCLUDE_DIR csound.h
+ PATH_SUFFIXES csound
+ HINTS
+ ${CSOUND_INCLUDE_DIR_HINT}
+ "$ENV{ProgramFiles}/Csound7/include"
+ "$ENV{ProgramW6432}/Csound7/include"
+ "$ENV{ProgramFiles\(x86\)}/Csound7/include"
+ )
+ else()
+ find_path(CSOUND_INCLUDE_DIR csound.h
+ PATH_SUFFIXES csound
+ HINTS ${CSOUND_INCLUDE_DIR_HINT}
+ )
+ endif()
endif()
if(APPLE)
+ # Hints are framework parent dirs; find_library resolves CsoundLib64.framework inside them.
find_library(CSOUND_LIBRARY
NAMES CsoundLib64
+ NO_DEFAULT_PATH
HINTS
- /Library/Frameworks/CsoundLib64.framework/
- "$ENV{HOME}/Library/Frameworks/CsoundLib64.framework"
${CSOUND_LIBRARY_DIR_HINT}
+ "$ENV{HOME}/Library/Frameworks"
+ /Applications/csound
+ /Library/Frameworks
)
else()
- find_library(CSOUND_LIBRARY
- NAMES csound64 csound
- HINTS ${CSOUND_LIBRARY_DIR_HINT}
- )
+ # Csound 7 on Windows is named csound7_64 (double) or csound7 (single).
+ if(WIN32)
+ set(_csound_library_names csound7_64 csound7 csound64 csound)
+ else()
+ set(_csound_library_names csound64 csound)
+ endif()
+ if(WIN32)
+ find_library(CSOUND_LIBRARY
+ NAMES ${_csound_library_names}
+ HINTS
+ ${CSOUND_LIBRARY_DIR_HINT}
+ "$ENV{ProgramFiles}/Csound7/lib"
+ "$ENV{ProgramW6432}/Csound7/lib"
+ )
+ else()
+ find_library(CSOUND_LIBRARY
+ NAMES ${_csound_library_names}
+ HINTS ${CSOUND_LIBRARY_DIR_HINT}
+ )
+ endif()
+ unset(_csound_library_names)
endif()
if(CSOUND_INCLUDE_DIR AND EXISTS "${CSOUND_INCLUDE_DIR}/version.h")
diff --git a/src/chips/CMakeLists.txt b/src/chips/CMakeLists.txt
new file mode 100644
index 0000000..4df0589
--- /dev/null
+++ b/src/chips/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Build chips opcodes (floooh/chips emulators)
+option(BUILD_CHIPS_OPCODES "Build the chips opcodes" ON)
+
+if(BUILD_CHIPS_OPCODES)
+ # m6581.h is header-only and vendored under this directory.
+ make_plugin(chips sid6581.cpp)
+ target_include_directories(chips PRIVATE
+ ${CSOUND_INCLUDE_DIRS}
+ "${CMAKE_CURRENT_SOURCE_DIR}")
+ target_compile_definitions(chips PRIVATE BUILD_PLUGINS)
+
+ message(STATUS "Building chips opcodes")
+endif()
diff --git a/src/chips/README.md b/src/chips/README.md
new file mode 100644
index 0000000..9cfac06
--- /dev/null
+++ b/src/chips/README.md
@@ -0,0 +1,241 @@
+# chips — `sid6581`
+
+A Csound 7 plugin wrapping chip emulators from
+[floooh/chips](https://github.com/floooh/chips). Its first opcodes, `sid6581`
+and `sid6581adsr`, wrap the MOS Technology 6581 "SID" sound chip
+(`chips/m6581.h`). The emulator is header-only and is **vendored in this
+directory** (`m6581.h`), so the plugin has no external dependencies.
+
+The SID was the sound chip of the Commodore 64: three independent voices,
+each with a 16-bit frequency register, a waveform generator and an ADSR
+envelope, mixed through a single multi-mode filter.
+
+## Opcode signatures
+
+```
+iEnv:Sid6581ADSR = sid6581adsr(kA, kD, kS, kR)
+iVoice:Sid6581Voice = sid6581voice(kFreq, kWave, kPw)
+
+aOut:a = sid6581(iVoice1, kGate1, iEnv1, \
+ iVoice2, kGate2, iEnv2, \
+ iVoice3, kGate3, iEnv3, \
+ kCutoff, kRes, iFilterMode, iRouteMask)
+```
+
+`sid6581adsr` builds a `Sid6581ADSR` value from A (attack seconds), D (decay
+seconds), S (sustain 0..1) and R (release seconds). `sid6581voice` builds a
+`Sid6581Voice` value from a frequency, a waveform bitmask and a pulse width.
+`sid6581` has one a-rate output (mono), and for each of its three voices a
+`Sid6581Voice`, a k-rate gate and a `Sid6581ADSR`, followed by two k-rate filter
+parameters and two i-rate filter parameters. All k-rate parameters are
+updated once per control cycle.
+
+## Parameters
+
+### `sid6581adsr`
+
+| Arg | Rate | Meaning |
+| --- | --- | --- |
+| `kA` | k | attack time in seconds (rise 0 -> full); sets `.att` |
+| `kD` | k | decay time in seconds (fall full -> sustain); sets `.dec` |
+| `kS` | k | sustain level 0..1; sets `.sus` |
+| `kR` | k | release time in seconds (fall sustain -> 0); sets `.rel` |
+
+### `sid6581voice`
+
+| Arg | Rate | Meaning | Range |
+| --- | --- | --- | --- |
+| `kFreq` | k | voice frequency in Hz; sets `.freq` | 0 .. ~3850 Hz |
+| `kWave` | k | waveform selection (bitmask, see below); sets `.wave` | 0 .. 15 |
+| `kPw` | k | pulse-width register; sets `.pw` | 0 .. 4095 |
+
+The members can be read and written with dot notation, e.g.
+`v.freq = 440`.
+
+### `sid6581`
+
+| Arg | Rate | Meaning | Range |
+| --- | --- | --- | --- |
+| `iVoice1..3` | Sid6581Voice | tone of each voice (from `sid6581voice`) | |
+| `kGate1..3` | k | envelope gate | >= 0.5 on, else off |
+| `iEnv1..3` | Sid6581ADSR | envelope for each voice (from `sid6581adsr`) | |
+| `kCutoff` | k | filter cutoff register | 0 .. 2047 |
+| `kRes` | k | filter resonance | 0 .. 15 |
+| `iFilterMode` | i | filter mode (bitmask, see below) | 0 .. 15 |
+| `iRouteMask` | i | voices routed through the filter (bitmask) | 0 .. 7 |
+
+### Waveform bitmask (`.wave`)
+
+The waveform is selected by the same bit encoding as the SID control
+register's waveform nibble. Values combine to give the classic SID mixed
+waveforms:
+
+| Bit | Value | Waveform |
+| --- | --- | --- |
+| 0 | 1 | triangle |
+| 1 | 2 | sawtooth |
+| 2 | 4 | pulse |
+| 3 | 8 | noise |
+
+Examples: `1` = triangle, `2` = saw, `4` = pulse, `8` = noise,
+`3` = triangle + saw, `6` = saw + pulse, `7` = tri + saw + pulse.
+
+`.pw` only affects the output when the pulse bit is set.
+
+### Envelope (`Sid6581ADSR`)
+
+Each voice has one ADSR envelope. `sid6581adsr` builds a `Sid6581ADSR` from four
+values, which are also available as **named members** you can read and write
+with dot notation:
+
+| Member | Meaning |
+| --- | --- |
+| `.att` | attack time in seconds |
+| `.dec` | decay time in seconds |
+| `.sus` | sustain level 0..1 |
+| `.rel` | release time in seconds |
+
+```csound
+env:Sid6581ADSR = sid6581adsr(0.01, 0.1, 0.8, 0.2) ; build one
+env.sus = 0.5 ; tweak a member
+kLevel:k = env.sus ; read a member
+```
+
+The SID's envelope registers are written only when a voice's gate changes:
+
+- **note-on** (gate rises): `.att`, `.dec`, `.sus` and `.rel` are latched;
+- **note-off** (gate falls): only `.rel` is refreshed.
+
+So k-rate changes to the `Sid6581ADSR` values take effect on the *next* note,
+never mid-note. Each voice is monophonic: a gate rising before a previous
+release has finished re-attacks and cuts the tail (authentic SID behaviour).
+To hear the full attack -> decay -> sustain -> release, the gate must rise
+and later fall.
+
+The SID stores ADSR as 4-bit register nibbles, so the seconds / 0..1 values
+are quantised to the nearest of 16 attack, decay, sustain and release values.
+The SID envelope timing is also non-linear (a fast linear attack and an
+exponential decay/release), so the values are musical approximations.
+
+### Filter mode (`iFilterMode`)
+
+| Bit | Value | Meaning |
+| --- | --- | --- |
+| 0 | 1 | low-pass |
+| 1 | 2 | band-pass |
+| 2 | 4 | high-pass |
+| 3 | 8 | voice 3 off (3OFF) |
+
+Modes combine (e.g. `3` = LP + BP). With `8`, voice 3 is silenced, except when voice 3 is included in
+`iRouteMask`: the emulator applies the mute only to voices outside the filter
+route, so with `iRouteMask = 7` (or any mask containing `4`) voice 3 is still
+heard.
+
+### Filter routing (`iRouteMask`)
+
+Selects which voices pass through the filter (voices not selected are sent
+directly to the output, unfiltered):
+
+| Bit | Value | Voice |
+| --- | --- | --- |
+| 0 | 1 | voice 1 |
+| 1 | 2 | voice 2 |
+| 2 | 4 | voice 3 |
+
+`7` routes all three voices through the filter; `0` bypasses the filter
+entirely.
+
+## Fixed settings
+
+A few SID parameters are not part of the signature and are fixed:
+
+- **master volume**: 15 (maximum)
+- **SID clock**: 985248 Hz (PAL), which the envelopes and filter are tuned to
+
+The output is single-channel and roughly in the range [-1, 1]; duplicate it
+to both channels for stereo.
+
+## Building
+
+The plugin builds with the rest of this repository. It requires a Csound 7
+installation and is enabled by default:
+
+```
+cmake -DBUILD_CHIPS_OPCODES=ON ..
+```
+
+Disable it with `-DBUILD_CHIPS_OPCODES=OFF`.
+
+## Examples
+
+The directory ships `example.csd`, a longer arranged track with lead, bass,
+arpeggio and drums, along with `chiptune.ogg`, a rendering of it. Both use
+the modern (functional) orchestra syntax with explicit type declarations.
+The listing below is a simplified chord and gated note.
+
+```csound
+
+
+-odac
+--opcode-lib=../../build/src/chips/Release/chips.dll
+
+
+sr = 44100
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+
+; A sustained three-voice chord, all voices low-pass filtered.
+instr 1
+ env1:Sid6581ADSR = sid6581adsr(0.01, 0.1, 0.8, 0.2)
+ env2:Sid6581ADSR = sid6581adsr(0.02, 0.2, 0.6, 0.4)
+ env3:Sid6581ADSR = sid6581adsr(0.005, 0.3, 0.7, 0.3)
+
+ v1:Sid6581Voice = sid6581voice(261.63, 1, 2048) ; C4 triangle
+ v2:Sid6581Voice = sid6581voice(329.63, 2, 1024) ; E4 sawtooth
+ v3:Sid6581Voice = sid6581voice(392.00, 4, 512) ; G4 pulse
+
+ out:a = sid6581(v1, 1, env1, \
+ v2, 1, env2, \
+ v3, 1, env3, \
+ 1200, 6, 1, 7) ; cutoff, res, low-pass, route all
+ outs(out, out)
+endin
+
+; A single gated voice, envelope triggered by a k-rate gate.
+instr 2
+ kGate:k = linseg(0, 0.05, 1, p3 - 0.1, 1, 0.05, 0)
+ env:Sid6581ADSR = sid6581adsr(0.01, 0.1, 0.7, 0.3)
+ voice:Sid6581Voice = sid6581voice(220, 4, 2048)
+ off:Sid6581Voice = sid6581voice(0, 0, 0)
+ a1:a = sid6581(voice, kGate, env, \
+ off, 0, env, \
+ off, 0, env, \
+ 0, 0, 0, 0)
+ outs(a1 * 0.5, a1 * 0.5)
+endin
+
+
+i 1 0 4
+i 2 0 0.5
+i 2 0.5 0.5
+e
+
+
+```
+
+The `--opcode-lib` option loads the plugin library. The example uses a
+relative path (`../../build/src/chips/Release/`) to the build output, so
+run it from this `src/chips` directory.
+
+On Windows the library is `chips.dll`; on Linux/macOS it is
+`chips.so`/`chips.dylib` (adjust the path to your build directory if it
+differs).
+
+## License
+
+The plugin source is licensed under the LGPL-2.1-or-later, like the rest of
+this repository. The vendored `m6581.h` is (c) 2018 Andre Weissflog,
+provided under the zlib/libpng license; its SID implementation is based on
+tedplay (c) 2012 Attila Grosz, used under the Unlicense. See the header
+comment in `m6581.h` for the full notices.
diff --git a/src/chips/chiptune.ogg b/src/chips/chiptune.ogg
new file mode 100644
index 0000000..ef0708f
Binary files /dev/null and b/src/chips/chiptune.ogg differ
diff --git a/src/chips/example.csd b/src/chips/example.csd
new file mode 100644
index 0000000..1b701b7
--- /dev/null
+++ b/src/chips/example.csd
@@ -0,0 +1,212 @@
+
+
+-odac --opcode-lib=../../build/src/chips/Debug/libchips.dylib
+
+
+sr = 44100
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+
+; BYTE RUNNER - a two-minute SID-style arcade track in A minor.
+; Sixteen-step patterns are arranged in eight-bar sections.
+
+; Two-bar lead phrases. Zeroes are rests.
+lead_a@global:i = ftgen(0, 0, 32, -2, \
+69, 0, 69, 72, 76, 0, 74, 72, 74, 0, 74, 71, 74, 0, 69, 0, \
+69, 0, 69, 72, 76, 0, 81, 79, 76, 74, 72, 74, 76, 72, 69, 0)
+
+lead_b@global:i = ftgen(0, 0, 32, -2, \
+76, 0, 74, 72, 69, 0, 72, 74, 76, 0, 79, 76, 74, 0, 72, 0, \
+72, 0, 74, 76, 79, 0, 81, 79, 76, 74, 72, 69, 72, 74, 69, 0)
+
+lead_hook@global:i = ftgen(0, 0, 32, -2, \
+81, 0, 81, 79, 76, 0, 79, 81, 84, 0, 81, 79, 76, 74, 72, 0, \
+81, 0, 81, 79, 76, 0, 79, 81, 86, 84, 81, 79, 76, 72, 69, 0)
+
+; Eighth-note bass rhythm in semitones from each bar's root (-1 is a rest).
+bass_pattern@global:i = ftgen(0, 0, 16, -2, \
+0, -1, 0, -1, 7, -1, 0, -1, 0, -1, 12, -1, 7, -1, 12, -1)
+
+; Arpeggio intervals from the current chord root.
+arp_pattern@global:i = ftgen(0, 0, 16, -2, \
+12, 19, 24, 19, 16, 19, 24, 19, 12, 19, 24, 28, 24, 19, 16, 19)
+
+; Drum hits are one eighth-note or shorter; rests let each SID envelope fall.
+kick_pattern@global:i = ftgen(0, 0, 16, -2, \
+1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0)
+snare_pattern@global:i = ftgen(0, 0, 16, -2, \
+0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0)
+hat_pattern@global:i = ftgen(0, 0, 16, -2, \
+1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0)
+
+; Am - F - C - G, one root per bar.
+chord_roots@global:i = ftgen(0, 0, 4, -2, 45, 41, 48, 43)
+
+instr 1
+
+ tempo:k = 140
+ trig:k = metro(tempo / 15)
+
+ step:k init 15
+ bar:k init -1
+ section:k init 0
+ lead_note:k init 0
+ bass_note:k init 0
+ arp_note:k init 0
+ lead_gate:k init 0
+ bass_gate:k init 0
+ arp_gate:k init 0
+ retrigger_lead:k init 0
+ kick_gate:k init 0
+ snare_gate:k init 0
+ snare_remaining:k init 0
+ hat_gate:k init 0
+
+ if trig == 1 then
+ step = (step + 1) % 16
+ if step == 0 then
+ bar = bar + 1
+ endif
+ section = int(bar / 8)
+
+ phrase_index:k = ((bar % 2) * 16) + step
+ chord:k = table(bar % 4, chord_roots)
+ bass_offset:k = table(step, bass_pattern)
+ bass_pitch:k = (bass_offset >= 0 ? chord + bass_offset : 0)
+ arp_offset:k = table(step, arp_pattern)
+
+ lead_note = 0
+ bass_note = 0
+ arp_note = 0
+
+ if section == 1 then
+ lead_note = table(phrase_index, lead_a)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ elseif section == 2 then
+ lead_note = table(phrase_index, lead_hook)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ elseif section == 3 then
+ lead_note = table(phrase_index, lead_b)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ elseif section == 4 then
+ if bar >= 36 then
+ bass_note = bass_pitch
+ endif
+ arp_note = chord + arp_offset
+ elseif section == 5 then
+ lead_note = table(phrase_index, lead_hook)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ elseif section == 6 then
+ lead_note = table(phrase_index, lead_hook)
+ lead_note = (lead_note > 0 ? lead_note + 12 : 0)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ elseif section == 7 then
+ if bar < 60 then
+ lead_note = table(phrase_index, lead_a)
+ bass_note = bass_pitch
+ arp_note = chord + arp_offset
+ else
+ arp_note = chord + arp_offset
+ endif
+ elseif section == 0 then
+ arp_note = chord + arp_offset
+ if bar >= 2 then
+ bass_note = bass_pitch
+ endif
+ endif
+
+ ; Force a brief gate low at every step so repeated notes re-trigger.
+ lead_gate = 0
+ bass_gate = 0
+ arp_gate = 0
+ retrigger_lead = 1
+ kick_gate = table(step, kick_pattern)
+ snare_gate = table(step, snare_pattern)
+ hat_gate = table(step, hat_pattern)
+
+ ; Intro and breakdown thin out the kit; the outro drops it entirely.
+ if section == 0 then
+ kick_gate = 0
+ snare_gate = 0
+ hat_gate = (bar >= 6 ? hat_gate : 0)
+ elseif section == 4 then
+ kick_gate = 0
+ snare_gate = 0
+ hat_gate = (bar >= 36 ? hat_gate : 0)
+ elseif section == 7 then
+ kick_gate = (bar < 60 ? kick_gate : 0)
+ snare_gate = (bar < 60 ? snare_gate : 0)
+ hat_gate = (bar < 60 ? hat_gate : 0)
+ endif
+
+ ; Shorten snare gate independently of the 107 ms sequencer step.
+ if (bar % 4) == 3 && step == 14 && section >= 1 && section <= 6 then
+ snare_gate = 1
+ endif
+ snare_remaining = (snare_gate > 0 ? 0.02 : 0)
+ elseif retrigger_lead == 1 then
+ lead_gate = (lead_note > 0 ? 1 : 0)
+ bass_gate = (bass_note > 0 ? 1 : 0)
+ arp_gate = (arp_note > 0 ? 1 : 0)
+ retrigger_lead = 0
+ else
+ if snare_remaining > 0 then
+ snare_remaining = snare_remaining - (ksmps / sr)
+ snare_gate = (snare_remaining > 0 ? 1 : 0)
+ endif
+ endif
+
+ lead_freq:k = (lead_note > 0 ? cpsmidinn(lead_note) : 0)
+ bass_freq:k = (bass_note > 0 ? cpsmidinn(bass_note) : 0)
+ arp_freq:k = (arp_note > 0 ? cpsmidinn(arp_note) : 0)
+
+ ; Build the envelopes for each voice
+ lead_env:Sid6581ADSR = sid6581adsr(0.004, 0.08, 0.35, 0.025)
+ bass_env:Sid6581ADSR = sid6581adsr(0.003, 0.08, 0.55, 0.025)
+ arp_env:Sid6581ADSR = sid6581adsr(0.002, 0.06, 0.22, 0.02)
+ music_cutoff:k = (section == 5 || section == 6 ? 1500 : 850 + (section * 90))
+
+ ; Build the voices for each instrument
+ lead_voice:Sid6581Voice = sid6581voice(lead_freq, 4, 1250)
+ bass_voice:Sid6581Voice = sid6581voice(bass_freq, 2, 2048)
+ arp_voice:Sid6581Voice = sid6581voice(arp_freq, 1, 2048)
+
+ ; Build the music signal
+ music:a = sid6581( lead_voice, lead_gate, lead_env, bass_voice, \
+ bass_gate, bass_env, arp_voice, arp_gate, arp_env,\
+ music_cutoff, 7, 1, 7)
+
+ ; Build the drum envelopes and voices
+ kick_env:Sid6581ADSR = sid6581adsr(0.001, 0.09, 0.0, 0.025)
+ snare_env:Sid6581ADSR = sid6581adsr(0.001, 0.0002, 0.0, 0.001)
+ hat_env:Sid6581ADSR = sid6581adsr(0.001, 0.035, 0.0, 0.012)
+
+ kick_voice:Sid6581Voice = sid6581voice(72, 1, 2048)
+ snare_voice:Sid6581Voice = sid6581voice(1200, 8, 2048)
+ hat_voice:Sid6581Voice = sid6581voice(2200, 8, 2048)
+
+ ; Build the drum signal
+ drums:a = sid6581(kick_voice, kick_gate, kick_env, \
+ snare_voice, snare_gate, snare_env, \
+ hat_voice, hat_gate, hat_env, \
+ 2047, 0, 0, 0)
+
+ ; Apply fade and mix the music and drum signals
+ fade:k = linseg(0, 1, 1, p3 - 3, 1, 1, 0)
+ mix:a = (music * 0.68) + (drums * 0.38)
+ outs(mix * fade, mix * fade)
+endin
+
+
+
+; 64 bars at 140 BPM: eight sections, each eight bars long.
+i 1 0 109.72
+e
+
+
diff --git a/src/chips/m6581.h b/src/chips/m6581.h
new file mode 100644
index 0000000..f41af03
--- /dev/null
+++ b/src/chips/m6581.h
@@ -0,0 +1,845 @@
+#pragma once
+/*#
+ # m6581.h
+
+ MOS Technology 6581 emulator (aka SID)
+
+ based on tedplay (c) 2012 Attila Grosz, used under Unlicense http://unlicense.org/
+
+ Do this:
+ ~~~C
+ #define CHIPS_IMPL
+ ~~~
+ before you include this file in *one* C or C++ file to create the
+ implementation.
+
+ Optionally provide the following macros with your own implementation
+ ~~~C
+ CHIPS_ASSERT(c)
+ ~~~
+
+ ## Emulated Pins
+
+ ***********************************
+ * +-----------+ *
+ * CS --->| |<--- A0 *
+ * RW --->| |... *
+ * | |<--- A4 *
+ * | m6581 | *
+ * | |<--> D0 *
+ * | |... *
+ * | |<--> D7 *
+ * | | *
+ * +-----------+ *
+ ***********************************
+
+ The emulation has an additional "virtual pin" which is set to active
+ whenever a new sample is ready (M6581_SAMPLE).
+
+ ## Links
+
+ - http://blog.kevtris.org/?p=13
+
+ ## zlib/libpng license
+
+ Copyright (c) 2018 Andre Weissflog
+ This software is provided 'as-is', without any express or implied warranty.
+ In no event will the authors be held liable for any damages arising from the
+ use of this software.
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software in a
+ product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not
+ be misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source
+ distribution.
+#*/
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// address bus pins A0..A4
+#define M6581_PIN_A0 (0)
+#define M6581_PIN_A1 (1)
+#define M6581_PIN_A2 (2)
+#define M6581_PIN_A3 (3)
+#define M6581_PIN_A4 (4)
+
+// data bus pins D0..D7
+#define M6581_PIN_D0 (16)
+#define M6581_PIN_D1 (17)
+#define M6581_PIN_D2 (18)
+#define M6581_PIN_D3 (19)
+#define M6581_PIN_D4 (20)
+#define M6581_PIN_D5 (21)
+#define M6581_PIN_D6 (22)
+#define M6581_PIN_D7 (23)
+
+// shared control pins
+#define M6581_PIN_RW (24) /* same as M6502_RW */
+
+// chip-specific pins
+#define M6581_PIN_CS (40) /* chip-select */
+#define M6581_PIN_SAMPLE (41) /* virtual "audio sample ready" pin */
+
+// pin bit masks
+#define M6581_A0 (1ULL<
+#ifdef _MSC_VER
+#define _USE_MATH_DEFINES
+#endif
+#include /* tanh */
+#ifndef CHIPS_ASSERT
+ #include
+ #define CHIPS_ASSERT(c) assert(c)
+#endif
+
+/* extract 8-bit data bus from 64-bit pins */
+#define M6581_GET_DATA(p) ((uint8_t)(((p)&0xFF0000ULL)>>16))
+/* merge 8-bit data bus value into 64-bit pins */
+#define M6581_SET_DATA(p,d) {p=(((p)&~0xFF0000ULL)|(((d)<<16)&0xFF0000ULL));}
+/* fixed point precision for sample period */
+#define M6581_FIXEDPOINT_SCALE (16)
+/* move bit into first position */
+#define M6581_BIT(val,bitnr) ((val>>bitnr)&1)
+/* filter constants */
+#define M6581_DCWAVE (0) // (0x380)
+#define M6581_DCMIXER (0) // (((-0xFFF*0xFF)/18) / (1<<7))
+#define M6581_DCVOICE (0) // (0x800*0xFF)
+
+static const uint32_t _m6581_rate_count_period[16] = {
+ 0x7F00, 0x0006, 0x003C, 0x0330, 0x20C0, 0x6755, 0x3800, 0x500E,
+ 0x1212, 0x0222, 0x1848, 0x59B8, 0x3840, 0x77E2, 0x7625, 0x0A93
+};
+
+static const uint8_t _m6581_env_gen_dr_divisors[256] = {
+ 30,30,30,30,30,30,16,16,16,16,16,16,16,16,8,8,
+ 8,8,8,8,8,8,8,8,8,8,4,4,4,4,4,4,
+ 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
+ 4,4,4,4,4,4,2,2,2,2,2,2,2,2,2,2,
+ 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+ 2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
+};
+
+static float _m6581_cutoff_freq[2048];
+
+static void _m6581_init_voice(m6581_voice_t* v) {
+ memset(v, 0, sizeof(*v));
+ v->noise_shift = 0x007FFFFC;
+ v->env_state = M6581_ENV_FROZEN;
+ v->env_counter = 0x7FFF;
+}
+
+static void _m6581_init_cutoff_table() {
+ for (int i = 0; i < 2048; i++) {
+ float x = i / 8.0f;
+ float cf = -0.0156f * x * x + 48.473f * x - 45.074f;
+ _m6581_cutoff_freq[i] = cf <= 0 ? 0 : cf;
+ }
+}
+
+static void _m6581_set_filter_cutoff(m6581_filter_t*);
+static void _m6581_set_resonance(m6581_filter_t*);
+
+static void _m6581_init_filter(m6581_filter_t* f, int sound_hz) {
+ memset(f, 0, sizeof(*f));
+ f->nyquist_freq = sound_hz / 2;
+ _m6581_set_filter_cutoff(f);
+ _m6581_set_resonance(f);
+}
+
+void m6581_init(m6581_t* sid, const m6581_desc_t* desc) {
+ CHIPS_ASSERT(sid && desc);
+ CHIPS_ASSERT(desc->tick_hz > 0);
+ CHIPS_ASSERT(desc->sound_hz > 0);
+ memset(sid, 0, sizeof(*sid));
+ sid->sound_hz = desc->sound_hz;
+ sid->sample_period = (desc->tick_hz * M6581_FIXEDPOINT_SCALE) / desc->sound_hz;
+ sid->sample_counter = sid->sample_period;
+ sid->sample_mag = desc->magnitude;
+ sid->sample_accum_count = 1.0f;
+ for (int i = 0; i < 3; i++) {
+ _m6581_init_voice(&sid->voice[i]);
+ }
+ _m6581_init_cutoff_table();
+ _m6581_init_filter(&sid->filter, sid->sound_hz);
+}
+
+void m6581_reset(m6581_t* sid) {
+ CHIPS_ASSERT(sid);
+ sid->bus_value = 0;
+ sid->bus_decay = 0x2000;
+ for (int i = 0; i < 3; i++) {
+ _m6581_init_voice(&sid->voice[i]);
+ }
+ _m6581_init_filter(&sid->filter, sid->sound_hz);
+ sid->sample_counter = sid->sample_period;
+ sid->sample = 0.0f;
+ sid->sample_accum = 0.0f;
+ sid->sample_accum_count = 1.0f;
+ sid->pins = 0;
+}
+
+/*--- VOICE IMPLEMENTATION ---------------------------------------------------*/
+static inline void _m6581_set_freq_lo(m6581_voice_t* v, uint8_t data) {
+ v->freq = (v->freq & 0xFF00) | data;
+}
+
+static inline void _m6581_set_freq_hi(m6581_voice_t* v, uint8_t data) {
+ v->freq = (data<<8) | (v->freq & 0x00FF);
+}
+
+static inline void _m6581_set_pw_lo(m6581_voice_t* v, uint8_t data) {
+ v->pulse_width = (v->pulse_width & 0x0F00) | data;
+}
+
+static inline void _m6581_set_pw_hi(m6581_voice_t* v, uint8_t data) {
+ v->pulse_width = ((data & 0x0F)<<8) | (v->pulse_width & 0x00FF);
+}
+
+static inline void _m6581_set_atkdec(m6581_voice_t* v, uint8_t data) {
+ v->env_attack_add = data >> 4;
+ v->env_decay_sub = data & 0x0F;
+ if (v->env_state == M6581_ENV_ATTACK) {
+ v->env_counter_compare = v->env_attack_add;
+ }
+ else if (v->env_state == M6581_ENV_DECAY) {
+ v->env_counter_compare = v->env_decay_sub;
+ }
+}
+
+static inline void _m6581_set_susrel(m6581_voice_t* v, uint8_t data) {
+ v->env_sustain_level = (data >> 4) * 0x11;
+ v->env_release_sub = data & 0x0F;
+ if (v->env_state == M6581_ENV_RELEASE) {
+ v->env_counter_compare = v->env_release_sub;
+ }
+}
+
+static inline void _m6581_set_ctrl(m6581_voice_t* v, uint8_t data) {
+ /* wave/noise generator */
+ if ((data & M6581_CTRL_TEST) && !(v->ctrl & M6581_CTRL_TEST)) {
+ v->wav_accum = 0;
+ uint32_t bit19 = (v->noise_shift >> 19) & 1;
+ v->noise_shift = (v->noise_shift & 0x007FFFFD) | ((bit19^1)<<1);
+ }
+ else if (!(data & M6581_CTRL_TEST) && (v->ctrl & M6581_CTRL_TEST)) {
+ uint32_t bit0 = ((v->noise_shift >> 22) ^ (v->noise_shift >> 17)) & 0x01;
+ v->noise_shift <<= 1;
+ v->noise_shift &= 0x007FFFFF;
+ v->noise_shift |= bit0;
+ }
+
+ /* envelope generator */
+ if ((data & M6581_CTRL_GATE) && !(v->ctrl & M6581_CTRL_GATE)) {
+ /* gate bit on: start attack */
+ v->env_state = M6581_ENV_ATTACK;
+ v->env_counter_compare = v->env_attack_add;
+ }
+ else if (!(data & M6581_CTRL_GATE) && (v->ctrl & M6581_CTRL_GATE)) {
+ /* gate bit off: start release */
+ v->env_state = M6581_ENV_RELEASE;
+ v->env_counter_compare = v->env_release_sub;
+ }
+
+ /* noise combined with other waveform? */
+ if ((data & 0xF0) > 0x80) {
+ v->noise_shift &= 0x007FFFFF^(1<<22)^(1<<20)^(1<<16)^(1<<13)^(1<<11)^(1<<7)^(1<<4)^(1<<2);
+ }
+
+ v->ctrl = data;
+}
+
+static inline uint32_t _m6581_wavnone(m6581_voice_t* v) {
+ if (v->wav_accum) {
+ uint32_t sm = (v->wav_accum >> 12);
+ v->wav_accum >>= 1;
+ return sm;
+ }
+ else {
+ return 0;
+ }
+}
+
+static inline uint32_t _m6581_triangle(m6581_voice_t* v, m6581_voice_t* v_sync) {
+ const bool ring = 0 != (v->ctrl & M6581_CTRL_RINGMOD);
+ uint32_t msb = (ring ? v->wav_accum ^ v_sync->wav_accum : v->wav_accum) & 0x00800000;
+ return ((msb ? ~v->wav_accum : v->wav_accum) >> 11) & 0x0FFF;
+}
+
+static inline uint32_t _m6581_sawtooth(m6581_voice_t* v) {
+ return v->wav_accum >> 12;
+}
+
+static inline uint32_t _m6581_pulse(m6581_voice_t* v) {
+ if (v->ctrl & M6581_CTRL_TEST) {
+ return 0x0FFF;
+ }
+ else {
+ if ((v->wav_accum >> 12) >= v->pulse_width) {
+ return 0x0FFF;
+ }
+ else {
+ return 0x0000;
+ }
+ }
+}
+
+static inline uint32_t _m6581_trisaw(m6581_voice_t* v, m6581_voice_t* v_sync) {
+ uint32_t sm = _m6581_triangle(v, v_sync) & _m6581_sawtooth(v);
+ return (sm >> 1) & (sm << 1);
+}
+
+static inline uint32_t _m6581_tripulse(m6581_voice_t* v, m6581_voice_t* v_sync) {
+ uint32_t sm = _m6581_triangle(v, v_sync) & _m6581_pulse(v);
+ return (sm >> 1) & (sm << 1);
+}
+
+static inline uint32_t _m6581_sawpulse(m6581_voice_t* v) {
+ uint32_t sm = _m6581_sawtooth(v) & _m6581_pulse(v);
+ return (sm >> 1) & (sm << 1);
+}
+
+static inline uint32_t _m6581_trisawpulse(m6581_voice_t* v, m6581_voice_t* v_sync) {
+ uint32_t sm = _m6581_triangle(v, v_sync) & _m6581_sawtooth(v) & _m6581_pulse(v);
+ return (sm >> 1) && (sm << 1);
+}
+
+static inline uint16_t _m6581_noise(m6581_voice_t* v) {
+ uint32_t s = v->noise_shift;
+ return (M6581_BIT(s,22)<<11) |
+ (M6581_BIT(s,20)<<10) |
+ (M6581_BIT(s,16)<<9) |
+ (M6581_BIT(s,13)<<8) |
+ (M6581_BIT(s,11)<<7) |
+ (M6581_BIT(s,7)<<6) |
+ (M6581_BIT(s,4)<<5) |
+ (M6581_BIT(s,2)<<4);
+}
+
+static inline void _m6581_voice_tick(m6581_t* sid, int voice_index) {
+ m6581_voice_t* v = &sid->voice[voice_index];
+
+ /* waveform generator */
+ if (0 == (v->ctrl & M6581_CTRL_TEST)) {
+ /* frequency accumulator */
+ uint32_t prev_accum = v->wav_accum;
+ v->wav_accum = (v->wav_accum + v->freq) & 0x00FFFFFF;
+ /* noise */
+ if ((v->wav_accum & 0x00080000) && !(prev_accum & 0x00080000)) {
+ uint32_t s = v->noise_shift;
+ uint32_t new_bit = ((s>>22)^(s>>17)) & 1;
+ v->noise_shift = ((s<<1)|new_bit) & 0x007FFFFF;
+ }
+ /* sync state */
+ v->sync = (v->wav_accum & 0x00800000) && !(prev_accum & 0x00800000);
+ }
+ m6581_voice_t* v_sync = &sid->voice[(voice_index+2)%3];
+ uint32_t sm;
+ switch ((v->ctrl>>4) & 0x0F) {
+ case 0: sm = _m6581_wavnone(v); break;
+ case 1: sm = _m6581_triangle(v, v_sync); break;
+ case 2: sm = _m6581_sawtooth(v); break;
+ case 3: sm = _m6581_trisaw(v, v_sync); break;
+ case 4: sm = _m6581_pulse(v); break;
+ case 5: sm = _m6581_tripulse(v, v_sync); break;
+ case 6: sm = _m6581_sawpulse(v); break;
+ case 7: sm = _m6581_trisawpulse(v, v_sync); break;
+ case 8: sm = _m6581_noise(v); break;
+ default: sm = 0; break;
+ }
+ v->wav_output = sm;
+
+ /* envelope generator */
+ uint32_t lfsr = v->env_counter;
+ if (lfsr != _m6581_rate_count_period[v->env_counter_compare & 0x0F]) {
+ const uint32_t feedback = ((lfsr >> 14) ^ (lfsr >> 13)) & 1;
+ lfsr = ((lfsr << 1) | feedback) & 0x7FFF;
+ v->env_counter = lfsr;
+ }
+ else {
+ v->env_counter = 0x7FFF;
+ if ((v->env_state == M6581_ENV_ATTACK) ||
+ (++v->env_exp_counter == _m6581_env_gen_dr_divisors[v->env_cur_level & 0xFF]))
+ {
+ v->env_exp_counter = 0;
+ switch (v->env_state) {
+ case M6581_ENV_ATTACK:
+ if (((++v->env_cur_level) & 0xFF) == 0xFF) {
+ v->env_state = M6581_ENV_DECAY;
+ v->env_counter_compare = v->env_decay_sub;
+ }
+ break;
+ case M6581_ENV_DECAY:
+ if (v->env_cur_level != v->env_sustain_level) {
+ v->env_cur_level = (v->env_cur_level - 1) & 0xFF;
+ if (0 == v->env_cur_level) {
+ v->env_state = M6581_ENV_FROZEN;
+ }
+ }
+ break;
+ case M6581_ENV_RELEASE:
+ v->env_cur_level = (v->env_cur_level - 1) & 0xFF;
+ if (0 == v->env_cur_level) {
+ v->env_state = M6581_ENV_FROZEN;
+ }
+ break;
+ case M6581_ENV_FROZEN:
+ v->env_cur_level = 0;
+ break;
+ }
+ }
+ }
+}
+
+static inline void _m6581_voice_sync(m6581_t* sid, int voice_index) {
+ m6581_voice_t* v = &sid->voice[voice_index];
+ m6581_voice_t* v_sync = &sid->voice[(voice_index+2)%3];
+ if (v->sync && (v_sync->ctrl & M6581_CTRL_SYNC)) {
+ v_sync->wav_accum = 0;
+ }
+}
+
+/*--- FILTER IMPLEMENTATION ---------------------------------------------------*/
+static void _m6581_set_filter_cutoff(m6581_filter_t* f) {
+ const float freq_domain_div_coeff = 2.0f * ((float)M_PI) * 1.048576f;
+ f->w0 = (int) (_m6581_cutoff_freq[f->cutoff] * freq_domain_div_coeff);
+ const float nyquist_freq = (float) f->nyquist_freq;
+ const float max_cutoff = nyquist_freq > 16000.0f ? 16000.0f : nyquist_freq;
+ const int w0_max_dt = (int)(max_cutoff * freq_domain_div_coeff);
+ if (f->w0 > w0_max_dt) {
+ f->w0 = w0_max_dt;
+ }
+}
+
+static void _m6581_set_resonance(m6581_filter_t* f) {
+ f->resonance_coeff_div_1024 = (int) (1024.0f/(0.707f + 1.9f * ((float)f->resonance) / 15.0f) + 0.5f);
+}
+
+static void _m6581_set_cutoff_lo(m6581_filter_t* f, uint8_t data) {
+ if ((data ^ f->cutoff) & 7) {
+ f->cutoff = (f->cutoff & 0x7F8) | (data & 7);
+ _m6581_set_filter_cutoff(f);
+ }
+}
+
+static void _m6581_set_cutoff_hi(m6581_filter_t* f, uint8_t data) {
+ f->cutoff = (data<< 3) | (f->cutoff & 7);
+ _m6581_set_filter_cutoff(f);
+}
+
+static void _m6581_set_resfilt(m6581_filter_t* f, uint8_t data) {
+ f->voices = data & 7;
+ f->resonance = data >> 4;
+ _m6581_set_resonance(f);
+}
+
+static void _m6581_set_modevol(m6581_t* sid, uint8_t data) {
+ sid->filter.volume = data & 0x0F;
+ sid->filter.mode = data >> 4;
+ sid->voice[2].muted = 0 != (sid->filter.mode & M6581_FILTER_3OFF);
+}
+
+static inline int _m6581_filter_output(m6581_filter_t* f, int vi) {
+ int w0_dt = f->w0 / (1<<6);
+ vi = vi / (1<<7);
+
+ int d_vlp = (w0_dt * f->v_bp) / (1<<14);
+ f->v_lp -= d_vlp;
+ int d_vbp = (w0_dt * f->v_hp) / (1<<14);
+ f->v_bp -= d_vbp;
+ f->v_hp = ((f->v_bp * f->resonance_coeff_div_1024) / (1<<10)) - f->v_lp - vi;
+
+ int vf = 0;
+ if (f->mode & M6581_FILTER_LP) {
+ vf += f->v_lp;
+ }
+ if (f->mode & M6581_FILTER_BP) {
+ vf += f->v_bp;
+ }
+ if (f->mode & M6581_FILTER_HP) {
+ vf += f->v_hp;
+ }
+ return vf * (1<<7);
+}
+
+/* tick the sound generation, return true when new sample ready */
+static uint64_t _m6581_tick(m6581_t* sid, uint64_t pins) {
+ /* decay the last written register value */
+ if (sid->bus_decay > 0) {
+ if (--sid->bus_decay == 0) {
+ sid->bus_value = 0;
+ }
+ }
+
+ /* tick wave and envelope generators */
+ for (int i = 0; i < 3; i++) {
+ _m6581_voice_tick(sid, i);
+ }
+ /* handle voice synchronization */
+ for (int i = 0; i < 3; i++) {
+ _m6581_voice_sync(sid, i);
+ }
+ /* filter */
+ int sum_filtered_outp = 0;
+ int sum_outp = 0;
+ for (int i = 0; i < 3; i++) {
+ m6581_voice_t* v = &sid->voice[i];
+ int wav_out = (int) v->wav_output;
+ int env_out = (int) v->env_cur_level;
+ if (sid->filter.voices & (1<muted) {
+ sum_outp += (0 - M6581_DCWAVE) * env_out + M6581_DCVOICE;
+ }
+ else {
+ sum_outp += (wav_out - M6581_DCWAVE) * env_out + M6581_DCVOICE;
+ }
+ }
+ }
+ int accu = (sum_outp + _m6581_filter_output(&sid->filter, sum_filtered_outp) + M6581_DCMIXER) * sid->filter.volume;
+ int sample = accu / (1<<12);
+ sid->sample_accum += (sample / 16384.0f);
+ sid->sample_accum_count += 1.0f;
+
+ /* new sample? */
+ sid->sample_counter -= M6581_FIXEDPOINT_SCALE;
+ if (sid->sample_counter <= 0) {
+ sid->sample_counter += sid->sample_period;
+ float s = sid->sample_accum / sid->sample_accum_count;
+ sid->sample = sid->sample_mag * s;
+ sid->sample_accum = 0.0f;
+ sid->sample_accum_count = 0.0f;
+ pins |= M6581_SAMPLE;
+ }
+ else {
+ pins &= ~M6581_SAMPLE;
+ }
+ return pins;
+}
+
+/* read a register */
+static uint64_t _m6581_read(m6581_t* sid, uint64_t pins) {
+ uint8_t reg = pins & M6581_ADDR_MASK;
+ uint8_t data;
+ switch (reg) {
+ case M6581_POT_X:
+ case M6581_POT_Y:
+ /* FIXME: potentiometers */
+ data = 0;
+ sid->bus_value = 0;
+ break;
+ case M6581_OSC3RAND:
+ /* FIXME */
+ sid->bus_value = 0;
+ data = sid->voice[2].wav_output >> 4;
+ break;
+ case M6581_ENV3:
+ sid->bus_value = 0;
+ data = sid->voice[2].env_cur_level;
+ break;
+ default:
+ data = sid->bus_value;
+ break;
+ }
+ M6581_SET_DATA(pins, data);
+ return pins;
+}
+
+/* write a register */
+static void _m6581_write(m6581_t* sid, uint64_t pins) {
+ uint8_t reg = pins & M6581_ADDR_MASK;
+ uint8_t data = M6581_GET_DATA(pins);
+ sid->bus_value = data;
+ sid->bus_decay = 0x2000;
+ switch (reg) {
+ case M6581_V1_FREQ_LO:
+ _m6581_set_freq_lo(&sid->voice[0], data);
+ break;
+ case M6581_V1_FREQ_HI:
+ _m6581_set_freq_hi(&sid->voice[0], data);
+ break;
+ case M6581_V1_PW_LO:
+ _m6581_set_pw_lo(&sid->voice[0], data);
+ break;
+ case M6581_V1_PW_HI:
+ _m6581_set_pw_hi(&sid->voice[0], data);
+ break;
+ case M6581_V1_CTRL:
+ _m6581_set_ctrl(&sid->voice[0], data);
+ break;
+ case M6581_V1_ATKDEC:
+ _m6581_set_atkdec(&sid->voice[0], data);
+ break;
+ case M6581_V1_SUSREL:
+ _m6581_set_susrel(&sid->voice[0], data);
+ break;
+ case M6581_V2_FREQ_LO:
+ _m6581_set_freq_lo(&sid->voice[1], data);
+ break;
+ case M6581_V2_FREQ_HI:
+ _m6581_set_freq_hi(&sid->voice[1], data);
+ break;
+ case M6581_V2_PW_LO:
+ _m6581_set_pw_lo(&sid->voice[1], data);
+ break;
+ case M6581_V2_PW_HI:
+ _m6581_set_pw_hi(&sid->voice[1], data);
+ break;
+ case M6581_V2_CTRL:
+ _m6581_set_ctrl(&sid->voice[1], data);
+ break;
+ case M6581_V2_ATKDEC:
+ _m6581_set_atkdec(&sid->voice[1], data);
+ break;
+ case M6581_V2_SUSREL:
+ _m6581_set_susrel(&sid->voice[1], data);
+ break;
+ case M6581_V3_FREQ_LO:
+ _m6581_set_freq_lo(&sid->voice[2], data);
+ break;
+ case M6581_V3_FREQ_HI:
+ _m6581_set_freq_hi(&sid->voice[2], data);
+ break;
+ case M6581_V3_PW_LO:
+ _m6581_set_pw_lo(&sid->voice[2], data);
+ break;
+ case M6581_V3_PW_HI:
+ _m6581_set_pw_hi(&sid->voice[2], data);
+ break;
+ case M6581_V3_CTRL:
+ _m6581_set_ctrl(&sid->voice[2], data);
+ break;
+ case M6581_V3_ATKDEC:
+ _m6581_set_atkdec(&sid->voice[2], data);
+ break;
+ case M6581_V3_SUSREL:
+ _m6581_set_susrel(&sid->voice[2], data);
+ break;
+ case M6581_FC_LO:
+ _m6581_set_cutoff_lo(&sid->filter, data);
+ break;
+ case M6581_FC_HI:
+ _m6581_set_cutoff_hi(&sid->filter, data);
+ break;
+ case M6581_RES_FILT:
+ _m6581_set_resfilt(&sid->filter, data);
+ break;
+ case M6581_MODE_VOL:
+ _m6581_set_modevol(sid, data);
+ break;
+ }
+}
+
+/* the all-in-one tick function */
+uint64_t m6581_tick(m6581_t* sid, uint64_t pins) {
+ CHIPS_ASSERT(sid);
+
+ /* first perform the regular per-tick actions */
+ pins = _m6581_tick(sid, pins);
+
+ /* register read/write */
+ if (pins & M6581_CS) {
+ if (pins & M6581_RW) {
+ pins = _m6581_read(sid, pins);
+ }
+ else {
+ _m6581_write(sid, pins);
+ }
+ }
+ sid->pins = pins;
+ return pins;
+}
+
+/* write a register without advancing the chip clock */
+void m6581_write_reg(m6581_t* sid, uint8_t reg, uint8_t data) {
+ CHIPS_ASSERT(sid);
+ _m6581_write(sid, (uint64_t)(reg & M6581_ADDR_MASK) | ((uint64_t)data << 16));
+}
+
+#endif /* CHIPS_IMPL */
diff --git a/src/chips/sid6581.cpp b/src/chips/sid6581.cpp
new file mode 100644
index 0000000..c7b6819
--- /dev/null
+++ b/src/chips/sid6581.cpp
@@ -0,0 +1,470 @@
+/*
+ sid6581.cpp
+
+ Csound opcode wrapping the floooh/chips m6581 (MOS 6581 "SID") emulator.
+
+ This opcodes is free software; you can redistribute it
+ and/or modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The opcode is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this opcode; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ 02110-1301 USA
+
+ The m6581 emulator (m6581.h) is vendored in this directory. It is
+ (c) 2018 Andre Weissflog and provided under the zlib/libpng license;
+ the SID implementation itself is based on tedplay (c) 2012 Attila
+ Grosz, used under the Unlicense. See m6581.h for the full notices.
+
+ OPCODE
+
+ iEnv:Sid6581ADSR sid6581adsr kA, kD, kS, kR
+ iVoice:Sid6581Voice sid6581voice kFreq, kWave, kPw
+
+ out = sid6581(iVoice1, kGate1, iEnv1, \
+ iVoice2, kGate2, iEnv2, \
+ iVoice3, kGate3, iEnv3, \
+ kCutoff, kRes, iFilterMode, iRouteMask)
+
+ Parameters
+ iVoice1..3 Sid6581Voice for each voice, built with sid6581voice. It
+ has three k-rate members accessible with dot notation:
+ .freq, .wave and .pw (e.g. v1.freq = 440).
+ .freq voice frequency in Hz (0..~4 kHz); converted to the
+ SID 16-bit frequency register.
+ .wave waveform bitmask: 1 = triangle, 2 = sawtooth,
+ 4 = pulse, 8 = noise; values combine (e.g. 3 = tri+saw,
+ 6 = saw+pulse).
+ .pw pulse width register value (0..4095, 12-bit). Ignored
+ unless the pulse waveform is selected.
+ kGate1..3 envelope gate: >= 0.5 gates on (attack/sustain),
+ otherwise off (release).
+ iEnv1..3 Sid6581ADSR envelope for each voice. A Sid6581ADSR value
+ is built with the sid6581adsr opcode and has four k-rate
+ members accessible with dot notation - .att (attack
+ seconds), .dec (decay seconds), .sus (sustain 0..1) and
+ .rel (release seconds) - which may be read or written
+ individually (e.g. env1.sus = 0.5). The SID envelope
+ registers are only written on a gate transition: A/D/S/R
+ are latched on note-on and R is refreshed on note-off,
+ so k-rate changes take effect on the next note.
+ kCutoff filter cutoff register value (0..2047, 11-bit).
+ kRes filter resonance (0..15, 4-bit).
+ iFilterMode filter mode bitmask: 1 = low-pass, 2 = band-pass,
+ 4 = high-pass, 8 = voice 3 off (3OFF). The mute applies
+ only to voices outside the filter route, so if voice 3
+ is in iRouteMask (e.g. iRouteMask = 7) it is still heard.
+ iRouteMask filter routing bitmask: 1 = voice 1, 2 = voice 2,
+ 4 = voice 3 (which voices pass through the filter).
+
+ The output volume is fixed at maximum (15) and the SID clock is the PAL
+ rate (985248 Hz); neither is exposed as an argument.
+*/
+
+/* m6581.h must be included before any Csound header so that its
+ include (with _USE_MATH_DEFINES on MSVC) is the first one in the
+ translation unit. Wrapping it in extern "C" keeps the C-linkage
+ declarations and the CHIPS_IMPL definitions in agreement when compiled
+ as C++. */
+#define CHIPS_IMPL
+extern "C" {
+#include "m6581.h"
+}
+
+#include
+#include
+
+/* SID clock frequency (PAL). */
+#define SID_TICK_HZ 985248
+
+/* SID master volume (0..15), fixed since it is not part of the signature. */
+#define SID_VOLUME 15
+
+/* Convert a frequency in Hz to the SID 16-bit frequency register value. */
+static uint16_t freq_to_reg(cs_float hz) {
+ double value;
+ if (hz <= FL(0.0))
+ return 0;
+ value = (double)hz * 16777216.0 / (double)SID_TICK_HZ;
+ if (value > 65535.0)
+ value = 65535.0;
+ return (uint16_t)(value + 0.5);
+}
+
+/* Build a voice control register from a waveform bitmask and a gate. */
+static uint8_t voice_ctrl(cs_float wave, cs_float gate) {
+ int w = (wave >= FL(0.0)) ? (int)wave : 0;
+ uint8_t ctrl = (uint8_t)((w & 0x0F) << 4);
+ if (gate >= FL(0.5))
+ ctrl |= M6581_CTRL_GATE;
+ return ctrl;
+}
+
+/* Register base addresses for each of the three voices, as
+ { freq_lo, pw_lo, ctrl }. */
+static const uint8_t VOICE_REGS[3][3] = {
+ { M6581_V1_FREQ_LO, M6581_V1_PW_LO, M6581_V1_CTRL },
+ { M6581_V2_FREQ_LO, M6581_V2_PW_LO, M6581_V2_CTRL },
+ { M6581_V3_FREQ_LO, M6581_V3_PW_LO, M6581_V3_CTRL },
+};
+static const uint8_t VOICE_ATKDEC[3] = {
+ M6581_V1_ATKDEC, M6581_V2_ATKDEC, M6581_V3_ATKDEC,
+};
+static const uint8_t VOICE_SUSREL[3] = {
+ M6581_V1_SUSREL, M6581_V2_SUSREL, M6581_V3_SUSREL,
+};
+
+/* -------------------------------------------------------------------------
+ Sid6581ADSR user-defined struct (named members A/D/S/R)
+ ------------------------------------------------------------------------- */
+
+/* The Sid6581ADSR orchestra type holds four k-rate members: att (attack
+ seconds), dec (decay seconds), sus (sustain 0..1) and rel (release
+ seconds). It is registered with csound->RegisterStruct() so the members
+ can be read and written with dot notation in the orchestra, e.g.
+ env1.sus = 0.5. Csound stores each member in its own CS_VAR_MEM block, so
+ the members are NOT contiguous; opcode code reads/writes them through
+ CS_STRUCT_VAR::members[i]->value. */
+static const CSOUND_STRUCT_MEMBER SID6581ADSR_MEMBERS[4] = {
+ { "att", "k" },
+ { "dec", "k" },
+ { "sus", "k" },
+ { "rel", "k" },
+};
+
+/* The Sid6581Voice orchestra type holds the three k-rate members that
+ describe a voice's tone: freq (Hz), wave (waveform bitmask) and pw
+ (pulse-width register). */
+static const CSOUND_STRUCT_MEMBER SID6581VOICE_MEMBERS[3] = {
+ { "freq", "k" },
+ { "wave", "k" },
+ { "pw", "k" },
+};
+
+/* Read the i-th numeric member of a struct value (0 if absent). */
+static cs_float struct_member(const CS_STRUCT_VAR *s, int i) {
+ if (s == NULL || s->members == NULL || s->members[i] == NULL)
+ return FL(0.0);
+ return s->members[i]->value;
+}
+
+/* Read the i-th member (0=att, 1=dec, 2=sus, 3=rel) of a Sid6581ADSR value. */
+static cs_float adsr_member(const CS_STRUCT_VAR *env, int i) {
+ return struct_member(env, i);
+}
+
+/* -------------------------------------------------------------------------
+ Envelope conversion: "friendly" seconds / 0..1 values -> SID nibbles
+ ------------------------------------------------------------------------- */
+
+/* Number of SID ticks per envelope step, one entry per rate nibble (0..15).
+ The SID envelope counter is a 15-bit LFSR loaded with 0x7FFF and clocked
+ until it matches _m6581_rate_count_period[nibble]; simulate it here to
+ recover the exact per-step tick count. */
+static double env_step_ticks[16];
+
+/* Cumulative sum of _m6581_env_gen_dr_divisors:
+ dr_prefix[k] = sum(0..k). Used to count envelope steps for decay/release,
+ which advance through the exponential divisor table. */
+static int dr_prefix[256];
+
+/* Fill env_step_ticks and dr_prefix. Must run once before any envelope
+ conversion. */
+static void build_env_tables(void) {
+ int n, i;
+ for (n = 0; n < 16; n++) {
+ uint32_t lfsr = 0x7FFF;
+ uint32_t target = _m6581_rate_count_period[n];
+ uint32_t steps = 0;
+ while (lfsr != target && steps < 0x8000) {
+ uint32_t fb = ((lfsr >> 14) ^ (lfsr >> 13)) & 1u;
+ lfsr = ((lfsr << 1) | fb) & 0x7FFFu;
+ steps++;
+ }
+ env_step_ticks[n] = (double)steps;
+ }
+ {
+ int acc = 0;
+ for (i = 0; i < 256; i++) {
+ acc += (int)_m6581_env_gen_dr_divisors[i];
+ dr_prefix[i] = acc;
+ }
+ }
+}
+
+/* Clamp a sustain value to a 4-bit nibble. */
+static int sustain_nibble(cs_float s) {
+ int n = (int)(s * 15.0 + 0.5);
+ if (n < 0)
+ n = 0;
+ if (n > 15)
+ n = 15;
+ return n;
+}
+
+/* Sustain level (0..255) for a sustain nibble. */
+static int sustain_level(int nib) { return nib * 0x11; }
+
+/* Nearest rate nibble whose per-step tick count matches `target`. */
+static uint8_t nearest_rate_nibble(double target) {
+ double best = env_step_ticks[0] - target;
+ uint8_t best_n = 0;
+ int n;
+ if (best < 0.0)
+ best = -best;
+ for (n = 1; n < 16; n++) {
+ double d = env_step_ticks[n] - target;
+ if (d < 0.0)
+ d = -d;
+ if (d < best) {
+ best = d;
+ best_n = (uint8_t)n;
+ }
+ }
+ return best_n;
+}
+
+/* Attack: 255 linear steps from level 0 to 255. */
+static uint8_t attack_nibble(cs_float sec) {
+ return nearest_rate_nibble((double)sec * (double)SID_TICK_HZ / 255.0);
+}
+
+/* Decay: exponential drop from level 255 down to the sustain level. */
+static uint8_t decay_nibble(cs_float sec, int s_nib) {
+ int steps = dr_prefix[255] - dr_prefix[sustain_level(s_nib)];
+ if (steps < 1)
+ steps = 1;
+ return nearest_rate_nibble((double)sec * (double)SID_TICK_HZ / (double)steps);
+}
+
+/* Release: exponential drop from the sustain level down to 0. */
+static uint8_t release_nibble(cs_float sec, int s_nib) {
+ int steps = dr_prefix[sustain_level(s_nib)] - dr_prefix[0];
+ if (steps < 1)
+ steps = 1;
+ return nearest_rate_nibble((double)sec * (double)SID_TICK_HZ / (double)steps);
+}
+
+/* Convert a Sid6581ADSR value into SID ATKDEC/SUSREL register bytes. */
+static void adsr_to_regs(const CS_STRUCT_VAR *env, uint8_t &atkdec,
+ uint8_t &susrel) {
+ int s = sustain_nibble(adsr_member(env, 2));
+ uint8_t a = attack_nibble(adsr_member(env, 0));
+ uint8_t d = decay_nibble(adsr_member(env, 1), s);
+ uint8_t r = release_nibble(adsr_member(env, 3), s);
+ atkdec = (uint8_t)((a << 4) | d);
+ susrel = (uint8_t)((s << 4) | r);
+}
+
+/* -------------------------------------------------------------------------
+ sid6581adsr: build a Sid6581ADSR value from k-rate A/D/S/R inputs
+ ------------------------------------------------------------------------- */
+struct Sid6581Adsr : csnd::Plugin<1, 4> {
+ /* Copy the four k-rate inputs into the output struct's members. */
+ int kperf() {
+ CS_STRUCT_VAR *out = (CS_STRUCT_VAR *)outargs.data(0);
+ if (out == NULL || out->members == NULL)
+ return OK;
+ out->members[0]->value = inargs[0]; /* A */
+ out->members[1]->value = inargs[1]; /* D */
+ out->members[2]->value = inargs[2]; /* S */
+ out->members[3]->value = inargs[3]; /* R */
+ return OK;
+ }
+};
+
+/* -------------------------------------------------------------------------
+ sid6581voice: build a Sid6581Voice value from k-rate freq/wave/pw inputs
+ ------------------------------------------------------------------------- */
+struct Sid6581VoiceOp : csnd::Plugin<1, 3> {
+ /* Copy the three k-rate inputs into the output struct's members. */
+ int kperf() {
+ CS_STRUCT_VAR *out = (CS_STRUCT_VAR *)outargs.data(0);
+ if (out == NULL || out->members == NULL)
+ return OK;
+ out->members[0]->value = inargs[0]; /* freq */
+ out->members[1]->value = inargs[1]; /* wave */
+ out->members[2]->value = inargs[2]; /* pw */
+ return OK;
+ }
+};
+
+/* -------------------------------------------------------------------------
+ sid6581: one full SID chip (three voices + filter)
+ Inputs: 3 x (Sid6581Voice, kGate, Sid6581ADSR), kCutoff, kRes, iMode, iRoute
+ ------------------------------------------------------------------------- */
+struct Sid6581 : csnd::Plugin<1, 13> {
+ /* state */
+ m6581_t sid;
+ uint8_t filter_mode;
+ uint8_t route_mask;
+ bool prev_gate[3];
+ uint8_t latched_sustain[3];
+
+ /* Write one SID register without advancing the emulated clock, so
+ register updates don't alter the chip's timing relative to Csound. */
+ void sid_write(uint8_t reg, uint8_t data) {
+ m6581_write_reg(&sid, reg, data);
+ }
+
+ /* Initialise and reset the emulator, set a default envelope on each
+ voice and read the i-rate filter mode and route mask. */
+ int init() {
+ m6581_desc_t desc;
+ desc.tick_hz = SID_TICK_HZ;
+ desc.sound_hz = (int)sr();
+ desc.magnitude = 1.0f;
+ m6581_init(&sid, &desc);
+ m6581_reset(&sid);
+
+ /* A default gate-friendly envelope (fast attack, full sustain, quick
+ release); it is overwritten on the first gate transition. */
+ for (int v = 0; v < 3; v++) {
+ sid_write(VOICE_ATKDEC[v], 0x20);
+ sid_write(VOICE_SUSREL[v], 0xF2);
+ prev_gate[v] = false;
+ latched_sustain[v] = 15;
+ }
+
+ /* i-rate inputs */
+ filter_mode = (uint8_t)((int)inargs[11] & 0x0F);
+ route_mask = (uint8_t)((int)inargs[12] & 0x07);
+ return OK;
+ }
+
+ /* Per k-cycle: update voice, envelope and filter registers, then render
+ ksmps samples of audio from the emulator. */
+ int kperf() {
+ cs_float *aout = outargs.data(0);
+ uint32_t ksmps = (uint32_t)insdshead->ksmps;
+ uint32_t n;
+ int v;
+
+ /* Write the voice registers from the k-rate inputs, latching each
+ voice's envelope on a gate transition. */
+ for (v = 0; v < 3; v++) {
+ int base = v * 3;
+ const CS_STRUCT_VAR *voice =
+ (const CS_STRUCT_VAR *)inargs.data(base);
+ cs_float gate = inargs[base + 1];
+ bool gate_on = gate >= FL(0.5);
+ const CS_STRUCT_VAR *env =
+ (const CS_STRUCT_VAR *)inargs.data(base + 2);
+
+ if (gate_on && !prev_gate[v]) {
+ /* note-on: latch A/D/S/R */
+ uint8_t atkdec, susrel;
+ adsr_to_regs(env, atkdec, susrel);
+ sid_write(VOICE_ATKDEC[v], atkdec);
+ sid_write(VOICE_SUSREL[v], susrel);
+ latched_sustain[v] = susrel >> 4;
+ }
+ else if (!gate_on && prev_gate[v]) {
+ /* note-off: refresh the release rate only */
+ uint8_t r = release_nibble(adsr_member(env, 3),
+ latched_sustain[v]);
+ sid_write(VOICE_SUSREL[v],
+ (uint8_t)((latched_sustain[v] << 4) | r));
+ }
+ prev_gate[v] = gate_on;
+
+ write_voice(VOICE_REGS[v][0], VOICE_REGS[v][1], VOICE_REGS[v][2],
+ struct_member(voice, 0), struct_member(voice, 1),
+ struct_member(voice, 2), gate);
+ }
+
+ /* Filter registers. */
+ {
+ uint16_t cutoff = 0;
+ uint8_t res = 0;
+ cs_float c = inargs[9];
+ cs_float r = inargs[10];
+ if (c > FL(0.0))
+ cutoff = (c > FL(2047.0)) ? 2047 : (uint16_t)(c + FL(0.5));
+ if (r > FL(0.0))
+ res = (r > FL(15.0)) ? 15 : (uint8_t)(r + FL(0.5));
+ sid_write(M6581_FC_LO, (uint8_t)(cutoff & 0x07));
+ sid_write(M6581_FC_HI, (uint8_t)((cutoff >> 3) & 0xFF));
+ sid_write(M6581_RES_FILT, (uint8_t)((res << 4) | (route_mask & 0x07)));
+ sid_write(M6581_MODE_VOL, (uint8_t)((filter_mode << 4) | SID_VOLUME));
+ }
+
+ /* Generate the audio vector, honouring the sample-accurate offset
+ and early-exit regions. */
+ for (n = 0; n < offset; n++)
+ aout[n] = FL(0.0);
+ for (n = offset; n < nsmps; n++) {
+ uint64_t pins;
+ do {
+ pins = m6581_tick(&sid, 0);
+ } while ((pins & M6581_SAMPLE) == 0);
+ aout[n] = (cs_float)sid.sample;
+ }
+ for (n = nsmps; n < ksmps; n++)
+ aout[n] = FL(0.0);
+
+ return OK;
+ }
+
+ /* Write the frequency, pulse-width and control registers for one voice. */
+ void write_voice(uint8_t freq_lo, uint8_t pw_lo, uint8_t ctrl_reg,
+ cs_float freq, cs_float wave, cs_float pw,
+ cs_float gate) {
+ uint16_t f = freq_to_reg(freq);
+ sid_write(freq_lo, (uint8_t)(f & 0xFF));
+ sid_write((uint8_t)(freq_lo + 1), (uint8_t)((f >> 8) & 0xFF));
+
+ uint16_t p = 0;
+ if (pw > FL(0.0))
+ p = (pw > FL(4095.0)) ? 4095 : (uint16_t)(pw + FL(0.5));
+ sid_write(pw_lo, (uint8_t)(p & 0xFF));
+ sid_write((uint8_t)(pw_lo + 1), (uint8_t)((p >> 8) & 0x0F));
+
+ sid_write(ctrl_reg, voice_ctrl(wave, gate));
+ }
+};
+
+/* Build the envelope tables, then register the Sid6581ADSR and Sid6581Voice
+ types and the sid6581adsr, sid6581voice and sid6581 opcodes. */
+static void onload(csnd::Csound *csound) {
+ CSOUND *cs = csound->get_csound();
+
+ /* Build the envelope conversion tables once, before any performance. */
+ build_env_tables();
+
+ /* Register the Sid6581ADSR struct type (named members A/D/S/R). */
+ if (cs->RegisterStruct != NULL)
+ cs->RegisterStruct(cs, "Sid6581ADSR", SID6581ADSR_MEMBERS, 4);
+
+ if (cs->RegisterStruct != NULL)
+ cs->RegisterStruct(cs, "Sid6581Voice", SID6581VOICE_MEMBERS, 3);
+
+ csnd::plugin(csound, "sid6581adsr", ":Sid6581ADSR;", "kkkk",
+ csnd::thread::k);
+ csnd::plugin(csound, "sid6581voice", ":Sid6581Voice;",
+ "kkk", csnd::thread::k);
+ csnd::plugin(
+ csound, "sid6581", "a",
+ ":Sid6581Voice;k:Sid6581ADSR;:Sid6581Voice;k:Sid6581ADSR;"
+ ":Sid6581Voice;k:Sid6581ADSR;kkii",
+ csnd::thread::ik);
+}
+
+#ifdef BUILD_PLUGINS
+#include
+/* Entry point used when the plugin is built as part of the plugins tree. */
+void csnd::on_load(csnd::Csound *csound) { onload(csound); }
+#else
+/* Entry point used when Csound loads the library as a standalone module. */
+extern "C" int32_t sid6581_init_modules(CSOUND *csound) {
+ onload((csnd::Csound *)csound);
+ return OK;
+}
+#endif
diff --git a/wasm/default.nix b/wasm/default.nix
index 7d1b3ec..e25615f 100644
--- a/wasm/default.nix
+++ b/wasm/default.nix
@@ -135,6 +135,7 @@ in wasiCxx.mkDerivation {
"-DBUILD_IMAGE_OPCODES=ON"
"-DBUILD_LINEAR_ALGEBRA_OPCODES=ON"
"-DBUILD_STK_OPCODES=ON"
+ "-DBUILD_CHIPS_OPCODES=OFF"
"-DBUILD_FLUID_OPCODES=OFF"
"-DBUILD_HDF5_OPCODES=OFF"
"-DBUILD_WEBSOCKET_OPCODE=OFF"