Normalised / RadioMusic

Virtual Radio module for Eurorack
0 stars 0 forks source link

Feature Request: Pitch and/or Start Point #8

Open erstlaub opened 7 years ago

erstlaub commented 7 years ago

Thanks again for this great work, I'm loving the pitchshifting but really missing the ability to dial in a reset point.

I fully appreciate the limitations of the hardware is but was wondering if it would be at all possible to separate the start knob and the start CV input in order to be able to set a start point via CV or even via the reset input?

At a very vague stab some of the following options might work if possible

Pitch via start knob, start position via start CVin, button resets Pitch via start knob + start CVin, start position set by CV into reset input, button reset to start point

Given the variety of ways that people use RM, perhaps a setting in the config could enable a 'pitch+start with some sacrifices' or 'original flavour' type selection?

The other thing that might an option could be increasing the press delay between bank select and allowing a button hold AND knob movement to change start point where just a knob read would affect pitch as it does?

It's just some spitballing given the brief time I've had tonight enjoying pitching things all the way down but recognising how nice it would be to then be able to restart/trigger certain mid transient whalesongs on demand rather than the reset button going all the way to the start.

Thanks in advance, more than happy for anyone smarter to come up with more logical solutions (or to be shot down entirely).

D

Normalised commented 7 years ago

That's a good idea. :)

We could add options to control what the start Pot and CV do so they can be pitch or start independently of each other.

I'm not sure if the reset input is just a digital 0/1 or if its a full CV like Station and Start. IF it is full CV then i'd certainly add configuration for that to a future upgrade.

In the long term i'd like to see an expander so you can control all the things at the same time but I have no idea if this would require completely new hardware on the unit itself.

erstlaub commented 7 years ago

Giving this a little more though overnight, I feel like the reset input should probably remain unmeddled with and the start knob/input split if possible. (re)Triggering from a sequencer is too useful a function to option out I reckon.

I was trying to think about the most useful configuration for me and I got to thinking that most users are likely to have either access to offsets and/or some sort of CV based control device -sequencer/KB/etc so it'd just be down each user deciding which worked better for them and setting I suppose a line in cfg to activate the split behaviour and then a second line to state whether you'd want pitch or start on the knob.

The one possible issue that would need some consideration in the absence of concord among manufacturers would be the ranges involved, particularly with pitch. Am I correct in thinking that the RM hardware can not add (well subtract really) a -ve voltage value from a parameter? Given that offsets (say -10 to +10v) vary from a CV keyboard (some claim 0v lowest value, some will go down to -3v or more) the playback might need to default to it's lowest possible value with no input and then have the CV values added (in order to be able to access the slower as well as faster pitched results). This is based on an ultra vague knowledge that arduino based processors (maybe all processors) tend not to cope will with things outside the 0-5v ranges (or have I made this up?).

Just a few little considerations that might help the thought process, save some cul de sacs or gain some useful insight from other users.

Best.

D

davidhbot commented 7 years ago

I also need to select the start currently I use the reset button to switch between picth/start (works with the immediate mode only)

if ((changes & BUTTON_SHORT_PRESS )&& !speedChangeMode){
   D(Serial.println("enter speedmode"););
     speedChangeMode =true;
} else if ((changes & BUTTON_SHORT_PRESS )&& speedChangeMode){
   D(Serial.println("exit speedmode"););
     speedChangeMode =false;
}

and :

    if((changes  & CHANGE_START_NOW  ) && speedChangeMode ) {
      speedChange = true;
    } else if((changes & CHANGE_START_NOW) && !speedChangeMode) {
   skipToStartPoint = true;
    }
erstlaub commented 7 years ago

Ive spent the last hour or so being too dumb to work out how to even be able to find the source code that I'd need to add this to in order to then make back into a hex file.

So far have tried opening the binary in some hex editors (nope), txt edit (nope) and the teensy app (just for actually uploading finished binaries).

Any tips?

//despair

Normalised commented 7 years ago

To compile your own firmware and upload do this :

  1. Install Arduino IDE : https://www.arduino.cc/en/Main/Software
  2. Install Teensyduino : https://www.pjrc.com/teensy/td_download.html
  3. Get a copy of the source code : https://github.com/Normalised/RadioMusic/archive/master.zip
  4. Unzip the source code somewhere
  5. Connect the Radio Music to your computer via USB. If you are on OSX or Win 10 you won't need drivers.
  6. Open RadioMusic.ino in Arduino IDE
  7. Click the upload button : Upload

As for adding the code changes from @davidhbot its best that he explains exactly where he made them. :)

erstlaub commented 7 years ago

Thanks for the info, I was actually just missing the teensyduino step.

Sadly once I got in looking at the arduino sketches I realised this is well beyond my previous 'make a stepper motor move at a certain speed' experience.

Would definitely love to hear how/where @davidhbot has implemented the code so I can attempt it myself.

davidhbot commented 7 years ago

Hi @erstlaub

here is my code, warning ! it's quick and dirty and should work only in immediate mode checks things around speedChangeMode

/*
 RADIO MUSIC
 https://github.com/TomWhitwell/RadioMusic

 Audio out: Onboard DAC, teensy3.1 pin A14/DAC

 Bank Button: 2
 Bank LEDs 3,4,5,6
 Reset Button: 8  
 Reset LED 11 
 Reset CV input: 9 
 Channel Pot: A9 
 Channel CV: A8 // check 
 Time Pot: A7 
 Time CV: A6 // check 
 SD Card Connections: 
 SCLK 14
 MISO 12
 MOSI 7 
 SS   10 

 NB: Compile using modified versions of: 
 SD.cpp (found in the main Arduino package) 
 play_sd_raw.cpp  - In Teensy Audio Library 
 play_sc_raw.h    - In Teensy Audio Library 

 from:https://github.com/TomWhitwell/RadioMusic/tree/master/Collateral/Edited%20teensy%20files

 Additions and changes:
 2016 by Jouni Stenroos - jouni.stenroos@iki.fi 
 - New bank change mode
 - Removing 330 file limit
 - Improving reset
 - File sorting
 - Audio crossfade
 - Some refactoring and organization of code.

 */
#include <EEPROM.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include "RadioMusic.h"
#include "AudioSystemHelpers.h"
#include "Settings.h"
#include "LedControl.h"
#include "FileScanner.h"
#include "AudioEngine.h"
#include "Interface.h"
#include "PlayState.h"

#ifdef DEBUG
#define D(x) x
#else
#define D(x)
#endif

// Press reset button to reboot
//#define RESET_TO_REBOOT
//#define ENGINE_TEST

#define EEPROM_BANK_SAVE_ADDRESS 0

#define FLASHTIME   10      // How long do LEDs flash for?
#define SHOWFREQ    250     // how many millis between serial Debug updates

#define peakFPS 30   //  FRAMERATE FOR PEAK METER

#define SD_CARD_CHECK_DELAY 20

// //////////
// TIMERS
// //////////

elapsedMillis showDisplay;
elapsedMillis resetLedTimer = 0;
elapsedMillis ledFlashTimer = 0;

elapsedMillis meterDisplayTimer; // Counter to hide MeterDisplay after bank change
elapsedMillis fps; // COUNTER FOR PEAK METER FRAMERATE

int prevBankTimer = 0;
boolean flashLeds = false;
boolean bankChangeMode = false;
boolean speedChangeMode = false;
File settingsFile;

Settings settings("SETTINGS.TXT");
LedControl ledControl;
FileScanner fileScanner;
AudioEngine audioEngine;
Interface interface;
PlayState playState;

int NO_FILES = 0;

void setup() {

#ifdef DEBUG_STARTUP
    while( !Serial );
    Serial.println("Starting");
#endif // DEBUG_STARTUP

    ledControl.init();
    ledControl.single(playState.bank);

    // MEMORY REQUIRED FOR AUDIOCONNECTIONS
    AudioMemory(20);
    // SD CARD SETTINGS FOR AUDIO SHIELD
    SPI.setMOSI(7);
    SPI.setSCK(14);

    boolean hasSD = openSDCard();
    if(!hasSD) {
        Serial.println("Rebooting");
        reBoot(0);
    }

    settings.init(hasSD);

    File root = SD.open("/");
    fileScanner.scan(&root, settings);

    getSavedBankPosition();

    audioEngine.init(settings);

    int numFiles = fileScanner.numFilesInBank[playState.bank];
    D(Serial.print("File Count ");Serial.println(numFiles););

    if(numFiles == 0) {
        NO_FILES = 1;
    }
    interface.init(fileScanner.fileInfos[playState.bank][0].size, fileScanner.numFilesInBank[playState.bank], settings, &playState);

    D(Serial.println("--READY--"););
}

void getSavedBankPosition() {
    // CHECK  FOR SAVED BANK POSITION
    int a = 0;
    a = EEPROM.read(EEPROM_BANK_SAVE_ADDRESS);
    if (a >= 0 && a <= fileScanner.activeBanks) {
        playState.bank = a;
        playState.channelChanged = true;
    } else {
        EEPROM.write(EEPROM_BANK_SAVE_ADDRESS, 0);
    };
}

boolean openSDCard() {
    int crashCountdown = 0;
    if (!(SD.begin(SS))) {

        Serial.println("No SD.");
        while (!(SD.begin(SS))) {
            ledControl.single(15);
            delay(SD_CARD_CHECK_DELAY);
            ledControl.single(crashCountdown % 4);
            delay(SD_CARD_CHECK_DELAY);
            crashCountdown++;
            Serial.print("Crash Countdown ");
            Serial.println(crashCountdown);
            if (crashCountdown > 4) {
                return false;
            }
        }
    }
    return true;
}

void loop() {

    #ifdef CHECK_CPU
    checkCPU();
//  audioEngine.measure();
    #endif

    if(NO_FILES) {
        // TODO : Flash the lights to show there are no files
        return;
    }

    updateInterfaceAndDisplay();

    audioEngine.update();

    if(audioEngine.error) {
        // Too many read errors, reboot
        Serial.println("Audio Engine errors. Reboot");
        reBoot(0);
    }

    if (playState.channelChanged) {
        D(
        Serial.print("RM: Going to next channel : ");
        if(playState.channelChanged) Serial.print("RM: Channel Changed. ");
        if(audioEngine.eof) Serial.print("End of file.");
        Serial.println("");
        );

        playState.currentChannel = playState.nextChannel;

        audioEngine.changeTo(&fileScanner.fileInfos[playState.bank][playState.nextChannel]);

        playState.channelChanged = false;

        resetLedTimer = 0;

    }

}

void updateInterfaceAndDisplay() {

    uint16_t changes = checkInterface();
    updateDisplay(changes);
}

void updateDisplay(uint16_t changes) {
    if (showDisplay > SHOWFREQ) {
        showDisplay = 0;
    }
    if (bankChangeMode) {
        ledControl.showReset(1);// Reset led is on continuously when in bank change mode..
        if(!flashLeds) {
            ledControl.multi(playState.bank);
        }

    } else {
        ledControl.showReset(resetLedTimer < FLASHTIME); // flash reset LED
    }

 if (speedChangeMode) {
  ledControl.showReset(100);

  }else {
   ledControl.showReset(resetLedTimer < FLASHTIME); // flash reset LED
  }

    if (flashLeds) {
        if (ledFlashTimer < FLASHTIME * 4) {
            ledControl.multi(0x0F);
        } else if(ledFlashTimer < FLASHTIME * 8) {
            ledControl.multi(0);
        } else {
            ledFlashTimer = 0;
        }
    } else if (settings.showMeter && !bankChangeMode) {
        peakMeter();
    }
}

// INTERFACE //

uint16_t checkInterface() {

    uint16_t changes = interface.update();

    #ifdef RESET_TO_REBOOT
    if (changes & BUTTON_SHORT_PRESS) {
        reBoot(0);
    }
    #endif

//   BANK MODE HANDLING
    if((changes & BUTTON_LONG_PRESS) && !bankChangeMode) {
        D(Serial.println("Enter bank change mode"););
        bankChangeMode = true;
        flashLeds = true;
        ledFlashTimer = 0;
    } else if((changes & BUTTON_LONG_RELEASE) && bankChangeMode) {
        D(Serial.println("Exit bank change mode"););
        flashLeds = false;
        bankChangeMode = false;
    }

    if(changes & BUTTON_PULSE) {
        flashLeds = false;
        if(bankChangeMode) {
            D(Serial.println("BUTTON PULSE"););
            nextBank();
        } else {
            D(Serial.println("Button Pulse but not in bank mode"););
        }

    }

    boolean resetTriggered = changes & RESET_TRIGGERED;

    bool skipToStartPoint = false;
    bool speedChange = false;

// if ( changes != 0){
// D(Serial.println("changes : " + (String) changes););
// }
    if(settings.speedControl) {

        if(resetTriggered && !settings.looping) {
            skipToStartPoint = true;
        }

    if((changes  & CHANGE_START_NOW  ) && speedChangeMode ) {
      speedChange = true;
    } else if((changes & CHANGE_START_NOW) && !speedChangeMode) {
   skipToStartPoint = true;
    }

        // If start Pot or start CV have changed and they are immediate
        // change speed
if ((changes & BUTTON_SHORT_PRESS )&& !speedChangeMode){
   D(Serial.println("enter speedmode"););
     speedChangeMode =true;
} else if ((changes & BUTTON_SHORT_PRESS )&& speedChangeMode){
   D(Serial.println("exit speedmode"););
     speedChangeMode =false;
}

        else    if(resetTriggered && !speedChangeMode ) {
     D(Serial.println("RESET"););
            skipToStartPoint = true;
        }
    }

    if((changes & CHANNEL_CHANGED) && resetTriggered) {
     D(Serial.println("channel changed"););
        playState.channelChanged = true;
    }

    if(speedChange) doSpeedChange();
    if(skipToStartPoint && !playState.channelChanged) {
        //if(settings.speedControl) {
        //  audioEngine.skipTo(0);
    //  } else {
            audioEngine.skipTo(interface.time);
    //  }

    }

    return changes;
}

void doSpeedChange() {
    // SPEED CHANGE
    // first map : -1650 to 1650
    float speed = 1.0;
    if(settings.quantizeNote) {
        speed = (float)map(interface.time, 0, 8192, 0, settings.noteRange);
        speed -= (int) settings.noteRange / 2;
        speed = pow(2,speed / 12);
    } else {
        //speed = (float)map(interface.time, 0, 8192, -1625,1625);
    speed = (float)map(interface.time, 0, 8192, -3000,1000);
        speed = pow(2, speed * 0.001);
    }
    audioEngine.setPlaybackSpeed(speed);
}

void nextBank() {

    if(fileScanner.activeBanks == 1) {
        D(Serial.println("Only 1 bank."););
        return;
    }
    playState.bank++;
    if (playState.bank > fileScanner.activeBanks) {
        playState.bank = 0;
    }
    if (playState.nextChannel >= fileScanner.numFilesInBank[playState.bank])
        playState.nextChannel = fileScanner.numFilesInBank[playState.bank] - 1;
    interface.setChannelCount(fileScanner.numFilesInBank[playState.bank]);
    playState.channelChanged = true;

    D(
        Serial.print("RM: Next Bank ");
        Serial.println(playState.bank);
    );

    meterDisplayTimer = 0;
    EEPROM.write(EEPROM_BANK_SAVE_ADDRESS, playState.bank);
}

#ifdef ENGINE_TEST
boolean tested = false;
int testIndex = 0;

void engineTest() {

    if(!tested) {
        audioEngine.test(fileScanner.fileInfos[playState.bank][0],fileScanner.fileInfos[playState.bank][1]);
        tested = true;
    }

    uint8_t changes = interface.update();

    if(changes & BUTTON_SHORT_PRESS) {
        testIndex += 2;
        if(testIndex >= fileScanner.numFilesInBank[playState.bank]) {
            Serial.println("Back to start");
            testIndex = 0;
        }
        audioEngine.test(fileScanner.fileInfos[playState.bank][testIndex],fileScanner.fileInfos[playState.bank][testIndex+1]);
    }

    return;
}
#endif

void peakMeter() {
  if( (fps < 50) || (meterDisplayTimer < settings.meterHide) ) return;

    float peakReading = audioEngine.getPeak();
    int monoPeak = round(peakReading * 4);
    monoPeak = round(pow(2, monoPeak));
    ledControl.multi(monoPeak - 1);
    fps = 0;
}