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"