10

【.NET 与树莓派】让喇叭播放音乐

 3 years ago
source link: https://www.cnblogs.com/tcjiaan/p/14395271.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【.NET 与树莓派】让喇叭播放音乐

如果你和老周一样,小时候特别喜欢搞破坏(什么电器都敢拆),那下面这样小喇叭你一定见过。

367389-20210210104057955-1808144245.jpg

这种喇叭其实以前很多录音机都用,包括上小学时买来做英语听力的便携录音机。嗯,就是放录音带的那种,录音带也叫磁带或卡带,有两个轮子,录音机的动力转轴会带动轮子转动,然后就能听到声音了。

367389-20210210104719074-384668758.png

小时候,放学从学校走回家,途中就能看到不少于十处卖录音带的,有文具店的,有蹲路边卖的,甚至连一些卖早餐的店也卖。至于是否正版,这个你懂的。反正 1.5 元到 2 元一盒,零花钱省点用的话,一个星期能买到两三盒,然后回到家里又可以嗨了。也不用担心被长辈们发现,因为他们也喜欢听,被发现了他们会和你一起分享。

玩具用的喇叭会稍微比这个小一点,有正圆形的,也有椭圆形的;老周曾经在一个电子琴里拆出有正方形的喇叭,以老周幼年的拆机经验,方形是极罕见的,正圆形居多。

网上购买,一般会买到有焊接杜邦线的,以及无焊接线的。

367389-20210210105506922-9749661.jpg

有焊接杜邦线的就爽了,直接上线;至于无线的,如果你焊接技术好的话,也可以自己焊,如果没电烙铁没焊锡丝也不要紧,可以用带杜邦头的鳄鱼夹,直接夹住接线孔就行了。毕竟两边的线离得比较远,两个鳄鱼夹不会碰到一起,就不必担心短路了。

367389-20210210105851391-722192984.jpg

除了上面介绍的喇叭,还有一种模块也能播放音乐,那就是蜂鸣器。

367389-20210210110446883-1469339565.jpg

注意蜂鸣分为无源和有源,上图中,左边的是无源蜂鸣器,右边的是有源蜂鸣器。

有源蜂鸣器,只要电平信号就能发声,而且只能产生固定音高的声音,所以,想让它播放音乐是没门的。从图中你会看到,有源蜂鸣器上那块黑色的圆柱体(像牛粪的那个)上贴着纸签,如果不撕掉,发出的声音刺耳但音量很小;如果把纸签撕掉,音量变大但没那么刺耳。我们这个让喇叭播放音乐的实验必须使用无源蜂鸣器,所以买的时候要看清楚是有源的还是无源的。

好了,上面介绍的是完成实验的器件,至于是选小喇叭还是蜂鸣器,你看着办,因为两者原理一样——我们物理课上学过,音高是由频率决定的。

上一篇烂文中,老周扯了 PWM 调光的实验,由于 PWM 能以不同的频率输出电平信号,所以设置不同的频率,再发送PWM方波,就能让喇叭发出不同音高的声音了。喇叭不能直接上电源,那样是不能放音乐的,只能听见地雷爆炸的声音。再次强调一下,是改变 PWM 的频率,不是占空比,改变占空比只能控制声音强度

下面开始实验,此次实验老周选了一首很简单的歌,大家都听过的,《世上只有妈妈好》。简谱如下:

速度是每分钟 80 拍,所以每一拍(四分音符)的时长为 60/80 = 750 毫秒。接下来咱们确定一下曲中各音符的时值。

1、带附点的四分音符,时值为 750 + 750/2 = 1125 ms。附点就是延长当前音符时值的一半,所以四分音符加附点就是加上半拍的时值。

2、四分音符:750ms。

3、八分音符:750 / 2 = 375ms。

4、二分音符,后面有一横线的,就是两拍,750 * 2= 1500 ms。

至于每个音符的频率,可以直接网上查。

367389-20210210114411010-1467678098.png

此处老周选用国际标准 A(中音 La)的频率(440 Hz)作为中音音域的参考点,于是得到各音阶的频率。

封装一个类,名为 NotePlayer,调用 PlayNote 方法播放指定频率的声音,持续 X 毫秒。

    class NotePlayer : IDisposable
    {
        private PwmChannel _pwmch = null;

        // 构造函数
        public NotePlayer() => _pwmch = PwmChannel.Create(0, 0);

        public void Dispose()
        {
            _pwmch?.Dispose();
        }

        /// <summary>
        /// 播放指定频率的声音
        /// </summary>
        /// <param name="freq">声音频率</param>
        /// <param name="duration">持续时间(毫秒)</param>
        public void PlayNote(int freq, int duration)
        {
            _pwmch.Frequency = freq;
            _pwmch.Start(); // 开始播放
            DelayHelper.DelayMillis(duration);
            _pwmch.Stop();   // 停止播放
        }
    }

核心部分是 PlayNote 方法,首先设置频率,然后调用 PwmChannel 的Start方法开始发送脉冲,随后持续一段时间(这段时间就是音符的时值,请看上文),播放完后,调用 Stop方法停止脉冲,喇叭不发声。

这里面有个辅助方法 DelayMillis,用来暂停 X 毫秒,你完全可以用 Thread.Sleep 方法,这里老周写这个方法,用的是另一种思路——这是参考微软的写法。

    class DelayHelper
    {
        public static void DelayMillis(int ms)
        {
            long ticks = ms * Stopwatch.Frequency / 1000;
            long targetTicks = Stopwatch.GetTimestamp() + ticks;
            do
            {
                Thread.SpinWait(1);
            }
            while (Stopwatch.GetTimestamp() < targetTicks);
        }
    }

原理是运用了 Stopwatch 类的计时器,GetTimestamp 方法总能返回计时器最新的 Tick,接着进入循环,每轮循环中调用 Thread.SpinWait(1) 只等待一个代码周期,这个时间很短,微秒级别的。循环退出条件是 GetTimestamp 方法返回的 Tick 达到我们预定好的时间。

这种方案适合对时间精度高的等待方案,比如等待几十微秒的。

这里要思考一件事:我们如果把每首曲子的音符都写进代码中,如果要播放其他曲子就得改一大遍代码,很不灵活。当然像 Arduino 那样没有操作系统且内部存储空间很小的板子,要么把代码写死,要么加个外部的 SD 卡模块,把音符信息放SD卡上,然后在代码中读。对于树莓派来说,这事情好办得要命。树莓派带操作系统,而且自身有 micro SD 卡接口,读写文件相当方便。

因此,老周把《世上只有妈妈好》的音符频率和时值输入到一个文本文件中,要换曲子直接换个文件就完事。格式很简单,每行一个音符,包括频率和时值,用空格分开。于是,《世上只有妈妈好》的文件如下:

440 1125
392 375
330 750
392 750
523 750
440 375
392 375
440 1500
0 750
330 750
392 375
440 375
392 750
330 375
294 375
262 375
220 375
392 375
330 375
294 1500
0 750
294 1125
330 375
392 750
392 375
440 375
330 1125
294 375
262 1500
0 750
392 1125
330 375
294 375
262 375
220 375
262 375
196 1500
0 750

其中,你会看到有几行,音符频率是 0,这个是为了让喇叭有停顿。

再写一个  MusicPlayer 类,可以控制播放整首曲子,并可以指定循环次数。

    public class MusicPlayer : IDisposable
    {
        private bool _playing = false; // 表示是否正在播放
        NotePlayer _noteplayer = null;
        Stream _stream = null;// 文件流

        #region 构造函数
        public MusicPlayer(string noteFilepath)
        {
            _noteplayer = new NotePlayer();
            _stream = File.OpenRead(noteFilepath);
        }
        #endregion

        /// <summary>
        /// 播放音乐
        /// </summary>
        /// <param name="count">重复次数,-1表示无限循环</param>
        public void Start(int count = 1)
        {
            _playing = true;

            if(count == -1)     // 无限循环
            {
                while(_playing)
                {
                    PlaySong();
                }
            }
            else
            {
                while (_playing && count > 0)
                {
                    PlaySong();
                    count--;
                }
            }
        }

        /// <summary>
        /// 停止播放
        /// </summary>
        public void Stop() => _playing = false;

        public void Dispose()
        {
            _stream?.Close();
            _stream?.Dispose();
            _noteplayer?.Dispose();
        }

        #region 私有方法
        private void PlaySong()
        {
            string line = null;
            _stream.Seek(0L, SeekOrigin.Begin);
            // 这里一定要让 leaveOpen 参数为 true
            // 不然 reader 关闭时会直接把文件给释放
            // 后面就不能播放第二遍了
            using StreamReader _noteReader = new(_stream, leaveOpen: true);
            line = _noteReader.ReadLine();
            int freq, dura;
            while (_playing && (line is not null))
            {
                string[] _s = line.Split(' ');
                if (!int.TryParse(_s[0].Trim(), out freq))
                {
                    continue;
                }
                if (!int.TryParse(_s[1].Trim(), out dura))
                {
                    continue;
                }
                if (freq < 0 || dura < 0)
                {
                    continue;
                }
                // 播放音符
                _noteplayer.PlayNote(freq, dura);
                // 播放完读下一个音符
                line = _noteReader.ReadLine();
            }
        }
        #endregion
    }

打开包含音符频率和时值的文件,一行一行地读。每读出一行,以空格作分隔符拆开字符串——可拆成两个元素的字符串数组。第一个元素为频率,第二个元素为时值,随后用前面封装的 PlayNote 播放。

注意实例化 StreamReader 时,一定要保证它被释放时不要关闭文件,不然打开文件后只能播放一次了,后续的重复播放就会报错。

回到程序的 Main 方法。

    class Program
    {
        // 声明字段
        static MusicPlayer ply = null;

        static void Main(string[] args)
        {
            // 当按取消键时清理资源
            Console.CancelKeyPress += (_,_) =>
            {
                ply?.Stop();
                ply?.Dispose();
            };

            ply = new("./test01.txt");
            // 尝试通过命令行参数获取播放次数
            int count = -1;
            if(args is { Length: > 0})
            {
                string s = args[0];
                if(!int.TryParse(s,out count))
                {
                    count = -1;
                }
            }
            Console.WriteLine($"播放{count}次……");
            ply.Start(count);

            ply.Dispose();
        }

    }

这里还实现了通过命令行参数来设定循环播放次数,-1为单曲循环。

最后是发布,上传到树莓派。

下面看怎么接线。

一、如果用小喇叭,注意正负极。如下图,左边是负极(接线孔右侧有“-”),右边是正极(接线孔左侧有“+”)。负极接树莓派的 GND(有多个,随便挑一个),正极串联一个大于 100 Ω 的电阻(电阻一定要接,不然会有破音,而且时间长了会烧掉喇叭,阻值 100 - 200 均可,电阻大了声音小一点)后接 GPIO 18,这个你看过上一篇文章就知道了,4B 只有这个引脚能产生第一路 PWM,其他树莓派你可以自己试。

367389-20210210123429657-2009003068.jpg

二、使用无源蜂鸣器。这个得看你买的模块是什么样子的,老周买的这个是三个引脚的。

367389-20210210123853434-1771895893.png

 VCC 接树莓派供电脚,3.3V 和 5V 均可,都兼容,放心烧不了,上面有100欧的电阻。

GND 接树莓派 GND。

IO 接树莓派的 GPIO 18。

执行程序,就可以欣赏音乐了。

示例源代码,请点击这里

可以试听一下效果

=====================================================================

补充一下,开发板只能产生方波,不能产生正弦(含余弦)波,更不能产生叠加的交流声波。所以,它只能依据频率来产生不同的音高,你不能控制其音色,更别指望变成自制 Midi。树莓派主板上是有 3.5 mm 音频接口的,要看电影要听歌,跟电脑一样,插个耳机或有源音箱(如低音炮)即可。也可以去买一块专门的功放模块(针对像 Arduino 那样没有音频接口的板子),不用写代码驱动,插上音响就能嗨。当然也有蓝牙功放模块,网购无极限,啥都有可能买到。所以这年头想DIY还是比较容易的。

下一篇烂文,老周会说一下用 PWM 来驱动舵机,以及调节风扇的转速。


Recommend

  • 191
    • Engadget 中国版 cn.engadget.com 6 years ago
    • Cache

    一门三杰,Amazon Echo 智慧喇叭全面更新

    亚马逊近年以搭载虚拟助手 Alexa 的智慧喇叭 Echo,在智能家居的市场打出一片天,他们也当然不会放过机会,不停更新硬体规格来让更多家庭都加入一台 Echo 喇叭了。亚马逊一口气就带来三款新款 Echo,包括搭载小萤幕的 Echo Spot、披新衣的 Echo、更强的 Echo Plus...

  • 123
    • Engadget 中国版 cn.engadget.com 6 years ago
    • Cache

    Anker 在汽水罐大小的智能喇叭里塞入了一个投影机

    虽然便携式投影机现在还没有真正流行起来,但 Anker 这家外设厂商还是在这个领域中看到了商机。就在不久前,他们将 Nebula Capsule 这款集蓝牙喇叭和投影机功能于一身的产品放到了 Indiegogo 上募资。它看起来只有汽水罐大小,但却内建了五个全向扬声器。投影的部...

  • 127
    • Engadget 中国版 cn.engadget.com 6 years ago
    • Cache

    UE 发表支持 Alexa 的「Blast」与「Megablast」Wi-Fi 喇叭

    最近支持 Alexa 的喇叭(甚至不是喇叭的也有)愈来愈多,已经变得不那么稀奇,但罗技 UE 这里却是另辟蹊径,从无线音箱角度下手,打造了自家自家的第一个搭载智慧助理的行动音箱。虽然说取名为「UE Blast」和「UE Megablast」的这两个产品无论在大小、外型上都与相...

  • 39
    • Engadget 中国版 cn.engadget.com 6 years ago
    • Cache

    LG G7 ThinQ 的喇叭音量将远高于一般手机

    LG G7 ThinQ 的官方预告真是一个接一个不断,继最大亮度 1,000 尼特的超亮屏幕之后,LG 今天又为大家介绍了一项名为「Boombox Speaker」的新特性。据称其在将 G7 ThinQ 的谐振腔体「做大十多倍」后,能够实现「把基础音量提高 6dB 并提供两倍低音输出」的效果。换...

  • 23

    欢迎关注“创事记”微信订阅号:sinachuangshiji 疫情之下,人们身陷“熔炉”,被大量的信息包裹着度日,也被动直面了许多不曾了解的土地...

  • 8

    喇叭也能折叠!研究人员开发卷对卷印刷扬声器, 薄如纸片-极果 喇叭也能折叠!研究人员开发卷对卷印刷扬声器, 薄如纸片

  • 8

    一起玩转树莓派(5)——让蜂鸣器播放音乐 前面博客中,我们尝试使用开关控制有源蜂鸣器的播放。有源蜂鸣器的一大特点是使用简单,无需复杂的程序控制即可发声,然而其缺陷也很明显,其发声的频率是一定的,我们无法通过频率控制器音调高低。本次实验,...

  • 5

    iPad Air 5曝光:A15处理器加持、升级双摄四喇叭 2021年07月29日10:12  快科技2018   我有话说(0人参与) 收藏本文 ...

  • 6

    广州全市全天禁鸣喇叭 9月1日起施行 有效期五年 2021年08月22日 10:54 15782 次阅读 稿源:快科技 9 条评论

  • 4

    【.NET 与树莓派】用 MPD 制作数字音乐播放器 树莓派的日常家居玩法多多,制作一台属于...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK