/*
SC-01 Controller v0.3
Anthony D'Angelo
February 2024
A sketch for controlling an SC-01 Speech Synthesizer chip.
Documentation for the SC-01 is here: http://redcedar.com/sc01/sc01.pdf
TODO:
- Implement 1v/octave tunable clock
- Implement dynamic prefs
- Implement interrupts/callbacks for control state updates
- Implement memorized phrases
CHANGELOG
- Added breadboard layout
- Added Vowel Grid Mode with joystick and pointers (!)
- Added serial output, removed parallel output
- Added switches for phoneme and advance mode select
- Added pushbutton for gate advance mode
- Added shift register and LEDs
*/
//Default preferences
const bool repeat = true; // only loops once if false
const bool repeatDelay = 1000; // delay between loops
//Saved phrases
const int mem1Len = 3;
const String mem1Symbols[mem1Len] = {"K", "AH1", "R"}; // car
const int mem1Inflections[mem1Len] = {3,2,1};
const int mem2Len = 14;
const String mem2Symbols[mem2Len] = {"H", "EH1", "UH3", "L", "UH3", "O1", "U1", "PA0", "W", "ER" ,"R","UH1", "L","D"}; // Hello world
const int mem2Inflections[mem2Len] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//Pins
const int arsim = 2; // Simulates SC-01 A/R pulse out
const int ar = 3; // To SC-01 A/R pin
const int stb = 4; // To SC-01 Strobe pin
const int phonemeModeSelect1 = 5; // To mode switch 1 right pin
const int phonemeModeSelect2 = 6; // to mode switch 2 right pin
const int datapin = 7; // To DS
const int latchpin = 8; // To STCP
const int clockpin = 9; // To SHCP
const int rdm = A0; // No connection; noise source for randomSeed()
const int gridX = A1; // +5v Eurorack CV
const int gridY = A2; // +5v Eurorack CV
//Valid phoneme symbols
const String validSymbols[64] = {
"EH3", "EH2", "EH1", "PA0", "DT", "A2", "A1", "ZH",
"AH2", "I3", "I2", "I1", "M", "N", "B", "V",
"CH", "SH", "Z", "AW1", "NG", "AH1", "OO1", "OO",
"L", "K", "J", "H", "G", "F", "D", "S",
"A", "AY", "Y1", "UH3", "AH", "P", "O", "I",
"U", "Y", "T", "R", "E", "W", "AE", "AE1",
"AW2", "UH2", "UH1", "UH", "O2", "O1", "IU", "U1",
"THV", "TH", "ER", "EH", "E1", "AW", "PA1", "STOP"
};
//Vowel grid
int vowelGrid[8][5] = {
{44, 59, 46, 51, 22},
{60, 2, 47, 50, 43},
{41, 1, 36, 49, 58},
{34, 0, 21, 35, 24},
{39, 32, 8, 38, 54},
{11, 6, 61, 53, 40},
{10, 5, 19, 52, 55},
{9, 33, 48, 23, 45}
};
bool arListen(){
//Return true if AR pulse is detected
digitalWrite(arsim,HIGH);
return digitalRead(ar)==HIGH ? true : false;
}
void strobe(){
//Trigger SC-01 latch
int strobeTime = 150;
digitalWrite(stb,HIGH);
Serial.println(F("Strobe!"));
delay(strobeTime);
digitalWrite(stb,LOW);
}
void shiftUpdate(byte data){
//Write a given byte to shift register
shiftOut(datapin, clockpin, MSBFIRST, data);
digitalWrite(latchpin,HIGH);
digitalWrite(latchpin,LOW);
}
int phonemeMode(){
bool sw1 = digitalRead(phonemeModeSelect1)==HIGH ? true:false;
bool sw2 = digitalRead(phonemeModeSelect2)==HIGH ? true:false;
//switch pair will be replaced with a three-position switch
if (!sw1 && !sw2){
Serial.println("Phoneme Mode: Memory");
return 1;
}
else if (sw1 && !sw2){
Serial.println("Phoneme Mode: Random");
return 2;
}
else if (!sw1 && !sw2){
Serial.println("Phoneme Mode: Vowel Grid");
return 3;
}
else if (!sw1 && sw2){
Serial.println("Phoneme Mode: Random");
return 3;
}
}
class Phrase{
public:
int len;
String symbols[16];
int inflections[16];
int randomLen(){
//Return a random length
randomSeed(analogRead(rdm));
int minLen = 4;
int maxLen = 10;
return random(minLen,maxLen);
}
int randomSymbolIndex(){
//Return a random index from validSymbols[]
randomSeed(analogRead(rdm));
int index = random(0,63);
return index;
}
int randomInflection(){
//Return a random inflection value
return random(0,3);
}
int indexFromVowelGrid(){
int x = map(analogRead(gridX),0,1023,0,7);
int y = map(analogRead(gridY),0,1023,0,4);
Serial.println("Vowel Grid index = " + String(x) + " , " + String(y));
return vowelGrid[x][y];
}
};
class Phoneme{
public:
String symbol = "PA0";
int inflection = 0;
int symbolIndex(){
// Return index of given phoneme symbol if found in validSymbols[],
// or default to PA0 if symbol is not found.
for (int i=0; i<64; i++){
if(symbol==validSymbols[i]){
return i;
}
}
return 3;
}
int checkInflection(){
// Return inflection if inflection is 0 to 3; otherwise, default to 0
if(inflection >= 0 && inflection<4){
return inflection;
}
else{
return 0;
}
}
byte encode(int phon, int inf){
// Return an 8-bit value where
// - the first two bits represent inflection
// - the last 6 bits represent the phoneme
byte newByte = inf << 6;
newByte = newByte | phon;
Serial.println(F("Encoded output: "));
for (int i=7; i>=0;i--){
Serial.print(bitRead(newByte,i));
}
Serial.println();
return newByte;
}
};
//SETUP=========================================================
void setup(){
Serial.begin(9600);
Serial.println(F("PROGRAM START."));
Serial.println();
int inputPins[6] = {ar,rdm,gridX,gridY,phonemeModeSelect1,phonemeModeSelect2};
for (int i=0;i<8;i++){
pinMode(inputPins[i],INPUT);
}
int dataPins[5]={datapin,clockpin,latchpin,stb,arsim};
for(int i=0; i<3; i++){
pinMode(dataPins[i],OUTPUT);
}
Serial.println(F("Pins initialized."));
Serial.println();
}
//LOOP===========================================================
void loop(){
Serial.println(F("MAIN LOOP START:"));
digitalWrite(arsim,LOW); // debug only
Phrase phrase;
switch (phonemeMode()){
case 1:
phrase.len = mem1Len;
for(int i=0; i<phrase.len; i++){
phrase.symbols[i]=mem1Symbols[i];
phrase.inflections[i]=mem1Inflections[i];
Serial.println("Phoneme " + String(i) + ": " + String(phrase.symbols[i]) + ", " + String(phrase.inflections[i]));
}
Serial.println();
break;
case 2:
phrase.len = phrase.randomLen();
for (int i=0; i<phrase.len; i++){
String* ptr = &validSymbols[phrase.randomSymbolIndex()];
phrase.symbols[i] = *ptr;
phrase.inflections[i] = 0;
Serial.println("Random phoneme " + String(i+1) + ": " + String(phrase.symbols[i]) + ", " + String(phrase.inflections[i]));
}
Serial.println();
break;
case 3:
phrase.len = 1;
String* ptr = &validSymbols[phrase.indexFromVowelGrid()];
phrase.symbols[0] = *ptr;
phrase.inflections[0]=0;
break;
}
for (int i=0; i<phrase.len; i++){
//Write phonemes in phrase
Phoneme p;
p.symbol = phrase.symbols[i];
p.inflection = phrase.inflections[i];
Serial.println("Writing phoneme " + String(i+1) + ": " + String(p.symbol) + ", " + String(p.inflection));
int a = p.symbolIndex();
int b = p.checkInflection();
shiftUpdate(p.encode(a,b));
while(!arListen()){
delay(1);
Serial.println(F("Waiting for AR pulse..."));
}
Serial.println(F("AR pulse detected!"));
strobe();
digitalWrite(arsim,LOW); // debug only
Serial.println(F("Ready for next phoneme. \n"));
}
if(!repeat){
Serial.println(F("END PROGRAM."));
delay(500);
exit(0);
}
delay(repeatDelay);
};