Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

32 floppy drives problem (and help with some program improvements) #224

Open
RadekCasual opened this issue Dec 26, 2024 · 21 comments
Open

Comments

@RadekCasual
Copy link

Hello, I have a problem with setting the program for this specific floppy drive configuration and some additional improvements.

  • sorry for my bad English, but it was translated by Google Translate.

I have 4 pcs Arduino UNO:

  • 8 floppy drives are connected to each of them, but I want to set the moppy 2 program (or in the microcontroller software) to divide these stations so that I end up with 8 separate voices (MIDI channels) with 4 floppy drives in each of them, is it possible, and if so, how?
  1. For additional improvements, I would like someone to help me with:
  • The velocity parameter in MIDI decided how many stations (of the 4 in each midi channel) were to be turned on.

  • Make ADSR-like shape and simulate a musical instrument, like a piano (exponential decay) or string instrument (sine, "vibrato"). Floppotron 2.0 had this option.

And most importantly:

  • So how can I also control flatbed scanners (for high notes) and hard drives (for drums, midi channel 10) using moppy 2? I am interested in purely software things, I know how to implement such a function at the hardware level.

I would like to thank everyone in advance for their help and interest. If I succeed, I promise that I will expand the project to insane proportions (it's my passion) and the videos will be available on YouTube and anyone who helps me in this post will definitely be tagged.

@RadekCasual
Copy link
Author

RadekCasual commented Dec 26, 2024

I don't know if this is true because ChatGPT said it, but this is how it "can" be implemented. I don't really know how to code, so I'm sending it for checking, and if it's useful for something, I don't know where to implement it in the code, but here it is:

To implement an envelope effect (ADSR) for floppy drives in Moppy, we need to simulate the dynamics of sound by modulating the motor's behavior over time. Here's how it can be done, with a focus on creating a customizable Attack, Decay, Sustain, and Release envelope effect.

  1. Concept of ADSR for Floppy Drives

An ADSR envelope shapes the volume or pitch of a sound over time

Attack: The sound ramps up from zero to full intensity.
Decay: The sound decreases to the sustain level.
Sustain: The sound holds at a steady level while the note is sustained.
Release: The sound fades out when the note ends.

For floppy drives

Intensity can be represented by the stepper motor pulse frequency (higher frequency = higher pitch).
By modulating this frequency over time, we can create the envelope effect.
  1. Implementation Details
    Variables for ADSR Parameters

We can define parameters for each stage of the envelope:

// ADSR Envelope Parameters (in milliseconds or steps)
unsigned int attackTime = 100;   // Time to reach peak (ms)
unsigned int decayTime = 200;    // Time to decay to sustain level (ms)
float sustainLevel = 0.5;        // Sustain level as a percentage of peak (0.0 - 1.0)
unsigned int releaseTime = 300;  // Time to fade out (ms)

// Note state
bool isNoteOn = false;           // Whether the note is active
unsigned long noteStartTime = 0; // Timestamp for the start of the note
unsigned long releaseStartTime = 0; // Timestamp for the start of release

Envelope Calculation

The envelope will be applied by adjusting the motor's pulse frequency dynamically in the loop() function or a dedicated timing interrupt.

float getEnvelopeLevel(unsigned long elapsedTime, unsigned long noteDuration) {
    if (elapsedTime < attackTime) {
        // Attack phase: ramp up
        return (float)elapsedTime / attackTime;
    } else if (elapsedTime < attackTime + decayTime) {
        // Decay phase: reduce to sustain level
        float decayProgress = (float)(elapsedTime - attackTime) / decayTime;
        return 1.0 - (1.0 - sustainLevel) * decayProgress;
    } else if (elapsedTime < noteDuration) {
        // Sustain phase: hold sustain level
        return sustainLevel;
    } else {
        // Release phase: fade out
        unsigned long releaseElapsed = elapsedTime - noteDuration;
        if (releaseElapsed < releaseTime) {
            return sustainLevel * (1.0 - (float)releaseElapsed / releaseTime);
        } else {
            // Note completely off
            return 0.0;
        }
    }
}

Using the Envelope to Control the Drive

We map the envelope level to the drive's pitch (step frequency). Here's an example:

void playNoteWithEnvelope(byte note, unsigned long noteDuration) {
    unsigned long startTime = millis();
    while (millis() - startTime < noteDuration + releaseTime) {
        unsigned long elapsedTime = millis() - startTime;
        float envelopeLevel = getEnvelopeLevel(elapsedTime, noteDuration);

        // Map envelope level to stepper motor frequency
        int baseFrequency = noteToFrequency(note); // Convert MIDI note to base frequency
        int currentFrequency = baseFrequency * envelopeLevel;

        // Drive the floppy motor at the calculated frequency
        if (currentFrequency > 0) {
            setMotorFrequency(currentFrequency); // Function to adjust motor frequency
        }
    }

    stopMotor(); // Stop motor after the envelope ends
}
  1. Adding Vibrato (Optional)

For vibrato (pitch modulation), add a sine wave oscillation to the base frequency:

float vibratoFrequency = 5.0; // Oscillations per second
float vibratoDepth = 0.1;     // Pitch deviation as a percentage of base frequency

int applyVibrato(int baseFrequency, unsigned long time) {
    float vibratoEffect = sin(2 * PI * vibratoFrequency * time / 1000.0) * vibratoDepth;
    return baseFrequency * (1.0 + vibratoEffect);
}

In the envelope function, replace:

int currentFrequency = baseFrequency * envelopeLevel;

With:

int currentFrequency = applyVibrato(baseFrequency, millis()) * envelopeLevel;
  1. Integration

    Timing Control: Use millis() for precise timing in the loop or implement a timer interrupt.
    ADSR Parameters: Allow users to customize ADSR parameters for each note or globally.
    MIDI Note Mapping: Map MIDI note-on/off events to trigger playNoteWithEnvelope().

  2. Full Example Snippet

Here’s a minimal example of ADSR applied to a floppy drive:

#include <Arduino.h>

// ADSR parameters
unsigned int attackTime = 100;
unsigned int decayTime = 200;
float sustainLevel = 0.5;
unsigned int releaseTime = 300;

// Note playing function
void playNoteWithEnvelope(byte note, unsigned long duration) {
    unsigned long startTime = millis();
    while (millis() - startTime < duration + releaseTime) {
        unsigned long elapsedTime = millis() - startTime;
        float envelopeLevel = getEnvelopeLevel(elapsedTime, duration);

        int baseFrequency = noteToFrequency(note);
        int currentFrequency = baseFrequency * envelopeLevel;

        setMotorFrequency(currentFrequency);
    }
    stopMotor();
}

// Envelope level calculation
float getEnvelopeLevel(unsigned long elapsedTime, unsigned long noteDuration) {
    if (elapsedTime < attackTime) {
        return (float)elapsedTime / attackTime;
    } else if (elapsedTime < attackTime + decayTime) {
        float decayProgress = (float)(elapsedTime - attackTime) / decayTime;
        return 1.0 - (1.0 - sustainLevel) * decayProgress;
    } else if (elapsedTime < noteDuration) {
        return sustainLevel;
    } else {
        unsigned long releaseElapsed = elapsedTime - noteDuration;
        if (releaseElapsed < releaseTime) {
            return sustainLevel * (1.0 - (float)releaseElapsed / releaseTime);
        } else {
            return 0.0;
        }
    }
}

// Placeholder for frequency setting
void setMotorFrequency(int frequency) {
    // Implementation to control motor at the given frequency
}

// Placeholder for stopping the motor
void stopMotor() {
    // Stop the motor
}

// Placeholder for MIDI note-to-frequency conversion
int noteToFrequency(byte note) {
    return 440 * pow(2, (note - 69) / 12.0); // A4 = 440Hz
}

@DJthefirst
Copy link

DJthefirst commented Dec 26, 2024

Just some initial thoughts and I might respond more later if I have time.

8 floppy drives are connected to each of them, but I want to set the moppy 2 program (or in the microcontroller software) to divide these stations so that I end up with 8 separate voices (MIDI channels) with 4 floppy drives in each of them, is it possible, and if so, how?

Yes, it is possible. You would need to create your own instrument class in the microcontroller code. The easiest way to do this would be to copy the existing Floppy class and add your changes. You should not need to implement anything on the GUI / Server side.

For additional improvements, I would like someone to help me with:
The velocity parameter in MIDI decided how many stations (of the 4 in each midi channel) were to be turned on.

Sammy would know off the top of his head but I would have to recheck the core code. Moppy might not implement velocity which means no ADSR. I did not see it in my quick scan through the code. My project Mechanical Midi Music does have velocity built in and I use it for controlling LEDs but it is still a work in progress with some bugs and limitations. I will talk about it more below.

Make ADSR-like shape and simulate a musical instrument, like a piano (exponential decay) or string instrument (sine, "vibrato"). Floppotron 2.0 had this option.

Floppytron had a custom hardware solution and higher resolution processors than the Arduino Uno does. I also assume he used some hardware-specific code optimizations as well. This leads to a limitation in the microcontroller. The interrupt is only toggled every 40 us on the Arduino Uno meaning you lose resolution with higher frequency notes and they become out of tune. Moppy might not have modulation control but it does have pitch bend. Pitch bend only works with low notes bc freq is logarithmic and you have more intermediate freq between low notes. For high notes, you hear stair stepping or it can't even play the note. I use an ESP32 which gets me down to 7-8 us which can handle mid-high notes well but it is still not enough for the highest notes and smooth pitch bends. I have a solution I am working on with SPI signal generators but you need a custom PCB and Amplifiers.

I have not done automatic ADSR shaping but I have manually duplicated MIDI tracks on my setup to play multiple of the same notes across floppy drives amplifying sound.

So how can I also control flatbed scanners (for high notes) and hard drives (for drums, midi channel 10) using moppy 2? I am interested in purely software things, I know how to implement such a function at the hardware level.

Scanners while working will be slightly out of tune with high notes on Arduino for the reasons listed above mid notes should be fine. The code should already be implemented for the scanners. Scanners are typically stepper motors so you will need a L29HN or easy driver for use with Moppy's existing instruments. Drums should also be already implemented as a different instrument class but I haven't looked into it.

I don't know if this is true because ChatGPT said it, but this is how it "can" be implemented. I don't really know how to code, so I'm sending it for checking, and if it's useful for something, I don't know where to implement it in the code, but here it is:

I didn't look at the GPT code but it should be easy to implement ADSR on the microcontroller in my opinion. The problem lies in whether Moppy implements velocity control and if the microcontroller is fast enough to handle ADSR.

I haven't worked on my codebase in the past few months after moving and getting a Job only filming videos every so often and working on Hardware. The main limitation of Mechanical Midi Music is It is written in Cpp 17 so only works on new microcontrollers primarily ESP32. I have plans to make a backward-compatible version but It is low on my to-do list. One benefit is my program is extremely easy to set up (potentially easier than Moppy I dare say) but the GUI is also web-based at the moment. I have a Desktop version that works but it is command line only and very poorly written in a hurry. I can fast-track my work on it if you are interested. The Main difference to Moppy is my Distributors run on the microcontroller vs Moppy mappers which map notes on the server. This means I have access to all midi controls on the microcontroller even breath control. For your use case you would need some shift registers to control 8 floppy drives off of one esp32 but it is theoretically doable.

My Youtube Channel https://www.youtube.com/watch?v=2lvIQnV8SYk&ab_channel=DJthefirst

@Sammy1Am
Copy link
Owner

8 floppy drives are connected to each of them, but I want to set the moppy 2 program (or in the microcontroller software) to divide these stations so that I end up with 8 separate voices (MIDI channels) with 4 floppy drives in each of them, is it possible, and if so, how?

It is possible. You'd end up with Device Addresses 1-4, and each would have two Sub Addresses. So you could map the notes like:

Channel Device Sub Address
0 1 1
1 1 2
2 2 1
3 2 2
...

As @DJthefirst mentioned though, you'll need to create a custom instrument here in order to combine four drives into a single Sub Address.

The velocity parameter in MIDI decided how many stations (of the 4 in each midi channel) were to be turned on.

Velocity is actually already being sent to the Arduino (see message construction here) but as far as I know isn't being consumed by any of the existing instruments. When you create a new custom instrument to combine the four drives into a single Sub Address, you can read the velocity value to set the appropriate number of drives to play.

So how can I also control flatbed scanners (for high notes) and hard drives (for drums, midi channel 10) using moppy 2? I am interested in purely software things, I know how to implement such a function at the hardware level.

Hardware is probably the way to go here. If you want to control e.g. a scanner or hard drive via software, you'll need to have a device driver for your OS that, essentially, can accept Moppy messages. There's no practical way to control the via software from an Arduino because it would be exceedingly difficult to make a USB scanner driver that ran on an Arduino. ESP chips might get closer as they can more easily do some USB communication, but you're still looking at reverse engineering the drivers.

I don't know if this is true because ChatGPT said it, but this is how it "can" be implemented.

It is not true. Right out of the gate ChatGPT says: Intensity can be represented by the stepper motor pulse frequency (higher frequency = higher pitch). but this is wrong. Higher pitched notes are not the same thing as being louder. The code ChatGPT generates goes on to wildly fluctuate the pitch of the notes rather than do anything at all about volume/intensity.

(The sine vibrato bit in theory could work, but is unnecessary as Moppy already supports MIDI pitch bending, so you can just vibrato that way.)

Make ADSR-like shape and simulate a musical instrument, like a piano (exponential decay)

You can certainly try this with four drives, but that's the equivalent of 2-bit audio, so you don't have a lot of range to work with, and certainly nothing exponential (i.e. "exponentially" decaying from 4 drives at once would look like 4 -> 2 -> OFF, which, is not only linear anyway, but doesn't even really use 1 or 3 drives-- might as well do linear decay 4->3->2->1->OFF). This, again, could be done using MIDI velocity, so if you add support for velocity in your instrument you can at least test it out by just editing your MIDI file and seeing how it sounds from there without having to implement any ADRS stuff in the embedded code at least at first.


I think adding an instrument with velocity support is a great idea. If you've got 32 drives, you could even group them as 4 instruments with 8 volume levels for some very expressive quartet playing. But I think writing the ADSR stuff for the Arduino may not be worth the effort. (I guess you could implement it in Java and just send additional messages that aren't mapped directly from the MIDI file, but that might start to get messy too)

@DJthefirst
Copy link

DJthefirst commented Dec 26, 2024

For the scanner if you plan to control it directly over the usb of the scanner you would need the driver stuff Sammy was talking about, but if you just control the internal stepper motor which most people do by splicing the wires to a stepper driver and Arduino you should be fine with the small caveat of high notes being out of tune on an uno.

Originally I missed the velocity data hidden in the payload[] bytes. It should be easy to implement the velocity volume control with the four drives. You could even do the fade in an out with a little extra effort though that might start getting risky with the processing power of an Arduino Uno. I don't think you should / can implement pitch ADSR or vibrato on the Uno due to the lack of frequency resolution. Moppy already has pitch bending though it is unusable at mid-high notes.

@RadekCasual
Copy link
Author

Yes, it is possible. You would need to create your own instrument class in the microcontroller code

It is possible. You'd end up with Device Addresses 1-4, and each would have two Sub Addresses.

As I mentioned before, I don't know much about programming and I rely 99% on chatGPT knowledge when it comes to this. If you can call it that, I'm more of a hardware type. But now, instead of chatGPT, I took AI strictly for programming and wrote longer and more comprehensive prompts, but I can't say whether it's perfect (I've already noticed an error related to the definition of 16 pins, while Uno has them from D0 to D13). If you have time, it would be a great help if you could check the code and tell me what's wrong with it @Sammy1Am @DJthefirst .

Modified FloppyDrives.h:

#ifndef SRC_MOPPYINSTRUMENTS_FLOPPYDRIVES_H_
#define SRC_MOPPYINSTRUMENTS_FLOPPYDRIVES_H_
#include <Arduino.h>
#include "MoppyTimer.h"
#include "MoppyInstrument.h"
#include "../MoppyConfig.h"
#include "../MoppyNetworks/MoppyNetwork.h"

namespace instruments {
  class FloppyDrives : public MoppyInstrument {
  public:
      void setup();
  protected:
      void sys_sequenceStop() override;
      void sys_reset() override;
      void dev_reset(uint8_t subAddress) override;
      void dev_noteOn(uint8_t subAddress, uint8_t payload[]) override;
      void dev_noteOff(uint8_t subAddress, uint8_t payload[]) override;
      void dev_bendPitch(uint8_t subAddress, uint8_t payload[]) override;
      void deviceMessage(uint8_t subAddress, uint8_t command, uint8_t payload[]);
  private:
    static unsigned int MIN_POSITION[];
    static unsigned int MAX_POSITION[];
    static unsigned int currentPosition[];
    static int currentState[];
    static unsigned int currentPeriod[];
    static unsigned int currentTick[];
    static unsigned int originalPeriod[];
    static byte DriveMap[];

    static const byte FIRST_DRIVE = 1;
    static const byte LAST_DRIVE = 8;
    static const byte DRIVES_PER_CHANNEL = 4;
    static const byte MAX_FLOPPY_NOTE = 71;

    static void resetAll();
    static void togglePin(byte driveNum, byte pin, byte direction_pin);
    static void haltAllDrives();
    static void reset(byte driveNum);
    static void tick();
    static void blinkLED();
    static void startupSound(byte driveNum);
    static void setMovement(byte driveNum, bool movementEnabled);
    static uint8_t getActiveDrivers(uint8_t velocity);
  };
}
#endif

Modified FloppyDrives.cpp:

#include "MoppyInstrument.h"
#include "FloppyDrives.h"

namespace instruments {

unsigned int FloppyDrives::MIN_POSITION[] = {0,0,0,0,0,0,0,0,0};
unsigned int FloppyDrives::MAX_POSITION[] = {158,158,158,158,158,158,158,158,158};
unsigned int FloppyDrives::currentPosition[] = {0,0,0,0,0,0,0,0,0};
int FloppyDrives::currentState[] = {0,0,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW};
unsigned int FloppyDrives::currentPeriod[] = {0,0,0,0,0,0,0,0,0};
unsigned int FloppyDrives::currentTick[] = {0,0,0,0,0,0,0,0,0};
unsigned int FloppyDrives::originalPeriod[] = {0,0,0,0,0,0,0,0,0};
byte FloppyDrives::DriveMap[] = {0, 2, 4, 6, 8, 10, 12, 14, 16};

void FloppyDrives::setup() {
  for(int i = 1; i <= LAST_DRIVE; i++) {
    pinMode(DriveMap[i], OUTPUT);
    pinMode(DriveMap[i] + 1, OUTPUT);
  }
  
  resetAll();
  delay(500);
  MoppyTimer::initialize(TIMER_RESOLUTION, tick);

  if (PLAY_STARTUP_SOUND) {
    startupSound(FIRST_DRIVE);
    delay(500);
    resetAll();
  }
}

uint8_t FloppyDrives::getActiveDrivers(uint8_t velocity) {
    if (velocity <= 31) return 1;
    if (velocity <= 63) return 2;
    if (velocity <= 95) return 3;
    return 4;
}

void FloppyDrives::dev_noteOn(uint8_t subAddress, uint8_t payload[]) {
    if (payload[0] <= MAX_FLOPPY_NOTE) {
        uint8_t note = payload[0];
        uint8_t velocity = payload[1];
        
        // Calculate starting drive index for this channel
        uint8_t baseIndex = ((subAddress - 1) * DRIVES_PER_CHANNEL) + 1;
        uint8_t activeDrivers = getActiveDrivers(velocity);
        
        // Set period for active drives in this channel
        for(int i = 0; i < DRIVES_PER_CHANNEL; i++) {
            uint8_t driveIndex = baseIndex + i;
            if(i < activeDrivers) {
                currentPeriod[driveIndex] = originalPeriod[driveIndex] = noteDoubleTicks[note];
            } else {
                currentPeriod[driveIndex] = originalPeriod[driveIndex] = 0;
            }
        }
    }
}

void FloppyDrives::dev_noteOff(uint8_t subAddress, uint8_t payload[]) {
    uint8_t baseIndex = ((subAddress - 1) * DRIVES_PER_CHANNEL) + 1;
    
    for(int i = 0; i < DRIVES_PER_CHANNEL; i++) {
        uint8_t driveIndex = baseIndex + i;
        currentPeriod[driveIndex] = originalPeriod[driveIndex] = 0;
    }
}

void FloppyDrives::tick() {
    for (int d = 1; d <= LAST_DRIVE; d++) {
        if (currentPeriod[d] > 0) {
            currentTick[d]++;
            if (currentTick[d] >= currentPeriod[d]) {
                togglePin(d, DriveMap[d], DriveMap[d]+1);
                currentTick[d] = 0;
            }
        }
    }
}

// Rest of the code remains unchanged...

Thanks in advance

@Sammy1Am
Copy link
Owner

I, unfortunately, don't have the time or resources to write this all for you at the moment. I can sort of steer you in the correct direction, but at the end of the day you're going to need to be able to do some troubleshooting and coding yourself.

The AI's attempt doesn't seem super far off though, so a few notes in case it's enough to get you there:

  • You'll also need to update MoppyConfig.h to set MAX_SUB_ADDRESS to 2.
  • Sometimes MIDI files will contain a note_on where velocity is 0, which is the same as turning the note off. Not sure what's up with this, but you'll likely want to check to make sure velocity is greater than 0 in either getActiveDrivers or dev_noteOn. It's possible I'm checking for this already on the Java side though, so maybe if you don't have trouble with this just don't worry about it until it comes up.
  • You said I've already noticed an error related to the definition of 16 pins, while Uno has them from D0 to D13, but pins A0-A5 are pin numbers 14-19.
  • The AI added FloppyDrives::DriveMap[], but includes pin 0, which cannot be used to control a drive as it's used for serial communication. However, the AI's code also starts the for loop in FloppyDrives::tick() at index 1, and skips pin 0. So... I think it's probably safe? But definitely a little weird. Not sure why it felt the need to change tick at all. Its version is a little more readable, but might require more runtime lookups which isn't ideal for this application (but the compiler might be smart enough to not actually do that).

More importantly: what happens when you try to run this code? Is it close to doing what you'd like?

@RadekCasual
Copy link
Author

Ok, let's simplify things a bit: Each Arduino is a separate voice (midi track). I won't bother with separating floppy drives anymore, so instead of 4 Arduinos, I'll buy 4 more. Why? I want to use this floppy shield: https://hackaday.com/2014/10/12/a-simple-floppy-music-controller/ . I want to use more durable, dedicated 34-pin cables for the floppy drive and make the wiring on the Arduino side more aesthetic. However, I still want to implement the velocity function and I will try to do it using the knowledge acquired here. My question is: Will moppy work the same with this floppy shield?

@Sammy1Am
Copy link
Owner

It looks like the creator of the shield says it will, but also there doesn't seem to be any chips or anything on there so it should work fine.

@RadekCasual
Copy link
Author

I meant, do I need to make any changes to the code, e.g. different pin definitions, etc.?
And Will each Arduino be a separate voice (MIDI channel) by default?

@Sammy1Am
Copy link
Owner

I meant, do I need to make any changes to the code, e.g. different pin definitions, etc.?

Ah, it looks like it should be fine. Could be STEP and DIRECTION are swapped, but there aren't any other pins in between so the loop math should still work.

And Will each Arduino be a separate voice (MIDI channel) by default?
No, the default mappings in the Control GUI map each channel to a separate drive on the first Arduino. You can change the mappings to send one channel to each Arduino though, and only Sub Address 1 if you're implementing an instrument that supports velocity.

@DJthefirst
Copy link

Sammy does the mapper support velocity where you can specify to only send notes to a sub address when the velocity is above a certain value?

@RadekCasual
Copy link
Author

I have a skill issue so the AI ​​has once again extended the Floppy Drives.cpp code a bit to:

I, unfortunately, don't have the time or resources to write this all for you at the moment. I can sort of steer you in the correct direction, but at the end of the day you're going to need to be able to do some troubleshooting and coding yourself.

The AI's attempt doesn't seem super far off though, so a few notes in case it's enough to get you there:

* You'll also need to update `MoppyConfig.h` to set `MAX_SUB_ADDRESS` to `2`.

* Sometimes MIDI files will contain a note_on where velocity is `0`, which is the same as turning the note off.  Not sure what's up with this, but you'll likely want to check to make sure velocity is greater than 0 in either `getActiveDrivers` or `dev_noteOn`.  It's possible I'm checking for this already on the Java side though, so maybe if you don't have trouble with this just don't worry about it until it comes up.

* You said `I've already noticed an error related to the definition of 16 pins, while Uno has them from D0 to D13`, but pins A0-A5 are pin numbers 14-19.

* The AI added `FloppyDrives::DriveMap[]`, but includes pin 0, which cannot be used to control a drive as it's used for serial communication.  _However_, the AI's code also starts the for loop in `FloppyDrives::tick()` at index 1, and skips pin 0.  So... I think it's probably safe?  But definitely a little weird.  Not sure why it felt the need to change `tick` at all.  Its version is a little more readable, but might require more runtime lookups which isn't ideal for this application (but the compiler _might_ be smart enough to not actually do that).

More importantly: what happens when you try to run this code? Is it close to doing what you'd like?

  • and introduced the decay envelope function:

Decay – time of the amplitude decay from the maximum level to the sustain level i.e. "voice drop" by reducing the number of floppy disk drives.

I asked him in the prompt to keep the code as close to the original as possible, but once again I don't know how he managed it.

Anyway, here it is:

/*
* FloppyDrives.cpp
*
* Output for controlling floppy drives. The _original_ Moppy instrument!
*/
#include "MoppyInstrument.h"
#include "FloppyDrives.h"

namespace instruments {

/* Arrays for floppy drive control */
unsigned int FloppyDrives::MIN_POSITION[] = {0, 0, 0, 0, 0};
unsigned int FloppyDrives::MAX_POSITION[] = {158, 158, 158, 158, 158};
unsigned int FloppyDrives::currentPosition[] = {0, 0, 0, 0, 0};
unsigned int FloppyDrives::currentPeriod[] = {0, 0, 0, 0, 0};
unsigned int FloppyDrives::currentTick[] = {0, 0, 0, 0, 0};
unsigned int FloppyDrives::originalPeriod[] = {0, 0, 0, 0, 0};
bool FloppyDrives::currentDirection[] = {true, true, true, true, true};
bool FloppyDrives::currentState[] = {LOW, LOW, LOW, LOW, LOW};
unsigned int FloppyDrives::decayCounters[] = {0, 0, 0, 0}; // Counters for decay

const int stepPins[4] = {2, 4, 6, 8}; // Step pins for drives 1-4
const int directionPins[4] = {3, 5, 7, 9}; // Direction pins for drives 1-4

const unsigned int DECAY_RATE = 50; // Rate of decay in timer ticks

void FloppyDrives::setup() {
// Prepare pins
for (int i = 0; i < 4; i++) {
pinMode(stepPins[i], OUTPUT);
pinMode(directionPins[i], OUTPUT);
}

// Reset all drives
resetAll();
delay(500);

// Setup timer to handle interrupts for floppy driving
MoppyTimer::initialize(TIMER_RESOLUTION, tick);

// If MoppyConfig wants a startup sound, play it on the first drive
if (PLAY_STARTUP_SOUND) {
startupSound(0); // Drive 0 is the first drive
delay(500);
resetAll();
}
}

// Play startup sound to confirm drive functionality
void FloppyDrives::startupSound(byte driveNum) {
unsigned int chargeNotes[] = {
noteDoubleTicks[31],
noteDoubleTicks[36],
noteDoubleTicks[38],
noteDoubleTicks[43],
0
};
byte i = 0;
unsigned long lastRun = 0;
while (i < 5) {
if (millis() - 200 > lastRun) {
lastRun = millis();
currentPeriod[driveNum] = chargeNotes[i++];
}
}
}

// Message Handlers
void FloppyDrives::sys_reset() {
resetAll();
}

void FloppyDrives::sys_sequenceStop() {
haltAllDrives();
}

void FloppyDrives::dev_reset(uint8_t subAddress) {
if (subAddress == 0x00) {
resetAll();
} else {
reset(subAddress);
}
}

void FloppyDrives::dev_noteOn(uint8_t subAddress, uint8_t payload[]) {
if (payload[1] > 0 && payload[0] <= MAX_FLOPPY_NOTE) { // Check velocity > 0
currentPeriod[subAddress] = originalPeriod[subAddress] = noteDoubleTicks[payload[0]];
decayCounters[subAddress] = 0; // Reset decay counter
} else {
dev_noteOff(subAddress, payload); // Treat as note off
}
}

void FloppyDrives::dev_noteOff(uint8_t subAddress, uint8_t payload[]) {
// Start decay process instead of immediate stop
decayCounters[subAddress] = DECAY_RATE; // Set decay counter
}

void FloppyDrives::dev_bendPitch(uint8_t subAddress, uint8_t payload[]) {
int16_t bendDeflection = payload[0] << 8 | payload[1];
currentPeriod[subAddress] = originalPeriod[subAddress] /
pow(2.0, BEND_OCTAVES * (bendDeflection / (float)8192));
}

void FloppyDrives::deviceMessage(uint8_t subAddress, uint8_t command, uint8_t payload[]) {
switch (command) {
case NETBYTE_DEV_SETMOVEMENT:
setMovement(subAddress, payload[0] == 0);
break;
}
}

void FloppyDrives::setMovement(byte driveNum, bool movementEnabled) {
if (movementEnabled) {
MIN_POSITION[driveNum] = 0;
MAX_POSITION[driveNum] = 158;
} else {
MIN_POSITION[driveNum] = 79;
MAX_POSITION[driveNum] = 81;
}
}

// Floppy driving functions

void FloppyDrives::tick() {
for (int d = 0; d < 4; d++) { // Operate on drives 0-3
if (decayCounters[d] > 0) { // Handle decay
decayCounters[d]--;
if (decayCounters[d] == 0) {
currentPeriod[d] = 0; // Fully stop drive after decay
}
}

if (currentPeriod[d] > 0) { // If drive is active
currentTick[d]++;
if (currentTick[d] >= currentPeriod[d]) { // If period reached
currentState[d] = !currentState[d];
digitalWrite(stepPins[d], currentState[d]);
currentTick[d] = 0;

// Update position and direction
if (currentState[d] == HIGH) {
if (currentDirection[d]) {
currentPosition[d]++;
if (currentPosition[d] >= MAX_POSITION[d]) {
currentDirection[d] = false; // Reverse direction
digitalWrite(directionPins[d], LOW);
}
} else {
if (currentPosition[d] > 0) {
currentPosition[d]--;
} else {
currentDirection[d] = true; // Reverse direction
digitalWrite(directionPins[d], HIGH);
}
}
}
}
}
}
}

void FloppyDrives::haltAllDrives() {
for (int d = 0; d < 4; d++) {
currentPeriod[d] = 0;
}
}

void FloppyDrives::reset(byte driveNum) {
currentPeriod[driveNum] = 0; // Stop note
decayCounters[driveNum] = 0; // Stop decay

digitalWrite(directionPins[driveNum], HIGH); // Reverse direction
for (unsigned int s = 0; s < MAX_POSITION[driveNum]; s++) {
digitalWrite(stepPins[driveNum], HIGH);
digitalWrite(stepPins[driveNum], LOW);
delay(5);
}
currentPosition[driveNum] = 0;
currentDirection[driveNum] = true; // Ready to go forward
digitalWrite(directionPins[driveNum], LOW);
}

void FloppyDrives::resetAll() {
for (int d = 0; d < 4; d++) {
currentPeriod[d] = 0;
decayCounters[d] = 0; // Stop decay
digitalWrite(directionPins[d], HIGH); // Reverse direction
}
for (unsigned int s = 0; s < MAX_POSITION[0]; s++) {
for (int d = 0; d < 4; d++) {
digitalWrite(stepPins[d], HIGH);
digitalWrite(stepPins[d], LOW);
}
delay(5);
}
for (int d = 0; d < 4; d++) {
currentPosition[d] = 0;
currentDirection[d] = true; // Ready to go forward
digitalWrite(directionPins[d], LOW);
}
}
} // namespace instruments

@RadekCasual RadekCasual reopened this Dec 28, 2024
@RadekCasual
Copy link
Author

Sorry, I accidentally closed it

@Sammy1Am
Copy link
Owner

Sammy does the mapper support velocity where you can specify to only send notes to a sub address when the velocity is above a certain value?

Oh! It does! Velocity is assigned to the variable v. So actually this could all be done in the GUI without any code changes at all!

I'll try to come up with the script tonight if you haven't got it by then.

@Sammy1Am
Copy link
Owner

Alright, give this a try with one Arduino and 4 drives:

Condition Device Address Sub Address Note
c==0 && v>=1 1 1 n
c==0 && v>=32 1 2 n
c==0 && v>=64 1 3 n
c==0 && v>=96 1 4 n

This should play everything from MIDI channel 1 (I'm fairly sure they're 0-indexed, but if nothing happens, try replacing the 0 with a 1) on the first Arduino. Every note will be played on the first drive, notes with velocity over 32 on the first and second, etc.

Basically should accomplish what you're after without having to make any modifications to the code (thanks @DJthefirst , great idea)

@RadekCasual
Copy link
Author

Oh! It does! Velocity is assigned to the variable v. So actually this could all be done in the GUI without any code changes at all!

That sounds cool!
Then I'm waiting for the script (it just arrived as I write this).

@DJthefirst I have a question about your comment from a few days ago:

The interrupt is only toggled every 40 us on the Arduino Uno meaning you lose resolution with higher frequency notes and they become out of tune

Can Arduino handle sounds with + - the same frequency as your stepper motors in this video https://www.youtube.com/watch?v=2lvIQnV8SYk

@RadekCasual
Copy link
Author

Alright, give this a try with one Arduino and 4 drives:

Thanks, it finally worked!

And how to make each subsequent Arduino a separate midi track but also with velocity support?

it seems to me that:

Condition:
c==1
c==2
e.t.c

and

Device Address:
2
3
e.t.c

am I right?

@DJthefirst
Copy link

C is channel which Sammy is saying is 0-15 or 1-16 for midi where ch0 = ch1 or ch1 =ch1

Address is Arduino 1,2,3,etc..

SubAddress is Floppy 1,2,3,etc.. on a given Arduino

So on 1 Arduino you will have 2 channels

As for the note resolution Arduino is 40us a c3 note is 130hz or 7692us c3# is 7216us a 476us difference or 4% off between notes. Barely notable to most people. But only 11 pitch bend steps between notes. I use esp32 which are faster with an 8us resolution < 1% accuracy
With 60 pitch bend steps.

a c5 note is 523hz or 1912us and c5# 1804us difference of 72us you can be off by 1/4 of a note at 40us very noticeable. pitch bend is 2 steps between notes. Esp32 is 9 steps

Now c6 is 956us and c6# is 902us a difference of 54us. With only a 40us resolution this means the note can be up to +- 1/2 a note. A pitch bend would almost be a whole note entirely unusable. Esp32 6 steps.

@RadekCasual
Copy link
Author

RadekCasual commented Dec 30, 2024

@DJthefirst Do you think it's worth replacing all the Arduinos in this project with esp32?

If so, how to connect floppy drives, a4988 drivers for stepper motors and hdd drives (everything I have in Arduino so far) to esp32?

How many pieces of esp32 do I need?

Does esp32 work with Moppy2?

@DJthefirst
Copy link

I don't think it is worth replacing for floppy drives. Moppy I think has support for esp32 but it might not be very well tested. You also have less pins. I was just making you aware of problems you might run into or to look out for. If you run into problems it might be worth looking into something like the Arduino due but I haven't used it to know what the minimum resolution is. Either way you still won't be able to pitch bend well on esp or Arduino with Moppy as is.

@RadekCasual
Copy link
Author

What about your Mechanical-Midi-Music?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants