mmontag / chip-player-js

Web-based music player for a variety of video game and chiptune music formats.
https://chiptune.app
GNU General Public License v3.0
345 stars 19 forks source link

How to Use the Game-Music-Emu Library to Play NSF Files in Mono? #130

Closed Thysbelon closed 1 year ago

Thysbelon commented 1 year ago

I admire your project. I wanted to use your code to embed a small "play NSF chiptune" button on my blog. I have successfully compiled your fork of game_music_emu with emscripten, emcmake cmake ../ -DUSE_GME_AY=0 -DUSE_GME_GBS=0 -DUSE_GME_GYM=0 -DUSE_GME_HES=0 -DUSE_GME_KSS=0 -DUSE_GME_SAP=0 -DUSE_GME_SPC=0 -DUSE_GME_VGM=0 -DENABLE_UBSAN=OFF -DBUILD_SHARED_LIBS=OFF (After editing your CMakeList to include ym2413, and editing gme.cpp to comment out all emu types except NSF and NSFe) and have written a few c and javascript helper functions. I want to be able to pass true or false into my javascript helper function to render the song in stereo or mono. I have no issue playing back NSF music in stereo, but I cannot get it to switch to mono. The gme_set_effects function described in gme.h looks like it should play the song in mono if I pass NULL into it, but it has no effect.

Here is my c code:

#include <stdlib.h>
#include <stdio.h>

#include <gme.h>
#include <emscripten/webaudio.h>

#include <math.h> //for sin

//uint8_t audioThreadStack[4096];
gme_t* emu;
void* nsfdata;
long nsfdataSize=0;
int track;
EM_BOOL stereopref;
int limitlogs=0;
int buf_size;

short* buf;

void handle_error( const char* str );

static void play_siren( long count, short* out )
    {
        static double a, a2;
        while ( count-- )
            *out++ = 0x2000 * sin( a += .1 + .05*sin( a2+=.00005 ) );
    }

EMSCRIPTEN_KEEPALIVE
int setupNSF(void* inputdata, long inputdataSize, int inputBufSize, int tracknum, EM_BOOL stereoInput)
{
    printf("setupNSF\n");

    nsfdata = inputdata;

    nsfdataSize = inputdataSize;

    track = tracknum;

    stereopref = stereoInput;

    buf_size = inputBufSize;

    int sample_rate = 44100;

    /* Open music file in new emulator */
    handle_error( gme_open_data( nsfdata, nsfdataSize, &emu, sample_rate ) );
    if (stereopref == EM_TRUE) {
        printf("STEREOPREF IS TRUE\n");
        struct gme_effects_t effects;
        effects.enabled = 1;
        effects.stereo = 0.5;
        effects.echo = 0.0;
        gme_set_effects( emu, &effects );
    } else {
        printf("STEREOPREF IS FALSE\n");
        gme_set_effects( emu, NULL ); // this does not work
        //struct gme_effects_t effects;
        //effects.enabled = 0;
        //effects.stereo = 0.0;
        //gme_set_effects( emu, &effects ); //these also do not work
    }

    // I have tried putting open_data here, it made no difference.

    printf("nsfdataSize: %li \n", nsfdataSize);

    /* Start track */
    handle_error( gme_start_track( emu, track ) );

    buf = malloc(sizeof(*buf) * buf_size); // https://stackoverflow.com/questions/4240331/c-initializing-a-global-array-in-a-function
    printf("array buf entry number 3: %d\n", buf[3]);

    printf("setupNSF done\n");
    return 0;
}

EMSCRIPTEN_KEEPALIVE
short* playNSF() { // run in script processor node's on audio process event
    handle_error( gme_play( emu, buf_size*2, buf ) );
    //play_siren(buf_size, buf);

    // the c code cannot return an array to javascript, use runtime method getValue to access the buf array.
    return buf;
}

void handle_error( const char* str )
{
    if ( str )
    {
        printf( "Error: %s\n", str ); //getchar();
        //exit( EXIT_FAILURE );
    }
}

Here is my Javascript:

document.getElementById("nsfplaybut").addEventListener("click",myclick,{once:true})

function myclick(){
    nsfplay("Akumajou Densetsu (VRC6).nsfe",0,false);
}

const INT16_MAX = 65535;
async function nsfplay(url, tracknum, stereopref) {
    var response = await fetch(url)

    const audioCtx=new AudioContext({latencyHint:"playback",sampleRate:44100});

    const bufferSize = Math.max( // Make sure script node bufferSize is at least baseLatency
        Math.pow(2, Math.ceil(Math.log2((audioCtx.baseLatency || 0.001) * audioCtx.sampleRate))), 2048);
    console.log(bufferSize);

    var audioNode=audioCtx.createScriptProcessor(bufferSize,2,2)
    audioNode.connect(audioCtx.destination)

    response = await response.arrayBuffer()
    var data=new Uint8Array(response)
    Module.ccall(
        "setupNSF",
        "number",
        ["array", "number", "number", "number", "boolean"],
        [data, data.length, bufferSize, tracknum, stereopref]
    )

    var playNSF=Module.cwrap("playNSF", "number", null)
    audioNode.onaudioprocess=function(e){
        let bufPtr=playNSF()
        let outputData=[];
        for(let channel=0; channel<e.outputBuffer.numberOfChannels; channel++){
            outputData[channel]=e.outputBuffer.getChannelData(channel); /*nested loop*/
            for(let i=0; i<bufferSize; i++){
                outputData[channel][i] = Module.getValue(bufPtr+i  * 2  * 2 + /* frame offset * bytes per sample * num channels + */ channel  * 2  /* channel offset * bytes per sample */, 'i16') / INT16_MAX /* convert int16 to float */
            }
        } 
    }
}

Thank you.

mmontag commented 1 year ago

Good question; before digging into your code, I'll just mention that I did a quick and dirty modification to game-music-emu to output the NES sound in stereo: https://github.com/mmontag/chip-player-js/commit/839b9c27aa994b21e987a11c0ab5c6f9db5a5a67

Simply put: the two 2A03 square channels are hard panned Left and Right.

That should be easy to undo, but if you want to make the stereo/mono switchable at runtime, you would have to do some further modifications.

You could always do a mixdown in Javascript to keep things simple. (Add both NSF channels to both output channels. Or create a mono Audio Node?)

Thysbelon commented 1 year ago

Thank you, that helps a lot!