blob: e14598a07c4e7cd3d4f8085e50e2736c08bd537a [file] [log] [blame] [raw]
/*
* Library to access OPL2/OPL3 hardware (YM3812 / YMF262)
*
* Copyright (c) 2015, Mateusz Viste
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef OPL
#include <conio.h> /* inp(), out() */
#include <dos.h>
#include <stdio.h>
#include <stdlib.h> /* calloc() */
#include <string.h> /* memset() */
#include "opl.h"
#include "opl-gm.h"
struct voicealloc_t {
unsigned short priority;
signed short timbreid;
signed char channel;
signed char note;
};
struct oplstate_t {
signed char notes2voices[16][128]; /* keeps the map of channel:notes -> voice allocations */
unsigned short channelpitch[16]; /* per-channel pitch level */
unsigned short channelvol[16]; /* per-channel pitch level */
struct voicealloc_t voices2notes[18]; /* keeps the map of what voice is playing what note/channel currently */
unsigned char channelprog[16]; /* programs (patches) assigned to channels */
int opl3; /* flag indicating whether or not the sound module is OPL3-compatible or only OPL2 */
};
struct oplstate_t *oplmem = NULL; /* memory area holding all OPL's current states */
static unsigned short freqtable[128] = { /* note # */
345, 365, 387, 410, 435, 460, 488, 517, 547, 580, 615, 651, /* 0 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 12 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 24 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 36 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 48 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 60 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 72 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 84 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 96 */
690, 731, 774, 820, 869, 921, 975, 517, 547, 580, 615, 651, /* 108 */
690, 731, 774, 820, 869, 921, 975, 517}; /* 120 */
static unsigned char octavetable[128] = { /* note # */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 */
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, /* 12 */
1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, /* 24 */
2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, /* 36 */
3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, /* 48 */
4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, /* 60 */
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, /* 72 */
6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, /* 84 */
7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, /* 96 */
8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, /* 108 */
9, 9, 9, 9, 9, 9, 9,10}; /* 120 */
static unsigned short pitchtable[256] = { /* pitch wheel */
29193U,29219U,29246U,29272U,29299U,29325U,29351U,29378U, /* -128 */
29405U,29431U,29458U,29484U,29511U,29538U,29564U,29591U, /* -120 */
29618U,29644U,29671U,29698U,29725U,29752U,29778U,29805U, /* -112 */
29832U,29859U,29886U,29913U,29940U,29967U,29994U,30021U, /* -104 */
30048U,30076U,30103U,30130U,30157U,30184U,30212U,30239U, /* -96 */
30266U,30293U,30321U,30348U,30376U,30403U,30430U,30458U, /* -88 */
30485U,30513U,30541U,30568U,30596U,30623U,30651U,30679U, /* -80 */
30706U,30734U,30762U,30790U,30817U,30845U,30873U,30901U, /* -72 */
30929U,30957U,30985U,31013U,31041U,31069U,31097U,31125U, /* -64 */
31153U,31181U,31209U,31237U,31266U,31294U,31322U,31350U, /* -56 */
31379U,31407U,31435U,31464U,31492U,31521U,31549U,31578U, /* -48 */
31606U,31635U,31663U,31692U,31720U,31749U,31778U,31806U, /* -40 */
31835U,31864U,31893U,31921U,31950U,31979U,32008U,32037U, /* -32 */
32066U,32095U,32124U,32153U,32182U,32211U,32240U,32269U, /* -24 */
32298U,32327U,32357U,32386U,32415U,32444U,32474U,32503U, /* -16 */
32532U,32562U,32591U,32620U,32650U,32679U,32709U,32738U, /* -8 */
32768U,32798U,32827U,32857U,32887U,32916U,32946U,32976U, /* 0 */
33005U,33035U,33065U,33095U,33125U,33155U,33185U,33215U, /* 8 */
33245U,33275U,33305U,33335U,33365U,33395U,33425U,33455U, /* 16 */
33486U,33516U,33546U,33576U,33607U,33637U,33667U,33698U, /* 24 */
33728U,33759U,33789U,33820U,33850U,33881U,33911U,33942U, /* 32 */
33973U,34003U,34034U,34065U,34095U,34126U,34157U,34188U, /* 40 */
34219U,34250U,34281U,34312U,34343U,34374U,34405U,34436U, /* 48 */
34467U,34498U,34529U,34560U,34591U,34623U,34654U,34685U, /* 56 */
34716U,34748U,34779U,34811U,34842U,34874U,34905U,34937U, /* 64 */
34968U,35000U,35031U,35063U,35095U,35126U,35158U,35190U, /* 72 */
35221U,35253U,35285U,35317U,35349U,35381U,35413U,35445U, /* 80 */
35477U,35509U,35541U,35573U,35605U,35637U,35669U,35702U, /* 88 */
35734U,35766U,35798U,35831U,35863U,35895U,35928U,35960U, /* 96 */
35993U,36025U,36058U,36090U,36123U,36155U,36188U,36221U, /* 104 */
36254U,36286U,36319U,36352U,36385U,36417U,36450U,36483U, /* 112 */
36516U,36549U,36582U,36615U,36648U,36681U,36715U,36748U}; /* 120 */
/* tables below provide register offsets for each voice. note, that these are
* NOT the registers IDs, but their direct offsets instead - this for simpler
* and faster computations. */
const unsigned short op1offsets[18] = {0x00,0x01,0x02,0x08,0x09,0x0a,0x10,0x11,0x12,0x100,0x101,0x102,0x108,0x109,0x10a,0x110,0x111,0x112};
const unsigned short op2offsets[18] = {0x03,0x04,0x05,0x0b,0x0c,0x0d,0x13,0x14,0x15,0x103,0x104,0x105,0x10b,0x10c,0x10d,0x113,0x114,0x115};
/* number of supported voices: 9 by default (OPL2), can go up to 18 (OPL3) */
static int voicescount = 9;
/* function used to write into a register 'reg' of the OPL chip located at
* port 'port', writing byte 'data' into it. this function supports also OPL3.
* to write into the secondary address of an OPL3, just OR your register with
* the 0x100 value (the 0x100 flag will be removed then, and the data will be
* written into port+3). */
static void oplregwr(unsigned short port, unsigned short reg, unsigned char data) {
int i;
/* remap 'high' registers to second port (OPL3) */
if ((reg & 0x100) != 0) {
reg &= 0xff;
port += 2;
}
/* select the register we want to write to, via the index register */
outp(port, reg);
/* OPL2 requires 3.3us to pass before writing to the data port. AdLib
* recommends reading 6 times from the index register to make time pass. */
i = 6;
while (i--) inp(port);
/* write the data to the data register */
outp(port+1, data);
/* OPL2 requires 23us to pass before writing to the data port. AdLib
* recommends reading 35 times from the index register to make time pass. */
i = 35;
while (i--) inp(port);
}
/* 'volume' is in range 0..127 - take care to change only the 'attenuation'
* part of the register, and never touch the KSL bits */
static void calc_vol(unsigned char *regbyte, int volume) {
int level;
/* invert bits and strip out the KSL header */
level = ~(*regbyte);
level &= 0x3f;
/* adjust volume */
level = (level * volume) / 127;
/* boundaries check */
if (level > 0x3f) level = 0x3f;
if (level < 0) level = 0;
/* invert the bits, as expected by the OPL registers */
level = ~level;
level &= 0x3f;
/* final result computation */
*regbyte &= 0xC0; /* zero out all attentuation bits */
*regbyte |= level; /* fill in the new attentuation value */
}
/* Initialize hardware upon startup - positive on success, negative otherwise
* Returns 0 for OPL2 initialization, or 1 if OPL3 has been detected */
int opl_init(unsigned short port) {
int x, y, i;
/* make sure we're not inited yet */
if (oplmem != NULL) return(-1);
/* detect the hardware and return error if not found */
oplregwr(port, 0x04, 0x60); /* reset both timers by writing 60h to register 4 */
oplregwr(port, 0x04, 0x80); /* enable interrupts by writing 80h to register 4 (must be a separate write from the 1st one) */
x = inp(port) & 0xE0; /* read the status register (port 388h) and store the result */
oplregwr(port, 0x02, 0xff); /* write FFh to register 2 (Timer 1) */
oplregwr(port, 0x04, 0x21); /* start timer 1 by writing 21h to register 4 */
i = 512;
while (i--) y = inp(port) & 0xE0; /* delay for at least 80 microseconds (and read the status register) */
oplregwr(port, 0x04, 0x60); /* reset both timers and interrupts (see steps 1 and 2) */
oplregwr(port, 0x04, 0x80); /* reset both timers and interrupts (see steps 1 and 2) */
/* test the stored results of steps 3 and 7 by ANDing them with E0h. The result of step 3 should be */
if (x != 0) return(-1); /* 00h, and the result of step 7 should be C0h. If both are */
if (y != 0xC0) return(-2); /* ok, an AdLib-compatible board is installed in the computer */
/* init memory */
oplmem = calloc(1, sizeof(struct oplstate_t));
if (oplmem == NULL) return(-3);
/* is it an OPL3 or just an OPL2? */
if ((inp(port) & 0x06) == 0) oplmem->opl3 = 1;
/* init the hardware */
voicescount = 9; /* OPL2 provides 9 melodic voices */
/* enable OPL3 (if detected) and put it into 36 operators mode */
if (oplmem->opl3 != 0) {
oplregwr(port, 0x105, 1); /* enable OPL3 mode (36 operators) */
oplregwr(port, 0x104, 0); /* disable four-operator voices */
voicescount = 18; /* OPL3 provides 18 melodic channels */
/* Init the secondary OPL chip
* NOTE: this I don't do anymore, it turns my Aztech Waverider mute! */
/* oplregwr(port, 0x101, 0x20); */ /* enable Waveform Select */
/* oplregwr(port, 0x108, 0x40); */ /* turn off CSW mode and activate FM synth mode */
/* oplregwr(port, 0x1BD, 0x00); */ /* set vibrato/tremolo depth to low, set melodic mode */
}
oplregwr(port, 0x01, 0x20); /* enable Waveform Select */
oplregwr(port, 0x04, 0x00); /* turn off timers IRQs */
oplregwr(port, 0x08, 0x40); /* turn off CSW mode and activate FM synth mode */
oplregwr(port, 0xBD, 0x00); /* set vibrato/tremolo depth to low, set melodic mode */
for (x = 0; x < voicescount; x++) {
oplregwr(port, 0x20 + op1offsets[x], 0x1); /* set the modulator's multiple to 1 */
oplregwr(port, 0x20 + op2offsets[x], 0x1); /* set the modulator's multiple to 1 */
oplregwr(port, 0x40 + op1offsets[x], 0x10); /* set volume of all channels to about 40 dB */
oplregwr(port, 0x40 + op2offsets[x], 0x10); /* set volume of all channels to about 40 dB */
}
opl_clear(port);
/* all done */
return(oplmem->opl3);
}
/* close OPL device */
void opl_close(unsigned short port) {
int x;
/* turns all notes 'off' */
opl_clear(port);
/* set volume to lowest level on all voices */
for (x = 0; x < voicescount; x++) {
oplregwr(port, 0x40 + op1offsets[x], 0x1f);
oplregwr(port, 0x40 + op2offsets[x], 0x1f);
}
/* if OPL3, switch the chip back into its default OPL2 mode */
if (oplmem->opl3 != 0) oplregwr(port, 0x105, 0);
/* free state memory */
free(oplmem);
oplmem = NULL;
}
/* turns off all notes */
void opl_clear(unsigned short port) {
int x, y;
for (x = 0; x < voicescount; x++) opl_noteoff(port, x);
/* reset the percussion bits at the 0xBD register */
oplregwr(port, 0xBD, 0);
/* mark all voices as unused */
for (x = 0; x < voicescount; x++) {
oplmem->voices2notes[x].channel = -1;
oplmem->voices2notes[x].note = -1;
oplmem->voices2notes[x].timbreid = -1;
}
/* mark all notes as unallocated */
for (x = 0; x < 16; x++) {
for (y = 0; y < 128; y++) oplmem->notes2voices[x][y] = -1;
}
/* pre-set emulated channel patches to default GM ids and reset all
* per-channel volumes */
for (x = 0; x < 16; x++) {
opl_midi_changeprog(x, x);
oplmem->channelvol[x] = 127;
}
}
void opl_noteoff(unsigned short port, unsigned short voice) {
/* if voice is one of the OPL3 set, adjust it and route over secondary OPL port */
if (voice >= 9) {
oplregwr(port, 0x1B0 + voice - 9, 0);
} else {
oplregwr(port, 0xB0 + voice, 0);
}
}
void opl_noteon(unsigned short port, unsigned short voice, unsigned int note, int pitch) {
unsigned int freq = freqtable[note];
unsigned int octave = octavetable[note];
if (pitch != 0) {
if (pitch > 127) {
pitch = 127;
} else if (pitch < -128) {
pitch = -128;
}
freq = ((unsigned long)freq * pitchtable[pitch + 128]) >> 15;
if (freq >= 1024) {
freq >>= 1;
octave++;
}
}
if (octave > 7) octave = 7;
/* if voice is one of the OPL3 set, adjust it and route over secondary OPL port */
if (voice >= 9) {
voice -= 9;
voice |= 0x100;
}
oplregwr(port, 0xA0 + voice, freq & 0xff); /* set lowfreq */
oplregwr(port, 0xB0 + voice, (freq >> 8) | (octave << 2) | 32); /* KEY ON + hifreq + octave */
}
void opl_midi_pitchwheel(unsigned short oplport, int channel, int pitchwheel) {
/*int x;*/
/* update the new pitch value for channel (used by newly played notes) */
oplmem->channelpitch[channel] = pitchwheel;
/* check all active voices to see who is playing on given channel now, and
* recompute all playing notes for this channel with the new pitch TODO */
/*for (x = 0; x < voicescount; x++) {
if (oplmem->voices2notes[x].channel != channel) continue;
opl_noteon(oplport, x, oplmem->voices2notes[x].note, pitchwheel + gmtimbres[oplmem->voices2notes[x].timbreid].finetune);
}*/
}
void opl_midi_controller(unsigned short oplport, int channel, int id, int value) {
int x;
switch (id) {
case 11: /* "Expression" (meaning "channel volume") */
oplmem->channelvol[channel] = value;
break;
case 123: /* 'all notes off' */
case 120: /* 'all sound off' - I map it to 'all notes off' for now, not perfect but better than not handling it at all */
for (x = 0; x < voicescount; x++) {
if (oplmem->voices2notes[x].channel != channel) continue;
opl_midi_noteoff(oplport, channel, oplmem->voices2notes[x].note);
}
break;
}
}
/* assign a new instrument to emulated MIDI channel */
void opl_midi_changeprog(int channel, int program) {
if (channel == 9) return; /* do not allow to change channel 9, it is for percussions only */
oplmem->channelprog[channel] = program;
}
void opl_loadinstrument(unsigned short oplport, unsigned short voice, struct timbre_t *timbre) {
/* KSL (key level scaling) / attenuation */
oplregwr(oplport, 0x40 + op1offsets[voice], timbre->modulator_40);
oplregwr(oplport, 0x40 + op2offsets[voice], timbre->carrier_40 | 0x3f); /* force volume to 0, it will be reajusted during 'note on' */
/* select waveform on both operators */
oplregwr(oplport, 0xE0 + op1offsets[voice], timbre->modulator_E862 >> 24);
oplregwr(oplport, 0xE0 + op2offsets[voice], timbre->carrier_E862 >> 24);
/* sustain / release */
oplregwr(oplport, 0x80 + op1offsets[voice], (timbre->modulator_E862 >> 16) & 0xff);
oplregwr(oplport, 0x80 + op2offsets[voice], (timbre->carrier_E862 >> 16) & 0xff);
/* attack rate / decay */
oplregwr(oplport, 0x60 + op1offsets[voice], (timbre->modulator_E862 >> 8) & 0xff);
oplregwr(oplport, 0x60 + op2offsets[voice], (timbre->carrier_E862 >> 8) & 0xff);
/* AM / vibrato / envelope */
oplregwr(oplport, 0x20 + op1offsets[voice], timbre->modulator_E862 & 0xff);
oplregwr(oplport, 0x20 + op2offsets[voice], timbre->carrier_E862 & 0xff);
/* feedback / connection */
if (voice >= 9) {
voice -= 9;
voice |= 0x100;
}
if (oplmem->opl3 != 0) { /* on OPL3 make sure to enable LEFT/RIGHT unmute bits */
oplregwr(oplport, 0xC0 + voice, timbre->feedconn | 0x30);
} else {
oplregwr(oplport, 0xC0 + voice, timbre->feedconn);
}
}
/* adjust the volume of the voice (in the usual MIDI range of 0..127) */
static void voicevolume(unsigned short port, unsigned short voice, int program, int volume) {
unsigned char carrierval = gmtimbres[program].carrier_40;
if (volume == 0) {
carrierval |= 0x3f;
} else {
calc_vol(&carrierval, volume);
}
oplregwr(port, 0x40 + op2offsets[voice], carrierval);
}
/* get the id of the instrument that relates to channel/note pair */
static int getinstrument(int channel, int note) {
int res;
res = oplmem->channelprog[channel];
if (channel == 9) { /* the percussion channel requires special handling */
if ((note < 35) || (note > 86)) return(-1); /* ignore unsupported notes */
res = (128 - 35) + note;
}
return(res);
}
void opl_midi_noteon(unsigned short port, int channel, int note, int velocity) {
int x, voice = -1;
int lowestpriorityvoice = 0;
int instrument;
/* get the instrument to play */
instrument = getinstrument(channel, note);
if (instrument < 0) return;
/* if note already playing, then reuse its voice to avoid leaving a stuck voice */
if (oplmem->notes2voices[channel][note] >= 0) {
voice = oplmem->notes2voices[channel][note];
} else {
/* else find a free voice, possibly with the right timbre, or at least locate the oldest note */
for (x = 0; x < voicescount; x++) {
if (oplmem->voices2notes[x].channel < 0) {
voice = x; /* preselect this voice, but continue looking */
/* if the instrument is right, do not look further */
if (oplmem->voices2notes[x].timbreid == instrument) {
break;
}
}
if (oplmem->voices2notes[x].priority < oplmem->voices2notes[lowestpriorityvoice].priority) lowestpriorityvoice = x;
}
/* if no free voice available, then abort the oldest one */
if (voice < 0) {
voice = lowestpriorityvoice;
opl_midi_noteoff(port, oplmem->voices2notes[voice].channel, oplmem->voices2notes[voice].note);
}
}
/* load the proper instrument, if not already good */
if (oplmem->voices2notes[voice].timbreid != instrument) {
oplmem->voices2notes[voice].timbreid = instrument;
opl_loadinstrument(port, voice, &(gmtimbres[instrument]));
}
/* update states */
oplmem->voices2notes[voice].channel = channel;
oplmem->voices2notes[voice].note = note;
oplmem->voices2notes[voice].priority = ((16 - channel) << 8) | 0xff; /* lower channels must have priority */
oplmem->notes2voices[channel][note] = voice;
/* set the requested velocity on the voice */
voicevolume(port, voice, oplmem->voices2notes[voice].timbreid, velocity * oplmem->channelvol[channel] / 127);
/* trigger NOTE_ON on the OPL, take care to apply the 'finetune' pitch correction, too */
if (channel == 9) { /* percussion channel doesn't provide a real note (FIXME what should I use as the 'note'??) */
opl_noteon(port, voice, 64, oplmem->channelpitch[channel] + gmtimbres[instrument].finetune);
} else {
opl_noteon(port, voice, note, oplmem->channelpitch[channel] + gmtimbres[instrument].finetune);
}
/* reajust all priorities */
for (x = 0; x < voicescount; x++) {
if (oplmem->voices2notes[x].priority > 0) oplmem->voices2notes[x].priority -= 1;
}
}
void opl_midi_noteoff(unsigned short port, int channel, int note) {
int voice = oplmem->notes2voices[channel][note];
if (voice >= 0) {
opl_noteoff(port, voice);
oplmem->voices2notes[voice].channel = -1;
oplmem->voices2notes[voice].note = -1;
oplmem->voices2notes[voice].priority = -1;
oplmem->notes2voices[channel][note] = -1;
}
}
#endif /* #ifdef OPL */