//
// 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