Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
folium-app
GitHub Repository: folium-app/Folium
Path: blob/a-new-beginning/Cherry/CherryEmulator.mm
2 views
//
//  CherryEmulator.mm
//  Cherry
//
//  Created by Jarrod Norwell on 20/1/2026.
//

#import "CherryEmulator.h"

#include <gearcoleco.h>

#include <atomic>
#include <condition_variable>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>

#include <AudioUnit/AudioUnit.h>
#include <AudioToolbox/AudioToolbox.h>
#include <CoreAudio/CoreAudioTypes.h>
#include <dispatch/dispatch.h>

class SoundQueue
{
public:
    SoundQueue();
    ~SoundQueue();

    bool Start(int sample_rate, int channel_count, int buffer_size, int buffer_count);
    void Stop();

    void Write(int16_t* samples, int count, bool sync);
    int GetSampleCount();
    bool IsOpen();
    int16_t* GetCurrentlyPlaying();

private:
    static OSStatus RenderCallback(void* inRefCon,
                                   AudioUnitRenderActionFlags* ioActionFlags,
                                   const AudioTimeStamp* inTimeStamp,
                                   UInt32 inBusNumber,
                                   UInt32 inNumberFrames,
                                   AudioBufferList* ioData);

    void FillBuffer(uint8_t* buffer, int byteCount);

    int16_t* Buffer(int index) {
        return m_buffers + (index * m_buffer_size);
    }

private:
    AudioUnit m_audioUnit = nullptr;

    int16_t* m_buffers = nullptr;
    int16_t* m_currently_playing = nullptr;

    dispatch_semaphore_t m_free_sem = nullptr;

    int m_buffer_size = 0;
    int m_buffer_count = 0;

    int m_write_buffer = 0;
    int m_write_position = 0;
    int m_read_buffer = 0;

    bool m_sound_open = false;
    bool m_sync_output = true;
};

SoundQueue::SoundQueue()
{
    m_buffers = nullptr;
    m_free_sem = nullptr;
    m_sound_open = false;
}

SoundQueue::~SoundQueue() {}

bool SoundQueue::Start(int sample_rate,
                       int channel_count,
                       int buffer_size,
                       int buffer_count)
{
    m_buffer_size  = buffer_size;
    m_buffer_count = buffer_count;

    m_buffers = new int16_t[m_buffer_size * m_buffer_count]();
    m_currently_playing = m_buffers;

    m_free_sem = dispatch_semaphore_create(m_buffer_count - 1);

    // Describe audio unit
    AudioComponentDescription desc = {};
    desc.componentType         = kAudioUnitType_Output;
#if TARGET_OS_IPHONE
    desc.componentSubType      = kAudioUnitSubType_RemoteIO;
#else
    desc.componentSubType      = kAudioUnitSubType_DefaultOutput;
#endif
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;

    AudioComponent comp = AudioComponentFindNext(nullptr, &desc);
    if (!comp) return false;

    if (AudioComponentInstanceNew(comp, &m_audioUnit) != noErr)
        return false;

    // Set format
    AudioStreamBasicDescription format = {};
    format.mSampleRate       = sample_rate;
    format.mFormatID         = kAudioFormatLinearPCM;
    format.mFormatFlags      = kAudioFormatFlagIsSignedInteger |
                               kAudioFormatFlagIsPacked;
    format.mBitsPerChannel   = 16;
    format.mChannelsPerFrame = channel_count;
    format.mFramesPerPacket  = 1;
    format.mBytesPerFrame    = 2 * channel_count;
    format.mBytesPerPacket   = format.mBytesPerFrame;

    AudioUnitSetProperty(m_audioUnit,
                         kAudioUnitProperty_StreamFormat,
                         kAudioUnitScope_Input,
                         0,
                         &format,
                         sizeof(format));

    // Set callback
    AURenderCallbackStruct callback;
    callback.inputProc = RenderCallback;
    callback.inputProcRefCon = this;

    AudioUnitSetProperty(m_audioUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Input,
                         0,
                         &callback,
                         sizeof(callback));

    if (AudioUnitInitialize(m_audioUnit) != noErr)
        return false;

    if (AudioOutputUnitStart(m_audioUnit) != noErr)
        return false;

    m_sound_open = true;
    return true;
}

void SoundQueue::Stop()
{
    if (m_sound_open)
    {
        AudioOutputUnitStop(m_audioUnit);
        AudioUnitUninitialize(m_audioUnit);
        AudioComponentInstanceDispose(m_audioUnit);
        m_audioUnit = nullptr;
        m_sound_open = false;
    }

    if (m_free_sem)
    {
        m_free_sem = nullptr;
    }

    delete[] m_buffers;
    m_buffers = nullptr;
}

OSStatus SoundQueue::RenderCallback(void* inRefCon,
                                    AudioUnitRenderActionFlags*,
                                    const AudioTimeStamp*,
                                    UInt32,
                                    UInt32 inNumberFrames,
                                    AudioBufferList* ioData)
{
    SoundQueue* self = static_cast<SoundQueue*>(inRefCon);

    int byteCount = inNumberFrames *
                    ioData->mBuffers[0].mNumberChannels *
                    sizeof(int16_t);

    self->FillBuffer((uint8_t*)ioData->mBuffers[0].mData,
                     byteCount);

    return noErr;
}

void SoundQueue::FillBuffer(uint8_t* buffer, int count)
{
    bool has_data;

    if (m_sync_output)
        has_data = (dispatch_semaphore_wait(m_free_sem, DISPATCH_TIME_NOW) != 0);
    else
        has_data = (m_read_buffer != m_write_buffer);

    if (has_data)
    {
        m_currently_playing = Buffer(m_read_buffer);

        memcpy(buffer,
               Buffer(m_read_buffer),
               count);

        m_read_buffer =
            (m_read_buffer + 1) % m_buffer_count;

        if (m_sync_output)
            dispatch_semaphore_signal(m_free_sem);
    }
    else
    {
        memset(buffer, 0, count);
    }
}

void SoundQueue::Write(int16_t* samples,
                       int count,
                       bool sync)
{
    if (!m_sound_open)
        return;

    m_sync_output = sync;

    while (count)
    {
        int n = m_buffer_size - m_write_position;
        if (n > count) n = count;

        memcpy(Buffer(m_write_buffer) + m_write_position,
               samples,
               n * sizeof(int16_t));

        samples += n;
        m_write_position += n;
        count -= n;

        if (m_write_position >= m_buffer_size)
        {
            m_write_position = 0;

            if (m_sync_output)
            {
                m_write_buffer =
                    (m_write_buffer + 1) % m_buffer_count;

                dispatch_semaphore_wait(m_free_sem,
                                        DISPATCH_TIME_FOREVER);
            }
            else
            {
                int next =
                    (m_write_buffer + 1) % m_buffer_count;

                if (next != m_read_buffer)
                    m_write_buffer = next;
            }
        }
    }
}


SoundQueue* soundQueue;

struct Object {
    std::jthread thread;
    uint8_t* fb;
    int16_t* ab;
} object;

GearcolecoCore* core;
std::atomic<bool> paused;
std::mutex mutex;
std::condition_variable_any cv;

@implementation CherryEmulator
+(CherryEmulator *) sharedInstance {
    static CherryEmulator *sharedInstance = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

-(BOOL) insertCartridge:(NSURL *)url {
    soundQueue = new SoundQueue();
    soundQueue->Start(GC_AUDIO_SAMPLE_RATE, 2, 4096, 2);
    
    core = new GearcolecoCore;
    core->Init();
    core->GetMemory()->LoadBios([[[[[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject] URLByAppendingPathComponent:@"Cherry"] URLByAppendingPathComponent:@"sysdata"] URLByAppendingPathComponent:@"bios.col"].path UTF8String]);
    
    object.fb = new uint8_t[GC_RESOLUTION_WIDTH * GC_RESOLUTION_HEIGHT * 3];
    object.ab = new int16_t[GC_AUDIO_BUFFER_SIZE];
    
    Cartridge::ForceConfiguration config;

    config.region = Cartridge::CartridgeUnknownRegion;
    config.type = Cartridge::CartridgeNotSupported;
    
    core->SaveRam();
    core->LoadROM([url.path UTF8String], &config);
    core->LoadRam();
    
    return TRUE;
}

-(void) start {
    object.thread = std::jthread([&](std::stop_token token) {
        using namespace std::chrono;
        
        const auto frameDuration = duration<double>(1.0 / (core->GetCartridge()->IsPAL() ? 50.0 : 60.0));

        while (!token.stop_requested()) {
            {
                std::unique_lock lock(mutex);
                cv.wait(lock, token, []() {
                    return !paused.load();
                });
                
                if (token.stop_requested())
                    break;
            }
            
            auto frameStart = steady_clock::now();
            
            int samples = 0;
            core->RunToVBlank(object.fb, object.ab, &samples);

            if (auto buffer = [[CherryEmulator sharedInstance] fb])
                dispatch_async(dispatch_get_main_queue(), ^{
                    buffer(object.fb);
                });
            
            if ((samples > 0) && !core->IsPaused())
                soundQueue->Write(object.ab, samples, true);

            // Limit FPS
            auto frameEnd = steady_clock::now();
            auto elapsed = frameEnd - frameStart;
            if (elapsed < frameDuration)
                std::this_thread::sleep_for(frameDuration - elapsed);
        }
    });
}

-(void) stop {
    object.thread.request_stop();
    if (object.thread.joinable())
        object.thread.join();
    
    SafeDelete(core);
    SafeDelete(soundQueue);
    
    delete [] object.fb;
    delete [] object.ab;
    
    paused.store(false);
}

-(BOOL) isPaused {
    return paused.load();
}

-(void) pause:(BOOL)pause {
    if (pause)
        paused.store(true);
    else {
        paused.store(false);
        cv.notify_all();
    }
}

-(void) button:(int)button player:(int)player pressed:(BOOL)pressed {
    if (pressed)
        core->KeyPressed(player == 0 ? Controller_1 : Controller_2, (GC_Keys)button);
    else
        core->KeyReleased(player == 0 ? Controller_1 : Controller_2, (GC_Keys)button);
            
}
@end