复古留声机式 arduino 音乐盒-Arduino中文社区 - Powered by Discuz!

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 18139|回复: 17

[项目] 复古留声机式 arduino 音乐盒

[复制链接]
发表于 2019-4-30 17:42 | 显示全部楼层 |阅读模式
0.jpg

前言

无论是 arduino 还是 51 还是啥单片机或 STEAM 教育,很多的音乐方面的练习就是生硬的机械的刺耳的哔哔声,如果让单片机发出悦耳一点的声音多好啊。比如清脆美妙的音乐盒的声音。
经过多番搜索,功夫不负有心人,找到了一些相关的内容能够让 arduino 发出漂亮的声音。于是打算动手做一个音乐盒。
电子的音乐盒自然想让功能多一点。想到最早的留声机那美丽的外观,就连现在很多音乐盒也用的留声机的外观呢,那么我们的电子音乐盒也做成留声机外观吧。
既然都做成留声机外观了,那不如也做出可以更换唱片的功能呗。其实就是机械的音乐盒也有可以更换唱片的,看上去非常的昂贵哦,也有用纸带的,这两种方式都是通过打孔来制作音乐音符。那么我们发挥电子的优势,把打孔的音符简化为用笔涂黑,使用反射式红外传感器(就是循迹小车的循迹传感器)来检测音符,从而播放自己画出来的唱片的音乐。

材料
1.     arduino pro mini 一块
2.     360度云台一个
3.     360度舵机一个
4.     喇叭一个,最好带功放
5.     电位器模块一个
6.     洞洞板 2 片
7.     反射式红外传感器8个
8.     2K可调电阻8个
9.     支架一套
10.  铜柱、螺丝若干
11.  导线等焊接材料
12.  排针及排母若干
13.  做装饰喇叭的牛皮纸一张
14.  做唱片的白纸若干


制作过程
  • 音符传感器的制作
    硬件小白,在制作的时候一直打退堂鼓,为了使做出来的外观像个留声机/唱片机,检测输入音符的这个“唱臂”也要做的细一些,不能直接用整块板,切这洞洞板就费了九牛之力,还要切成三片。这也算是平生第一次做的三层板了,还是立体的呢!把走线藏在里面,最后做出来的成品还算过得去,对于非专业的自己,对这个成果已经很满意了。
    1.jpg
    xx.jpg
  • 底座的制作
    机械小白,看到论坛有云台硬件申请,看起来刚好像是一个唱片的机构,直接拿来当底座用,装个电位器用来控制转盘的旋转速度,以根据不同的曲风控制乐曲的播放快慢,加一个支架用来装“唱臂”,装喇叭。测试的时候用的 arduino UNO,为了把所有部件都塞进底座,最后组装的时候使用 arduino pro mini。为了连线,焊接了个洞洞板 IO 扩展板,这个其实非常简单,就是把排针往洞洞板上焊上就行了。
    2.jpg
  • 大喇叭的制作
    留声机最大的特点就是有一个大喇叭,3D 小白,不会建模,就用纸张做一个喽。那天在垃圾桶发现丢弃的一个文件袋,这颜色还真是和留声机的铜喇叭有几分相似,就从垃圾桶里捡出了这个袋子,经过尝试,剪剪粘粘做出了一个大喇叭。当然,这仅仅是一个外观的装饰用的,如果真正的那个喇叭可以调整安装位置的话,这个装饰的喇叭倒是可以用来做个扩音的功能。
    3.jpg
  • 组装成品及绘制唱片
    唱片根据云台的大小切个圆纸片,根据传感器的大小距离画出八个同心圆,然后用量角器等分圆。分的太细的话,对传感器来说精度和灵敏度要求比较高,最后还是用一个不那么细分的唱片来测试,上面画的是《小星星》的音符,才两句半的乐曲,将就一下吧……这里没有体现出可以同时发出多个音符的特点,实际上可以同时涂三个音哦。把大喇叭装上,拍一张唯美的照片,感觉还过得去啊。
    注意传感器阵列那里每两个之间用了个黑纸隔开,以避免邻近的红外光的干扰。
    在弄这个传感器的时候突然有一个大胆的想法,直接拿一个条码扫码器来扫描应该也是个好主意,精度可以更好呐!
    4.jpg

程序流程
  • 参考大神们的实现,调整和封装了一下音符播放的功能
  • 读取传感器状态,播放对应的音符
  • 读取电位器数据,控制转速

代码
这里附上两个代码,
一个是不需要唱片输入的程序内置音乐的程序,简单起见只固定一首乐曲,大家可以参考格式自己输入喜欢的音乐哦。这里的示例音乐是《千与千寻》的《与我同在》,挑的简单版本,没有很多的伴奏音,只有偶尔的双音,就这样已经让我手工输入了很久,保守估计40分钟吧,需要做一个工具来简化输入啊。网上参考来源的大神用的是 midi 文件导出的方式,我是为了做唱片输入,所以将格式做了调整,两份代码的音乐格式不通用哦。
另一个是使用手绘唱片来输入播放乐曲的代码,不过由于电机声挺大的,其实最终效果不怎么理想,仅仅是实现它,验证了一下想法。
大家可以使用第一个程序来做实验玩,只需要拿个喇叭一头接地一头接 UNO 的 11 引脚就可以开始了。
完整程序大家下载附件,下面展示主程序,头文件比较长影响版面就略过。
ArduinoMusicBoxFixed.zip (9.13 KB, 下载次数: 46)
ArduinoMusicBox.zip (8.11 KB, 下载次数: 22)


  • 内置音乐版本的代码
    [mw_shl_code=arduino,true]// ArduinoMusicBoxFixed.ino
    // 实现一个 Arduino 音乐盒,内置固定音乐版本
    // By seesea, 2019-03-20
    //
    // 很多的 arduino 示例做音乐播放或电子琴什么的,就只是一个 tone() 哔哔哔的声音,就想着怎样才能让 arduino 播放好听一点的声音呢。
    // 在逛一些论坛的时候有人在讨论日本的一个大神做的 avr 程序播放的很好听,但是没有源码。
    // 后来多番搜索有了一些收获,下面是几个比较相关的链接可以参考,按相关程度排序:
    // 1. arduino 音乐盒播放(页面末有代码下载链接),本程序参考来源:
    //     https://www.nutsvolts.com/magazine/article/april2012_Lindley
    // 2. arduino wav 声音播放器,arduino 联合创始人的文章,值得一看,上一链接作者的代码我看应该很大程度参考自这里:
    //     https://www.elektormagazine.com/files/attachment/331
    // 3. makezine 的教学,浅显易懂,推荐学习:
    //     https://makezine.com/projects/ma ... no-sound-synthesis/
    // 4. 日本大神的音乐盒,主要是原理介绍值得学习:
    //     http://elm-chan.org/works/mxb/report.html
    // 5. 直接数字合成器,理论讲解很详细:
    //     http://www.cs.nott.ac.uk/~pszjm2/?p=674
    // 6. 声音采样知识介绍:
    //     http://www.quintadicopertina.com ... ng-digitized-audio/
    // 7. arduino 声音合成库,功能强大:
    //     http://sensorium.github.io/Mozzi

    #include <stdint.h>
    #include <avr/interrupt.h>
    #include <avr/io.h>
    #include <avr/pgmspace.h>
    #include <util/atomic.h>
    #include "wavetableData.h"
    #include "pitchs.h"
    #include "songData.h"

    #define AUDIO_OUTPUT_PIN        11
    #define GENERATOR_COUNT         2
    #define SAMPLE_RATE             32000
    #define SUSTAIN_LENGTH          128
    #define SHIFT_BIT_NUM           8
    #define SENSOR_COUNT            8
    #define POTENTIOMETER_PIN       A0
    #define MOTOR_PWM_PIN           6

    const uint16_t WAVETABLE_LENGTH    = sizeof(waveTableData) / sizeof(waveTableData[0]);
    const uint16_t ENVELOPTABLE_LENGTH = sizeof(envelopeData) / sizeof(envelopeData[0]);

    // 注意,使用8个传感器,如果不是,需要调整相关代码
    // 功能按顺序为:高八度标记,音符 7 6 5 4 3 2 1
    const uint8_t sensorPins[SENSOR_COUNT] = { 2, 3, 4, 5, 7, 8, 9, 10 };

    // 声音合成器结构
    // 包括:相位步进,相位累计,包络数据表的索引
    // 后续如需扩展多乐器什么的,可以在这里增加乐器波表指针
    struct
    {
        uint16_t phaseStep;
        uint32_t phaseAccumulator;
        uint16_t envelopeIndex;
    } generators[GENERATOR_COUNT];

    // 初始化
    void systemInit()
    {
        // cli();

        pinMode(AUDIO_OUTPUT_PIN, OUTPUT);
        digitalWrite(AUDIO_OUTPUT_PIN, LOW);

        initSensorPins();

        for (int i = 0; i < GENERATOR_COUNT; i++)
        {
            generators.phaseStep        = 0;
            generators.phaseAccumulator = 0;
            generators.envelopeIndex    = 0;
        }

        initTimers();

        // sei();
    }

    // 配置定时器
    // 注意此函数调用前关中断
    // 比较底层,直接照搬大神的
    // 参考:
    //      https://www.nutsvolts.com/magazine/article/april2012_Lindley
    //      https://www.elektormagazine.com/files/attachment/331
    void initTimers()
    {
        /*
        * Timer 1 Configuration
        * Set up Timer 1 to generate interrupt at SAMPLE_RATE.
        */
        // Set CTC mode (Clear Timer on Compare Match)
        // Have to set OCR1A *after*, otherwise it gets reset to 0!
        TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
        TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));

        // No prescaler
        TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set the compare register (OCR1A).
        // OCR1A is a 16-bit register, so we have to do this with
        // interrupts disabled to be safe.
        OCR1A = F_CPU / SAMPLE_RATE;    // 16e6 / 32000 = 500

        // Enable interrupt when TCNT1 == OCR1A
        TIMSK1 |= _BV(OCIE1A);

        /*
        * Timer 2 Configuration
        * Set up Timer 2 to do pulse width modulation at frequency of 62,500 Hz
        */
        // Set fast PWM mode
        TCCR2A |= _BV(WGM21) | _BV(WGM20);
        TCCR2B &= ~_BV(WGM22);

        // Do non-inverting PWM on pin OC2A. Arduino pin 11
        TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
        TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));

        // No prescaler so PWM frequency is 16000000 / 256 or 62,500 Hz
        TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set initial pulse width to zero.
        OCR2A = 0;
    }

    // 定时器 1 的中断处理函数
    // 参考:https://www.nutsvolts.com/magazine/article/april2012_Lindley
    ISR(TIMER1_COMPA_vect)
    {

        int32_t  pwmSample = 0;
        uint32_t phase;
        uint32_t waveTableIndex;
        int32_t  sampleValue;
        uint8_t  envlopeValue;

        for (uint8_t i = 0; i < GENERATOR_COUNT; i++)
        {
            phase          = generators.phaseAccumulator;        // 取出步进累计值
            waveTableIndex = (phase >> SHIFT_BIT_NUM);              // 去除低位做为波表的索引
            sampleValue    = (int8_t) pgm_read_byte(waveTableData + waveTableIndex);
            envlopeValue   = pgm_read_byte(envelopeData + generators.envelopeIndex);
            pwmSample     += sampleValue * envlopeValue;            // 把波形值和包络相乘做为 pwm 的参数(需去除低位),注意的是这是所有的声音合成器的综合

            // 累加相位步进值
            // 如果到波表的结尾了,则开始增加包络的索引,进行声音大小的改变
            generators.phaseAccumulator += generators.phaseStep;
            if (generators.phaseAccumulator >= ((uint32_t) WAVETABLE_LENGTH) << SHIFT_BIT_NUM)
            {
                generators.phaseAccumulator -= (((uint32_t) SUSTAIN_LENGTH) << SHIFT_BIT_NUM);

                if (generators.envelopeIndex < ENVELOPTABLE_LENGTH - 1)
                {
                    generators.envelopeIndex++;
                }
            }
        }

        // 去除低位,留高位,缩小数值到波表的长度内
        pwmSample >>= (SHIFT_BIT_NUM + 2);

        // 加上偏移量避免负数后,设置给定时器 2
        OCR2A = (uint8_t)(127 + pwmSample);
    }

    // 将音符频率换算为相位步进值
    // 计算公式为:2^32 * 频率 / 采样率
    // 2^32 来源是相位累加 generator_t.phaseAccumulator 是 32 位的
    inline unsigned int hzToPhaseStep(float hz)
    {
        return (unsigned int) (hz * 4.096);
    }

    // 传入音符对应的频率来播放音符
    void playPitch(uint16_t pitchFreq)
    {
        static int8_t generatorIndex = 0;
        uint16_t step = hzToPhaseStep(pitchFreq);

        ++generatorIndex;
        if (generatorIndex >= GENERATOR_COUNT) {
            generatorIndex = 0;
        }

        // 注意关中断进行设置
        cli();
        generators[generatorIndex].phaseStep        = step;
        generators[generatorIndex].phaseAccumulator = 0;
        generators[generatorIndex].envelopeIndex    = 0;
        sei();
    }

    // 初始化传感器引脚,设置为内部上拉的输入
    void initSensorPins()
    {
        for (int i = 0; i < SENSOR_COUNT; ++i)
        {
            pinMode(sensorPins, INPUT_PULLUP);
        }
    }

    // 目前限制在8个传感器,所以用8位的变量返回值
    // 返回值的每一位表示一个传感器的状态
    uint8_t getAllSensorsInput()
    {
        uint8_t sensorsStatus = 0;

        for (int i = 0; i < SENSOR_COUNT; ++i)
        {
            sensorsStatus <<= 1;

            if (digitalRead(sensorPins) == LOW)
            {
                sensorsStatus |= 0x01;
            }
        }

        return sensorsStatus;
    }

    void readSensorAndPlayNote()
    {
        // 存放至少 16 个音符,这里为两个八度 C 大调音阶,可根据自己的乐曲定制相应的音符
        static const uint16_t noteBox[] = {
            NOTE_C3, NOTE_D3, NOTE_E3, NOTE_F3, NOTE_G3, NOTE_A3, NOTE_B3,
            NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4
        };

        static uint8_t lastSensorStatus = 0;
        uint8_t sensorStatus = getAllSensorsInput();
        uint8_t pitchOffset  = 0;

        // 除了高八度标记位外的其它音符位没有变化的时候,认为还没扫描到下一次音符输入,不做任何操作
        if ((0xEF & sensorStatus) == lastSensorStatus)
            return;

        // 高八度
        if ((0x80 & sensorStatus) != 0)
            pitchOffset = 7;

        // 从低到高检查每一位标志来进行音符播放
        sensorStatus &= 0xEF;
        for (uint8_t i = 0; i < SENSOR_COUNT - 1; ++i)
        {
            if (sensorStatus & 0x01)
            {
                playPitch(noteBox[i + pitchOffset]);
            }
            sensorStatus >>= 1;
        }

        // 记录旧值用于下次比较
        // 注意:sensorStatus 已经是 sensorStatus & 0xEF 了
        lastSensorStatus = sensorStatus;
    }

    void playSong(const uint16_t *songAddr)
    {   
        int index = 0;
        uint16_t note = 0;
        uint16_t beatTime = 0;
        unsigned long lastTime = millis();

        while (true)
        {
            delay(beatTime);
            /*
            Serial.println(millis());
            Serial.println("-----------");
            if (millis() - lastTime < beatTime)
                continue;
            */
            
            note     = pgm_read_word_near(songAddr + index);
            beatTime = pgm_read_word_near(songAddr + index + 1);

            if (note < 0)
                break;

            playPitch(note);
            index += 2;
            lastTime = millis();
            /*
            Sefrial.println(note);
            Serial.println(beatTime);
            Serial.println(lastTime);
            */
        }
    }
    void setup()
    {
        Serial.begin(115200);
        delay(1000);
        systemInit();
        playSong(songAlwaysWithMe);
    }

    void loop()
    {

    }
    [/mw_shl_code]
  • 唱片式音乐盒代码
    [mw_shl_code=arduino,true]// ArduinoMusicBox.ino
    // 实现一个 Arduino 音乐盒
    // By seesea, 2019-03-20
    //
    // 很多的 arduino 示例做音乐播放或电子琴什么的,就只是一个 tone() 哔哔哔的声音,就想着怎样才能让 arduino 播放好听一点的声音呢。
    // 在逛一些论坛的时候有人在讨论日本的一个大神做的 avr 程序播放的很好听,但是没有源码。
    // 后来多番搜索有了一些收获,下面是几个比较相关的链接可以参考,按相关程度排序:
    // 1. arduino 音乐盒播放(页面末有代码下载链接),本程序参考来源:
    //     https://www.nutsvolts.com/magazine/article/april2012_Lindley
    // 2. arduino wav 声音播放器,arduino 联合创始人的文章,值得一看,上一链接作者的代码我看应该很大程度参考自这里:
    //     https://www.elektormagazine.com/files/attachment/331
    // 3. makezine 的教学,浅显易懂,推荐学习:
    //     https://makezine.com/projects/ma ... no-sound-synthesis/
    // 4. 日本大神的音乐盒,主要是原理介绍值得学习:
    //     http://elm-chan.org/works/mxb/report.html
    // 5. 直接数字合成器,理论讲解很详细:
    //     http://www.cs.nott.ac.uk/~pszjm2/?p=674
    // 6. 声音采样知识介绍:
    //     http://www.quintadicopertina.com ... ng-digitized-audio/
    // 7. arduino 声音合成库,功能强大:
    //     http://sensorium.github.io/Mozzi

    #include <stdint.h>
    #include <avr/interrupt.h>
    #include <avr/io.h>
    #include <avr/pgmspace.h>
    #include <util/atomic.h>
    #include "wavetableData.h"
    #include "pitchs.h"

    #define AUDIO_OUTPUT_PIN        11
    #define GENERATOR_COUNT         3
    #define SAMPLE_RATE             32000
    #define SUSTAIN_LENGTH          128
    #define SHIFT_BIT_NUM           8
    #define SENSOR_COUNT            8
    #define POTENTIOMETER_PIN       A0
    #define MOTOR_PWM_PIN           6

    const uint16_t WAVETABLE_LENGTH    = sizeof(waveTableData) / sizeof(waveTableData[0]);
    const uint16_t ENVELOPTABLE_LENGTH = sizeof(envelopeData) / sizeof(envelopeData[0]);

    // 注意,使用8个传感器,如果不是,需要调整相关代码
    // 功能按顺序为:高八度标记,音符 7 6 5 4 3 2 1
    const uint8_t sensorPins[SENSOR_COUNT] = { 2, 3, 4, 5, 7, 8, 9, 10 };

    // 声音合成器结构
    // 包括:相位步进,相位累计,包络数据表的索引
    // 后续如需扩展多乐器什么的,可以在这里增加乐器波表指针
    struct
    {
        uint16_t phaseStep;
        uint32_t phaseAccumulator;
        uint16_t envelopeIndex;
    } generators[GENERATOR_COUNT];

    // 初始化
    void systemInit()
    {
        cli();

        pinMode(AUDIO_OUTPUT_PIN, OUTPUT);
        digitalWrite(AUDIO_OUTPUT_PIN, LOW);

        initSensorPins();

        for (int i = 0; i < GENERATOR_COUNT; i++)
        {
            generators.phaseStep        = 0;
            generators.phaseAccumulator = 0;
            generators.envelopeIndex    = 0;
        }

        initTimers();

        sei();
    }

    // 配置定时器
    // 注意此函数调用前关中断
    // 比较底层,直接照搬大神的
    // 参考:
    //      https://www.nutsvolts.com/magazine/article/april2012_Lindley
    //      https://www.elektormagazine.com/files/attachment/331
    void initTimers()
    {
        /*
        * Timer 1 Configuration
        * Set up Timer 1 to generate interrupt at SAMPLE_RATE.
        */
        // Set CTC mode (Clear Timer on Compare Match)
        // Have to set OCR1A *after*, otherwise it gets reset to 0!
        TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
        TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));

        // No prescaler
        TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set the compare register (OCR1A).
        // OCR1A is a 16-bit register, so we have to do this with
        // interrupts disabled to be safe.
        OCR1A = F_CPU / SAMPLE_RATE;    // 16e6 / 32000 = 500

        // Enable interrupt when TCNT1 == OCR1A
        TIMSK1 |= _BV(OCIE1A);

        /*
        * Timer 2 Configuration
        * Set up Timer 2 to do pulse width modulation at frequency of 62,500 Hz
        */
        // Set fast PWM mode
        TCCR2A |= _BV(WGM21) | _BV(WGM20);
        TCCR2B &= ~_BV(WGM22);

        // Do non-inverting PWM on pin OC2A. Arduino pin 11
        TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
        TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));

        // No prescaler so PWM frequency is 16000000 / 256 or 62,500 Hz
        TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);

        // Set initial pulse width to zero.
        OCR2A = 0;
    }

    // 定时器 1 的中断处理函数
    // 参考:https://www.nutsvolts.com/magazine/article/april2012_Lindley
    ISR(TIMER1_COMPA_vect)
    {

        int32_t  pwmSample = 0;
        uint32_t phase;
        uint32_t waveTableIndex;
        int32_t  sampleValue;
        uint8_t  envlopeValue;

        for (uint8_t i = 0; i < GENERATOR_COUNT; i++)
        {
            phase          = generators.phaseAccumulator;        // 取出步进累计值
            waveTableIndex = (phase >> SHIFT_BIT_NUM);              // 去除低位做为波表的索引
            sampleValue    = (int8_t) pgm_read_byte(waveTableData + waveTableIndex);
            envlopeValue   = pgm_read_byte(envelopeData + generators.envelopeIndex);
            pwmSample     += sampleValue * envlopeValue;            // 把波形值和包络相乘做为 pwm 的参数(需去除低位),注意的是这是所有的声音合成器的综合

            // 累加相位步进值
            // 如果到波表的结尾了,则开始增加包络的索引,进行声音大小的改变
            generators.phaseAccumulator += generators.phaseStep;
            if (generators.phaseAccumulator >= ((uint32_t) WAVETABLE_LENGTH) << SHIFT_BIT_NUM)
            {
                generators.phaseAccumulator -= (((uint32_t) SUSTAIN_LENGTH) << SHIFT_BIT_NUM);

                if (generators.envelopeIndex < ENVELOPTABLE_LENGTH - 1)
                {
                    generators.envelopeIndex++;
                }
            }
        }

        // 去除低位,留高位,缩小数值到波表的长度内
        pwmSample >>= (SHIFT_BIT_NUM + 2);

        // 加上偏移量避免负数后,设置给定时器 2
        OCR2A = (uint8_t)(127 + pwmSample);
    }

    // 将音符频率换算为相位步进值
    // 计算公式为:2^32 * 频率 / 采样率
    // 2^32 来源是相位累加 generator_t.phaseAccumulator 是 32 位的
    inline unsigned int hzToPhaseStep(float hz)
    {
        return (unsigned int) (hz * 4.096);
    }

    // 传入音符对应的频率来播放音符
    void playPitch(uint16_t pitchFreq)
    {
        static int8_t generatorIndex = 0;
        uint16_t step = hzToPhaseStep(pitchFreq);

        ++generatorIndex;
        if (generatorIndex >= GENERATOR_COUNT) {
            generatorIndex = 0;
        }

        // 注意关中断进行设置
        cli();
        generators[generatorIndex].phaseStep        = step;
        generators[generatorIndex].phaseAccumulator = 0;
        generators[generatorIndex].envelopeIndex    = 0;
        sei();
    }

    // 初始化传感器引脚,设置为内部上拉的输入
    void initSensorPins()
    {
        for (int i = 0; i < SENSOR_COUNT; ++i)
        {
            pinMode(sensorPins, INPUT_PULLUP);
        }
    }

    // 目前限制在8个传感器,所以用8位的变量返回值
    // 返回值的每一位表示一个传感器的状态
    uint8_t getAllSensorsInput()
    {
        uint8_t sensorsStatus = 0;

        for (int i = 0; i < SENSOR_COUNT; ++i)
        {
            sensorsStatus <<= 1;

            if (digitalRead(sensorPins) == LOW)
            {
                sensorsStatus |= 0x01;
            }
        }

        return sensorsStatus;
    }

    void readSensorAndPlayNote()
    {
        // 存放至少 16 个音符,这里为两个八度 C 大调音阶,可根据自己的乐曲定制相应的音符
        static const uint16_t noteBox[] = {
            NOTE_C3, NOTE_D3, NOTE_E3, NOTE_F3, NOTE_G3, NOTE_A3, NOTE_B3,
            NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4
        };

        static uint8_t lastSensorStatus = 0;
        uint8_t sensorStatus = getAllSensorsInput();
        uint8_t pitchOffset  = 0;

        // 除了高八度标记位外的其它音符位没有变化的时候,认为还没扫描到下一次音符输入,不做任何操作
        if ((0xEF & sensorStatus) == lastSensorStatus)
            return;

        // 高八度
        if ((0x80 & sensorStatus) != 0)
            pitchOffset = 7;

        // 从低到高检查每一位标志来进行音符播放
        sensorStatus &= 0xEF;
        for (uint8_t i = 0; i < SENSOR_COUNT - 1; ++i)
        {
            if (sensorStatus & 0x01)
            {
                playPitch(noteBox[i + pitchOffset]);
            }
            sensorStatus >>= 1;
        }

        // 记录旧值用于下次比较
        // 注意:sensorStatus 已经是 sensorStatus & 0xEF 了
        lastSensorStatus = sensorStatus;
    }

    void setup()
    {
        systemInit();
        Serial.begin(9600);
    }

    void loop()
    {
        readSensorAndPlayNote();
        analogWrite(MOTOR_PWM_PIN, map(analogRead(POTENTIOMETER_PIN), 0, 1024, 0, 255));
    }
    [/mw_shl_code]


演示视频


参考文档
按与本制作相关程度排序


发表于 2019-4-30 18:29 | 显示全部楼层
哇哦,不错哦,感谢楼主分享
发表于 2019-4-30 20:39 | 显示全部楼层
很有意思的设计啊
 楼主| 发表于 2019-4-30 21:27 | 显示全部楼层
小明来了 发表于 2019-4-30 18:29
哇哦,不错哦,感谢楼主分享

谢谢支持,很有趣,也试试吧
 楼主| 发表于 2019-4-30 21:27 | 显示全部楼层
Zoologist 发表于 2019-4-30 20:39
很有意思的设计啊

:D 谢谢支持,咱就搞点小玩意儿,各位大神才是玩大的
发表于 2019-5-3 04:23 | 显示全部楼层
大佬的代码总是那么牛逼闪闪!
 楼主| 发表于 2019-5-3 13:29 来自手机 | 显示全部楼层
mostblack 发表于 2019-5-3 04:23
大佬的代码总是那么牛逼闪闪!

不敢装逼不敢闪
 楼主| 发表于 2019-5-3 22:13 | 显示全部楼层

过奖过奖,都是大神们做好的,改造一下
发表于 2019-10-11 17:34 | 显示全部楼层
谢谢楼主分享!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino中文社区

GMT+8, 2024-11-28 02:27 , Processed in 0.107424 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表