Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question on the FFI wrapper #1

Open
shazz opened this issue Feb 19, 2023 · 10 comments
Open

Question on the FFI wrapper #1

shazz opened this issue Feb 19, 2023 · 10 comments

Comments

@shazz
Copy link

shazz commented Feb 19, 2023

Hi Hiromasa,

I'm trying to use your FFI wrapper to simply replay a YM registers dump file (captured every 50Hz) but the result is not good at all. I guess I don't use your wrapper correctly, can you tell me what is wrong ? I guess this is due to the frequency I call ymfm_generate but I don't undertstand why.

Here is the principle (in Zig but not really different from Rust):

(if not clear in the comments)

  1. I call ymfm_add_chip to set up a YM2149 at 2000000 Hz and get the sampling rate: 31250
  2. I loop over the blocks of 14 YM2149 registers contained in my YM dump file
    • I write the 14 registers using ymfm_write
    • I generate 31250/50 ticks with ymfm_generate
      • I write the first i32 of the buffer (mono channel) in a file for each tick
  3. Done!
pub fn main() !void {

    // YM2149 clock: 2MHz
    const YM2149_clock: u32 = 2e6;
    const fYM2149_clock: f32 = @intToFloat(f32, YM2149_clock);

    // Get YM2149 enum value, basically 0
    const ym2149: u16 = @enumToInt(ChipType.CHIP_YM2149);

    // create wav file
    const file = try std.fs.cwd().createFile(
        "sound.wav",
        .{ .read = true },
    );
    defer file.close();

    // Get a buffered writer to write in this file
    var buf_writer = std.io.bufferedWriter(file.writer());
    const writer = buf_writer.writer();    

    // add a YM2149, YM2149_clock == 
    var sampling_rate: u32 = ymfm.ymfm_add_chip(ym2149, YM2149_clock);

    std.debug.print("Sampling Rate: {} for master clock: {}\n", .{ sampling_rate, YM2149_clock });
    std.debug.print("Dump length: {} frames = {} seconds\n", .{ dump_b.len / 14, dump_b.len / 14 / 50 });
    std.debug.print("Tick per frame: {}\n", .{ sampling_rate / 50 });

    // counters for the YM dump file
    var counter: u64 = 0;
    var dump_loop: u32 = 0;

    // the YM dump file is composed of 50Hz frames capturing the first 14 YM2149 registers
    // loop on those frames
    while(dump_loop < dump_b.len / 14) : (dump_loop += 1) {

        // write the 14 registers
        var i: u32 = 0;
        while( i < 14) : ( i += 1) {
            ymfm.ymfm_write(ym2149, 0, i, dump_b[counter]);  
            counter += 1;
        }     

        // generate some sound for 50 Hz, so tick the YM (sampling_rate / 50) times
        // write every sample output to the file
        var tick: u32 = 0;
        var buffer: [2]i32 = undefined;
        while (tick < sampling_rate / 50) : ( tick += 1) {
            ymfm.ymfm_generate(ym2149, 0, &buffer);

            // store only left channel
            const slice = buffer[0];            
            try writer.writeAll(std.mem.asBytes(&slice));       
        }
                
        // std.debug.print("Frame {} done\n", .{ dump_loop } );
        
    }
    try buf_writer.flush();
}
Sampling Rate: 31250 for master clock: 2000000
Dump length: 8833 frames = 176 seconds
Tick per frame: 625

Any idea what I did wrong ?

@h1romas4
Copy link
Owner

h1romas4 commented Feb 21, 2023

Hello. Thank you for your report.

This wrapper to ymfm allows write commands to be queued in order to accommodate high sampling YM registers dump files (e.g., in VGM format).

// handle a register write: just queue for now
virtual void write(uint32_t reg, uint8_t data) override
{
m_queue.push_back(std::make_pair(reg, data));
}
// generate one output sample of output
virtual void generate(int32_t *buffer) override
{
uint32_t addr1 = 0xffff, addr2 = 0xffff;
uint8_t data1 = 0, data2 = 0;
// see if there is data to be written; if so, extract it and dequeue
if (!m_queue.empty())
{
auto front = m_queue.front();
addr1 = 0 + 2 * ((front.first >> 8) & 3);
data1 = front.first & 0xff;
addr2 = addr1 + ((m_type == CHIP_YM2149) ? 2 : 1);
data2 = front.second;
m_queue.erase(m_queue.begin());
}

The queued commands are executed in units of the sound chip's sampling rate in the generate function.

Therefore, if you want to set a register in one cycle (one generate), you may not get the expected wave.

Removing the m_queue processing from ymfmffi.cpp may give the expected result. 😀

@h1romas4
Copy link
Owner

h1romas4 commented Feb 21, 2023

In the next part, registers 0 through 13 are set in order, one tick at a time.

        while( i < 14) : ( i += 1) {
            ymfm.ymfm_write(ym2149, 0, i, dump_b[counter]);  
            counter += 1;
        }  

I thought that the order in which the registers are set might cause the sound chip to not sound properly.

It may be a good practice to write only the changes between sampled registers as a sound log.

@shazz
Copy link
Author

shazz commented Feb 22, 2023

Hello,

So I removed the m_queue:

    // handle a register write: just queue for now
    virtual void write(uint32_t reg, uint8_t data) override
    {
        // m_queue.push_back(std::make_pair(reg, data));
        m_chip.write(reg, data);
    }

And I commented the m_queue / m_chip.write part in generate()
But the result is really not good.

Did I do something wrong?

For the registers order, I am using YM dump files (http://leonard.oxg.free.fr/ymformat.html) so this is hard to know in which order they have to be written. I tried using another library (emu2149 and the same loop (same way to write the 14 registers) and it works.

My test code and YM dumps are here: https://github.com/shazz/ZigYmFm/blob/main/src/main.zig (without those modifications)

@h1romas4
Copy link
Owner

Thank you very much. I understand about the dump file format.

If the queue is not used, It may be necessary to include the following address translation process in the generate function in the write method.

// see if there is data to be written; if so, extract it and dequeue
if (!m_queue.empty())
{
auto front = m_queue.front();
addr1 = 0 + 2 * ((front.first >> 8) & 3);
data1 = front.first & 0xff;
addr2 = addr1 + ((m_type == CHIP_YM2149) ? 2 : 1);
data2 = front.second;
m_queue.erase(m_queue.begin());
}
// write to the chip
if (addr1 != 0xffff)
{
// if (LOG_WRITES)
// printf("%10.5f: %s %03X=%02X\n", double(m_clocks) / double(m_chip.sample_rate(m_clock)), m_name.c_str(), data1, data2);
m_chip.write(addr1, data1);
m_chip.write(addr2, data2);
}

@shazz
Copy link
Author

shazz commented Feb 23, 2023

Hi,

This is also something I tried (even if I don't really understand this address translation for the YM2149):

full source: ymfmffi.cpp

    virtual void write(uint32_t reg, uint8_t data) override
    {
        // m_queue.push_back(std::make_pair(reg, data));
     
        uint32_t addr1 = 0xffff, addr2 = 0xffff;
        uint8_t data1 = 0, data2 = 0;

        addr1 = 0 + 2 * ((reg >> 8) & 3);
        data1 = reg & 0xff;

        addr2 = addr1 + ((m_type == CHIP_YM2149) ? 2 : 1);
        data2 = data;

        // write to the chip
        if (addr1 != 0xffff)
        {
            m_chip.write(addr1, data1);
            m_chip.write(addr2, data2);
        }
    }

The result is even worse, just noise.

This is really weird.... could it be an issue with the YM2149 implementation?

@h1romas4
Copy link
Owner

h1romas4 commented Feb 23, 2023

Hello.

Sorry for the precious time.

Since ymfm is a clock architecture, writing registers at the same time (tick) at the same time may not work well. ymfm operates by emulating the state of each clock.

To avoid this, ymfmffi.cpp includes a queue mechanism for write and geneate. (The process here is different from the actual YM clock, as the transmission is in units of the sound chip's native sampling rate.)

How about reverting the queue mechanism and keeping the last written register value in an array, and writing only the register that has changed?

        while( i < 14) : ( i += 1) {
            ymfm.ymfm_write(ym2149, 0, i, dump_b[counter]);  
            counter += 1;
        }  

Similar to the VGM format, it is simulated as a sound log sent from the host to the YM by tick. You may also need to pay attention to the order in which the registers are written.

@shazz
Copy link
Author

shazz commented Feb 25, 2023

Hello,
Don't apologize! That's really kind of you to try to help me!

So as you proposed I reverted the queue and now I'm writing the registers only if they have changed from the last frame but the result is not better.
Here are the results

The new loop is:

    // counters for the YM dump file
    var counter: u64 = 0;
    var dump_loop: u32 = 0;
    var audio_buffer: [31250 / 50]i32 = undefined;
    var last_frame: [14]u8 = std.mem.zeroes([14]u8);

    // the YM dump file is composed of 50Hz frames capturing the first 14 YM2149 registers
    // loop on those frames
    while(dump_loop < dump_ym2149_b.len / 14) : (dump_loop += 1) {

        // write the 14 registers
        var i: u32 = 0;
        while( i < 14) : ( i += 1) {
            const reg_val:u8 = dump_ym2149_b[counter];
            if(reg_val != last_frame[i])
                ymfm.ymfm_write(ym2149, 0, i, reg_val);  

            counter += 1;
            last_frame[i] = reg_val;
        }     

        // generate some sound for 50 Hz, so tick the YM (sampling_rate / 50) times
        // write every sample output to the file
        var tick: u32 = 0;
        var buffer: [2]i32 = undefined;
        while (tick < sampling_rate / 50) : ( tick += 1) {
            ymfm.ymfm_generate(ym2149, 0, &buffer);

            // store only left channel
            audio_buffer[tick] = buffer[0];
        }
        try writer.writeAll(std.mem.asBytes(&audio_buffer));       
        
        if(dump_loop % 1000 == 0) {
            std.debug.print("{} frames done\n", .{ dump_loop } );
        }

    }
    try buf_writer.flush();

https://github.com/shazz/ZigYmFm/blob/main/src/ymfm2149_player.zig#L91

Is it what you have in mind? Would you have time to write a very simple cpp example loading the same ym dump and check to be sure it is not some dumb zig issues?

Thanks for your help!

@shazz
Copy link
Author

shazz commented Feb 25, 2023

Oh, something I just noticed in the ffi:

        else if (m_type == CHIP_YM2149)
        {
            int32_t out0 = m_output.data[0];
            int32_t out1 = m_output.data[1 % ChipType::OUTPUTS];
            int32_t out2 = m_output.data[2 % ChipType::OUTPUTS];
            *buffer++ += (out0 + out1 + out2) / 2;
            *buffer++ += (out0 + out1 + out2) / 2;

        }

Why do you add the new sample to the previous one ( += ) ?
I removed the + and now it looks a little better, at least I can start to recognize the music :)
https://github.com/shazz/ZigYmFm/blob/main/wav/libymfm2149.wav

But the fact you are adding the sample to the previous one made me think I did not understand something, can you explain? Is it to mix outputs of different chips ?

in addition, why at each clock cycle are there 2 writes to the chip?

        // write to the chip
        if (addr1 != 0xffff)
        {
            // if (LOG_WRITES)
            //     printf("%10.5f: %s %03X=%02X\n", double(m_clocks) / double(m_chip.sample_rate(m_clock)), m_name.c_str(), data1, data2);
            m_chip.write(addr1, data1);
            m_chip.write(addr2, data2);
        }

and also, this specific translation for ym2149, is it due to the chip itself or the VGM data format? in my case I simply have reg address and reg value.

            auto front = m_queue.front();
            addr1 = 0 + 2 * ((front.first >> 8) & 3);
            data1 = front.first & 0xff;
            addr2 = addr1 + ((m_type == CHIP_YM2149) ? 2 : 1);
            data2 = front.second;
            m_queue.erase(m_queue.begin());

@shazz
Copy link
Author

shazz commented Feb 25, 2023

Ah ok I understood the address translation and the 2 writes, this is not a direct write to the YM2149 register, this is set address then set data. Ok.

void ym2149::write(uint32_t offset, uint8_t data)
{
	switch (offset & 3)	// BC2,BC1
	{
		case 0: // address
			write_address(data);
			break;

		case 1: // inactive
			break;

		case 2: // write
			write_data(data);
			break;

		case 3: // address
			write_address(data);
			break;
	}
}

So I understood this part but still n ot why the sound is corrupted...

@h1romas4
Copy link
Owner

Hello.

Why do you add the new sample to the previous one ( += ) ?
I removed the + and now it looks a little better, at least I can start to recognize the music :)

oh.. You are correct in pointing this out. The process was for mixing with other chips. I missed it. Sorry about that...

So I understood this part but still n ot why the sound is corrupted...

I too could not find the problem in the source code..
I will build the program over the weekend and give it a try. (please wait a bit)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants