Skip to content

Commit

Permalink
Merge pull request #80 from jroweboy/cc65_c_bindings
Browse files Browse the repository at this point in the history
CC65 C bindings
  • Loading branch information
BleuBleu committed Jul 17, 2021
2 parents 7a97d8d + 0d34ee4 commit cc87f97
Show file tree
Hide file tree
Showing 18 changed files with 495 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Expand Up @@ -30,3 +30,8 @@ SoundEngine/UnitTests/*.inc
FamiStudio/FamiStudio.args.json
*.tmp
*.ilk
.vscode

# This file is generated from the C file
SoundEngine/DemoSource/demo_cc65.s

51 changes: 27 additions & 24 deletions Docs/docs/soundengine.md
Expand Up @@ -27,13 +27,13 @@ The basic feature set that is always available in engine is:
* Looping sections in envelopes.
* Release points for volume envelopes.
* Ability to change the speed (FamiTracker tempo mode only).
* Ability to loop over a portion of the song.
* Ability to loop over a portion of the song.
* Up to 64 instruments per export, an export can consist of as little as 1 song, or as many as 17.

Features that can be toggled on/off depending on the needs of your projects:

* Audio expansions chips (at most one can be enabled) : VRC6, VRC7, FDS, S5B and N163.
* PAL/NTSC playback support.
* PAL/NTSC playback support.
* DPCM sample support
* Sound effect support (with configurable number of streams)
* Blaarg Smooth Vibrato technique to eliminate "pops" on square channels
Expand Down Expand Up @@ -68,33 +68,36 @@ A small demo is included with the engine sound code. The demo is available for a

The source code for the demo is located in the \DemoSource subfolder.

* CA65: DemoSource\demo_ca65.s
* NESASM: DemoSource\demo_nesasm.asm
* ASM6: DemoSource\demo_asm6.asm
* CA65: `DemoSource\demo_ca65.s`
* CC65: `DemoSource\demo_cc65.c`
* NESASM: `DemoSource\demo_nesasm.asm`
* ASM6: `DemoSource\demo_asm6.asm`

The songs used in the demo are available in the demo songs that are included with FamiStudio:

* Silver Surfer.fms
* Journey To Silius.fms
* Shatterhand.fms
* `Silver Surfer.fms`
* `Journey To Silius.fms`
* `Shatterhand.fms`

The sound effects used in the demo are available in SFX.fms, which is in the \DemoAssets folder that comes with the sound engine.

## Integrating in your game

The sound engine is contained in a single file which can be simply included in one of your assembly file, like it is done in the demo.
The sound engine is contained in a single file which can be simply included in one of your assembly file, like it is done in the demo.

* CA65: famistudio_ca65.s
* NESASM: famistudio_nesasm.asm
* ASM6: famistudio_asm6.asm
* CA65: `famistudio_ca65.s`
* NESASM: `famistudio_nesasm.asm`
* ASM6: `famistudio_asm6.asm`

Another approach would be to compile the engine as a seperate obj file and link it. This might require you to import the famistudio_xxx calls in other parts of your project.

All the instructions to use it in your project are included as comments at the top of these files.

## Interface
For using the C bindings with CC65, you will need to include the `famistudio_cc65.h` header and also include the `famistudio_ca65.s` into your assembly startup routine. Make sure that `FAMISTUDIO_CFG_C_BINDINGS = 1` is set either as part of your external config or in the file or else the linker will be unable link object files.

The interface is pretty much the same as FamiTone2, with a slightly different naming convention. The subroutines you can call from your game are:
## Interface

The interface is pretty much the same as FamiTone2, with a slightly different naming convention. The subroutines you can call from your game are:

* **famistudio_init** : Initialize the engine with some music data.
* **famistudio_music_play** : Start music playback with a specific song.
Expand All @@ -121,23 +124,23 @@ There are four main things to configure in the engine:
3. Global engine parameters
4. Supported features

Note that unless specified, the engine uses "if" and not "ifdef" for all boolean values so you need to define these to non-zero values. Undefined values will be assumed to be zero.
Note that unless specified, the engine uses `if` and not `ifdef` for all boolean values so you need to define these to non-zero values. Undefined values will be assumed to be zero.

### 1. Segments Configuration

You need to tell where you want to allocate the zeropage, RAM and code. This section will be slightly different for each assembler.

#### CA65
#### CA65

For CA65, you need to specify the name of your ZEROPAGE, RAM/BSS and CODE/PRG segments as c-style macros (.define) like the example below.
For CA65, you need to specify the name of your ZEROPAGE, RAM/BSS and CODE/PRG segments as c-style macros (`.define`) like the example below.

.define FAMISTUDIO_CA65_ZP_SEGMENT ZP
.define FAMISTUDIO_CA65_RAM_SEGMENT RAM
.define FAMISTUDIO_CA65_CODE_SEGMENT PRG

#### NESASM

For NESASM, you may specify the .rsset location for ZP/BSS and the .bank/.org location for the code. Optionally, the .zp/.bss/.code directives may be emitted. Please refer to the demo or engine assembly file for an example of how to use this.
For NESASM, you may specify the .rsset location for ZP/BSS and the `.bank`/`.org` location for the code. Optionally, the `.zp`/`.bss`/`.code` directives may be emitted. Please refer to the demo or engine assembly file for an example of how to use this.

; Define this to emit the ".zp" directive before the zeropage variables.
FAMISTUDIO_NESASM_ZP_SECTION = 1
Expand All @@ -162,15 +165,15 @@ For NESASM, you may specify the .rsset location for ZP/BSS and the .bank/.org lo

#### ASM6

For ASM6, you simply need to specify the location at which to allocate the ZP/BSS and CODE.
For ASM6, you simply need to specify the location at which to allocate the `ZP`/`BSS` and `CODE`.

FAMISTUDIO_ASM6_ZP_ENUM = $0000
FAMISTUDIO_ASM6_BSS_ENUM = $0200
FAMISTUDIO_ASM6_CODE_BASE = $8000

### 2. Audio Expansions Configuration

You can enable up to one audio expansion (FAMISTUDIO_EXP_XXX). Enabling more than one expansion will lead to undefined behavior. Memory usage goes up as more complex expansions are used. The audio expansion you choose **MUST MATCH** with the data you will load in the engine. Loading a FDS song while enabling VRC6 will lead to undefined behavior.
You can enable up to one audio expansion (`FAMISTUDIO_EXP_XXX`). Enabling more than one expansion will lead to undefined behavior. Memory usage goes up as more complex expansions are used. The audio expansion you choose **MUST MATCH** with the data you will load in the engine. Loading a FDS song while enabling VRC6 will lead to undefined behavior.

; Konami VRC6 (2 extra square + saw)
FAMISTUDIO_EXP_VRC6 = 1
Expand All @@ -193,7 +196,7 @@ You can enable up to one audio expansion (FAMISTUDIO_EXP_XXX). Enabling more tha

### 3. Global Engine Configuration

These are parameters that configures the engine, but are independent of the data you will be importing, such as which platform (PAL/NTSC) you want to support playback for, whether SFX are enabled or not, etc. They all have the form FAMISTUDIO_CFG_XXX.
These are parameters that configures the engine, but are independent of the data you will be importing, such as which platform (`PAL`/`NTSC`) you want to support playback for, whether SFX are enabled or not, etc. They all have the form `FAMISTUDIO_CFG_XXX`.

; One of these MUST be defined (PAL or NTSC playback).
; Note that only NTSC support is supported when using any of the audio expansions.
Expand All @@ -214,10 +217,10 @@ These are parameters that configures the engine, but are independent of the data
; Must be enabled if you are calling sound effects from a different
; thread than the sound engine update.
FAMISTUDIO_CFG_THREAD = 1

### 4. Supported Features Configuration

Every feature supported in FamiStudio is supported by this sound engine. If you know for sure that you are not using specific features in your music, you can disable them to save memory/processing time. Using a feature in your song and failing to enable it will likely lead to crashes (BRK), or undefined behavior. They all have the form FAMISTUDIO_USE_XXX.
Every feature supported in FamiStudio is supported by this sound engine. If you know for sure that you are not using specific features in your music, you can disable them to save memory/processing time. Using a feature in your song and failing to enable it will likely lead to crashes (`BRK`), or undefined behavior. They all have the form `FAMISTUDIO_USE_XXX`.

; Must be enabled if the songs you will be importing have been created using FamiTracker tempo mode.
; If you are using FamiStudio tempo mode, this must be undefined. You cannot mix and match tempo modes,
Expand Down Expand Up @@ -254,7 +257,7 @@ Every feature supported in FamiStudio is supported by this sound engine. If you

; Must be enabled if any song uses the "Duty Cycle" effect (equivalent of FamiTracker Vxx, also called "Timbre").
FAMISTUDIO_USE_DUTYCYCLE_EFFECT = 1

## Exporting Music/SFX to the engine

You can export music or sound effect data to the engine by using either the [Export Dialog](export.md) or from the [command line](cmdline.md).
Expand Down
12 changes: 12 additions & 0 deletions FamiStudio/Source/IO/FamitoneMusicFile.cs
Expand Up @@ -84,6 +84,18 @@ private int OutputHeader(bool separateSongs)

lines.Add($";this file for {(kernel == FamiToneKernel.FamiTone2 ? "FamiTone2 library" : "FamiStudio Sound Engine")} generated by FamiStudio");
lines.Add("");

if (assemblyFormat == AssemblyFormat.CA65)
{
// For CA65 we add a re-export of the symbol prefixed with _ for the C code to see
// For some reason though, we can only rexport these symbols either before they are defined,
// or after all of the data is completely written.
lines.Add(".if FAMISTUDIO_CFG_C_BINDINGS");
lines.Add($".export _{name}_music_data={name}_music_data");
lines.Add(".endif");
lines.Add("");
}

lines.Add($"{name}_music_data:");
lines.Add($"\t{db} {project.Songs.Count}");
lines.Add($"\t{dw} {ll}instruments");
Expand Down
13 changes: 13 additions & 0 deletions FamiStudio/Source/IO/FamitoneSoundEffectFile.cs
Expand Up @@ -65,6 +65,19 @@ public bool Save(Project project, int[] songIds, int format, int machine, int ke
var lines = new List<string>();

lines.Add($";this file for FamiTone2 libary generated by FamiStudio\n");

if (format == AssemblyFormat.CA65)
{
// For CA65 we add a re-export of the symbol prefixed with _ for the C code to see
// For some reason though, we can only rexport these symbols either before they are defined,
// or after all of the data is completely written.
lines.Add("");
lines.Add(".if FAMISTUDIO_CFG_C_BINDINGS");
lines.Add(".export _sounds=sounds");
lines.Add(".endif");
lines.Add("");
}

lines.Add($"sounds:");

lines.Add($"\t{dw} {ll}{modeStrings[0]}");
Expand Down
10 changes: 10 additions & 0 deletions SoundEngine/DemoSource/build_demo_cc65.bat
@@ -0,0 +1,10 @@

echo FAMISTUDIO_DEMO_USE_C = 1 > demo_ca65.inc
..\..\..\NES\tools\bin\cc65 -t nes demo_cc65.c -Oisr --add-source
..\..\..\NES\tools\bin\ca65 demo_cc65.s -g -o demo_cc65.o
..\..\..\NES\tools\bin\ca65 demo_ca65.s -g -o demo_ca65.o
..\..\..\NES\tools\bin\ld65 -C demo_ca65.cfg -o demo_cc65.nes demo_ca65.o demo_cc65.o nes.lib --mapfile demo_cc65.map --dbgfile demo_cc65.dbg
copy demo_cc65.nes ..\
del demo_cc65.nes *.o

echo ; This file intentionally empty. When building the C demo, this sets FAMISTUDIO_DEMO_USE_C = 1 > demo_ca65.inc
15 changes: 11 additions & 4 deletions SoundEngine/DemoSource/demo_ca65.cfg
@@ -1,24 +1,25 @@
MEMORY {
HEADER: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $ff;
ZEROPAGE: start = $00, size = $0100, type = rw, file = "";
STACK: start = $0100, size = $100;
OAM: start = $0200, size = $0100, type = rw, file = "";
RAM: start = $0300, size = $0500, type = rw, file = "";
RAM: start = $0300, size = $0400, type = rw, file = "";
STACK: start = $0700, size = $0100, type = rw, file = ""; # C stack location
CODE: start = $8000, size = $2000, type = ro, file = %O, fill = yes, fillval = $ff;
SONG1: start = $a000, size = $2000, type = ro, file = %O, fill = yes, fillval = $ff;
SONG2: start = $c000, size = $1000, type = ro, file = %O, fill = yes, fillval = $ff;
SONG3: start = $d000, size = $1000, type = ro, file = %O, fill = yes, fillval = $ff;
DPCM: start = $e000, size = $1ffa, type = ro, file = %O, fill = yes, fillval = $ff;
DPCM: start = $e000, size = $1FFA, type = ro, file = %O, fill = yes, fillval = $ff;
VECTORS: start = $fffa, size = $6, file = %O, fill = yes;
CHARS: start = $0000, size = $2000, type = ro, file = %O, fill = yes, fillval = $ff;
}

SEGMENTS {

HEADER: load = HEADER, type = ro;
ZEROPAGE: load = ZEROPAGE, type = zp;
OAM: load = OAM, type = bss, align = $100;
RAM: load = RAM, type = bss;
BSS: load = RAM, type = bss;
DATA: load = CODE, run = RAM, type = rw, define = yes;
CODE: load = CODE, type = ro;
SONG1: load = SONG1, type = ro;
SONG2: load = SONG2, type = ro;
Expand All @@ -27,3 +28,9 @@ SEGMENTS {
VECTORS: load = VECTORS, type = ro, start = $FFFA;
CHARS: load = CHARS, type = ro;
}

SYMBOLS {
# Define a few linker symbols for the C stack location
__STACKSIZE__: type = weak, value = $00FF; # 1 page stack
__STACK_START__: type = weak, value = $0700;
}
1 change: 1 addition & 0 deletions SoundEngine/DemoSource/demo_ca65.inc
@@ -0,0 +1 @@
; This file intentionally empty. When building the C demo, this sets FAMISTUDIO_DEMO_USE_C = 1
73 changes: 66 additions & 7 deletions SoundEngine/DemoSource/demo_ca65.s
@@ -1,3 +1,32 @@

; include the file below to set if we are building the C version of the demo
; the C version of the demo simply replaces small portions of the code with a c version

.include "demo_ca65.inc"

.ifndef FAMISTUDIO_DEMO_USE_C
FAMISTUDIO_DEMO_USE_C = 0
.endif
.if FAMISTUDIO_DEMO_USE_C
; functions that the C library expects
.export __STARTUP__:absolute=1
; this is for the C stack and are set in the mapper file
.import __STACK_START__, __STACKSIZE__
.include "zeropage.inc"

; Functions defined in C (using C decl instead of fastcall)
.import _play_song, _update, _init

; Variables and functions used in the C code must be prefixed with underscore
; so re-export the necessary ones here
.exportzp _gamepad_pressed=gamepad_pressed, _p0=p0
.exportzp sp
.export _song_title_silver_surfer=song_title_silver_surfer
.export _song_title_jts=song_title_jts
.export _song_title_shatterhand=song_title_shatterhand
.export _update_title=update_title
.endif

.segment "HEADER"
INES_MAPPER = 0 ; 0 = NROM
INES_MIRROR = 1 ; 0 = horizontal mirroring, 1 = vertical mirroring
Expand Down Expand Up @@ -34,6 +63,10 @@ r4: .res 1

; General purpose pointers.
p0: .res 2
.if FAMISTUDIO_DEMO_USE_C
; Pointer to the C stack head
sp = $80
.endif

.segment "RAM"
; TODO: These 2 arent actually used at the same time... unify.
Expand All @@ -60,6 +93,10 @@ FAMISTUDIO_USE_ARPEGGIO = 1
FAMISTUDIO_CFG_SMOOTH_VIBRATO = 1
FAMISTUDIO_DPCM_OFF = $e000

.if FAMISTUDIO_DEMO_USE_C
FAMISTUDIO_CFG_C_BINDINGS = 1
.endif

; CA65-specifc config.
.define FAMISTUDIO_CA65_ZP_SEGMENT ZEROPAGE
.define FAMISTUDIO_CA65_RAM_SEGMENT RAM
Expand Down Expand Up @@ -89,6 +126,7 @@ song_title_shatterhand:

NUM_SONGS = 3

_exit:
reset:

sei ; mask interrupts
Expand Down Expand Up @@ -131,6 +169,14 @@ reset:
inx
inx
bne @clear_oam_loop
.if FAMISTUDIO_DEMO_USE_C
; Initialize the C stack
lda #<(__STACK_START__+__STACKSIZE__)
sta sp
lda #>(__STACK_START__+__STACKSIZE__)
sta sp + 1
lda #$40
.endif
; wait for second vblank
@wait_vblank_loop2:
bit $2002
Expand Down Expand Up @@ -354,7 +400,16 @@ gamepad_poll_dpcm_safe:
rts

play_song:

.if FAMISTUDIO_DEMO_USE_C
jsr _play_song
rts
update_title:
ldx #2
ldy #15
jsr draw_text
jsr ppu_update
rts
.else
@text_ptr = p0

lda song_index
Expand Down Expand Up @@ -405,8 +460,8 @@ play_song:
ldy #15
jsr draw_text
jsr ppu_update

rts
.endif

equalizer_lookup:
.byte $f0, $f0, $f0, $f0 ; 0
Expand Down Expand Up @@ -499,15 +554,21 @@ main:

jsr ppu_update

.if FAMISTUDIO_DEMO_USE_C
jsr _init
.else
; Load SFX
ldx #<sounds
ldy #>sounds
jsr famistudio_sfx_init
.endif

@loop:

jsr gamepad_poll_dpcm_safe


.if FAMISTUDIO_DEMO_USE_C
jsr _update
.else
@check_right:
lda gamepad_pressed
and #PAD_R
Expand Down Expand Up @@ -585,11 +646,9 @@ main:
ldx #FAMISTUDIO_SFX_CH1
jsr famistudio_sfx_play
beq @draw

@draw:

jsr famistudio_update ; TODO: Call in NMI.

.endif
lda #0
jsr update_equalizer
lda #1
Expand Down

0 comments on commit cc87f97

Please sign in to comment.