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

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

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

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

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

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

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

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

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

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

上一篇烂文中,老周扯了 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。

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

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

  音阶 频率 最终取值
低音部分 低音 5 195.998 196 Hz
低音 6 220.0 220 Hz
低音 7 246.942 247 Hz
中音部分 中音 1 261.626 262 Hz
中音 2 293.665 294 Hz
中音 3 329.628 330 Hz
中音 4  349.228  349 Hz
 中音 5  391.995  392 Hz
 中音 6 440.0 440 Hz 
 中音 7  493.883  494 Hz
 高音部分  高音 1  523.251  523 Hz

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

  1. class NotePlayer : IDisposable
  2. {
  3. private PwmChannel _pwmch = null;
  4.  
  5. // 构造函数
  6. public NotePlayer() => _pwmch = PwmChannel.Create(0, 0);
  7.  
  8. public void Dispose()
  9. {
  10. _pwmch?.Dispose();
  11. }
  12.  
  13. /// <summary>
  14. /// 播放指定频率的声音
  15. /// </summary>
  16. /// <param name="freq">声音频率</param>
  17. /// <param name="duration">持续时间(毫秒)</param>
  18. public void PlayNote(int freq, int duration)
  19. {
  20. _pwmch.Frequency = freq;
  21. _pwmch.Start(); // 开始播放
  22. DelayHelper.DelayMillis(duration);
  23. _pwmch.Stop(); // 停止播放
  24. }
  25. }

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

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

  1. class DelayHelper
  2. {
  3. public static void DelayMillis(int ms)
  4. {
  5. long ticks = ms * Stopwatch.Frequency / 1000;
  6. long targetTicks = Stopwatch.GetTimestamp() + ticks;
  7. do
  8. {
  9. Thread.SpinWait(1);
  10. }
  11. while (Stopwatch.GetTimestamp() < targetTicks);
  12. }
  13. }

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

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

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

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

  1. 440 1125
  2. 392 375
  3. 330 750
  4. 392 750
  5. 523 750
  6. 440 375
  7. 392 375
  8. 440 1500
  9. 0 750
  10. 330 750
  11. 392 375
  12. 440 375
  13. 392 750
  14. 330 375
  15. 294 375
  16. 262 375
  17. 220 375
  18. 392 375
  19. 330 375
  20. 294 1500
  21. 0 750
  22. 294 1125
  23. 330 375
  24. 392 750
  25. 392 375
  26. 440 375
  27. 330 1125
  28. 294 375
  29. 262 1500
  30. 0 750
  31. 392 1125
  32. 330 375
  33. 294 375
  34. 262 375
  35. 220 375
  36. 262 375
  37. 196 1500
  38. 0 750

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

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

  1. public class MusicPlayer : IDisposable
  2. {
  3. private bool _playing = false; // 表示是否正在播放
  4. NotePlayer _noteplayer = null;
  5. Stream _stream = null;// 文件流
  6.  
  7. #region 构造函数
  8. public MusicPlayer(string noteFilepath)
  9. {
  10. _noteplayer = new NotePlayer();
  11. _stream = File.OpenRead(noteFilepath);
  12. }
  13. #endregion
  14.  
  15. /// <summary>
  16. /// 播放音乐
  17. /// </summary>
  18. /// <param name="count">重复次数,-1表示无限循环</param>
  19. public void Start(int count = 1)
  20. {
  21. _playing = true;
  22.  
  23. if(count == -1) // 无限循环
  24. {
  25. while(_playing)
  26. {
  27. PlaySong();
  28. }
  29. }
  30. else
  31. {
  32. while (_playing && count > 0)
  33. {
  34. PlaySong();
  35. count--;
  36. }
  37. }
  38. }
  39.  
  40. /// <summary>
  41. /// 停止播放
  42. /// </summary>
  43. public void Stop() => _playing = false;
  44.  
  45. public void Dispose()
  46. {
  47. _stream?.Close();
  48. _stream?.Dispose();
  49. _noteplayer?.Dispose();
  50. }
  51.  
  52. #region 私有方法
  53. private void PlaySong()
  54. {
  55. string line = null;
  56. _stream.Seek(0L, SeekOrigin.Begin);
  57. // 这里一定要让 leaveOpen 参数为 true
  58. // 不然 reader 关闭时会直接把文件给释放
  59. // 后面就不能播放第二遍了
  60. using StreamReader _noteReader = new(_stream, leaveOpen: true);
  61. line = _noteReader.ReadLine();
  62. int freq, dura;
  63. while (_playing && (line is not null))
  64. {
  65. string[] _s = line.Split(' ');
  66. if (!int.TryParse(_s[0].Trim(), out freq))
  67. {
  68. continue;
  69. }
  70. if (!int.TryParse(_s[1].Trim(), out dura))
  71. {
  72. continue;
  73. }
  74. if (freq < 0 || dura < 0)
  75. {
  76. continue;
  77. }
  78. // 播放音符
  79. _noteplayer.PlayNote(freq, dura);
  80. // 播放完读下一个音符
  81. line = _noteReader.ReadLine();
  82. }
  83. }
  84. #endregion
  85. }

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

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

回到程序的 Main 方法。

  1. class Program
  2. {
  3. // 声明字段
  4. static MusicPlayer ply = null;
  5.  
  6. static void Main(string[] args)
  7. {
  8. // 当按取消键时清理资源
  9. Console.CancelKeyPress += (_,_) =>
  10. {
  11. ply?.Stop();
  12. ply?.Dispose();
  13. };
  14.  
  15. ply = new("./test01.txt");
  16. // 尝试通过命令行参数获取播放次数
  17. int count = -1;
  18. if(args is { Length: > 0})
  19. {
  20. string s = args[0];
  21. if(!int.TryParse(s,out count))
  22. {
  23. count = -1;
  24. }
  25. }
  26. Console.WriteLine($"播放{count}次……");
  27. ply.Start(count);
  28.  
  29. ply.Dispose();
  30. }
  31.  
  32. }

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

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

下面看怎么接线。

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

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

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

GND 接树莓派 GND。

IO 接树莓派的 GPIO 18。

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

示例源代码,请点击这里

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

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

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

【.NET 与树莓派】让喇叭播放音乐的更多相关文章

  1. ESP32 LyraT音频开发板试玩(二):播放音乐

    我是卓波,很高兴你来看我的博客. 系列文章: ESP32 LyraT音频开发板试玩(一):搭建开发环境 ESP32 LyraT音频开发板试玩(二):播放音乐 本文延续上一篇博客 将D:\msys32\ ...

  2. pcDuino-V2利用madplay播放音乐

    在pcDuino的UBUNTU系统下,打开控制台,利用apt-get来下载madplay软件. sudo apt-get install madplay 播放音乐: madplay xxx.mp3 x ...

  3. matlab播放音乐

    最近在做计算,写了一些matlab代码,脑壳还疼,所以决定发挥一下逗B精神,写一个程序玩一下. 想了想,既然写代码的时候喜欢听歌,而且我的电脑打开网易音乐的速度巨慢(不知道为什么..),那些一个程序直 ...

  4. go语言让windows发出声音,或者播放音乐

    go语言让windows发出声音,或者播放音乐的例子:会发出alert警告的声音 ( 这是我应群员的求助写的, 如果你需要了解其中的调用原理或过程 或更多go语言调用win32api的资料,加群: 2 ...

  5. Inno调用dll --- 实现背景播放音乐

    Inno 播放音乐可以调用系统api函数mciSendString来实现,而mciSendString函数存在C:\Windows\System32\winmm.dll中,因此最终只要成功从该dll文 ...

  6. 【分享】4412开发板ubuntu 12.0.4播放音乐没有声音解决方法

    转自迅为论坛:http://bbs.topeetboard.com 准备工作 1.下载 vim 在命令行上输入 apt-get install vim 下载 vim 2.输入 vim /etc/hos ...

  7. 在Linux终端命令行下播放音乐的命令(Ubuntu)

    现在的 Linux 桌面已经发展的很好了,在桌面下播放音乐操作起来也很简单.那么我们还记得在桌面不是那么好的时候我们是怎么播放音乐的么?哎,我是想不起来了,实在是太难了. 不过现在我们可以先安装一个小 ...

  8. C# 调用系统winmm.dll 播放音乐wav mp3

    using System.Runtime.InteropServices;//放wav public partial class frmMain : Form { 系统放 播放音乐 wav mp3 [ ...

  9. iOS开发系列--扩展--播放音乐库中的音乐

    众所周知音乐是iOS的重要组成播放,无论是iPod.iTouch.iPhone还是iPad都可以在iTunes购买音乐或添加本地音乐到音乐 库中同步到你的iOS设备.在MediaPlayer.fram ...

随机推荐

  1. 【MYSQL】win7安装mysql-5.7.10绿色版

    1.下载 :mysql下载地址 2.解压缩 3.环境变量配置 MYSQL_HOME=D:\mysql-5.7.11-win32 PATH=%MYSQL_HOME%\bin 4.修改配置文件 a.)将m ...

  2. kubernets之secret资源

    一  对于一些保密度比较高的文件,k8s又是如何存储的呢? 针对那些保密度比较高的配置文件,例如证书以及一些认证配置不能直接存储在configmap中,而是需要存储在另外一种资源中,需要对存储在里面的 ...

  3. [分享] 最流行的 10 个 JavaScript 库

    1. Lodash https://github.com/lodash/lodash 一个工具库,用得还蛮多. 2. Chalk https://github.com/chalk/chalk 给终端加 ...

  4. mybatis源码解析之架构理解

    mybatis是一个非常优秀的开源orm框架,在大型的互联网公司,基本上都会用到,而像程序员的圣地-阿里虽然用的是自己开发的一套框架,但其核心思想也无外乎这些,因此,去一些大型互联网公司面试的时候,总 ...

  5. 架构风格 vs. 架构模式 vs. 设计模式(译)

    4.架构风格 vs. 架构模式 vs. 设计模式(译) - 简书 https://www.jianshu.com/p/d8dce27f279f

  6. pywin32 pywin32 docx文档转html页面 word doc docx 提取文字 图片 html 结构

    https://blog.csdn.net/X21214054/article/details/78873338# python docx文档转html页面 - 程序猿tx - 博客园 https:/ ...

  7. LOJ10132

    在 Adera 的异时空中有一张地图.这张地图上有 N 个点,有 N-1 条双向边把它们连通起来.起初地图上没有任何异象石,在接下来的 M 个时刻中,每个时刻会发生以下三种类型的事件之一: 地图的某个 ...

  8. 利用Javascript制作网页特效(其他常见特效)

    设置为首页和加入收藏夹 ①:在body标签内输入以下代码: <a onclick="this.style.behavior='url(#default#homepage)'; this ...

  9. Java8中流的性能

    流(Stream)是Java8为了实现最佳性能而引入的一个全新的概念.在过去的几年中,随着硬件的持续发展,编程方式已经发生了巨大的改变,程序的性能也随着并行处理.实时.云和其他一些编程方法的出现而得到 ...

  10. 使用 Shiro,从架构谈起,到框架集成!

    使用 Shiro,从架构谈起,到框架集成! 一.架构 1.使用用户的登录信息创建令牌 2.执行登陆动作 3.判断用户 4.两条重要的英文 二.实现Realm 1.缓存机制 2.散列算法与加密算法 3. ...