Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
442 lines
14 KiB
C
442 lines
14 KiB
C
// ____________________________
|
|
// ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█ │ ▄▄▄ ▄▄
|
|
// ██ ▀ █▄ ▀██▄ ▀ ▄█ ▄▀▀ █ │ ▀█▄ ▄▀██ ▄█▄█ ██▀▄ ██ ▄███
|
|
// █ █ █ ▀▀ ▄█ █ █ ▀▄█ █▄ │ ▄▄█▀ ▀▄██ ██ █ ██▀ ▀█▄ ▀█▄▄
|
|
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘ ▀▀
|
|
// Arkos Tracker II sample
|
|
//─────────────────────────────────────────────────────────────────────────────
|
|
// This sample program showcases the use of the 3 available Arkos Tracker 2 (AK2)'s replayers.
|
|
//
|
|
// In the context of a game, you will probably only need one of them.
|
|
// Check the AK2 documentation to choose the replayer that best matches your needs.
|
|
// https://www.julien-nevo.com/arkostracker/index.php/the-players/
|
|
//
|
|
// Note that AK2 music data has to be prepared to be replayed at a given address.
|
|
// This address has to be defined when exporting data from AK2 and data must be
|
|
// visible to the CPU at this address when calling AT2 functions in your code.
|
|
//─────────────────────────────────────────────────────────────────────────────
|
|
|
|
//=============================================================================
|
|
// DEFINES
|
|
//=============================================================================
|
|
|
|
// Includes
|
|
#include "msxgl.h"
|
|
#include "psg.h"
|
|
#include "device/darky.h"
|
|
#include "arkos/akg_player.h"
|
|
#include "arkos/akm_player.h"
|
|
#include "arkos/aky_player.h"
|
|
#include "arkos/aky_6ch_player.h"
|
|
#include "arkos/aky_darky_player.h"
|
|
|
|
// Library's logo
|
|
#define MSX_GL "\x01\x02\x03\x04\x05\x06"
|
|
|
|
#define REPLAYER_NUM numberof(g_PlayerEntry)
|
|
#define MUSIC_ADDR 0xA000
|
|
#define MUSIC_NUM 4
|
|
#define SFX_ADDR 0xE000
|
|
|
|
// Music entry structure
|
|
struct MusicEntry
|
|
{
|
|
const c8* Name; // Music display name
|
|
const u8* Data; // Address of the data in the segment
|
|
u8 Segment; // Segment number where the data is located
|
|
};
|
|
|
|
// Init function callback
|
|
typedef void (*cbPlay)(u8, const void*);
|
|
// Play function callback
|
|
typedef bool (*cbUpdate)();
|
|
// Init SFX function callback
|
|
typedef u8 (*cbPlaySFX)(const void*);
|
|
// Play SFX function callback
|
|
typedef void (*cbUpdateSFX)(u8, u8, u8);
|
|
// Stop SFX function callback
|
|
typedef void (*cbStopSFX)(u8);
|
|
|
|
// Player entry structure
|
|
struct PlayerEntry
|
|
{
|
|
const c8* Name; // Player display name
|
|
cbPlay Init; // Pointer to player's music initialization and playback function
|
|
cbUpdate Update; // Pointer to player's update function
|
|
callback Stop; // Pointer to player's music stop function
|
|
cbPlaySFX InitSFX; // Pointer to player's SFX initialization function
|
|
cbUpdateSFX PlaySFX; // Pointer to player's SFX playback function
|
|
cbStopSFX StopSFX; // Pointer to player's SFX stop function
|
|
const struct MusicEntry* Musics; // List of musics for this player
|
|
};
|
|
|
|
// Symbols for music data in mapper segments (see segment/s_arkos*.c)
|
|
extern const u8* g_AKG_jinj_med;
|
|
extern const u8* g_AKG_HocusPocus;
|
|
extern const u8* g_AKG_Justaddcream;
|
|
extern const u8* g_AKG_Sarkboteur;
|
|
extern const u8* g_AKM_jinj_med;
|
|
extern const u8* g_AKM_HocusPocus;
|
|
extern const u8* g_AKM_Justaddcream;
|
|
extern const u8* g_AKM_Sarkboteur;
|
|
extern const u8* g_AKY_jinj_med;
|
|
extern const u8* g_AKY_AHarmlessGrenade;
|
|
extern const u8* g_AKY_Justaddcream;
|
|
extern const u8* g_AKY_Sarkboteur;
|
|
extern const u8* g_AKY_6ch_TimelessMini;
|
|
|
|
// Function prototypes
|
|
void AKY3ch(u8 song, const void* data);
|
|
void AKY6ch(u8 song, const void* data);
|
|
void AKYDarky(u8 song, const void* data);
|
|
|
|
|
|
//=============================================================================
|
|
// READ-ONLY DATA
|
|
//=============================================================================
|
|
|
|
// Font data
|
|
#include "font/font_mgl_sample6.h"
|
|
|
|
// SFX data (generated using Arkos Tracker 2's AKX exporter and converted to header file using MSXbin)
|
|
#include "content/arkos/akx_effects.h"
|
|
|
|
// Animation characters (used as sign-of-life)
|
|
const u8 g_ChrAnim[] = { '|', '\\', '-', '/' };
|
|
|
|
// Musics list for AKG replayer
|
|
const struct MusicEntry g_MusicEntryAKG[] =
|
|
{
|
|
{ "Cancion Nueva (by ?) ", g_AKG_jinj_med, 4 },
|
|
{ "Hocus Pocus (by Targhan) ", g_AKG_HocusPocus, 5 },
|
|
{ "Just add cream (by Excellence in Art)", g_AKG_Justaddcream, 6 },
|
|
{ "Sarkboteur (by Rob Hubbard) ", g_AKG_Sarkboteur, 7 },
|
|
};
|
|
|
|
// Musics list for AKM replayer
|
|
const struct MusicEntry g_MusicEntryAKM[] =
|
|
{
|
|
{ "Cancion Nueva (by ?) ", g_AKM_jinj_med, 8 },
|
|
{ "Hocus Pocus (by Targhan) ", g_AKM_HocusPocus, 9 },
|
|
{ "Just add cream (by Excellence in Art)", g_AKM_Justaddcream, 10 },
|
|
{ "Sarkboteur (by Rob Hubbard) ", g_AKM_Sarkboteur, 11 },
|
|
};
|
|
|
|
// Musics list for AKY replayer
|
|
const struct MusicEntry g_MusicEntryAKY[] =
|
|
{
|
|
{ "Cancion Nueva (by ?) ", g_AKY_jinj_med, 12 },
|
|
{ "A Harmless Grenade (by Targhan) ", g_AKY_AHarmlessGrenade, 13 },
|
|
{ "Just add cream (by Excellence in Art)", g_AKY_Justaddcream, 14 },
|
|
{ "Sarkboteur (by Rob Hubbard) ", g_AKY_Sarkboteur, 15 },
|
|
};
|
|
|
|
// Musics list for AKY 6 channels replayer
|
|
const struct MusicEntry g_MusicEntryAKY_6CH[] =
|
|
{
|
|
{ "Timeless (by Doclands / C.Wild) ", g_AKY_6ch_TimelessMini, 16 },
|
|
{ "Timeless (by Doclands / C.Wild) ", g_AKY_6ch_TimelessMini, 16 },
|
|
{ "Timeless (by Doclands / C.Wild) ", g_AKY_6ch_TimelessMini, 16 },
|
|
{ "Timeless (by Doclands / C.Wild) ", g_AKY_6ch_TimelessMini, 16 },
|
|
};
|
|
|
|
// Replayers data
|
|
const struct PlayerEntry g_PlayerEntry[] =
|
|
{
|
|
{ "AKG (generic)", AKG_Play, AKG_Update, AKG_Stop, AKG_InitSFX, AKG_PlaySFX, AKG_StopSFX, g_MusicEntryAKG },
|
|
{ "AKM (minimalist)", AKM_Play, AKM_Update, AKM_Stop, AKM_InitSFX, AKM_PlaySFX, AKM_StopSFX, g_MusicEntryAKM },
|
|
{ "AKY (fast)", AKY3ch, AKY_Update, NULL, NULL, NULL, NULL, g_MusicEntryAKY },
|
|
{ "AKY 6ch", AKY6ch, AKY6ch_Update, NULL, NULL, NULL, NULL, g_MusicEntryAKY_6CH },
|
|
{ "AKY Darky", AKYDarky, AKYDarky_Update, NULL, NULL, NULL, NULL, g_MusicEntryAKY_6CH },
|
|
};
|
|
|
|
//=============================================================================
|
|
// MEMORY DATA
|
|
//=============================================================================
|
|
|
|
// Pointer to current player data
|
|
const struct PlayerEntry* g_CurrentPlayer = NULL;
|
|
|
|
// Current player index
|
|
u8 g_PlayerIdx = 0;
|
|
|
|
// Current music index
|
|
u8 g_MusicIdx = 0;
|
|
|
|
// Current SFX index
|
|
u8 g_SFXIdx = 0;
|
|
|
|
// Number of SFX in the bank
|
|
u8 g_SFXNum = 0;
|
|
|
|
// Is current frequency 50 Hz?
|
|
bool g_Freq50Hz = FALSE;
|
|
|
|
// V-blank synchronization flag
|
|
bool g_VBlank = FALSE;
|
|
|
|
//=============================================================================
|
|
// HELPER FUNCTIONS
|
|
//=============================================================================
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// H_TIMI interrupt hook (called at 50/60 Hz)
|
|
void VBlankHook()
|
|
{
|
|
g_VBlank = TRUE;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Wait for V-Blank period to synchronize main loop with display frequency
|
|
void WaitVBlank()
|
|
{
|
|
while (!g_VBlank) {}
|
|
g_VBlank = FALSE;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Dummy function to match AKY_Play signature
|
|
void AKY3ch(u8 song, const void* data)
|
|
{
|
|
song;
|
|
AKY_Play(data);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Dummy function to match AKY6ch_Play signature
|
|
void AKY6ch(u8 song, const void* data)
|
|
{
|
|
song;
|
|
AKY6ch_Play(data);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Dummy function to match AKYDarky_Play signature
|
|
void AKYDarky(u8 song, const void* data)
|
|
{
|
|
song;
|
|
AKYDarky_Play(data);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Display the current frquency
|
|
void DisplayFreq()
|
|
{
|
|
Print_SetPosition(0, 20);
|
|
Print_DrawFormat("Freq: %s", (g_Freq50Hz) ? "50Hz" : "60Hz");
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Display information about SFX
|
|
void DisplaySFX()
|
|
{
|
|
Print_SetPosition(0, 15);
|
|
if (g_CurrentPlayer->InitSFX)
|
|
Print_DrawFormat("SFX: %i/%i ", g_SFXNum ? g_SFXIdx + 1 : 0, g_SFXNum);
|
|
else
|
|
Print_DrawText("SFX: Unsupported");
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Handle music event callback
|
|
void HandleEvent(u8 event)
|
|
{
|
|
VDP_SetColor((event % 8) + 1);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Mute both PSGs
|
|
void MuteMusic()
|
|
{
|
|
loop (i, 3)
|
|
{
|
|
// Mute internal PSG
|
|
g_PSG_RegPort = PSG_REG_AMP_A + i;
|
|
g_PSG_DataPort = 0;
|
|
|
|
// Mute external PSG
|
|
g_PSG_Ext_RegPort = PSG_REG_AMP_A + i;
|
|
g_PSG_Ext_DataPort = 0;
|
|
|
|
// Mute Darky PSGs
|
|
g_Darky_PSG1_IndexPort = PSG_REG_AMP_A + i;
|
|
g_Darky_PSG1_DataPort = 0;
|
|
g_Darky_PSG2_IndexPort = PSG_REG_AMP_A + i;
|
|
g_Darky_PSG2_DataPort = 0;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Set the new music to be played
|
|
void SetMusic(u8 idx)
|
|
{
|
|
g_MusicIdx = idx;
|
|
g_SFXIdx = 0;
|
|
|
|
// Get current music data
|
|
const struct MusicEntry* mus = &g_PlayerEntry[g_PlayerIdx].Musics[idx];
|
|
|
|
// If current replayer supports SFX, initialize the sound bank
|
|
// Note: SFX data have been exported to be replayed at address SFX_ADDR
|
|
if (g_CurrentPlayer->InitSFX)
|
|
{
|
|
Mem_Copy(g_AKX_effects, (void*)SFX_ADDR, sizeof(g_AKX_effects));
|
|
g_SFXNum = g_CurrentPlayer->InitSFX((const void*)SFX_ADDR); // can be AKG_InitSFX or AKM_InitSFX
|
|
}
|
|
|
|
// Update the current music information on screen
|
|
Print_SetPosition(0, 11);
|
|
Print_DrawFormat("Music [%i/%i]:\n\n %s", idx + 1, MUSIC_NUM, mus->Name);
|
|
|
|
// Update the SFX information on screen
|
|
DisplaySFX();
|
|
|
|
// Select music mapper segment in bank #3 (0xA000~0xBFFF)
|
|
SET_BANK_SEGMENT(3, mus->Segment);
|
|
|
|
// Call the current replayer's initialize function (also start the music playback)
|
|
// Function can be AKG_Init, AKY_Init or AKM_Init
|
|
g_CurrentPlayer->Init(0, mus->Data);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Set the curren Arkos Tracker 2's replayer
|
|
void SetPlayer(u8 idx)
|
|
{
|
|
// Stop previous playing song (using the previous replayer)
|
|
if (g_CurrentPlayer)
|
|
{
|
|
if (g_CurrentPlayer->Stop)
|
|
g_CurrentPlayer->Stop();
|
|
}
|
|
MuteMusic();
|
|
|
|
// Select the new replayer
|
|
g_PlayerIdx = idx;
|
|
g_CurrentPlayer = &g_PlayerEntry[idx];
|
|
|
|
// Display cursor on front of the selected replayer
|
|
for (u8 i = 0; i < REPLAYER_NUM ; i++)
|
|
Print_DrawCharAt(0, i + 5, (i == idx) ? '\x8A' : ' ');
|
|
|
|
// Play the current music with the newly selected replayer
|
|
SetMusic(g_MusicIdx);
|
|
}
|
|
|
|
//=============================================================================
|
|
// MAIN LOOP
|
|
//=============================================================================
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Program entry point
|
|
void main()
|
|
{
|
|
// Initialize screen mode 0 (text)
|
|
VDP_SetMode(VDP_MODE_SCREEN0);
|
|
VDP_ClearVRAM();
|
|
VDP_EnableVBlank(TRUE);
|
|
VDP_SetColor(0xF4);
|
|
|
|
// Initialize font
|
|
Print_SetTextFont(g_Font_MGL_Sample6, 1);
|
|
|
|
// Dislay static information on screen
|
|
Print_DrawTextAt(0, 0, MSX_GL " ARKOS TRACKER SAMPLE");
|
|
Print_DrawLineH(0, 1, 40);
|
|
|
|
// Display the list of the replayers
|
|
Print_DrawTextAt(0, 3, "Player: ");
|
|
for (u8 i = 0; i < REPLAYER_NUM ; i++)
|
|
Print_DrawTextAt(2, i + 5, g_PlayerEntry[i].Name);
|
|
|
|
// Select first replayer and fist music
|
|
AKG_SetEventCallback(HandleEvent);
|
|
SetPlayer(0);
|
|
SetMusic(0);
|
|
|
|
// Detect current machine VDP frequency and display this information
|
|
g_Freq50Hz = Sys_Is50Hz() ? 1 : 0;
|
|
DisplayFreq();
|
|
Print_SetPosition(20, 20);
|
|
Print_DrawFormat("BIOS: %s", Sys_Is50Hz() ? "50Hz" : "60Hz");
|
|
|
|
// Display footer
|
|
Print_DrawLineH(0, 22, 40);
|
|
Print_DrawTextAt(0, 23, "\x82:Player \x8D:Music \x83:Stop \x84:Freq \x85:SFX");
|
|
|
|
// Enable interruption for screen frequency synchronization
|
|
VDP_EnableVBlank(TRUE);
|
|
BIOS_SetHookCallback(H_TIMI, VBlankHook); // VBlankHook() will be call at each interruption
|
|
|
|
u8 prevRow7 = 0xFF;
|
|
u8 prevRow8 = 0xFF;
|
|
u8 frameCount = 0;
|
|
while (!Keyboard_IsKeyPressed(KEY_ESC))
|
|
{
|
|
// Wait for synchronizaion signal
|
|
WaitVBlank();
|
|
|
|
if (g_Freq50Hz || (frameCount % 6) != 0)
|
|
{
|
|
VDP_SetColor(0xF5);
|
|
g_CurrentPlayer->Update();
|
|
VDP_SetColor(0xF4);
|
|
}
|
|
|
|
// Increment frame counter and dispaly the sign-of-life
|
|
u8 chr = frameCount++ & 0x03;
|
|
Print_DrawCharAt(39, 0, g_ChrAnim[chr]);
|
|
|
|
// Handle input
|
|
u8 row7 = Keyboard_Read(7);
|
|
u8 row8 = Keyboard_Read(8);
|
|
// Change played music
|
|
if (IS_KEY_PUSHED(row8, prevRow8, KEY_RIGHT))
|
|
{
|
|
g_MusicIdx = (g_MusicIdx + 1) % MUSIC_NUM;
|
|
SetMusic(g_MusicIdx);
|
|
}
|
|
else if (IS_KEY_PUSHED(row8, prevRow8, KEY_LEFT))
|
|
{
|
|
g_MusicIdx = (g_MusicIdx + MUSIC_NUM - 1) % MUSIC_NUM;
|
|
SetMusic(g_MusicIdx);
|
|
}
|
|
// Change Arkos replayer
|
|
if (IS_KEY_PUSHED(row8, prevRow8, KEY_UP))
|
|
{
|
|
g_PlayerIdx = (g_PlayerIdx + REPLAYER_NUM - 1) % REPLAYER_NUM;
|
|
SetPlayer(g_PlayerIdx);
|
|
}
|
|
else if (IS_KEY_PUSHED(row8, prevRow8, KEY_DOWN))
|
|
{
|
|
g_PlayerIdx = (g_PlayerIdx + 1) % REPLAYER_NUM;
|
|
SetPlayer(g_PlayerIdx);
|
|
}
|
|
// Stop music playback
|
|
if (IS_KEY_PUSHED(row8, prevRow8, KEY_SPACE))
|
|
{
|
|
if (g_CurrentPlayer->Stop)
|
|
g_CurrentPlayer->Stop();
|
|
}
|
|
// SFX playback
|
|
if (IS_KEY_PUSHED(row7, prevRow7, KEY_BACK))
|
|
{
|
|
if (g_CurrentPlayer->PlaySFX)
|
|
{
|
|
DisplaySFX();
|
|
g_CurrentPlayer->PlaySFX(g_SFXIdx, ARKOS_CHANNEL_C, 0);
|
|
g_SFXIdx++;
|
|
if (g_SFXIdx >= g_SFXNum)
|
|
g_SFXIdx = 0;
|
|
}
|
|
}
|
|
// Change frequency
|
|
if (IS_KEY_PUSHED(row7, prevRow7, KEY_RETURN))
|
|
{
|
|
g_Freq50Hz = !g_Freq50Hz;
|
|
DisplayFreq();
|
|
}
|
|
prevRow7 = row7;
|
|
prevRow8 = row8;
|
|
}
|
|
} |